@davvidbaker/flambe-cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @davvidbaker/flambe-cli
2
2
 
3
- Zero-dependency Node 22 CLI that streams coding-agent work into a Flambe trace.
3
+ Node 22 CLI that streams coding-agent work into a Flambe trace.
4
4
 
5
5
  ## Install
6
6
 
@@ -61,3 +61,69 @@ FLAMBE_TRACE_ID=1
61
61
  Mint a token in Flambe Settings → API tokens. See the
62
62
  [repository README](https://github.com/davvidbaker/flambe#connect-coding-agents)
63
63
  for commands (`start`, `end`, `message`, `observe`, local `serve`).
64
+
65
+ ## Single Burner AI review
66
+
67
+ When running `flambe serve`, optional AI review is configured in that server
68
+ process with `FLAMBE_LOCAL_REDUCER_MODE=off|agent|api|local`. It is off by default;
69
+ provider failures never reroute work to Cloud. See the
70
+ [Single Burner configuration guide](../single-burner/README.md#optional-ai-reducer)
71
+ for model, endpoint, credential, timeout, and data-boundary settings. Client
72
+ processes only need their normal local Flambe connection settings.
73
+
74
+ In `agent` mode, your current work AI reads the review returned by `flambe message`
75
+ and submits its decision with `flambe decide <review-id> --file <decision.json>`.
76
+ No separate model or inference API key is required.
77
+
78
+ ## Local Codex / Single Burner hooks
79
+
80
+ With Single Burner running, put its printed connection values in the target
81
+ project's `.env`, including `FLAMBE_RUNTIME_MODE=local_self_contained`. From
82
+ that project root run:
83
+
84
+ ```sh
85
+ flambe setup codex
86
+ ```
87
+
88
+ This installs project-scoped Codex hooks and the bundled `flambe-cli` skill.
89
+ It preserves unrelated hooks, backs up an existing `hooks.json` once, and keeps
90
+ an existing custom skill rather than overwriting it. Restart local Codex, open
91
+ `/hooks`, and review/trust the new definitions. Hook loading remains subject to
92
+ project trust and workspace policy. No ChatGPT plugin, Platform organization,
93
+ tunnel, or model API key is needed for this local route.
94
+
95
+ Keep `flambe` available on PATH to the Codex process. Hooks use `flambe.cmd` on
96
+ Windows and `flambe` on macOS/Linux, with no machine-specific checkout paths.
97
+ If you installed the CLI while Codex was already open, restart the app so it can
98
+ see the updated PATH.
99
+
100
+ - Session start, resume, prompts, subagent start, and compaction restore context.
101
+ - Tool hooks use a small cache, refresh local status at most once per 30 seconds
102
+ during ordinary work, and request a fresh stack check every ten tool calls or
103
+ 90 seconds while tools continue. Explicit lifecycle checkpoints reset the
104
+ call counter; a timer does not run while the agent is idle.
105
+ - Stop requests at most one reconciliation continuation after substantial tool
106
+ work without a verified checkpoint. It does not automatically close parents.
107
+ - Interrupt records a pending local reconciliation reminder; it does not mutate
108
+ activities or prevent interruption.
109
+
110
+ The agent still names/nests activities, decides when work changed, and submits
111
+ requested reducer decisions. Hooks only send status requests to the configured
112
+ numeric-loopback server. They do not call a model, inspect transcripts, or
113
+ queue activity mutations. Missing configuration, an unavailable server, or an
114
+ unidentifiable agent produces bounded feedback and permits work to continue.
115
+ The local cache contains counters/fingerprints, not prompts, command text,
116
+ credentials, or activity names. See [ADR-023](../docs/ADR-023-portable-local-codex-hooks.md).
117
+
118
+ ```sh
119
+ flambe setup codex --check
120
+ flambe setup codex --remove
121
+ ```
122
+
123
+ `--check` inspects installed files and local connection configuration; it cannot
124
+ confirm a live server, Codex trust, or workspace policy.
125
+ `--remove` retains skills and unrelated hooks. For a live check, restart Codex,
126
+ confirm these hooks are trusted in `/hooks`, then ask it to track a short local
127
+ activity, request/submit an agent-mode review, and end or suspend the activity.
128
+ Verify that the chart and that agent's lane reflect the result. A fresh install
129
+ on the work laptop still needs this native Codex verification.
package/bin/flambe.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  import { run } from '../src/cli.mjs';
4
4
  import { loadProjectEnv } from '../src/env.mjs';
5
5
 
6
- loadProjectEnv();
6
+ // Hook input selects its installed project root; do not preload a nested .env.
7
+ if (process.argv[2] !== 'hook') loadProjectEnv();
7
8
 
8
9
  try {
9
10
  await run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davvidbaker/flambe-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI for streaming coding-agent activity into Flambe",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,11 +9,14 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "src",
12
- "README.md"
12
+ "README.md",
13
+ "skills",
14
+ "scripts/prepare-skills.mjs"
13
15
  ],
14
16
  "scripts": {
15
17
  "test": "node --test",
16
- "prepublishOnly": "npm test"
18
+ "prepublishOnly": "npm test",
19
+ "prepack": "node scripts/prepare-skills.mjs"
17
20
  },
18
21
  "engines": {
19
22
  "node": ">=22"
@@ -30,5 +33,9 @@
30
33
  },
31
34
  "publishConfig": {
32
35
  "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/node": "2.1.1",
39
+ "@modelcontextprotocol/server": "2.3.1"
33
40
  }
34
41
  }
@@ -0,0 +1,12 @@
1
+ import {copyFileSync, existsSync, mkdirSync} from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {dirname} from 'node:path';
4
+
5
+ const source = fileURLToPath(new URL('../../.codex/skills/flambe-cli/SKILL.md', import.meta.url));
6
+ const target = fileURLToPath(new URL('../skills/flambe-cli/SKILL.md', import.meta.url));
7
+ if (existsSync(source)) {
8
+ mkdirSync(dirname(target), {recursive: true});
9
+ copyFileSync(source, target);
10
+ } else if (!existsSync(target)) {
11
+ throw new Error('The CLI package is missing its canonical flambe-cli skill.');
12
+ }
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: flambe-cli
3
+ description: Track coding-agent work in a Flambe trace with the flambe CLI. Use when logging substantial or multi-step work in a project configured with FLAMBE_URL, FLAMBE_API_TOKEN, and FLAMBE_TRACE_ID.
4
+ ---
5
+
6
+ # Flambe CLI
7
+
8
+ Log semantic work units as a live flame chart — not commands, file opens, or hidden reasoning. Skip trivial questions. If `FLAMBE_URL`, `FLAMBE_API_TOKEN`, or `FLAMBE_TRACE_ID` is set neither in `.env` nor in the shell environment (Cloud Agent secrets arrive as environment variables), skip silently.
9
+
10
+ Needs Node 22+ and `flambe` on `PATH`. If it is missing, `npm install -g @davvidbaker/flambe-cli`; in this repo without a global install, use `node cli/bin/flambe.mjs`. You do not need to pick a name: the reducer names each agent on its first request, the CLI remembers it (`~/.flambe/agent-names.json`) and prints `reducer named this agent "X"` on stderr once. `flambe whoami` shows your id, name, and platform. `FLAMBE_AGENT_PLATFORM` is the product you run on (`"Cursor Cloud"`, `"Codex"`; derived from the session when unset) and is shared by many agents; it is not your name. Set `FLAMBE_AGENT_NAME` only to override; never put `FLAMBE_AGENT_ID`, `FLAMBE_AGENT_NAME`, or `FLAMBE_AGENT_PLATFORM` in `.env`.
11
+
12
+ ## Think in a stack
13
+
14
+ Always treat attention as a **call stack**, not a flat to-do list.
15
+
16
+ You will often produce some progress on a parent goal, then go down a rabbit hole about one specific thing (a bug, a prerequisite, a design question, a test failure). That digression is a **push**: start a nested activity under the still-running parent. When the rabbit hole is done, **pop**: end the nested activity with the outcome, and continue the parent where you left off.
17
+
18
+ - Leave parents running while you dig. Ending the parent when you dive deeper flattens the chart and loses the “come back up” story.
19
+ - Nested steps belong under the workstream they serve — not as siblings of the top objective.
20
+ - When attention moves to a true sibling of the current leaf, end the leaf first, then start the sibling under the same parent.
21
+ - Use `--root` only for a genuinely new top-level workstream, not for every digression.
22
+ - Keep the active stack truthful as work changes. Do not plan a detailed chart up front or reconstruct one afterward.
23
+
24
+ ## Local Codex hooks
25
+
26
+ For a Single Burner project, run `flambe setup codex` from its root after setting
27
+ its local connection values in `.env`. Setup merges project hooks into
28
+ `.codex/hooks.json` and installs this skill if it is missing. Existing custom
29
+ skills and unrelated hooks are retained. Restart local Codex and review/trust
30
+ these hooks with `/hooks`; installed files alone do not prove they are running.
31
+ Use `flambe setup codex --check` to inspect the files, and `--remove` to remove
32
+ only Flambe's own hook handlers. Keep `flambe` on PATH; Windows hooks use
33
+ `flambe.cmd` so PowerShell script policy does not select `flambe.ps1`.
34
+
35
+ Hooks restore workflow context, request periodic stack checks, and can request
36
+ one reconciliation continuation before a substantive turn ends. They make no
37
+ inference calls or semantic activity changes. You still start/nest/end the real
38
+ work and complete requested work-AI reviews. A turn ending does not mean its
39
+ parent task ended. Missing servers or identity produce feedback without blocking
40
+ ordinary work. Hooks require `local_self_contained`; they do not route to Cloud.
41
+
42
+ ## Commands
43
+
44
+ ```sh
45
+ flambe start "Fix USERPROFILE resolution" # prints id; nests under the current active activity
46
+ flambe start "Add Windows support" --root # new top-level workstream
47
+ flambe end "$ID" "Resolved USERPROFILE on Windows"
48
+ flambe suspend "$ID" "Waiting for product decision" # only when explicitly tabling work
49
+ flambe resume "$ID" "Decision received"
50
+ flambe observe carbon 312.4 --unit gCO2eq/kWh --on 2026-09-04 --payload '{"source":"us-ba-mean"}'
51
+ flambe message "Auth fix needs the session module refactored too; widening scope" # ask the Reducer Agent
52
+ ```
53
+
54
+ `observe` writes a user overlay (carbon, mood, …), not an activity. Same `kind` + `--on` date upserts and merges payload keys. Omit `--on` to always insert.
55
+
56
+ **255-character hard limit on activity names and `--description`.** The column is `varchar(255)`. A longer value returns a 500 and can wedge the offline queue. Count before `start`. Prefer a short concrete name; put detail in the end/suspend/resume message (those are unbounded).
57
+
58
+ Reuse this conversation's activity id when you already have one. Run `flambe status --active --json` only to find a matching open activity from this session; ignore unrelated active work. Do not fetch threads or categories to decide placement; that is the reducer's job.
59
+
60
+ Every command is a proposal; the reducer owns the stack. Without `--parent`/`--root`, `start` nests under **your own** newest active activity (other agents' work is never your parent), puts a child on its parent's thread, and gives it the parent's categories; a new root gets its thread and categories chosen by the reducer a few seconds later. `FLAMBE_THREAD` (name, unique slug, or id) is sent as `--thread` when that flag is omitted; do not list threads and pick one by project name. Pass `--thread` / `--parent` / `--category` only when you are certain. Offline `offline-…` ids from `start` are valid for `end`. Do not retry queued operations. Offline results carry no immediate reducer output; nothing is dropped. Local `serve` always applies deterministic rules, including duplicate reuse, ancestor resumption, and descendant closure. Optional review is off by default; an explicitly configured server can ask the current work AI to review or call a separate model.
61
+
62
+ ## Read every result; the reducer may rewrite
63
+
64
+ Any command (`start`, `end`, `suspend`, `resume`, `message`) may come back with `direction`, `reply`, and `actions_applied`, not only `message`. The reducer records what you proposed and then corrects the structure around it when needed: re-parent, rename, treat your `start` as a resume of an already-open same-name activity, resume a suspended ancestor before nesting under it, or close open descendants when you end a parent (`--force`). The CLI prints each rewrite on stderr (`reducer nested 43 under 42`, `actions`). The returned state is the truth: keep using the ids it gives you, do not undo a rewrite, and do not re-issue the proposal.
65
+
66
+ The reducer may answer a proposal with a question in `reply`, usually when a name does not fit its ancestors. Answer before your next step: rename the activity, or reply with `flambe message`. The CLI reminds you on the next command if a question is still open; it never refuses.
67
+
68
+ A returned `direction` is binding: act on it before doing more work. On `pause`, `stop`, or `escalate`, stop and surface the reply to David rather than deciding yourself. `direction -` or none means continue as you were. A model-judged `start` can take a few seconds; wait for it.
69
+
70
+ ## Review with the AI doing the work
71
+
72
+ Review only at meaningful scope/approach decisions or when placement is uncertain.
73
+ Routine lifecycle tracking uses deterministic local rules. Do not review every
74
+ start/end, and do not request full history for routine bookkeeping. Work-AI
75
+ review context contains the current path and this agent's open work. Compact
76
+ lifecycle replies mark their state scope; omitted activities are not ended.
77
+
78
+ When a local result contains `review` and `model_review.status: awaiting_decision`,
79
+ you are the reviewer. This is the current work AI evaluating its own context,
80
+ not independent approval. Do not start another model, ask for provider keys, or
81
+ claim a separate reviewer approved your work.
82
+
83
+ Read `review.instructions`, `review.context`, and `review.decision_schema`.
84
+ Evaluate the user's intent, current path, and proposed scope. Choose concise
85
+ assessment, direction, reply, rationale, and at most one permitted action.
86
+ Write that decision as JSON and submit it with:
87
+
88
+ ```sh
89
+ flambe decide <review.review_id> --file <decision.json>
90
+ ```
91
+
92
+ `flambe message` returns the review JSON in this mode. Flagged starts normally
93
+ print only a small review-available notice on stderr; request review only when
94
+ scope or placement needs judgment. They include full review JSON only when the
95
+ server explicitly enables automatic start reviews. Stdout remains the recorded id. Keep the same local
96
+ connection, trace, and stable agent id for both steps. Ordinary
97
+ lifecycle tracking continues to use start/end/suspend/resume.
98
+
99
+ Complete a requested review before continuing substantive work. Follow the
100
+ returned direction after submission; escalate unresolved human judgment with no
101
+ actions and ask the user. Do not repeat the recorded start to request review.
102
+ On `REDUCER_STATE_CHANGED` or `REVIEW_EXPIRED`, request a fresh review with
103
+ `flambe message` for that activity, reassess it, and submit against the new id.
104
+ Do not resubmit the old decision blindly. Identical retries of a completed
105
+ submission return a receipt; `replayed: true` means no new action was applied.
106
+ If the server lacks work-AI review support, report that instead of changing instances.
107
+
108
+ ## End of work
109
+
110
+ When the work you were doing is finished, check limbo and scheduled activities before you stop. Run `flambe status --unstarted --json`. If something there is worth doing next, propose that candidate with `flambe message` and begin it only if the reply says continue. Do not begin it unasked, and do not ask David in chat.
111
+
112
+ ## Ask the reducer before you drift
113
+
114
+ `flambe message "<update>"` is your own channel: it sends your update plus the live flame (open and suspended work, the path to the current activity, and ended work from the last 21 days) and answers with `assessment`, `direction`, `reply`, and at most one stack change. Message when you are about to:
115
+
116
+ - widen scope beyond the activity you started
117
+ - change approach after the plan stalled
118
+ - end a root workstream
119
+ - continue while suspecting the work has drifted from what David asked
120
+ - answer a question the reducer asked
121
+
122
+ Do not message for routine progress, and never instead of `start`/`end`. Defaults to your newest active activity; pass `--activity <id>` to target another. Needs a reachable server with model review configured (not queued). Local self-contained `serve` has optional review, off by default, including an `agent` mode that returns review context for you to evaluate. On `REDUCER_NOT_CONFIGURED`, do not retry or switch to a remote server; surface unresolved judgment to the user and continue deterministic logging. A lifecycle result with `model_review.status: failed` still recorded the activity: keep its returned id/state, surface the review failure, and do not repeat the start or retarget another instance.
123
+
124
+ ## What to record
125
+
126
+ - Start a new activity when the goal or scope meaningfully changes (a specific implementation, a discovered prerequisite, a concrete bug, focused tests, verification) — that is a stack push.
127
+ - Do not start an activity for committing or pushing. Those are wrap-up of the current leaf; mention them in the end-message if useful. Exception: the assigned work *is* getting code onto a remote.
128
+ - Name the concrete action and object (`Fix USERPROFILE config resolution`), not `Investigate` / `Implement` / `Work on tests`. Stay well under 255 characters; do not compensate with a long name or `--description`.
129
+ - End-message: the outcome, not `done`. That records a resolution. Omitting the message is a plain end. No secrets, tokens, or transcripts. Ending the leaf is the pop; then resume the parent mentally and in the log.
package/src/cli.mjs CHANGED
@@ -27,6 +27,8 @@ const usage = `Usage:
27
27
  flambe ping
28
28
  flambe whoami [--json]
29
29
  flambe message <text> [--activity <id>] [--json]
30
+ flambe decide <review-id> --file <decision.json>
31
+ flambe setup codex [--check | --remove]
30
32
  flambe observe <kind> <value> [--unit <unit>] [--on <YYYY-MM-DD>] [--payload <json>] [--at <ISO-8601>]
31
33
  flambe serve [--port <n>] [--db <path>] [--static <dir>]
32
34
  flambe export [file] [--db <path>]
@@ -55,6 +57,7 @@ Optional:
55
57
  FLAMBE_QUEUE_PATH Local offline queue path (default: ~/.flambe/event-queue.json)
56
58
  FLAMBE_LOCAL_DB SQLite path for flambe serve / export (default: ~/.flambe/local.sqlite)
57
59
  FLAMBE_RUNTIME_MODE local_self_contained restricts CLI requests to numeric loopback; cloud preserves hosted access
60
+ FLAMBE_LOCAL_REDUCER_MODE off (default), agent, api, or local; see the Single Burner README
58
61
 
59
62
  serve / export do not need FLAMBE_URL. import needs FLAMBE_URL and FLAMBE_API_TOKEN (the production instance).`;
60
63
 
@@ -336,6 +339,11 @@ function formatReducerNote(note) {
336
339
  const lines = [];
337
340
  if (note.direction) lines.push(`direction\t${note.direction}\n`);
338
341
  if (note.reply) lines.push(`reply\t${note.reply}\n`);
342
+ if (note.modelReview?.status === 'available') lines.push(`review available\tRequest flambe message --activity ${note.activityId} when scope judgment is needed.\n`);
343
+ if (note.review) lines.push(`review\t${JSON.stringify(note.review)}\n`);
344
+ if (note.modelReview?.status === 'failed') {
345
+ lines.push(`model review\t${note.modelReview.code}: ${note.modelReview.message} Recorded work was preserved.\n`);
346
+ }
339
347
  if (note.actionsApplied?.length) {
340
348
  lines.push(`actions\t${note.actionsApplied.map(formatAction).join('; ')}\n`);
341
349
  }
@@ -353,9 +361,43 @@ function formatReducerNote(note) {
353
361
  return `reducer: ${JSON.stringify(note)}\n`;
354
362
  }
355
363
 
356
- export async function run(argv, { env = process.env, stdout = process.stdout, stderr = process.stderr, client, questionsPath } = {}) {
364
+ export async function run(argv, { env = process.env, stdout = process.stdout, stderr = process.stderr, stdin = process.stdin, client, questionsPath } = {}) {
357
365
  const [command, ...args] = argv;
358
366
 
367
+ if (command === 'hook' && args.length === 1 && args[0] === 'codex') {
368
+ const {readHookInput, runCodexHook} = await import('./codex-hook.mjs');
369
+ let output;
370
+ try { output = await runCodexHook(await readHookInput(stdin), {env}); }
371
+ catch { output = {systemMessage: 'Single Burner hook input could not be read; work may continue.'}; }
372
+ stdout.write(`${JSON.stringify(output)}\n`);
373
+ return;
374
+ }
375
+
376
+ if (command === 'setup') {
377
+ if (args[0] !== 'codex' || args.length > 2
378
+ || (args[1] !== undefined && !['--check', '--remove'].includes(args[1]))) {
379
+ throw new Error('Usage: flambe setup codex [--check | --remove]');
380
+ }
381
+ const {setupCodex} = await import('./codex-setup.mjs');
382
+ const action = args[1] === '--check' ? 'check' : args[1] === '--remove' ? 'remove' : 'install';
383
+ const result = setupCodex({action});
384
+ let localConnectionConfigured = false;
385
+ try {
386
+ localConnectionConfigured = env.FLAMBE_RUNTIME_MODE === 'local_self_contained' && Boolean(clientFromEnv(env));
387
+ } catch { /* Only report whether the config is usable; never echo credentials. */ }
388
+ if (action === 'check') stdout.write(`${JSON.stringify({...result, localConnectionConfigured})}\n`);
389
+ else if (action === 'remove') stdout.write(`Single Burner Codex hooks ${result.removed ? 'removed' : 'were not installed'}. Existing skills and unrelated hooks were retained.\n`);
390
+ else {
391
+ stdout.write(`Single Burner Codex hooks installed in ${result.root}\n`);
392
+ stdout.write(result.skillInstalled ? 'Installed the flambe-cli skill.\n' : 'Kept the existing flambe-cli skill.\n');
393
+ if (!result.skillMatches) stdout.write('The existing skill differs from the bundled version; review it for current work-AI review instructions.\n');
394
+ if (!localConnectionConfigured) stdout.write('Copy Single Burner connection values into this project .env, including FLAMBE_RUNTIME_MODE=local_self_contained, before using the hooks.\n');
395
+ stdout.write('Restart the local Codex session. Review and trust these project hooks with /hooks before relying on them. Workspace policy can restrict hooks.\n');
396
+ stdout.write('Run flambe setup codex --check to inspect installed files and connection configuration. This does not confirm Codex trust or live hook execution.\n');
397
+ }
398
+ return;
399
+ }
400
+
359
401
  if (!command || command === 'help' || command === '--help' || command === '-h') {
360
402
  stdout.write(`${usage}\n`);
361
403
  return;
@@ -364,7 +406,7 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
364
406
  if (command === 'serve') {
365
407
  const options = parseServe(args, env);
366
408
  const { listenLocal, printServeBanner } = await loadServe();
367
- const listening = await listenLocal(options);
409
+ const listening = await listenLocal({ ...options, reducer: { env } });
368
410
  printServeBanner(listening, stdout);
369
411
  await new Promise((resolveClose, reject) => {
370
412
  listening.server.on('close', resolveClose);
@@ -518,11 +560,24 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
518
560
  return;
519
561
  }
520
562
 
563
+ if (command === 'decide') {
564
+ const { value: file, rest } = takeOption(args, '--file');
565
+ if (!file || rest.length !== 1 || rest[0].startsWith('--')) {
566
+ throw new Error('Usage: flambe decide <review-id> --file <decision.json>');
567
+ }
568
+ const content = readFileSync(resolve(file), 'utf8');
569
+ if (Buffer.byteLength(content) > 65_536) throw new Error('Decision file exceeds the size limit');
570
+ const result = await flambe.decide({ reviewId: rest[0], decision: JSON.parse(content) });
571
+ if (persistQuestions && !(result.actions_applied ?? []).some(a => a.type === 'ask')) clearQuestion(traceId, questionFile);
572
+ stdout.write(`${JSON.stringify(result)}\n`);
573
+ return;
574
+ }
575
+
521
576
  if (command === 'message') {
522
577
  const { text, activityId, json } = parseMessage(args);
523
578
  const decision = await flambe.message({ activityId, text });
524
- if (persistQuestions) clearQuestion(traceId, questionFile);
525
- if (json) {
579
+ if (persistQuestions && !decision.review) clearQuestion(traceId, questionFile);
580
+ if (json || decision.review) {
526
581
  stdout.write(`${JSON.stringify(decision)}\n`);
527
582
  } else {
528
583
  stdout.write(`activity\t${decision.activityId}\n`);
package/src/client.mjs CHANGED
@@ -155,6 +155,7 @@ export class FlambeClient {
155
155
  || REQUIRED_AGENT_COMMANDS.some(command => !commands.includes(command))) {
156
156
  throw new Error('Flambe API returned an invalid agent-command capability document');
157
157
  }
158
+ this.agentCommandDetails = payload.data;
158
159
  return true;
159
160
  })
160
161
  .catch(error => {
@@ -636,13 +637,15 @@ export class FlambeClient {
636
637
  const actions = result.actions_applied ?? [];
637
638
  const direction = result.direction ?? null;
638
639
  const reply = result.reply ?? null;
639
- if (!direction && !reply && actions.length === 0) return;
640
+ if (!direction && !reply && actions.length === 0 && !result.model_review) return;
640
641
  this.onReducerNote?.({
641
642
  type: 'command_result',
642
643
  activityId: result.activity_id,
643
644
  direction,
644
645
  reply,
645
646
  actionsApplied: actions,
647
+ ...(result.model_review ? { modelReview: result.model_review } : {}),
648
+ ...(result.review ? { review: result.review } : {}),
646
649
  asked: actions.some(action => action.type === 'ask'),
647
650
  });
648
651
  }
@@ -699,6 +702,16 @@ export class FlambeClient {
699
702
  return this.request('/api/imports', { method: 'POST', body: bundle });
700
703
  }
701
704
 
705
+ /** Work-AI decisions are never queued or sent to a substitute connection. */
706
+ async decide({ reviewId, decision }) {
707
+ if (!(await this.supportsAgentCommands()) || !this.agentCommandDetails?.reducer?.work_ai_reviews) {
708
+ throw new Error('This connection does not support work-AI reviews. Configure agent reducer mode on Single Burner.');
709
+ }
710
+ const result = await this.agentCommand('decide', { ...this.commandIdentity(), review_id: reviewId, decision });
711
+ this.reportCommandResult(result);
712
+ return result;
713
+ }
714
+
702
715
  /**
703
716
  * Send a free-text update to the Reducer Agent. It answers with assessment /
704
717
  * direction / reply and, as the single writer for the stack (ADR-011), may apply at
@@ -726,7 +739,7 @@ export class FlambeClient {
726
739
  message: text.trim(),
727
740
  });
728
741
  const decision = { activityId: result.activity_id ?? resolvedActivityId, ...result };
729
- this.reportCommandResult(result);
742
+ if (!result.review) this.reportCommandResult(result);
730
743
  return decision;
731
744
  }
732
745