RIFF band, /riff pane with tabs, /riff-status and /riff-context, read from the RIFF CLI

██████╗ ██╗███████╗███████╗
██╔══██╗██║██╔════╝██╔════╝
██████╔╝██║█████╗ █████╗
██╔══██╗██║██╔══╝ ██╔══╝
██║ ██║██║██║ ██║
╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
Build like a band of six. Ship like one.
Turn an idea into working software, one checked step at a time, in Claude Code or Codex.
You describe what you want. RIFF helps your coding agent keep a clear plan, build it in useful steps, have another model review the result, and remember where to continue. You can follow the work without having to direct every move.
RIFF runs in two coding tools, called hosts: Claude Code and Codex. Connecting a project sets it up for both, so you can switch hosts without reinstalling. The slogan comes from the original RIFF framework; here, the agent in your host coordinates the work and brings in extra help when it's useful.
Visual field manual · Installation · Everyday use · Model advice · How it works
The visual manual is available online at riff-codex-doc.vercel.app.
riff review run asks a model from another family, GPT-6 Astra by default, to attack each phase, dossier and final version read-only.You still need Claude Code or Codex installed and signed in. The Codex CLI is recommended in both cases, since it runs the default reviewer.
flowchart TD
You[You describe what you want] --> Brief[Complete version dossier and design]
Brief --> Plan[Roadmap and independent planning review]
Plan --> Pick[Choose the next ready step]
Pick --> Build[The agent builds it]
Build --> Check[Check, then review by another model]
Check -->|Pass| Save[Save the checked result and progress]
Check -->|Fail| Fix[Try a focused correction]
Fix --> CheckAgain[Check again]
CheckAgain -->|Pass| Save
CheckAgain -->|Still fails| Stop[Record the blocker]
Save -->|More ready work| Pick
Save -->|All phases complete| Final[Whole-version checks and independent review]
Final -->|Pass| Done[Verified local result]
Final -->|Defect| Correction[Plan an in-scope correction phase]
Correction --> Plan
Plan -.-> View[Dashboard: view progress]
Save -.-> View
Stop -.-> View
The brief is saved as PROJECT.md; the steps are saved as ROADMAP.yaml. For production applications, start prepares the complete version dossier and an independent planning review before implementation. ASCII wireframes can be handed to another design model; its returned reference and tokens become part of the dossier. The dashboard reads progress. You start the work in Claude Code or Codex.
By default, RIFF continues through ready steps automatically. You can choose guided mode if you prefer a pause between steps. Uploading code to GitHub or putting an application online still needs your instruction.
RIFF can advise which available model and reasoning effort fit a task. Ask in ordinary language before a RIFF request or phase:
Before you start this RIFF phase, advise which model and reasoning effort fit it.
Do not begin the work or change models.
RIFF leaves the primary model selected in your host unchanged. During a wave it also advises profiles for the already planned subagents; the coordinating agent applies verified profiles through native launch tools when delegation is authorized. Under Claude Code, each Anthropic profile is installed as a subagent with its model and effort fixed, such as riff:sonnet-5-5-medium. A plan does not itself start work, create workers, or pause a loop. The model actually used remains unknown unless the runtime or the user supplies evidence.
The default local mode means the current agent applies RIFF's portable profile catalogue. It does not mean on-device inference. Optional jev mode sends only a curated decision summary to Jev through OpenRouter and requires an explicit project opt-in. OpenAI models continue to use the Codex subscription; conditional DeepSeek profiles use Ollama Cloud and are a separate form of consumption.
As a guide, Luna fits bounded work, Sol fits well-scoped work with several connected steps, and Astra fits planning or long runs with costly dependencies. On the Anthropic side, Haiku fits simple reading, Sonnet bounded changes and exploration, and Opus the default for coding and long work. High and XHigh reasoning are for a concrete difficulty, not task length alone. These are decision hypotheses, not measured success rates. See model advice for modes, privacy controls, natural-language examples, and the advanced CLI.
riff init now sets up a project for Claude Code and Codex together. In Claude Code, skills are called as /riff:wave, model profiles are installed as subagents, and RIFF's context is injected when a session starts, resumes, or restarts after /clear or a compaction. Compaction is set to 400,000 tokens for the project. See installation.riff. riff-codex remains an alias. Project state moves from .riff-codex-state/ to .riff-data/ on the next riff resync, with a backup.riff review run has GPT-6 Astra attack each phase, dossier and final version read-only through the Codex CLI, with GPT-6.1 Sol and then a fresh Claude session as fallbacks when a reviewer can't run. The receipt names the reviewer that really ran. Security findings are corrected without stopping the wave; points that need an expert ship with a documented interim decision and are listed at the end. See independent reviews.evolve challenges a requested change against the current application, updates only the affected plan, and stops at planning readiness. wave remains the separate implementation step.onboard now includes mapping, debug covers live incidents, and start covers promotion of a prototype to production. The former map, incident and promote skills are gone.For this update, run riff resync and riff doctor once in each connected project, after its active work finishes. This updates local installation files for both hosts, moves the project state to .riff-data/ with a backup, and preserves the project brief, roadmap and preferences. If a phase is still active, resync waits to move the state and says so.
Projects linked to the same RIFF checkout already read its updated files; they don't each need a Git pull or a reinstall. Start a fresh session to load the updated skill instructions. A separately installed plugin or another checkout must be updated separately.
To use Claude Code, also register the plugin marketplace once per computer, as shown in Install. If a dashboard was already running, restart that dashboard process and reload the page to load server changes. See Update RIFF for commands and hook approval details.
You need Git, Node.js 20 or newer (which includes npm), Bun for the dashboard and the full installation check, and Claude Code or Codex. The repository is currently private, so your GitHub account needs access. The installation guide explains each requirement.
1. In Terminal, download RIFF once. Keep this folder in place; your projects will use it.
git clone https://github.com/alexadark/riff-codex.git ~/riff-codex
cd ~/riff-codex
npm install
npm link
2. For Claude Code, register the plugin once per computer. Skip this if you only use Codex.
claude plugin marketplace add ~/riff-codex/riff
3. In Terminal, connect your project to RIFF. Replace the example path with your project's folder. It must already use Git; the guide includes the new-folder setup.
cd /path/to/your-project
riff init
Choose your languages, how much explanation you want, and whether RIFF should continue automatically. Press Enter to accept the suggested choices. Then check the setup:
riff doctor
4. Open the project in your host.
.claude/settings.local.json.riff doctor reports system-managed hooks, reload Codex configuration; no individual hook approval is required. Otherwise, open /hooks, review and approve RIFF's automatic checks, then record that approval in the project's terminal with riff doctor --record-hooks-approved. This records your approval and checks the setup; it doesn't approve anything for you.5. Recommended: sign the Codex CLI in to ChatGPT with codex login, even if you build in Claude Code. It runs the default reviewer. Without it, reviews fall back to a fresh Claude session, labeled same-family.
The examples below go in the conversation, not Terminal. Claude Code uses /riff:... and Codex uses $riff:...; the skills are the same. In Codex, if those names are unavailable, choose the corresponding RIFF skill from the skill picker; the installation guide explains the two ways to make Codex skills available.
For a new idea:
/riff:start I want a simple app where I can save and find my recipes.
For an application that already exists:
/riff:onboard Map the current application and establish its existing behavior as the RIFF baseline.
/riff:evolve Plan how to make finding a recipe easier.
Production start prepares and reviews the complete dossier, including design before building. onboard maps the application and preserves its documents and behavior. Once preparation is complete, start building:
/riff:wave
A “wave” means working through the next ready steps. Run the same command when you return to continue unfinished work. If RIFF has recorded a blocker, it needs to be resolved before that work can resume.
RIFF keeps one branch and one PR for a coherent evolution or initial version across successive waves. When you authorize publication, it opens a draft after the first validated phase and updates that same PR as later phases complete. In loop, creating a PR doesn't pause work; guided keeps its existing between-phase pauses. After whole-version verification and finish --check, RIFF marks the PR ready for review and returns its verified URL. Merge and deployment require their own authorization.
Without publication authorization, RIFF completes local work and prepares the PR description before requesting delivery. A standalone quick change follows the same branch/PR workflow with its bounded checks. The skills perform Git and GitHub operations; the CLI verification commands don't push or create PRs themselves. See the Git delivery contract for resume and concurrent-work rules.
For an existing application that isn't connected to RIFF, the complete path is explicit:
riff init # Terminal, once for this project
/riff:onboard # Conversation: map the system and establish the baseline
/riff:evolve Add team invitations while preserving individual accounts.
/riff:wave # Conversation: implement the reviewed plan
onboard inspects the current system and, when the map will be reused, saves it as .riff-data/MAP.md. It records the existing behavior as a baseline; it doesn't invent historical delivery phases. If the application is already onboarded, use evolve directly for a product change that needs reconsideration or affects more than one future phase. Evolve analyzes the current code, data and access boundaries, preserves completed and active phase contracts, and ends at planning readiness. It never starts a wave itself.
| You want to… | Claude Code | Codex |
|---|---|---|
| See progress and the next step | /riff:status | $riff:status |
| Open the dashboard | /riff:dashboard | $riff:dashboard |
| Make a small, separate change | /riff:quick Make the empty screen easier to understand. | $riff:quick ... |
| Fix something that is broken | /riff:debug The save button does nothing. | $riff:debug ... |
| Add one already-clear outcome | /riff:add-phase Let people share a recipe. | $riff:add-phase ... |
| Reconsider an existing product change | /riff:evolve Let recipe owners invite teammates. | $riff:evolve ... |
| Continue building | /riff:wave | $riff:wave |
You can also open the dashboard from Terminal with riff dashboard. It normally opens at http://127.0.0.1:4000 on your computer.
| Guide | What it answers |
|---|---|
| Visual field manual | See the whole workflow, explore the diagram, and copy starter commands. |
| Installation | What do I need? How do I connect a project in each host, set up reviews, update RIFF, or fix setup problems? |
| Everyday use | What should I type for a new project, existing app, fix, or interruption? |
| Model advice | How do I ask for advice, choose a mode, and understand what was actually selected? |
| How it works | Who does what? Where does the plan live? How do checks, reviews and progress connect? |
| Technical reference | Which files, checks, and internal rules power RIFF? |
Already using the original RIFF framework, with its .riff and .riff-state/ folders? Both can share the same project brief and roadmap. Use only one of them to work on a given step at a time. See the coexistence details.
hooks/register.ts 163 lines1// RIFF cockpit: a band above the prompt with the project's RIFF progress, a /riff pane
2// with Roadmap, Phase, Findings and Reviews tabs, and /riff-status and /riff-context,
3// which answer at once without a Claude turn. Outside a RIFF project it draws nothing.
4//
5// Refreshed when the session starts, after each main turn and after each Bash call that
6// runs the RIFF CLI. The mod never writes RIFF state: it runs the project's own CLI
7// through the `.riff-cli` link (`.riff-codex` before the rename), so it shows exactly what `status --json` and
8// `wave context --json` report.
9
10import {
11 clip,
12 describe,
13 describeContext,
14 parseContext,
15 parseStatus,
16 summary,
17 tabLines,
18 TAB_LABELS,
19 TABS,
20 type RiffStatus,
21 type Tab,
22 type WaveContext,
23} from './core.ts'
24
25// Node may be missing from the app's PATH (nvm), so known locations follow.
26const NODES = ['node', '/usr/local/bin/node', '/opt/homebrew/bin/node']
27const PANE = 'riff'
28const NOT_RIFF = 'Not a RIFF project: no .riff-cli or .riff-codex link at the repository root.'
29// Projects not yet migrated by `riff resync` only have the pre-rename link.
30const LINKS = ['.riff-cli', '.riff-codex']
31const RIFF_COMMAND = /(^|[\s;&|/])(riff|riff-codex|riff\.mjs)(\s|$)/
32
33let status: RiffStatus | null = null
34let raw: string | null = null
35let context: WaveContext | null = null
36let contextError: string | null = null
37let tab: Tab = 'roadmap'
38
39export function register(on) {
40 on('session.start', async ($, e, next) => {
41 const result = await next(e)
42 await $.command.register({ name: 'riff', description: 'Open the RIFF cockpit: roadmap, phase, findings, reviews' })
43 await $.command.register({ name: 'riff-status', description: 'RIFF progress for this project, read from the RIFF CLI' })
44 await $.command.register({ name: 'riff-context', description: 'The RIFF phase in progress, its checkpoint and references' })
45 await refresh($)
46 return result
47 })
48
49 on('turn.complete', async ($, e, next) => {
50 const result = await next(e)
51 if (!e.agentId) await refresh($)
52 return result
53 })
54
55 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
56 const ran = await next(e)
57 if (RIFF_COMMAND.test(e.command ?? '')) await refresh($)
58 return ran
59 })
60
61 on('command.run', { command: 'riff-status' }, async ($) => {
62 await refresh($)
63 if (status) return { text: describe(status) }
64 return { text: raw ?? NOT_RIFF }
65 })
66
67 on('command.run', { command: 'riff-context' }, async ($) => {
68 await refresh($)
69 return { text: status ? describeContext(context, contextError) : NOT_RIFF }
70 })
71
72 on('command.run', { command: 'riff' }, async ($) => {
73 await refresh($)
74 if (!status) return { text: NOT_RIFF }
75 const opened = await $.ui.open({ id: PANE, title: `RIFF · ${status.name}`, closeOnEscape: true })
76 if (opened.isPlaced) return { text: 'RIFF cockpit opened.' }
77 // No pane here (narrow terminal, an extension, a headless run): answer in text.
78 return { text: TABS.map((one) => `## ${TAB_LABELS[one]}\n${tabLines(one, status!, context, contextError).join('\n')}`).join('\n\n') }
79 })
80
81 on('ui.render', { component: 'Pane', requestId: PANE }, ($, e) => {
82 const { Box, Text, Button } = $.ui.resolve(e)
83 if (!status) return Text({ dimColor: true, children: NOT_RIFF })
84 const columns = e.props?.bodyColumns ?? e.viewport?.columns ?? 80
85 const room = Math.max(1, (e.viewport?.rows ?? 24) - 4)
86 const tabs = TABS.map((one, index) =>
87 Button({
88 key: `tab-${one}`,
89 hotkey: String(index + 1),
90 plain: true,
91 dimColor: one !== tab,
92 label: TAB_LABELS[one],
93 onPress: () => {
94 tab = one
95 $.ui.invalidate('ui.render')
96 },
97 }),
98 )
99 const lines = tabLines(tab, status, context, contextError)
100 return Box({
101 flexDirection: 'column',
102 children: [
103 Box({ flexDirection: 'row', gap: 2, children: tabs }),
104 ...lines.slice(0, room).map((line) => Text({ children: clip(line, Math.max(20, columns - 2)) })),
105 ...(lines.length > room ? [Text({ dimColor: true, children: `… ${lines.length - room} more` })] : []),
106 ],
107 })
108 })
109
110 on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
111 if ((e.props?.hasSurvey ?? e.hasSurvey) || !status) return next(e)
112 const { Box, Text } = $.ui.resolve(e)
113 const columns = e.props?.bodyColumns ?? e.bodyColumns ?? 80
114 const rows = [
115 Box({
116 flexDirection: 'row',
117 children: [
118 Text({ color: 'magenta', bold: true, children: 'RIFF ' }),
119 Text({ children: summary(status, columns) }),
120 ],
121 }),
122 ]
123 if (status.humanAction) {
124 rows.push(Text({ color: 'red', children: clip(`Needs you: ${status.humanAction}`, Math.max(20, columns - 2)) }))
125 }
126 return Box({ flexDirection: 'column', paddingX: 1, children: rows })
127 })
128}
129
130async function refresh($) {
131 const cwd = await $.session.cwd()
132 const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd, timeoutMs: 5000 })
133 const root = top.exitCode === 0 ? top.stdout.trim() : null
134 let script: string | null = null
135 for (const link of root ? LINKS : []) {
136 if (await $.fs.exists(`${root}/${link}/bin/riff.mjs`)) { script = `${root}/${link}/bin/riff.mjs`; break }
137 }
138 // An older CLI ignores --json on status and prints its text output, which parseStatus also reads.
139 const statusRun = script ? await runCli($, script, root, ['status', '--json']) : null
140 raw = statusRun?.ok ? statusRun.text : null
141 status = raw ? parseStatus(raw) : null
142 // `wave context` has always printed JSON; no flag keeps older CLIs answering.
143 const contextRun = status ? await runCli($, script, root, ['wave', 'context']) : null
144 context = contextRun?.ok ? parseContext(contextRun.text) : null
145 contextError = contextRun && !contextRun.ok ? contextRun.text : null
146 $.ui.invalidate('ui.render')
147}
148
149// The CLI's answer, or the first line of its error so the cockpit can say why it failed.
150async function runCli($, script: string, root: string, args: string[]): Promise<{ ok: boolean; text: string }> {
151 let failure = { ok: false, text: 'node was not found' }
152 for (const node of NODES) {
153 try {
154 const run = await $.process.run([node, script, ...args], { cwd: root, timeoutMs: 10000 })
155 if (run.exitCode === 0) return { ok: true, text: run.stdout.trim() }
156 failure = { ok: false, text: (run.stderr || run.stdout).trim().split('\n')[0] || `exit code ${run.exitCode}` }
157 } catch {
158 // try the next location
159 }
160 }
161 return failure
162}
163hooks/core.ts 156 lines1// Pure rules for the RIFF cockpit: read `riff status --json` and `wave context --json`,
2// decide what the band, the pane tabs and the text commands show.
3// The CLI stays the source of truth; the text parser only serves projects linked to an older CLI.
4
5type Phase = { id: string; title: string | null; status: string }
6type Finding = { id: string; severity: string | null; summary: string | null; phase: string | null }
7type Review = { phase: string; status: string; summary: string | null; valid: boolean }
8
9export type RiffStatus = {
10 name: string
11 objective: string | null
12 completed: number
13 total: number
14 active: string | null
15 next: string | null
16 humanAction: string | null
17 // Absent when the project's CLI predates these fields.
18 phases: Phase[] | null
19 findings: Finding[] | null
20 reviews: { functional: Review | null; security: Review | null } | null
21}
22
23export type WaveContext = {
24 phase: { id: string; title: string | null; status: string; outcome: string | null } | null
25 checkpoint: { summary?: string; next?: string } | null
26 checkpointStale: boolean
27 references: string[]
28}
29
30export const TABS = ['roadmap', 'phase', 'findings', 'reviews'] as const
31export type Tab = (typeof TABS)[number]
32export const TAB_LABELS: Record<Tab, string> = { roadmap: 'Roadmap', phase: 'Phase', findings: 'Findings', reviews: 'Reviews' }
33
34const none = (value: string) => (value === 'none' ? null : value)
35
36/** Reads `riff status --json`, falling back to the text output of CLIs older than the JSON contract. */
37export function parseStatus(text: string): RiffStatus | null {
38 return parseStatusJson(text) ?? parseStatusText(text)
39}
40
41function parseStatusJson(text: string): RiffStatus | null {
42 let data
43 try {
44 data = JSON.parse(text)
45 } catch {
46 return null
47 }
48 if (data?.schema !== 'riff.status/1') return null
49 return {
50 name: data.project?.name ?? 'Unshaped project',
51 objective: data.project?.objective ?? null,
52 completed: data.progress.completed,
53 total: data.progress.total,
54 active: data.active ?? null,
55 next: data.next?.id ?? null,
56 humanAction: data.humanAction?.reason ?? null,
57 phases: data.phases ?? null,
58 findings: data.findings ?? null,
59 reviews: data.reviews ?? null,
60 }
61}
62
63function parseStatusText(text: string): RiffStatus | null {
64 const lines = String(text).trim().split('\n')
65 const progress = lines[1]?.match(/^(\d+)\/(\d+) phases completed\. Active: (.+?)\. Next: (.+?)\.$/)
66 const human = lines[2]?.match(/^Human action: (.+)$/)
67 if (!progress || !human) return null
68 const split = lines[0].indexOf(': ')
69 return {
70 name: split > 0 ? lines[0].slice(0, split) : lines[0],
71 objective: split > 0 ? none(lines[0].slice(split + 2)) : null,
72 completed: Number(progress[1]),
73 total: Number(progress[2]),
74 active: none(progress[3]),
75 next: none(progress[4]),
76 humanAction: none(human[1]),
77 phases: null,
78 findings: null,
79 reviews: null,
80 }
81}
82
83export function parseContext(text: string): WaveContext | null {
84 try {
85 const data = JSON.parse(text)
86 return data && typeof data === 'object' && 'phase' in data ? data : null
87 } catch {
88 return null
89 }
90}
91
92/** The /riff-status answer, in the same three lines as `riff status`. */
93export function describe(status: RiffStatus): string {
94 return [
95 `${status.name}: ${status.objective ?? 'no objective yet'}`,
96 `${status.completed}/${status.total} phases completed. Active: ${status.active ?? 'none'}. Next: ${status.next ?? 'none'}.`,
97 `Human action: ${status.humanAction ?? 'none'}`,
98 ].join('\n')
99}
100
101export function clip(text: string, max: number): string {
102 return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`
103}
104
105/** The band's summary line, shortened to the terminal width. */
106export function summary(status: RiffStatus, columns: number): string {
107 const parts = [`${status.completed}/${status.total} phases`]
108 if (status.active) parts.push(`active ${status.active}`)
109 else if (status.next) parts.push(`next ${status.next}`)
110 const findings = status.findings?.length ?? 0
111 if (findings) parts.push(`${findings} open finding${findings > 1 ? 's' : ''}`)
112 return clip(`${status.name} · ${parts.join(' · ')}`, Math.max(20, columns - 8))
113}
114
115const OLD_CLI = 'Update the RIFF CLI linked by .riff-cli to see this tab.'
116
117/** The lines of one pane tab; also the text of /riff where no pane can be drawn. */
118export function tabLines(tab: Tab, status: RiffStatus, context: WaveContext | null, contextError: string | null = null): string[] {
119 if (tab === 'roadmap') {
120 if (!status.phases) return [OLD_CLI]
121 if (!status.phases.length) return ['No phases yet.']
122 return status.phases.map((phase) => `${mark(phase.status)} ${phase.id} ${phase.title ?? ''}`.trimEnd())
123 }
124 if (tab === 'phase') return describeContext(context, contextError).split('\n')
125 if (tab === 'findings') {
126 if (!status.findings) return [OLD_CLI]
127 if (!status.findings.length) return ['No open findings.']
128 return status.findings.map((f) => `${f.severity ?? '?'} ${f.summary ?? f.id}${f.phase ? ` (${f.phase})` : ''}`)
129 }
130 if (!status.reviews) return [OLD_CLI]
131 return (['functional', 'security'] as const).map((type) => {
132 const review = status.reviews![type]
133 if (!review) return `${type}: none yet`
134 return `${type}: ${review.status} on ${review.phase}${review.valid ? '' : ' (stale)'}${review.summary ? ` · ${review.summary}` : ''}`
135 })
136}
137
138/** The /riff-context answer: the phase in progress or next, its checkpoint and references. */
139export function describeContext(context: WaveContext | null, error: string | null = null): string {
140 if (!context) return error ? `No wave context. ${error}` : 'No wave context: the RIFF CLI did not answer.'
141 if (!context.phase) return 'No active or ready phase.'
142 const { phase, checkpoint } = context
143 const lines = [`${phase.id} (${phase.status}): ${phase.title ?? ''}`.trimEnd()]
144 if (phase.outcome) lines.push(`Outcome: ${phase.outcome}`)
145 if (checkpoint) {
146 lines.push(`Checkpoint${context.checkpointStale ? ' (stale: the code changed since)' : ''}: ${checkpoint.summary ?? ''}`)
147 if (checkpoint.next) lines.push(`Next: ${checkpoint.next}`)
148 } else lines.push('No checkpoint yet.')
149 if (context.references.length) lines.push(`References: ${context.references.join(', ')}`)
150 return lines.join('\n')
151}
152
153function mark(status: string): string {
154 return { completed: '✓', skipped: '–', active: '▶', blocked: '!', parked: '‖', awaiting_human: '?' }[status] ?? '·'
155}
156