openfeel 1.2.4 → 1.3.1

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.
Files changed (51) hide show
  1. package/README.en.md +40 -74
  2. package/README.md +39 -62
  3. package/README.zh-CN.md +40 -74
  4. package/dist/commands/setup.d.ts.map +1 -1
  5. package/dist/commands/setup.js +90 -5
  6. package/dist/commands/setup.js.map +1 -1
  7. package/dist/commands/update.d.ts.map +1 -1
  8. package/dist/commands/update.js +7 -2
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/core/config.js +2 -2
  11. package/dist/core/dsh-profile.d.ts +93 -0
  12. package/dist/core/dsh-profile.d.ts.map +1 -0
  13. package/dist/core/dsh-profile.js +400 -0
  14. package/dist/core/dsh-profile.js.map +1 -0
  15. package/dist/core/i18n-data/en.d.ts.map +1 -1
  16. package/dist/core/i18n-data/en.js +21 -2
  17. package/dist/core/i18n-data/en.js.map +1 -1
  18. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  19. package/dist/core/i18n-data/zh-CN.js +21 -2
  20. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  21. package/dist/core/setup.d.ts +12 -1
  22. package/dist/core/setup.d.ts.map +1 -1
  23. package/dist/core/setup.js +25 -10
  24. package/dist/core/setup.js.map +1 -1
  25. package/dist/core/template-loader.d.ts.map +1 -1
  26. package/dist/core/template-loader.js +18 -9
  27. package/dist/core/template-loader.js.map +1 -1
  28. package/dist/core/update.d.ts +2 -0
  29. package/dist/core/update.d.ts.map +1 -1
  30. package/dist/core/update.js +25 -7
  31. package/dist/core/update.js.map +1 -1
  32. package/dsh/ACTION-GUIDE.md +366 -0
  33. package/dsh/ADAPTER.md +65 -0
  34. package/dsh/README.md +264 -0
  35. package/dsh/cordis.patch.yml +26 -0
  36. package/dsh/docs/compat/commands.md +873 -0
  37. package/dsh/docs/compat/kb-skills.md +883 -0
  38. package/dsh/docs/compat/persistence.md +1013 -0
  39. package/dsh/docs/compat/pipeline.md +1565 -0
  40. package/dsh/docs/compat/req.md +1243 -0
  41. package/dsh/docs/compat/stage.md +1437 -0
  42. package/dsh/lib/adapters/cli.js +624 -0
  43. package/dsh/lib/adapters/dashboard.js +393 -0
  44. package/dsh/lib/adapters/workspace.js +150 -0
  45. package/dsh/lib/index.js +189 -0
  46. package/dsh/lib/surface/command.js +186 -0
  47. package/dsh/lib/surface/prompt.js +131 -0
  48. package/dsh/lib/tools/index.js +1245 -0
  49. package/dsh/lib/tools/shared.js +320 -0
  50. package/dsh/package.json +45 -0
  51. package/package.json +9 -2
@@ -0,0 +1,873 @@
1
+ # OpenFeel CLI — Command Surface & Logging/Observability Contract
2
+
3
+ Behavioral specification for a byte-faithful reimplementation.
4
+
5
+ - **Source of truth**: `OpenFeel` @ `package.json` version **1.2.1**, TypeScript, ESM, Node `>=20.17.0`.
6
+ - **Analyzed files**: `src/cli/index.ts`, `src/cli/repl.ts`, `src/cli/output-encoding.ts`, `src/cli/deploy-check-output.ts`, `bin/openfeel.js`, all 17 `src/commands/*.ts` registrars, `src/commands/shared/errors.ts`, `src/core/{public-logger,runtime-log,session,metrics,pipeline-schema,config,deployment-check,global-paths}.ts`, `src/core/fs/file-lock.ts`, `src/core/view/entry.ts`, `src/core/req/*`, `src/core/plan/*`, `src/core/archive/merge.ts`, `src/core/flow-archive/*`, `src/core/workspace/knowledge.ts`, `src/core/model-config.ts`.
7
+ - **Notation**: `<arg>` = required positional; `[arg]` = optional positional; `--flag <v>` = required value; `[boolean]` = optional-value flag; defaults are as declared to commander.
8
+ - **All help text / output prose is i18n**: every user-visible line is `t('<key>', lang, vars)` resolved from `src/core/i18n-data/{zh-CN,en}.ts`; `{var}` placeholders are substituted. English strings quoted below are `en` values. A reimplementation must keep the **key → line** mapping if it wants identical output. Log lines (§7) and machine JSON (§5) are **not** i18n and are frozen.
9
+
10
+ ---
11
+
12
+ ## 1. Process model & entry points
13
+
14
+ `bin/openfeel.js` is the single throat. Startup order is fixed:
15
+
16
+ ```
17
+ installOutputEncoding() // wrap stdout/stderr encoders (no-op unless target != utf8)
18
+ installRuntimeLog() // read --no-log/--log-file/--debug/OPENFEEL_* → module config
19
+ installExitHooks() // exit / SIGINT / SIGTERM → `cli done` line
20
+ applyHelpI18n(program) // rewrite description()/option descriptions/arg descriptions from i18n
21
+ args.length === 0 ? startRepl(program) : runCli()
22
+ ```
23
+
24
+ `runCli()`:
25
+
26
+ ```
27
+ logCliStart(process.argv.slice(2))
28
+ emitGlobalDeployCheck() // passive stale-deployment hint to stderr (gated, silent-on-error)
29
+ try { program.parse() } catch (err) { handleCliError(err) }
30
+ logCliDone(process.exitCode ?? 0)
31
+ ```
32
+
33
+ `startRepl(program)`:
34
+
35
+ - `program.exitOverride()`; `readline` prompt `repl.prompt`; prints `repl.welcome`; `emitGlobalDeployCheck()` (once per process); `rl.prompt()`.
36
+ - Empty line → re-prompt. `exit` / `quit` → print `repl.bye`, close, `process.exit(0)`.
37
+ - `help` / `?` → print `repl.helpTitle` then ` <name> <description>` for each **top-level** command.
38
+ - Any other line is split with `/\s+/` and dispatched as `program.parse(['node','openfeel', ...args])`.
39
+ - During dispatch `process.exit` is temporarily replaced by a throwing `ReplExitSignal`, so command-level `exit(1)`/`exit(2)` **does not kill the REPL**. `ReplExitSignal` is swallowed; `commander.helpDisplayed` / `commander.help` / `commander.version` are swallowed; any other error prints `repl.errorPrefix + message` to stderr.
40
+ - `process.exit` is restored in `finally`; `rl.on('close')` → `process.exit(0)`.
41
+ - **Known limitation**: REPL input is whitespace-split — no shell-like quoting, so `--title "a b"` arrives as two tokens.
42
+
43
+ `shouldUseColor(options)`: `options.color === false` (from `--no-color`) → false; else `process.env.NO_COLOR` present **and non-empty** → false; else true. **No ANSI coloring is implemented anywhere**; the function is a contract for future use only. Do not emit color.
44
+
45
+ ---
46
+
47
+ ## 2. Global options (root program `openfeel`)
48
+
49
+ | Flag | Value domain | Default | Meaning |
50
+ |---|---|---|---|
51
+ | `-v, --version` | — | — | Print `pkg.version`, exit (commander). |
52
+ | `--no-color` | negate → `color=false` | color enabled | Disables color; contract-only today. |
53
+ | `--encoding <encoding>` | `auto\|utf8\|utf-8\|gbk\|gb2312\|gb18030\|big5\|shift_jis\|sjis\|cp932\|euc-kr\|cp949` | `auto` | Display-only for `--help`; **actual resolution re-reads argv/env** (see below). |
54
+ | `--log-file <path>` | filesystem path | `~/.openfeel/cli/logs/openfeel-YYYY-MM-DD.log` | Override runtime-log destination. |
55
+ | `--no-log` | negate | logs enabled | Disable runtime log. |
56
+ | `--debug` | flag | off | Raise runtime-log min level to `debug`. |
57
+
58
+ Root description: `AI Agent 开发流程治理 CLI 工具`.
59
+
60
+ **Encoding resolution** (`resolveTargetEncoding`, pure function, first match wins):
61
+
62
+ 1. argv contains `--json` → `utf8` (machine contract; overrides everything).
63
+ 2. explicit `--encoding <v>` / `--encoding=<v>` → alias-mapped value.
64
+ 3. `OPENFEEL_ENCODING` env → alias-mapped value.
65
+ 4. platform !== `win32` **or** stream `isTTY` → `utf8`.
66
+ 5. win32 non-TTY → `utf8` (changed from legacy GBK; GBK only via 2/3).
67
+
68
+ Only `target !== 'utf8'` installs a wrapper; wrapper transcodes **string** chunks via `iconv-lite` (unencodable → `?`, no warning), passes `Buffer` through unchanged, discards caller-supplied encoding (UTF-8 string semantics only). Install is idempotent.
69
+
70
+ ---
71
+
72
+ ## 3. Complete command tree
73
+
74
+ **17 registrars, 80 leaf subcommands** (leaf = a fully-qualified argv path that dispatches an action).
75
+
76
+ Registration order in `src/cli/index.ts` (affects `--help` ordering): `init, flow, plan, view, archive, roadmap, instructions, update, knowledge, stage, project, config, lint, migrate, model, setup, req`.
77
+
78
+ ### 3.1 `flow` — pipeline state (`src/commands/flow.ts`) — 29 leaves
79
+
80
+ | Command | Positional args | Flags (domain / default) | JSON |
81
+ |---|---|---|---|
82
+ | `flow status` | — | `--verbose`; `-n, --lines <n>` (default `'5'`, parsed `max(1, parseInt‖5)`); `--json` | ✅ |
83
+ | `flow overview` | — | `--json` | ✅ |
84
+ | `flow current` | — | `--json` | ✅ |
85
+ | `flow metrics` | — | `--json` | ✅ |
86
+ | `flow phases` | — | `--json`; `--mode <full\|fast\|general>`; `--stage <id>` | ✅ |
87
+ | `flow mode` | — | `--stage <id>`; `--json` | ✅ |
88
+ | `flow stage add` | `<stageId>` | — | — |
89
+ | `flow stage remove` | `<stageId>` | `--force`; `--dry-run`; `--purge` | — |
90
+ | `flow stage set` | `<stageId>` | `--deps <ids...>` (space or comma separated; absent = clear) | — |
91
+ | `flow stage reset` | `<stageId>` | **`--to <phase>` (required)**; `--dry-run` | — |
92
+ | `flow advance` | — | `--op <id>`; **`--to <phase>` (required)**; `--stage <id>` (required at runtime); `--force`; `--dry-run`; `--review-tier <full\|lean\|skip>`; `--quiet` | — |
93
+ | `flow attempt` | — | **`--op <id>` (req)**; **`--result <pass\|fail>` (req)** | — |
94
+ | `flow ops list` | — | `--stage <id>`; `--json` | ✅ |
95
+ | `flow log` | — | `--last <n>` (default `'10'`, parsed `max(1, parseInt‖10)`) | — |
96
+ | `flow review add` | — | **`--op <id>` (req)**; `--title <title>`; `--auto-fix <detail>`; `--blocking [boolean]` (default `'true'`; `'false'`/`false` → false) | — |
97
+ | `flow review resolve` | `<rev-id>` | — | — |
98
+ | `flow review update` | `<rev-id>` | `--priority <high\|medium\|low>`; `--title <text>`; `--blocking <true\|false>` (≥1 required) | — |
99
+ | `flow review remove` | `<rev-id>` | — | — |
100
+ | `flow retry` | — | **`--op <id>` (req)** | — |
101
+ | `flow repair` | — | `--dry-run`; `--backup`; `--prune-orphans` | — |
102
+ | `flow migrate` | — | `--dry-run`; `--no-backup` (commander: `backup=false`) | — |
103
+ | `flow checkpoint list` | `[stage]` | — | — |
104
+ | `flow checkpoint restore` | `<checkpoint-file>` | `--force` (mandatory unless `--dry-run`); `--stage <id>`; `--dry-run` | — |
105
+ | `flow archive` | `<version>` (prefix `v` optional) | `--dry-run`; `--json` | ✅ |
106
+ | `flow archive list` | — | `--json` | ✅ |
107
+ | `flow archive show` | `<version>` | `--json` | ✅ |
108
+ | `flow health` | — | `--quick`; `--json`; `--fix`; `--dry-run` | ✅ |
109
+ | `flow recover` | — | — | — |
110
+ | `flow wizard` | — | — (interactive; requires stdin+stdout TTY) | — |
111
+
112
+ Group notes (behavioral, must be preserved):
113
+
114
+ - **`flow status`** (no `--verbose`): `mgr.summary(lang)` block, then `\nCurrent Active Stage: <stage> (<phase>)` when `pipeline.current.stage` resolves, then `\nStage Duration:` with one line per stage ` <stageId> [<phase>]: <duration>` (+ ` common.inProgress` suffix if `end_time` empty). `formatDuration`: `0ms` if ≤0; `<ms>ms` if <1000; `x.y s` if <60000; `Nm[ Ss]` beyond.
115
+ `--verbose` prints: `flow.status.verboseTitle`, 8 basic lines, `cascadeTitle` + fixed table (`key.padEnd(18)` × profile/config/`*`status/effective, rule line `─`×49) + `cascadeNote`, `recentTitleTmpl(n)` + header + rule `─`×55 + rows (`time.padEnd(17) agent.padEnd(14) change.padEnd(20) description`), `downstreamTitle` + `─`×34 rows, then `recoveryTitle` block (stage/op/status/blockedBy/pendingTasks ≤10 + overflow notice).
116
+ - **`flow overview`** (text): box banner `╔═×42╗ / ║ <title padStart(38)> ║ / ╚═×42╝`; sections current status, stages overview (bar `█`×done + `░`×(total−done), `phase=<p> <bar> <done>/<total> <← current>`), reviews (open/resolved/closed + per-open `🔴`/`🟡` + priority `↑/= /↓`), bug stats from `.openfeel/bugs/index.md` (`open:`/`closed:` regexes, `i` flag), last 5 log entries, health (quick) with pass/warn/fail counts and `❌ section: message` per fail, closing rule `═`×44.
117
+ - **`flow current`**: `getPhase()` (meta: active/paused/done), stage, stage phase, op, retry. When stage exists but op empty → `flow.current.noOpTmpl {stage}`.
118
+ - **`flow metrics`**: `MetricsStore.getInstance()` → `.load()` → text `summary(lang)` or JSON.
119
+ - **`flow phases`**: validates `--mode` via `getConfigFieldLegalValues('workflow_mode')` (exit 1 with `stage.set.invalidValueTmpl`), validates `--stage` format then existence (registered in flow.json **or** a resolvable `status.md`); derives mode = explicit `--mode` ?? `resolveStageWorkflowMode(stage).value`; compares runtime transitions vs built-in defaults (`missing`/`extra`/`changed`); text emits phases list, `flow.mode.lineTmpl {mode, source}`, tier line `flow.phases.tierLineTmpl {tier, mode}` (tier = `MODE_DERIVED_TIER[mode]`: full→full, fast→lean, general→full), transitions ` <key> → [a, b]`, optional diff/custom-phase notices.
120
+ - **`flow mode`**: read-only; `source ∈ {status.md, config.yaml, profile.yaml, builtin, requirement}`; JSON also returns the three transition tables.
121
+ - **`flow stage remove`**: normalizes short id → full id; `--dry-run` runs `checkRemovable` and prints stage / `ops: n` / `current: <stage><←?>` / referencing list, then exits 1 if not removable; `--purge` without `--force` requires TTY (else exit 1), on TTY asks `confirm` (default false); order is unregister-in-memory → `save()` → `rmSync(dir, {recursive,force})`.
122
+ - **`flow stage set`**: dangling deps are a **hard error** (exit 1, zero write); deps that are explicitly version-prefixed and in `meta.archived_versions` are accepted with an info line.
123
+ - **`flow stage reset`**: `--to` must be a valid `PipelinePhase`; regression allowed; `--to done` still subject to blocking-REV check; core mutates memory only, command layer `save()`s.
124
+ - **`flow advance`** (the most complex command — full algorithm):
125
+ 1. `--stage` missing → `flow.advance.errorNoStage`, exit 1.
126
+ 2. `--review-tier` not in `REVIEW_TIERS` → exit 1 **before any disk touch**. Effective tier = explicit ?? `full` (default = zero pruning; derived tier is display-only).
127
+ 3. not loaded → exit 1.
128
+ 4. normalize stage; `autoRepairInconsistency(stage, {dryRun})`; if fixed print `autoRepaired` / `autoRepairPreview` and `save()` unless dry-run.
129
+ 5. `validate()`; warnings printed as `[WARN] <w>` + `warnAutoCorrect`; invalid → `errorInvalidFormat` + ` - <err>` per error, exit 1.
130
+ 6. Unless `--force`: `--to` must satisfy `PipelinePhaseSchema` (15 phases) else `errorInvalidPhaseTmpl` + `labelValidPhases: [ … ]` + `hintUseForceFuzzy`.
131
+ 7. Unless `--force`: `findPhasePath(stage, to, tier)`; `no-path`/`ambiguous`/`depth-exceeded` → reason-specific message + `errorPhaseJumpTmpl` + `currentPhaseTmpl` + available targets (`flow.advance.availableTargets: [ … ]` or `noAvailableTargets`) + `hintUseForce`, exit 1 (a `general`-mode stage gets `flow.advance.generalNoPipeline` instead); `already-at-target` → `alreadyAtTargetTmpl` (suppressed by `--quiet`), return 0. BFS depth cap = `PHASE_PATH_MAX_DEPTH` = **8**.
132
+ 8. `--to done` from `exec_running`/`review_pending` → warn `warnSkipReview`.
133
+ 9. explicit tier `lean`/`skip` → warn `warnTierTmpl {tier, gates}` (`TIER_PRUNED_PHASES`: lean→`scheme_review`, skip→`plan_review,scheme_review`).
134
+ 10. `--to done`: blocking open REVs for the stage (`r.op === stage || r.op.startsWith(stage + '.')`, `blocking !== false`, `status === 'open'`) → `revBlockedTitleTmpl` + per-REV `revBlockedItemTmpl` on **stderr** (via `console.warn`), then `blockingRevRefused` + `blockingRevHint`, exit 1. `--force` only adds `forceRevRefused` — **it cannot bypass REV blocking**.
135
+ 11. `--dry-run`: optionally warn `dryRunForceWarn`; print `dryRunTitle`, stage, from, to, optional tier line, optional multi-step path `<from> → … → <to>`, blank line, `dryRunOk`; zero writes.
136
+ 12. Execute **step by step** along the unique path (`advanceStagePhase(stage, next, 'cli', {reviewTier})` then `save()` per step). Multi-step failure → `partialFailTmpl {done, failed, remaining}` + generic error, exit 1 (no rollback). Multi-step per-step success line `stepOkTmpl` (single-step stays silent). After each step of a multi-step path, re-check blocking REVs → `blockedByRevTmpl`.
137
+ 13. Metrics: load store; for each **exited** phase in `PHASE_AGENT_MAP` (plan_pending/plan_review→planner, scheme_pending/scheme_review→schemer, exec_running→executor, review_pending/review_failed→reviewer, test_pending/test_failed→feel-tester, archiving→archiver) record `recordRun(agent, now − enteredAt, 'success', 0)` when a baseline exists (baseline = prior `advance_stage_phase`/`reset_stage_phase` log entry with `detail.to === fromPhase`, or `register_stage` for `plan_pending`); no baseline → `runtimeLog('debug', 'metrics skip (no baseline): stage=<s> phase=<p>')` and no record. `save()` once if anything recorded.
138
+ 14. `--quiet` suppresses success line, step lines, git warnings, op label.
139
+ 15. Non-quiet success `okTmpl {stage, to}`; when `--to done` or the stage auto-archived: `git status --porcelain` (cwd, timeout 5000) and if dirty print the 4-line `gitDirtyBox*` warning box (git errors silent). `--op` → `opLabelTmpl`.
140
+ 16. On stage `done` transition the manager may archive → `autoCommitOnDone(stage)` **after** `save()`.
141
+ - **`flow attempt`**: refuses draft op (`flow.attempt.draftRefusedTmpl`) with exit 1; `pass` → `passTmpl`; `fail`+retry → `failRetryTmpl`; `fail`+exhausted → `failReplanTmpl` **and** auto-advances the op to `scheme_pending` + `autoReplan`; always ends with current-pointer line (`currentOpTmpl` / `currentOpEmptyTmpl`).
142
+ - **`flow ops list`**: per op `stage, opId, title, state, fill ∈ {empty,partial,filled}` (`detectFillState`; `readOpTemplate` returning `null` ⇒ `filled`). Text groups active (state ≠ draft) then draft; each line ` <stage>.<opId> [<state>] (<fill>) <title>`; `empty` adds `flow.ops.emptyWarningTmpl {opId}`. JSON adds `warning` only when `fill === 'empty'`.
143
+ - **`flow log`**: last n from `flow.json.log` (not truncated to newest-first — ascending slice then forward print), `[<time first 19 chars, T→space>] <agent normalized> — <action>` + ` Detail: <json>` when detail non-empty + blank line.
144
+ - **`flow review add`**: delegates to the single implementation `addReviewEntry`; default title = `flow.review.detail + ': ' + opId`; priority fixed `medium`; error codes → `notLoaded|invalidOpId|stageNotFound|opNotFound|invalidPriority` messages, exit 1.
145
+ - **`flow repair`**: optional `--backup` copies `flow.json` → `flow.json.bak`; `repair(dryRun, {pruneOrphans})`; prints recovered / each change / fix-done|dry-run-hint|no-fix; `fixFailed` on inconsistency → exit 1; suggests `flow migrate` when `needsMigration()`; always reports key-orphans and file-orphans (file orphans: count, first 5, note — **no cleanup entry point exists**); `--prune-orphans` prunes only key orphans.
146
+ - **`flow migrate`**: no-op with `alreadyNew` when already v4.1; `--dry-run` prints preview only; otherwise `migrate(false, noBackup)`, `save()`, print changes.
147
+ - **`flow checkpoint restore`**: `--dry-run` needs no `--force` (prints diffs or `restoreNoDiff`); conflicts → `flow.manager.snapshotRestoreRefused`, exit 1; without `--force` → `restoreNeedForce`, exit 1.
148
+ - **`flow health`**: items from `healthCheck(quick)`, plus (non-quick, loaded) archive-consistency items and requirement-reconciliation items (both **warn-only**). `--fix` runs `reconcileStatusMd({dryRun})` (only the status.md `状态` field). Icons: pass `✅`, warn `⚠️ ` (with trailing space), fail `❌`. Not ok → exit 1 (both text and JSON).
149
+ - **`flow recover`**: read-only recovery report (global status, phase, current op, stage status, blockedBy, pending tasks, stage durations).
150
+ - **`flow wizard`**: requires both stdin and stdout TTY (else `flow.wizard.nonTtyHint` and `process.exitCode = 1`); loops: refresh, stop when meta phase `done` or every stage `done`; no stages → offer to create one (prompts); >1 stage → `select`; first-time workflow-mode prompt when status.md has no `工作流模式` field **and** project `config.yaml` has no explicit `defaults.workflow_mode` (writes the field + `setConfigValue('workflow_mode', chosen)`); shows status block; lists `getAvailablePhases`; confirmation; `advanceStagePhase(stage, target, 'cli')` + `save()` + optional `autoCommitOnDone`.
151
+
152
+ ### 3.2 `req` — requirements (`src/commands/req.ts`) — 11 leaves
153
+
154
+ All `req` commands use `process.exitCode = 1` (never a hard `process.exit(1)`), so buffered stdout always flushes.
155
+
156
+ | Command | Positional | Flags (domain / default) | JSON |
157
+ |---|---|---|---|
158
+ | `req add` | `<title>` | `--target-version <v>`; `--priority <P0\|P1\|P2>`; `--workflow-mode <full\|fast\|general>` | — |
159
+ | `req list` | — | `--status <draft\|scheduled\|in_progress\|done\|blocked\|cancelled\|rejected>`; `--target-version <v>`; `--owner <o>`; `--json` | ✅ |
160
+ | `req show` | `<id>` (`REQ-\d{3,}`) | `--json` | ✅ |
161
+ | `req claim` | `<id>` | `--owner <owner>` (default chain: `.openfeel/.info.json` `user` → `git config user.name` → refuse with `req.claim.ownerUnresolvedTmpl`) | — |
162
+ | `req release` | `<id>` | — | — |
163
+ | `req set` | `<id>` | `--status <s>`; `--title <t>`; `--target-version <v>`; `--priority <P0\|P1\|P2>`; `--workflow-mode <mode>` (≥1 required) | — |
164
+ | `req link` | — | **`--stage <stageId>` (req)**; `--requirement <id>` XOR `--unlink` (exactly one) | — |
165
+ | `req advance` | `<id>` | **`--to <status>` (req)** | — |
166
+ | `req split` | `<id>` | `--into <title>` (**repeatable**, accumulating; ≥1); `--json` | ✅ |
167
+ | `req board` | — | `--target-version <v>`; `--json` | ✅ |
168
+ | `req next` | — | `--owner <o>`; `--limit <n>` (integer ≥1, default `1`); `--json` | ✅ |
169
+
170
+ Semantics that must be preserved:
171
+
172
+ - **Status domain** `REQ_STATUSES = [draft, scheduled, in_progress, done, blocked, cancelled, rejected]` (fixed order — this order drives board groups). New requirement status = `draft`.
173
+ - **Transition table** (single source): `draft → scheduled|blocked|cancelled|rejected`; `scheduled → in_progress|blocked|cancelled|rejected`; `in_progress → done|blocked|cancelled|rejected`; `blocked → scheduled|in_progress|cancelled|rejected`; `done|cancelled|rejected → []`. `done` additionally requires `canCompleteRequirement` (all linked stages done, unless `workflow_mode === 'general'`).
174
+ - **Error codes** mapped to messages + exit 1: `not_found`, `stage_not_found`, `already_claimed`, `already_linked`, `invalid_status`, `invalid_field`, `general_mode_conflict`.
175
+ - `req add` writes the flow.json entry **and** a body skeleton `.openfeel/requirements/REQ-NNN.md` (never overwrites an existing body); body write failure ⇒ no `save()` (flow.json untouched).
176
+ - `req claim`: same-owner re-claim is idempotent (`req.claim.idempotentTmpl`, **no save**, revision unchanged). Terminal statuses rejected.
177
+ - `req release`: unclaimed → `req.release.noopTmpl`, no save, exit 0.
178
+ - `req set`: no fields → `req.set.noFieldsTmpl` exit 1; `changed === false` → `req.set.noopTmpl`; when `--workflow-mode` given, after saving, the mode is pushed to every stage whose `requirement_id === id` via `syncConfigFieldToStages('workflow_mode', …)` and `req.set.modeSyncedTmpl {id, mode, count}` is printed only when ≥1 stage was written (zero linked stages ⇒ zero side effects, no line).
179
+ - `req link`: stage key normalized once; linking to a terminal requirement rejected (`req.link.terminalTmpl`); a stage already owned by another requirement → `already_linked`; on link, if the requirement carries `workflow_mode`, sync it to that stage and print `req.link.modeSyncedTmpl`.
180
+ - `req advance`/`req set --status done`: when the static table allows `done` but the engine refused, this can only be the completion gate → `req.err.doneGateTmpl`; otherwise `req.err.invalidStatusTmpl {id, to, current, targets}` with `targets` = allowed next statuses.
181
+ - `req split`: children are created **published** (`scheduled`), get their own body skeleton, and `parent_id` set; JSON form returns the children **bodies**.
182
+ - `req board`: always 7 groups in `REQ_STATUSES` order, **including empty groups**; unknown/out-of-domain statuses are not rendered.
183
+ - `req next` ordering (frozen): ① `target_version` ascending, unspecified always last ② `created_at` ascending ③ `id` ascending. Candidates = `scheduled` AND (`!owner` OR (`--owner` given AND `owner === --owner`)). `--limit` validation is on the **parsed number** (`/^\d+$/` then `Number`; must be integer ≥1) — explicit `1` behaves exactly like default.
184
+ - `--json` detection helper: `options.json === true || command.parent?.opts().json === true` (defensive parent merge; `req` itself declares no `--json`).
185
+
186
+ ### 3.3 `stage` — status.md atomic ops (`src/commands/stage.ts`) — 4 leaves
187
+
188
+ | Command | Positional | Flags | JSON |
189
+ |---|---|---|---|
190
+ | `stage status` | `[stageId]` | — | — |
191
+ | `stage set` | `<stageId>` | `--status <value>` (∈ `STAGE_STATUS_VALUES`); `--exec-mode <manual\|auto>`; `--auto-advance <enabled\|disabled>`; `--review-agent <agent>` (non-empty after trim); `--workflow-mode <full\|fast>` (`general` rejected) | — |
192
+ | `stage task` | `<stageId> [taskNo]` | `--add <desc>` (mutually exclusive with the others); `--done`; `--undone` | — |
193
+ | `stage create` | `<stageId>` | — (**deprecated**: prints deprecation on TTY stderr; registers into flow.json only) | — |
194
+
195
+ - `STAGE_STATUS_VALUES = [planned, review_failed, review_passed, testing, archiving, done]`.
196
+ - `stage status` (no arg): scans `.openfeel/plan/**/status.md`, prints `stage.status.foundTmpl {n}` then ` <icon> <stageId> → <status>` where icon = `✅` done / `🔧` coding / `📋` planned / two spaces otherwise, then a hint. With an arg: prints `\n── <stageId> ──\n`, fields in fixed order `执行模式, 自动推进, 状态, 当前责任 Agent, 上一责任 Agent, 更新时间, 工作模式, 分支名, Session 名称, 合并状态, 清理策略, 前置依赖, 依赖状态`, then any extra fields in file order, then tasks (`✅`/`⬜` + `stage.task.taskItemTmpl {number, desc}`), then `blockedBy` when the `## 阻塞 / 暂停原因` section is neither empty nor `无`. Missing stage → `errorStageNotFoundTmpl` + `errorCheckStageId`, exit 1.
197
+ - `stage set` write order is fixed: 状态 → 执行模式 → 自动推进 → 工作流模式 → 当前责任 Agent. Enum violations → exit 1 zero-write. No options → `stage.set.noOptionTmpl`, exit 1. Each field is **probed first**: `not-found` → for `工作流模式` only, falls back to insert-with-anchor (`stage.set.insertedTmpl`; `no-anchor` → `noAnchorTmpl` exit 1); other fields → `errorFieldNotFoundTmpl` exit 1 (partial writes reported first via `multiPartialTmpl`). `unchanged` → `noopTmpl` (no `.bak`). `will-change` → backup to `.openfeel/tmp/status.<stageId>.bak` then atomic write, `updatedTmpl`. Writing `工作流模式` also syncs the flow.json redundancy `stages[id].workflow_mode` when the stage is registered.
198
+ - `stage task` (toggle): requires exactly one of `--done`/`--undone` (equal booleans → `errorMutualExclusive`), `taskNo` ≥1 integer (else `errorInvalidTaskNo`), backs up status.md, flips `- [ ] 任务N:` ↔ `- [x] 任务N:`; not found → exit 1. `--add <desc>` appends `- [ ] 任务N:<desc>` with N = max+1 (`TASK_SECTION_MISSING` → `stage.task.sectionMissingTmpl`). Missing `taskNo` without `--add` → `stage.task.addUsageTmpl` exit 1.
199
+ - All status.md read-modify-write happens under lock `.openfeel/tmp/locks/status-<stageDir>.lock`.
200
+
201
+ ### 3.4 `plan` — plans & schemes (`src/commands/plan.ts`) — 8 leaves
202
+
203
+ | Command | Positional | Flags | JSON |
204
+ |---|---|---|---|
205
+ | `plan stage add` | `<name>` | `--deps <ids...>`; `--tasks <items...>` (both: space **or** comma separated, re-split on `,`, trimmed, empties dropped) | — |
206
+ | `plan stage list` | — | — | — |
207
+ | `plan scheme create` | `<stage> [title]` | `--draft`; `--from-tasks` | — |
208
+ | `plan scheme publish` | `<stage> <opId>` | — | — |
209
+ | `plan scheme rename` | `<stage> <opId>` | **`--title <text>` (required)** | — |
210
+ | `plan scheme list` | `[stage]` | — | — |
211
+ | `plan scheme remove` | `<stage> <opId>` | `--force`; `--dry-run` | — |
212
+ | `plan scheme register` | `<stage> [opId]` | `--dry-run` | — |
213
+
214
+ - `plan stage add` also takes `--exec-mode <manual\|auto>`, `--auto-advance <enabled\|disabled>`, `--workflow-mode <full\|fast>` (overrides passed to the skeleton; `general` rejected with `common.generalOnlyRequirement`). Invalid stage id → `stageIdInvalidTmpl` + `stageIdSuggestTmpl`, exit 1. Deps must be registered **or** an archived explicit version (info line), else `plan.stage.invalidDepsTmpl` exit 1. Creates the directory + `overview.md`/`status.md` and registers in flow.json (`plan.stage.createdTmpl`).
215
+ - `plan stage list`: `- <name> <path>` lines (path like `.openfeel/plan/v1/stage-01/`), sorted by name; empty → `plan.stage.empty`.
216
+ - `plan scheme create`: `--from-tasks` is mechanical (no agent calls) — mutually exclusive with `title` (`fromTasksMutualTmpl`), reads status.md tasks, writes `ops/op-NNN.md` skeletons + `ops/deps.yaml` placeholder, prints `plan.scheme.fromTasks.okTmpl {count, ops, skipped, stage}` or `emptyTmpl`; `STATUS_NOT_FOUND` → `stage.errorStageNotFoundTmpl`. Without `--from-tasks`, `title` is mandatory at runtime (`titleRequiredTmpl`). Unregistered stage is implicitly registered (`plan.scheme.implicitRegisterTmpl`). Prints `plan.scheme.createdTmpl {opId, stage}`.
217
+ - `plan scheme publish`: `draft → pending`; failure reasons `empty-template` (`publish.emptyTmpl`), `not-draft` (`notDraftTmpl`), else `notFoundTmpl`; all exit 1.
218
+ - `plan scheme rename`: empty/blank title → `rename.emptyTitleTmpl` exit 1; `title-unchanged` → info line, exit 0; `file-missing` / not found → exit 1.
219
+ - `plan scheme list`: `[<stage>] <opId> — <title>` with ` [draft]` suffix when the op state is `draft`; empty → `plan.scheme.empty`.
220
+ - `plan scheme remove`: only deletes the flow.json registration key (**never** the file). Guard failures: `op-done`, `has-checkpoint` (both overridable with `--force`), otherwise `notFoundTmpl`; exit 1. `--dry-run` prints `remove.dryRunTmpl` and writes nothing. Orphan note when the file remains.
221
+ - `plan scheme register`: back-fills `ops/` files not yet registered (file orphans); `--dry-run` previews; specific `opId` with no file → `notFoundTmpl` exit 1; all-registered → `register.noopTmpl`.
222
+
223
+ ### 3.5 `config` (`src/commands/config.ts`) — 6 leaves
224
+
225
+ | Command | Positional | Flags | JSON |
226
+ |---|---|---|---|
227
+ | `config get-lang` | — | — | — |
228
+ | `config set-lang` | `<lang>` (`zh-CN`\|`en`) | — | — |
229
+ | `config list-projects` | — | — | — |
230
+ | `config get` | `[key]` | `-g, --global` | — |
231
+ | `config set` | `<key> <value>` | `-g, --global`; `--sync-stages`; `--stage <ids...>` | — |
232
+ | `config effective` | `[key]` | — | — |
233
+
234
+ - **Global profile keys** (`~/.config/openfeel/profile.yaml`) whitelist + domains: `user.name` (any string), `user.lang` (`zh-CN`\|`en`), `preferences.auto_advance` (`enabled`\|`disabled`), `preferences.review_mode` (`full`\|`skip_small_changes`), `preferences.communication` (`concise`\|`detailed`), `preferences.confirm_threshold` (`low`\|`medium`\|`high`).
235
+ - **Project keys** (`.openfeel/config.yaml` `defaults` block), schema-derived: `execution_mode` (`manual`\|`auto`, default `manual`), `auto_advance` (`disabled`\|`enabled`, default `enabled`), `merge_mode` (`manual`\|`auto`, default `manual`), `workflow_mode` (`full`\|`fast`\|`general`, default `fast`). `defaults.X` and `X` are equivalent (normalized); the user's original spelling is echoed back.
236
+ - `config get` (project): `key` is mandatory → else `config.get.noProject {err:'key 未指定…'}`, exit 1; null value → `config.get.result {key, value: common.noConfig}`. `--global`: no key prints the six known keys (`user.name`, `user.lang`, `preferences.*`); unknown key → `globalKeyNotFound`; parse errors warn on stderr (`config.get.parseErrorWarn`) **without** changing the exit code.
237
+ - `config set --global`: whitelist violation → `globalInvalidKey` + `globalAllowedKeys`, exit 1; value outside domain → `globalInvalidValue`, exit 1; unparsable profile → refuse to write (`[profile] … 已拒绝写入…`), exit 1; success validates the whole document with `ProfileSchema` then writes.
238
+ - `config set` (project) preconditions, all exit 1 and **zero writes**: `--stage` without `--sync-stages`; `--stage`/`--sync-stages` with `--global`; key outside `ConfigDefaultsSchema` (`config.set.invalidKey` listing `<k>(或 defaults.<k>)`); value outside the enum (`config.set.invalidValue`); `workflow_mode=general` at project level (`common.generalOnlyRequirement`); `--stage` ids each normalized and required to be registered **or** an archived explicit version (archived ⇒ downgrade note `config.set.syncArchivedStageTmpl`, status.md only).
239
+ - `config set --sync-stages` then calls `syncConfigFieldToStages(key, value, {onlyStages})` per stage; per-result lines: `updated` / `inserted` / `noop` / `skipped-no-anchor` (stderr) / `skipped-no-status` (stderr) / `skipped-no-field` (stderr), then a summary `config.set.syncSummaryTmpl {updated, inserted, skipped}`. Exit code stays 0 even when stages were skipped. Field mapping: `auto_advance→自动推进`, `execution_mode→执行模式`, `workflow_mode→工作流模式` (`merge_mode` has no stage field → `skipped-no-field`).
240
+ - `config effective [key]`: resolves the four managed keys `execution_mode, auto_advance, merge_mode, workflow_mode` through the cascade **profile.yaml < config.yaml < status.md < builtin** and prints `key=value (source)`; source ∈ `status.md`|`config.yaml`|`profile.yaml`|`builtin`|`requirement`. Unknown key → `effective.unknownKey` listing the valid keys, exit 1.
241
+
242
+ ### 3.6 `knowledge` (`src/commands/knowledge.ts`) — 5 leaves
243
+
244
+ | Command | Positional | Flags | JSON |
245
+ |---|---|---|---|
246
+ | `knowledge list` | — | `--type <architecture\|patterns\|troubleshooting\|setup>` | — |
247
+ | `knowledge add` | `<category> <title>` | `--content <text>` (falls back to stdin when piped) | — |
248
+ | `knowledge search` | `<query>` | `--limit <n>` (default `'10'`, `max(1, parseInt‖10)`); `--offset <n>` (default `'0'`, `max(0, parseInt‖0)`) | — |
249
+ | `knowledge index` | — | — | — |
250
+ | `knowledge dedup` | `[content]` | `--project <path>`; `--category <name>`; `--threshold <n>` (0–1, default 0.8) | — |
251
+
252
+ - Categories are fixed: `architecture | patterns | troubleshooting | setup`. Invalid category → `knowledge.add.errorInvalidCategoryTmpl`, exit 1.
253
+ - `knowledge list` renders an aligned text table with columns `Category | Title | Date | Status` where status is `knowledge.list.enabled`/`disabled`; separator row uses `-` runs joined with `-+-`; empty → `knowledge.list.empty`.
254
+ - `knowledge add`: content required (`--content`, else stdin when `!isTTY`, else `errorNoContent`); blank content → `errorEmptyContent`; success `knowledge.add.okTmpl {category, title}`. Mutates `.openfeel/kb/<category>.md`.
255
+ - `knowledge search`: `knowledge.search.foundTmpl {n}` then per hit `[<category>] <title> (<date>) [<status>]` + a 100-char content summary (with `…`); no hits → `noResultsTmpl` (+ `offsetOutOfBoundsTmpl` when `--offset > 0`).
256
+ - `knowledge index`: category overview (name `padEnd(20)` + description), then recent-updates table (`Date | Category | Title`).
257
+ - `knowledge dedup`: **read-only advisory** (never writes kb). Invalid threshold → `invalidThresholdTmpl` exit 1; invalid category → `invalidCategoryTmpl` exit 1; no content → `noContentTmpl` exit 1; prints `dedup.title`, per-similar hit `dedup.itemTmpl {category, title, pct, flag}` (flag `dedup.suggestFlag` when similarity > threshold; similarity ≤ 0 skipped), `dedup.noneTmpl {threshold}` when nothing, and always ends with the fixed `dedup.readOnlyNote` line.
258
+
259
+ ### 3.7 `lint` (`src/commands/lint.ts`) — 2 leaves
260
+
261
+ | Command | Positional | Flags | JSON |
262
+ |---|---|---|---|
263
+ | `lint i18n` | — | — | — |
264
+ | `lint kb` | — | — | — |
265
+
266
+ See §9.
267
+
268
+ ### 3.8 `archive` — stage archive (top-level, `src/commands/archive.ts`) — 1 leaf
269
+
270
+ | Command | Positional | Flags | JSON |
271
+ |---|---|---|---|
272
+ | `archive` | `<stage>` | — | — |
273
+
274
+ Writes `.openfeel/log/archive-<stage>.md` (full Markdown summary: header bullets for 归档时间/阶段名称/阶段状态/依赖阶段, an ops table `| ID | 标题 | 状态 | 尝试次数 |`, a reviews table `| ID | 标题 | 状态 | 优先级 |`), appends `archive_stage` to `flow.json.log`, `save()`s, then forces every `closed`/`resolved` review of the stage into KB entry `patterns` with content `归档自阶段 <stage>`. Returns `null` (⇒ `archive.errorArchiveFailedTmpl`, exit 1) when flow.json isn't loaded or the stage is unknown. Output: `archive.okTmpl {stage}`, ` Ops: n`, ` Reviews: n`, optional ` Knowledge Extracts:` + ` - <entry>`, ` Archive Path: <abs path>`.
275
+
276
+ ### 3.9 `roadmap` (`src/commands/roadmap.ts`) — 2 leaves
277
+
278
+ | Command | Positional | Flags | JSON |
279
+ |---|---|---|---|
280
+ | `roadmap create` | `<version>` (`v` prefix stripped) | — | — |
281
+ | `roadmap show` | `[version]` | — | — |
282
+
283
+ `create` writes `.openfeel/roadmap/v<ver>.md` skeleton (目标 / 阶段划分 / 里程碑 / 备注, each `> 待补充`); existing file → prints `大纲已存在: v<ver>.md` and does **not** overwrite. `show` with a version prints the raw file (missing → error via `common.errorTmpl`, `process.exitCode = 1`); without a version lists `v*.md` filenames one per line; absent directory → `暂无分期大纲(.openfeel/roadmap/ 目录不存在)`. **Note**: the strings in this module are hard-coded Chinese (not i18n).
284
+
285
+ ### 3.10 `project` (`src/commands/project.ts`) — 1 leaf
286
+
287
+ | Command | Positional | Flags | JSON |
288
+ |---|---|---|---|
289
+ | `project overview` | — | — | — |
290
+
291
+ Read-only scan producing a fixed box/section report: banner box (`╔═×42╗`, title `padEnd(40)`, `╚═×42╝`), `📋 Basic Info`, `📁 Directory Structure` (`src/` with cli/commands/core/utils file counts; `.opencode/` agents + skills counts; `.openfeel/` kb/plan/code_review/bugs counts), `📊 Statistics` (ts source files, agent definitions, CLI modules, KB entries parsed from `.openfeel/kb/index.md` category table column index 3, plan versions), `🚪 Entry Points`, `🔧 Tech Stack` (versions read from `package.json` deps, `^`/`~` stripped, defaults `Node.js ≥20` / `TypeScript 5.7+` / `Commander 14+` / `Zod 4+` / `fast-glob` / `Vitest 3+`).
292
+
293
+ ### 3.11 `model` (`src/commands/model.ts`) — 3 leaves
294
+
295
+ | Command | Positional | Flags | JSON |
296
+ |---|---|---|---|
297
+ | `model set` | `<agent> <model>` | `--scope <default\|global\|project>` (default `project`); `--build`; `--force` | — |
298
+ | `model get` | `<agent>` | `--scope <default\|global\|project>` | — |
299
+ | `model list` | — | `--scope <default\|global\|project>` | — |
300
+
301
+ - Invalid scope → `model.error.scope`, exit 1.
302
+ - `--scope default` guards: non-TTY without `--force` and without `--build` → `model.set.needConfirm`, exit 1; framework source not present (no `src/core/templates-data/opencode/agents`) → `model.set.noSourceRepo`, exit 1.
303
+ - `model set` prints `model.set.ok {agentId, model, scope}`, then each changed file ` <path>`, optional `model.set.warning`, then `needsBuild` hint or, with `--build`, `model.set.buildTriggered` and `execSync('npm run build', {stdio:'inherit', timeout: 600000})`; build timeout (ETIMEDOUT/SIGTERM) → `Error: build timeout (10min)` with `process.exitCode = 1`, other build failures rethrow.
304
+ - Resolution chain for `effective`: `default > project > global`. `model get` prints `Effective: …` + all three scopes (`(none)` for unset), `inconsistent` notice when the default layer has two disagreeing sources, and `model.get.shadowed` when the default layer masks explicit project/global values. `model list` prints `model.list.title` then ` <agentId>: <value|(none)>`.
305
+ - Writes: `default` → framework source templates + `src/core/opencode-config.ts` (requires rebuild); `global` → `~/.config/opencode/opencode.jsonc` (+ global agent/skill files); `project` → project `.opencode/` + `opencode.jsonc`.
306
+
307
+ ### 3.12 `view` — review entries (`src/commands/view.ts`) — 2 leaves
308
+
309
+ | Command | Positional | Flags | JSON |
310
+ |---|---|---|---|
311
+ | `view list` | — | `--op <id>` | — |
312
+ | `view accept` | `<rev-id>` | — | — |
313
+
314
+ Group `view` adds a trailing help note (`help.view.note`) pointing at `flow review *` as the only mutation entry point — **`view` has no `add`/`update`/`remove`**. See §8.
315
+
316
+ ### 3.13 `instructions` (`src/commands/instructions.ts`) — 1 leaf
317
+
318
+ | Command | Positional | Flags | JSON |
319
+ |---|---|---|---|
320
+ | `instructions` | `<artifactId>` | **`--change <name>` (required)**; `--json`; `--schema <name>` (default `spec-driven`) | ✅ |
321
+
322
+ Loads the schema via `resolveSchema(schemaName, projectPath)`, then emits either XML (`<?xml version="1.0" encoding="UTF-8"?>` + `<artifact id change schema>` with `<task> <project_context> <rules> <dependencies> <output> <instruction> <template> <unlocks>`) or the JSON equivalent (§5.11). Errors → `common.errorTmpl`, `process.exit(1)`.
323
+
324
+ ### 3.14 `init` (`src/commands/init.ts`) — 1 leaf
325
+
326
+ | Command | Positional | Flags | JSON |
327
+ |---|---|---|---|
328
+ | `init` | `[path]` (default `cwd`) | `--demo`; `--lang <zh-CN\|en>`; `--workspace-only`; `--non-interactive` | — |
329
+
330
+ - Path must exist (`init.errorPathNotExistTmpl`), exit 1.
331
+ - Prints `init.initializingTmpl {path}`, then `+ item` for created, `~ item` for updated, `- item` for skipped; nothing created/updated → `init.alreadyUpToDate`, else `init.complete`.
332
+ - `--workspace-only` → `initWorkspaceOnly` (no global rules / no platform adapter config). `--non-interactive` is **accepted but unused** (workspace-only is already non-interactive).
333
+ - `--demo` additionally lays down a demo project skeleton (`init.demoCreating` / `demoCreated` / `demoSkipped` / `demoComplete`); demo language defaults `zh-CN` unless `--lang` is `en`/`zh-CN`.
334
+ - Writes: `.openfeel/` directory tree, `config.yaml` (skipped if it exists), `flow.json` (created/updated via `FlowManager.initFlow`), `.info.json`, `dev/dev_core.md`, `dev/current.md`, `dev/decisions.md`, `kb/index.md`, `opencode.jsonc`, `AGENTS.md`, possibly `package.json` (script edit; skipped if backup fails). Errors → `common.errorTmpl` + `process.exitCode = 1`.
335
+
336
+ ### 3.15 `setup` (`src/commands/setup.ts`) — 1 leaf
337
+
338
+ | Command | Positional | Flags | JSON |
339
+ |---|---|---|---|
340
+ | `setup` | — | `--lang <zh-CN\|en>` (default `zh-CN`); `--check`; `--json`; `--tools <csv>` | ✅ (only with `--check`) |
341
+
342
+ - `--check` is a **read-only short-circuit** (zero writes): `checkGlobalDeployment()` → `{status: ok|mismatch|missing|unknown, cliVersion, deployedVersion}`; JSON = `{schemaVersion:1, status, cliVersion, deployedVersion}`; text uses `setup.checkOkTmpl` / `checkMismatchTmpl` / `checkMissingTmpl` / `checkUnknownTmpl`. **Exit 0 iff `status === 'ok'`, else `process.exitCode = 1`.**
343
+ - Without `--check`: `--tools` parsed by `parseToolList` (comma separated; unknown or empty item → `update.toolsInvalidTmpl` listing valid ids, exit 1; default `['opencode']`); deploys global framework (`setupGlobalFramework(lang, tools)`); prints created `+ `, updated `~ `, appended `⚠ `, skipped `- `, then `setup.complete` and `setup.restartHarnessTmpl {harness}` where harness = `Claude Code` / `opencode / Claude Code` / `opencode`; with `claude` also `update.claudeReloadHintTmpl`. Supported tool ids: `opencode` (default enabled), `claude`.
344
+
345
+ ### 3.16 `update` (`src/commands/update.ts`) — 1 leaf
346
+
347
+ | Command | Positional | Flags | JSON |
348
+ |---|---|---|---|
349
+ | `update` | `[path]` (default `cwd`) | `--lang <lang>`; `--force`; `--tools <list>` | — |
350
+
351
+ - `--tools` is validated **first** (fail-fast, before auto-init) — invalid → `update.toolsInvalidTmpl`, exit 1. Nonexistent path → `errorPathNotExistTmpl`, exit 1.
352
+ - If `.openfeel/` is absent → auto-init (`update.autoInitTmpl` + created list).
353
+ - Tool selection: `--tools` → that list; else `[path]` given → **all enabled** tools; else interactive `checkbox` (falls back to all enabled when interaction is unavailable, printing `update.nonInteractiveToolsTmpl`). Empty selection → `update.cancelled`, exit 0.
354
+ - Then `updateProject(path, tools, lang, {force, interactive:true, lang})`; prints created/updated/skipped, conflict block (`update.conflictsTitle` + `⚠ item` + `conflictsHint`), appended block (`appendedTitle {n}` + `⚠ item` + `appendedHint`, plus `appendedManyWarning` when > 10), then `alreadyUpToDate` or `complete`. Deploy errors → `update.errorDeployFailedTmpl`, exit 1.
355
+
356
+ ### 3.17 `migrate` — legacy layout (`src/commands/migrate.ts`) — 2 leaves
357
+
358
+ | Command | Positional | Flags | JSON |
359
+ |---|---|---|---|
360
+ | `migrate` | `[path]` (default `cwd`) | `--dry-run`; `--remap-assignee`; `--clean-global-core-md` | — |
361
+ | `migrate rollback` | — | `--dry-run` | — |
362
+
363
+ - `migrate`: path must exist; `migrateProject(target, {dryRun, remapAssignee, lang, cleanGlobalCoreMd})`; any failure → `migrate.error.aborted` (+ "runnable rollback" hint) exit 1.
364
+ - Report order (`printMigrateReport`): deprecated global `core.md` notice + hint, deprecated project `AGENTS.md` notice, then `not legacy` → `migrate.legacy.alreadyLatest` and **return**; dry-run title; detection report (`① agents / ② skills / ③ instructions` counts, `④ jsonc skills/instructions`, `⑤ mixed state`); cleaned `✗ path`; kept custom `↺ path` + `migrate.clean.keptCustom`; assignee report (`old → new (count)`) + `migrate.assignee.hint` when not remapped; then dry-run note or `migrate.legacy.done` + `migrate.backup.dirTmpl {dir}`.
365
+ - `migrate rollback`: refuses outside a project root (no `.openfeel/backup/`) → `migrate.rollback.wrongDir`, exit 1. `--dry-run` (read from **either** its own opts or the parent's opts — commander attaches it to the parent) → `rollback.dryRunTitle` + ` [<op>] <source>` per manifest entry, no writes. Otherwise → `rollback.done` + ` ↺ <path>` per restored path; errors → `rollback.failed`, exit 1.
366
+
367
+ ---
368
+
369
+ ## 4. Read-only vs writing
370
+
371
+ **READ-ONLY (zero disk writes, no `save()`, no `appendLog`)** — safe for a DSH tool surface:
372
+
373
+ | Command | Notes |
374
+ |---|---|
375
+ | `flow status`, `flow overview`, `flow current`, `flow metrics`, `flow phases`, `flow mode`, `flow ops list`, `flow log`, `flow retry`, `flow recover` | `flow metrics` reads `.openfeel/metrics.json`; `flow mode`/`phases` explicitly guarantee no save. |
376
+ | `flow health` (without `--fix`) | read-only; `--fix` writes only status.md's `状态`. |
377
+ | `flow checkpoint list` | lists `.openfeel/checkpoints/`. |
378
+ | `flow archive list`, `flow archive show` | never read flow.json. |
379
+ | `flow repair --dry-run` | preview only. |
380
+ | `flow migrate --dry-run`, `flow stage remove --dry-run`, `flow stage reset --dry-run`, `flow advance --dry-run`, `flow checkpoint restore --dry-run` | all four write nothing. |
381
+ | `req list`, `req show`, `req board`, `req next` | explicit zero-write guarantee. |
382
+ | `stage status`, `plan stage list`, `plan scheme list`, `config get-lang`, `config list-projects`, `config get`, `config effective`, `knowledge list`, `knowledge search`, `knowledge index`, `knowledge dedup`, `lint i18n`, `lint kb`, `project overview`, `roadmap show`, `view list`, `setup --check`, `migrate --dry-run`, `migrate rollback --dry-run` | |
383
+ | `instructions` | reads schemas/config/templates, writes nothing. |
384
+
385
+ **WRITING** — mutations:
386
+
387
+ | Command | Mutates |
388
+ |---|---|
389
+ | `flow advance` | `.openfeel/flow.json` (phases, status, log, revision, checkpoints), possibly `.openfeel/metrics.json`, possibly git commit (auto-archive on `done`), `.openfeel/checkpoints/*`. |
390
+ | `flow attempt` | flow.json (op attempts/state, pipeline.current, log), possibly auto-advance to `scheme_pending`. |
391
+ | `flow stage add` / `stage create` | flow.json `stages` + log. |
392
+ | `flow stage remove` (`--purge`) | flow.json registration; `--purge` also deletes `.openfeel/plan/<series>/<stageDir>/`. |
393
+ | `flow stage set` | flow.json `stages[id].deps`. |
394
+ | `flow stage reset` | flow.json stage `phase`/`status` + log. |
395
+ | `flow review add/resolve/update/remove` | flow.json `reviews[]` + log. |
396
+ | `flow repair` (`--backup`, `--prune-orphans`) | `flow.json.bak`, flow.json repairs, key-orphan pruning. |
397
+ | `flow migrate` | flow.json rewritten to v4.1 (+ `flow.json.v4.0.bak`). |
398
+ | `flow checkpoint restore` (`--force`) | flow.json overwritten (or one stage subtree). |
399
+ | `flow archive <version>` | `.openfeel/flow-archive/<version>.json` (archive file), flow.json (removals, `meta.archived_versions`, pipeline disposition, log). |
400
+ | `flow health --fix` | status.md `状态` field only. |
401
+ | `flow wizard` | flow.json + status.md + `.openfeel/config.yaml` (`defaults.workflow_mode`). |
402
+ | `req add` | flow.json `requirements[]` + log + `.openfeel/requirements/REQ-NNN.md`. |
403
+ | `req claim`, `req release`, `req set`, `req advance`, `req link` | flow.json requirement entry + log; `req set --workflow-mode` / `req link` also sync linked stages' status.md `工作流模式` + flow.json redundancy. |
404
+ | `req split` | flow.json children + log + child body skeletons. |
405
+ | `stage set` | `.openfeel/plan/**/status.md` (+ `.openfeel/tmp/status.<stage>.bak`), flow.json redundancy for `工作流模式`. |
406
+ | `stage task` | status.md (+ `.bak`). |
407
+ | `plan stage add` | `.openfeel/plan/<series>/<stage>/` (`overview.md`, `status.md`), flow.json registration. |
408
+ | `plan scheme create` / `publish` / `rename` / `remove` / `register` | `.openfeel/plan/**/ops/op-NNN.md` (+ `ops/deps.yaml`), flow.json `ops` keys/state/title. |
409
+ | `plan scheme remove` | **flow.json key only** (files preserved). |
410
+ | `config set-lang`, `config set` | `~/.openfeel/config.json` / `~/.config/openfeel/profile.yaml` / `.openfeel/config.yaml`; `--sync-stages` also rewrites status.md fields. |
411
+ | `knowledge add`, `archive` | `.openfeel/kb/*.md` (and `kb/index.md`). |
412
+ | `archive` | `.openfeel/log/archive-<stage>.md`, flow.json log, `.openfeel/kb/patterns.md`. |
413
+ | `roadmap create` | `.openfeel/roadmap/v<ver>.md`. |
414
+ | `model set` | framework templates / `opencode-config.ts` / `~/.config/opencode/*` / project `.opencode/*`; may run `npm run build`. |
415
+ | `view accept` | flow.json review `status: closed` + log. |
416
+ | `init`, `update`, `setup` (non-`--check`), `migrate`, `migrate rollback` | project workspace files, global deployment files, backups. |
417
+
418
+ ---
419
+
420
+ ## 5. `--json` contract and shapes
421
+
422
+ Modeled on one rule: **`--json` prints exactly one JSON document on stdout, never mixing human prose or stderr text, and `schemaVersion: 1` is always present at the top level** (except `flow archive show`, which emits the archive file whose own `schemaVersion` is `1`). Failures still print a JSON document (and set a non-zero exit code) rather than human text.
423
+
424
+ 18 commands accept `--json`: `flow status|overview|current|metrics|phases|mode`, `flow ops list`, `flow archive`, `flow archive list`, `flow archive show`, `flow health`, `req list|show|split|board|next`, `setup --check`, `instructions`.
425
+
426
+ Because `openfeel` never globally registers `--json`, sub-commands take it locally; `flow archive list/show` and `req *` therefore also merge the **parent** command's opts (`command.parent?.opts().json === true`) so `openfeel flow archive list --json` works.
427
+
428
+ ### 5.1 `flow status --json`
429
+ ```json
430
+ { "schemaVersion": 1,
431
+ "pipeline": { "phase": "active", "current": { "stage": "v1-stage-01", "op": "op-001" } },
432
+ "stages": [ { "id": "v1-stage-01", "phase": "exec_running", "status": "coding", "deps": [], "ops": { "op-001": "done" } } ],
433
+ "counts": { "total": 1, "done": 0, "active": 1 } }
434
+ ```
435
+ Not-loaded fallback: `{schemaVersion:1, pipeline:{phase:"", current:{stage:"",op:""}}, stages:[], counts:{total:0,done:0,active:0}}`.
436
+
437
+ ### 5.2 `flow overview --json`
438
+ ```json
439
+ { "schemaVersion": 1, "phase": "active",
440
+ "current": { "stage": "", "op": "" },
441
+ "summary": { "phase": "...", "currentOp": null, "retryCount": 0, "stagesCount": 0, "opsCount": 0, "reviewItemsOpen": 0, "recentLogs": 0 },
442
+ "stages": [ { "id": "...", "phase": "...", "status": "...", "opsTotal": 0, "opsDone": 0 } ],
443
+ "reviews": { "open": 0, "blockingOpen": 0, "nonBlockingOpen": 0, "resolved": 0, "closed": 0 },
444
+ "health": { "ok": true, "pass": 0, "warn": 0, "fail": 0 },
445
+ "logs": 0 }
446
+ ```
447
+ Not loaded: `{ "schemaVersion": 1, "initialized": false }` (printed **without** indent).
448
+
449
+ ### 5.3 `flow current --json`
450
+ `{schemaVersion:1, stage:"<s>", op:"<o>", phase:"<active|paused|done|''>"}` — empty strings, never `null`. Not loaded → all three empty strings.
451
+
452
+ ### 5.4 `flow metrics --json`
453
+ `{schemaVersion:1, agents:[{agentName,totalRuns,successfulRuns,failedRuns,totalRetries,totalDurationMs,avgDurationMs,successRate /* number|null */}]}` (spread of `getSummaryData()`).
454
+
455
+ ### 5.5 `flow phases --json`
456
+ ```json
457
+ { "schemaVersion": 1,
458
+ "phases": ["plan_pending", "..."],
459
+ "transitions": { "plan_pending": ["plan_review"] },
460
+ "advanceAccepted": ["plan_pending","plan_review","plan_passed","scheme_pending","scheme_review","scheme_passed","exec_running","review_pending","review_failed","review_passed","test_pending","test_failed","test_passed","archiving","done"],
461
+ "transitionsDiff": { "missing": [], "extra": [], "changed": [] },
462
+ "workflowMode": "fast",
463
+ "reviewTier": "lean" }
464
+ ```
465
+ Key order is significant (`schemaVersion`, `phases`, `transitions`, `advanceAccepted`, `transitionsDiff`, `workflowMode`, `reviewTier`).
466
+
467
+ ### 5.6 `flow mode --json`
468
+ `{schemaVersion:1, stage:"<normalized id|''>", mode:"full|fast|general", source:"status.md|config.yaml|profile.yaml|builtin|requirement", modes:{full:{…},fast:{…},general:{…}}}` — printed even when flow.json is not initialized.
469
+
470
+ ### 5.7 `flow ops list --json`
471
+ `{schemaVersion:1, ops:[{stage,opId,title,state,fill, warning?}]}` — `warning` present only when `fill === "empty"`. Not loaded: `{schemaVersion:1, ops:[]}` (no indent).
472
+
473
+ ### 5.8 `flow archive --json`
474
+ - dry-run success: `{schemaVersion:1, version:"v1.1.7", stages:N, reviews:N, log:N, dryRun:true}`
475
+ - real success: same with `dryRun:false`
476
+ - failures (all + `process.exitCode = 1`):
477
+ - `{schemaVersion:1, error:"no-init"}`
478
+ - `{schemaVersion:1, error:"invalid-version", input:"<raw>"}`
479
+ - `{schemaVersion:1, error:"version-not-active", version:"<normalized>"}`
480
+ - `{schemaVersion:1, error:"gate-rejected", version, notDone:["<stage>"], blockingOpen:["REV-001"]}`
481
+ - `{schemaVersion:1, error:"archive-write-failed", version}`
482
+
483
+ ### 5.9 `flow archive list --json` / `flow archive show --json`
484
+ - list: `{schemaVersion:1, versions:[{version:"v1.1.7", counts:{stages,reviews,log}|null}]}` (dictionary order from the engine; unreadable archive → `counts:null`).
485
+ - show: the **entire** `ArchiveFile` (`schemaVersion:1`, `version`, `archived_at`, `counts`, `stages`, `reviews`, `log`) verbatim; failures → `{schemaVersion:1, error:"invalid-version"|"not-found", … , available:[...]}` and exit code 1.
486
+
487
+ ### 5.10 `flow health --json`
488
+ `{schemaVersion:1, ok:boolean, items:[{name, status:"pass"|"warn"|"fail", detail}], reconciled?:[{stage,from,to,result:"planned"|"applied"|"skipped-not-found"}]}`. `reconciled` only with `--fix`. `ok === false` ⇒ `process.exit(1)` after printing.
489
+
490
+ ### 5.11 `instructions --json`
491
+ ```json
492
+ { "artifact": { "id": "...", "change": "...", "schema": "spec-driven" },
493
+ "task": "...",
494
+ "project_context": { "tech_stack": "TypeScript", "workspace": ".openfeel/ 工作区已初始化", "project": "OpenFeel",
495
+ "config": { "execution_mode": null, "auto_advance": null, "merge_mode": null } },
496
+ "rules": ["..."],
497
+ "dependencies": [ { "id": "...", "status": "done|pending", "path": "...", "description": "..." } ],
498
+ "output": "<generates>",
499
+ "instruction": "...",
500
+ "template": "..." ,
501
+ "unlocks": [ { "id": "...", "description": "..."|null } ] }
502
+ ```
503
+ `template` is `null` when the artifact declares none.
504
+
505
+ ### 5.12 `setup --check --json`
506
+ `{schemaVersion:1, status:"ok"|"mismatch"|"missing"|"unknown", cliVersion:"1.2.1", deployedVersion:"1.2.1"|null}`.
507
+
508
+ ### 5.13 `req * --json`
509
+ - `req list`: `{schemaVersion:1, requirements:[RequirementData…]}` (RequirementData = `{id, title, status, target_version?, priority?, owner?, claimed_at?, created_at, updated_at, body_path?, parent_id?, workflow_mode?}`).
510
+ - `req show`: `{schemaVersion:1, requirement:RequirementData, stages:["<stageId>"…], parent:"REQ-001"|null, children:["REQ-002"…]}`.
511
+ - `req split`: `{schemaVersion:1, parent:"REQ-001", children:[RequirementData…]}` (full child bodies).
512
+ - `req board`: `{schemaVersion:1, groups:{draft:[…], scheduled:[…], in_progress:[…], done:[…], blocked:[…], cancelled:[…], rejected:[…]}}` — all seven keys always present.
513
+ - `req next`: `{schemaVersion:1, requirement: RequirementData|null}` when the effective limit is 1 (including explicit `--limit 1`); when limit > 1 it appends `candidates: [RequirementData…]` (the original two keys kept verbatim, order `schemaVersion, requirement, candidates`).
514
+ - Failures (`process.exitCode = 1`, single JSON document on stdout):
515
+ - `{schemaVersion:1, error:"no-init"}`
516
+ - `{schemaVersion:1, error:"invalid-argument", field:"status"|"id"|"into"|"limit", value?:"…", allowed?:[...]}`
517
+ - `{schemaVersion:1, error:"not_found", id:"REQ-001"}`
518
+ - `{schemaVersion:1, error:"<RequirementErrorCode>", id:"REQ-001"}` (split path)
519
+
520
+ ---
521
+
522
+ ## 6. Exit codes and the error envelope
523
+
524
+ | Code | Meaning | How produced |
525
+ |---|---|---|
526
+ | **0** | Success (also for deliberate no-ops: noop set/claim/release/link/advance, `already-at-target`, empty result lists, `update` cancelled selection). | normal return |
527
+ | **1** | **Any failure**: validation rejection, missing prerequisite, gate refusal, not-found, health `ok=false`, lint problems, `setup --check` non-ok. | `process.exit(1)` (hard) or `process.exitCode = 1` (deferred) |
528
+ | **2** | **Concurrent-write conflict** on `flow.json` — the only structurally distinct non-zero code; signals "retryable". | `handleConcurrentConflict` → `console.error(common.concurrentConflict)` then `process.exit(2)` |
529
+ | **130** / **143** | SIGINT / SIGTERM | `installExitHooks` |
530
+ | commander's own | Unknown command, unknown option, missing required option/argument, `--help`, `--version` | commander calls `program.error()` → prints its own usage message to stderr and exits **1**; it does **not** throw, so `handleCliError` is never reached and no `cli error:` line is written. |
531
+
532
+ ### 6.1 Exit-code style matters (observable via `cli done exit=`)
533
+
534
+ - **Deferred** (`process.exitCode = 1`, buffered output still flushes; REPL keeps living): all of `req *`, `lint *`, `roadmap *`, `init`, `setup --check`, `model set` build-timeout, `flow archive *`, `flow wizard` (non-TTY hint and general failure).
535
+ - **Hard** (`process.exit(1)`, immediate): everything else, including `flow health`, `flow advance`, `flow status`-family error paths.
536
+
537
+ ### 6.2 Error envelope for humans
538
+
539
+ There is **no structured error object on the human path** — the envelope is a single localized line on **stderr**:
540
+
541
+ ```
542
+ common.errorTmpl → "Error: {msg}" (en)
543
+ common.errorNoInit → "Error: flow.json not initialized, please run openfeel init first"
544
+ common.concurrentConflict → "[concurrent conflict] flow.json was modified by another process (expected revision={expected}, on-disk={actual}); this change was not written, please retry."
545
+ ```
546
+
547
+ Exit 1, stdout untouched (except for whatever the command already printed). Commands reuse `common.errorTmpl` for their own `{msg}` or emit a dedicated key (`config.set.invalidValue`, `flow.advance.blockingRevRefused`, `stage.set.noAnchorTmpl`, `migrate.error.aborted`, …). Commands **never** print stack traces on expected failures.
548
+
549
+ ### 6.3 Refusal vs internal error — the key distinction
550
+
551
+ | | **Refusal (gate not met)** | **Internal error** |
552
+ |---|---|---|
553
+ | Cause | Domain rule: gate/validation/enum/not-found/conflict | Unexpected exception (bug, IO, corrupt state) |
554
+ | Mechanism | Command layer returns early after printing an i18n line (or calls `handleConcurrentConflict`) | Exception propagates out of the action |
555
+ | stderr | One (or a few) localized lines, no stack | `cli error: <message>` is written to the **runtime log** at `error` level, then the error is **rethrown** → Node prints `Error: <message>` **plus the stack trace**, process exits 1 |
556
+ | Exit | **1** (or **2** for concurrency) | **1** (uncaught exception; Node's default) |
557
+ | Runtime log | No `error` line | `[<iso>][ERROR][<pid>] cli error: <message>` |
558
+ | Retryable? | Usually not; **exit 2 is the retryable signal** | No |
559
+
560
+ Two specialized shared handlers implement the refusal vocabulary:
561
+ - `handleConcurrentConflict(err, lang)` → i18n concurrent line, `exit 2`.
562
+ - `handleAddStageError(err, lang)` (shared by `stage create` and `flow stage add`): concurrency → exit 2; `StageDirConflictError` → `common.stageDirConflictTmpl {stage, other}` + exit 1; other → `common.errorTmpl {msg}` + exit 1.
563
+
564
+ `flow advance`'s REV gate is the canonical *refusal that `--force` cannot override*: `--force` adds `forceRevRefused` but the command still exits 1. Similarly `flow archive`'s gates (`all-done`, no blocking open REV) reject with a structured, enumerable report.
565
+
566
+ ---
567
+
568
+ ## 7. Logging & observability contract
569
+
570
+ Four **independent** log surfaces. They must not be conflated.
571
+
572
+ | Surface | Path | Writer | Purpose |
573
+ |---|---|---|---|
574
+ | CLI runtime log | `~/.openfeel/cli/logs/openfeel-YYYY-MM-DD.log` | `runtime-log.ts` | Cross-project process diagnostics (argv, exit, errors) |
575
+ | Public/audit log | `<project>/.openfeel/log/YYYY/MM/DD/<date>-<user>-NNN.md` (+ `day_index.md`, `index.md`, `log.md`) | `public-logger.ts` | Team-level audit trail of milestone/state events |
576
+ | Pipeline audit | `<project>/.openfeel/flow.json` → `log[]` | `FlowManager.appendLog` | Machine-readable pipeline state audit |
577
+ | Agent metrics | `<project>/.openfeel/metrics.json` | `MetricsStore` | Per-agent duration/success aggregates |
578
+
579
+ Plus one deployment record: `~/.openfeel/update_infos.md` and `~/.openfeel/update_state.json` (consumed by `setup --check`).
580
+
581
+ ### 7.1 Runtime log — exact format
582
+
583
+ **Destination resolution** (`resolveRuntimeLogConfig(env, argv)`):
584
+
585
+ | Setting | Rule | Default |
586
+ |---|---|---|
587
+ | enabled | disabled iff argv contains `--no-log` **or** `OPENFEEL_LOG === '0'` **or** `OPENFEEL_NO_LOG === '1'` | enabled |
588
+ | minLevel | `'debug'` iff argv contains `--debug` **or** `OPENFEEL_DEBUG === '1'`, else `'info'` | `info` |
589
+ | filePath | `--log-file <p>` / `--log-file=<p>` → `resolve(p)`; else non-empty `OPENFEEL_LOG_FILE` → `resolve(it)`; else `~/.openfeel/cli/logs/openfeel-<local YYYY-MM-DD>.log` | per-day |
590
+
591
+ **Line format** (byte-exact, one line per event, always terminated by `\n`, always UTF-8):
592
+
593
+ ```
594
+ [<new Date().toISOString()>][<LEVEL>][<process.pid>] <message>
595
+ ```
596
+
597
+ `LEVEL` is uppercase: `DEBUG` | `INFO` | `WARN` | `ERROR`. Example:
598
+
599
+ ```
600
+ [2026-02-11T09:41:07.123Z][INFO][24812] cli start: flow advance --stage v1-stage-03 --to exec_running [project=C:\work\proj] [session=env:9f2c…]
601
+ [2026-02-11T09:41:07.460Z][INFO][24812] cli done exit=0 [duration_ms=337] [project=C:\work\proj] [session=env:9f2c…]
602
+ ```
603
+
604
+ **Emitted messages** (the complete set today):
605
+
606
+ | Level | Message | Emitted by |
607
+ |---|---|---|
608
+ | `INFO` | `cli start: <argv joined with single spaces>` + meta suffix | `logCliStart(argv)` at `runCli()` start |
609
+ | `INFO` | `cli done exit=<code>` + ` [duration_ms=<n>]` + meta suffix | `logCliDone(code)` (one-shot) |
610
+ | `ERROR` | `cli error: <error message>` | `handleCliError` (rethrown path only) |
611
+ | `DEBUG` | `metrics skip (no baseline): stage=<s> phase=<p>` | `flow advance` metrics windowing |
612
+ | any | `runtimeLog(level, message)` — general API used by `flow.ts` (`import { runtimeLog }`) | |
613
+
614
+ **Meta suffix** (identical on start and done, appended in this order, bracketed `key=value` so paths with spaces are safe):
615
+
616
+ ```
617
+ [project=<process.cwd() absolute>] [session=<getSessionId()>]
618
+ ```
619
+
620
+ **The three observability fields**
621
+
622
+ | Field | Meaning | Source | Caveat |
623
+ |---|---|---|---|
624
+ | `duration_ms` | Wall-clock milliseconds from **module-load time of `runtime-log.ts`** (`PROCESS_START_MS = Date.now()` at import) to the moment the `cli done` line is written. Covers module resolution/loading of the command plus the whole command execution; excludes Node bootstrap and anything earlier. Not reset by reinstalling the log config. | `Date.now() - PROCESS_START_MS` | Present **only** on the `cli done` line. |
625
+ | `project` | `process.cwd()` at the time of writing — the *invocation* directory, not a detected workspace root. | `process.cwd()` | Commands themselves also use `process.cwd()` as the project path, so they agree. |
626
+ | `session_id` | Correlated run identity, prefixed by provenance so automation vs. human is machine-distinguishable. | `getSessionId()` (§7.2) | Memoized per resolved `projectPath` for the process lifetime. |
627
+
628
+ **Write discipline**: `mkdirSync(dirname, {recursive:true})`; `appendFileSync(file, line, {encoding:'utf-8'})` inside an advisory lock at `~/.openfeel/locks/runtime-log.lock`; **any** failure (disk, permission, lock timeout) is swallowed — logging never aborts the CLI. Logging is **best-effort and non-transactional**; there is no fsync and no ordering guarantee against stdout.
629
+
630
+ **One-shot semantics**: `cliDoneLogged` is set **before** attempting the write, so exactly one `cli done` line is produced per process even when the log is disabled; the `exit` hook covers `process.exit()`, normal termination and uncaught exceptions, and does **not** double-write after the normal path.
631
+
632
+ **Library default = disabled**: `config` is `null` until `installRuntimeLog()` is called (only `bin/openfeel.js` calls it). Importing the module in-process (tests, embedding) is therefore a no-op. There is intentionally **no** `VITEST`/env guard.
633
+
634
+ **Rotation / retention**: **none**. One file per local day, never trimmed, never compressed, never deleted. `--log-file` may point anywhere (including an absolute file with a different name); with it, all days share one file.
635
+
636
+ **Not logged** (deliberate): stdout/stderr content, JSON payloads, i18n messages, user input from the REPL, and commander parse-time errors (they never reach `handleCliError`).
637
+
638
+ ### 7.2 `session_id` derivation (three-tier, first hit wins)
639
+
640
+ 1. **env** — `CLAUDE_CODE_SESSION_ID` (the only registered candidate). Value is `.trim()`ed and must be non-empty. Result id = `env:<value>`, `source='env'`. Side effect: best-effort write of `<project>/.openfeel/tmp/session.json` = `{ "session_id": "<value>", "source": "env", "started_at": "<ISO>" }` (skipped when the file already holds the same value; all failures silent).
641
+ 2. **project** — read `<project>/.openfeel/tmp/session.json` → `session_id` (string, trimmed, non-empty). Result id = `project:<value>`, `source='project'`.
642
+ 3. **proc** — `proc:<first 8 chars of randomUUID()>`, `source='proc'`. **Never persisted** (keeps the "single process" semantic; prevents an automated run from being frozen into a cross-process session).
643
+
644
+ No TTL, no rotation, no expiry logic — the id changes only when the env value changes. `resetSessionCache()` clears the memo (tests only).
645
+
646
+ ### 7.3 Public audit log (`PublicLogger`)
647
+
648
+ - **Path**: `<project>/.openfeel/log/<yyyy>/<MM>/<dd>/<yyyy-MM-dd>-<username>-<NNN>.md`.
649
+ - `<username>`: `.openfeel/.info.json` → `user`; fallback `unknown`.
650
+ - `<NNN>`: 3-digit zero-padded, **daily-recomputed** as *max existing NNN for that user/day + 1*, reserved atomically via `reserveSequence` (O_EXCL) under `.openfeel/tmp/locks/log.lock` (so a concurrently reserved empty placeholder still consumes a number).
651
+ - Future writes are **always** nested `yyyy/MM/dd/`. Legacy flat `log/yyyy-MM-dd/` directories are **never migrated, rewritten or deleted** — they are only read-scanned into the root index with a layout tag.
652
+ - **File content** (exact template):
653
+ ```
654
+ # <yyyy-MM-dd>-<username>-<NNN>
655
+
656
+ - **时间**:<ISO 8601>
657
+ - **操作者**:<username>
658
+ - **事件类型**:<状态变更|审查事件|测试事件|里程碑>
659
+ - **操作**:<action>
660
+ - **关联操作**:<opId> ← only when provided
661
+ - **变更前**:<from> ← only when provided
662
+ - **变更后**:<to> ← only when provided
663
+
664
+ ## 补充信息 ← only when extra has keys
665
+ - **<key>**:<String(value)>
666
+ ```
667
+ - **Index maintenance** (same critical section as the entry write):
668
+ - `<day>/day_index.md`: header `# <date>` + `## 日志列表` + `| 文件 | 操作者 | 描述 |`, then appended rows `| [<file>](<file>) | <user> | <desc> |` (idempotent by filename).
669
+ - `<project>/.openfeel/log/index.md`: `# 日志索引` / `## 日期索引` / `| 日期 | 摘要 |`; adds one row per day (`| [<date>](<yyyy>/<MM>/<dd>/day_index.md) | <desc> <nestedTag> |`) plus read-only legacy rows (`| [<name>](<name>/day_index.md) | … <legacyTag> |` or `| <name> | … |` when no `day_index.md`); each row added only if absent.
670
+ - `<project>/.openfeel/log/log.md`: `# 最近日志` / `| 文件 | 用户 | 描述 |`; new rows inserted **at the top** (newest first), file **truncated to the 30 most recent data rows** — this is the only retention limit anywhere.
671
+ - **Failure policy**: any error → `console.warn(t('flow.log.writeFailedTmpl', {err}))` only; the pipeline is never blocked.
672
+ - **Event vocabulary** (arg → 事件类型): `logPhaseChange`→`状态变更`, `logReviewEvent`→`审查事件`, `logTestEvent`→`测试事件`, `logMilestone(title, extra)`→`里程碑` with `action = extra.action ?? 'milestone'` and all other extra fields merged.
673
+ - **Short description** for indexes (`getShortDesc`): `opId` prefix (if any) + one of `advance_phase`→`阶段推进` + `from → to` / `attempt_pass`→`执行通过` / `attempt_fail_retry`→`执行失败,重试中` / `attempt_fail_exhausted`→`执行失败,重试耗尽` / `auto_fix_review`→`自动修复审查` / `stage_completed`→`extra.title` or `阶段完成` / default → the raw action; joined by spaces, falling back to the event type.
674
+
675
+ ### 7.4 Pipeline audit (`flow.json.log[]`)
676
+
677
+ Append-only array of `{ time: string, agent: string, action: string, detail: Record<string, unknown> }`. `time` is filled in by `appendLog`; the display layer normalizes legacy agent names (`normalizeAgentName`) and renders `time.substring(0,19).replace('T',' ')`. Observed actions include `register_stage`, `advance_stage_phase`, `reset_stage_phase`, `review_add`, `review_accept`, `archive_stage`, `archive_flow_version`, `requirement_*`. This array participates in `flow.json`'s optimistic `meta.revision` concurrency check; it is also **archived** by `flow archive <version>` (`log` count) — that is why `flow log`/`flow status` counts can drop after archiving.
678
+
679
+ ### 7.5 Agent metrics (`metrics.json`)
680
+
681
+ `<project>/.openfeel/metrics.json`, atomic write, **no lock** (low concurrency, non-critical):
682
+
683
+ ```json
684
+ { "agents": { "openfeel-executor": { "agentName": "openfeel-executor", "totalRuns": 3, "successfulRuns": 3, "failedRuns": 0, "totalRetries": 0, "totalDurationMs": 42000, "avgDurationMs": 14000 } } }
685
+ ```
686
+
687
+ `avgDurationMs = round(totalDurationMs / totalRuns)`. A "run" is a **phase window** (time between entering and exiting a phase), not pure agent execution time — this caveat is printed as `metrics.summary.caliberNote`.
688
+
689
+ ### 7.6 Passive deployment hint (stderr, not a log)
690
+
691
+ `emitGlobalDeployCheck()` runs once per process (from `runCli` and once from `startRepl`). Gate `shouldRunDeployCheck` — output is **silenced** when any of: not a TTY; already warned this process; argv contains `--json` or `--quiet`; argv contains `--version|-v|--help|-h`; the first non-option token is one of `setup|update|init|migrate`; `CI` or `OPENFEEL_NO_UPDATE_CHECK` is truthy (`1`/`true`, case/space-insensitive — other values such as `CI=yes` are *not* recognized, the non-TTY check covers those). When the gate passes: `status === 'mismatch'` → `update.globalStaleWarnTmpl {deployed, cli}` on stderr; `missing` → `update.globalMissingWarnTmpl`; `ok`/`unknown` → silent. Exit code is **never** changed; all exceptions are swallowed.
692
+
693
+ ---
694
+
695
+ ## 8. `view` / board rendering
696
+
697
+ ### 8.1 `view list [--op <id>]` (text only)
698
+
699
+ Data: review entries from `flow.json.reviews`, optionally filtered by exact `op`, **sorted by `filed_at` descending**. Empty (or flow.json not loaded) → `view.list.empty` (`No review entries`). Otherwise, per entry, exactly two lines:
700
+
701
+ ```
702
+ <id> [<status>] <priority> — <title>
703
+ <common.op>: <op> <view.list.filedBy>: <filed_by normalized> <view.list.filedAt>: <filed_at>
704
+ ```
705
+
706
+ - `<status>` is the raw stored value (`open` | `resolved` | `closed`).
707
+ - `<priority>` raw (`high` | `medium` | `low`).
708
+ - Separator is an em dash `—` preceded by one space.
709
+ - `filed_by` passes through `normalizeAgentName` (legacy agent names → `openfeel-*`).
710
+ - No JSON, no flags besides `--op`. Errors → `common.errorTmpl`, exit 1.
711
+
712
+ ### 8.2 `view accept <rev-id>` (text only)
713
+
714
+ Marks a review `closed` (via upsert + `review_accept` audit log) and prints `view.accept.okTmpl {id}`. Unknown id → `view.accept.errorNotFoundTmpl {id}` on stderr, **exit 1**. Exceptions → `common.errorTmpl`, exit 1. This is the only mutation in the `view` group; all other review mutation lives in `flow review *`.
715
+
716
+ ### 8.3 `flow overview` (the board/progress view) — text
717
+
718
+ Fixed upward-drawing order; a reimplementation must keep section order and markers:
719
+
720
+ 1. Banner box (42-wide): title padded to 38 + 14 spaces.
721
+ 2. `Current Status` — stage phase, `stage.op` or `(none)`, retry count (suffix localized: `times` in English, `次` when the localized word is not `retry`).
722
+ 3. `Stages Overview` — `Total stages (n):` then per stage ` <id>: phase=<phase> <bar> <done>/<total> <marker>` where bar = `█`×done + `░`×(total−done), `(no ops)` when total is 0, marker `← Current` for the current stage.
723
+ 4. `Reviews` — `Open: n(Blocking b / Non-blocking c)`, `Resolved: n`, `Closed: n`, then per **open** review ` <🔴|🟡> [<↑|=|↓>] <id>: <title> (<op>)`.
724
+ 5. `Bugs` — parsed from `.openfeel/bugs/index.md` (`open:`/`closed:` numbers) → `Open: n Closed: n`; unreadable → `bugUnreadable`; missing → `bugUninitialized`.
725
+ 6. `Recent Logs` — last 5 `flow.json.log` entries **reversed**: ` [<time 19>] <agent>: <action>`.
726
+ 7. `Health` — `✅ n ⚠️ m ❌ k`, then ` ❌ <section>: <message>` per failing item.
727
+ 8. Closing rule `════════════════════════════════════════════` (44).
728
+
729
+ ### 8.4 `req board` — the requirement board
730
+
731
+ Text (7 groups always, in `REQ_STATUSES` order, empty groups included):
732
+
733
+ ```
734
+ draft (0)
735
+ scheduled (2)
736
+ - REQ-001 Add login (claimed by alice)
737
+ in_progress (1)
738
+ done (0)
739
+ blocked (0)
740
+ cancelled (0)
741
+ rejected (0)
742
+ ```
743
+
744
+ Templates: `req.board.groupTmpl` = `{status} ({n})`; `req.board.itemTmpl` = ` - {id} {title}`; `req.board.ownedSuffixTmpl` = ` (claimed by {owner})` appended when `owner` is set. JSON: `{schemaVersion:1, groups:{…}}` (§5.13).
745
+
746
+ ### 8.5 `flow status` / `stage status` / `flow recover` progress rendering
747
+
748
+ - `flow status` (plain) = 10-line summary: `OpenFeel Pipeline Status` header, `Global Status: <meta phase>`, `Current Stage: <stage> — Stage Phase: <phase>`, `Current Op: <stage.op>`, `Retry Count`, `Stages`, `Ops` (**draft ops excluded** from the count), `Pending Reviews`, `Total Logs`, `Last Updated`. Then `\nCurrent Active Stage: …` and `\nStage Duration:` per-stage lines.
749
+ - `stage status` (list) = `Found n stage(s)` then ` <icon> <stageId> → <status>` (two spaces around `→`), icons `✅`/`🔧`/`📋`/` `.
750
+ - `flow recover` = box-less report: title, global status, phase, current op, stage status, optional blockedBy, numbered pending tasks, per-stage duration with `(completed|in progress)`, closing `═══════════════════════════`.
751
+
752
+ ---
753
+
754
+ ## 9. `lint`
755
+
756
+ ### 9.1 `lint i18n`
757
+
758
+ Validates that the two i18n tables are key-complete and non-empty. Builds `zhMap` from `allDomains` of `i18n-data/zh-CN.ts` (value = `entry.zh`) and `enMap` from `i18n-data/en.ts` (value = `entry.en`), then detects four problem classes:
759
+
760
+ | Class | Condition | Message key / en template |
761
+ |---|---|---|
762
+ | empty zh | `entry.zh === ''` in the zh table | `lint.i18n.emptyZhTmpl` → `[empty zh-CN] {key}` |
763
+ | empty en | `entry.en === ''` in the en table | `lint.i18n.emptyEnTmpl` → `[empty en] {key}` |
764
+ | zh-only key | present in zhMap, absent in enMap | `lint.i18n.zhOnlyTmpl` → `[zh-CN only] {key}` |
765
+ | en-only key | present in enMap, absent in zhMap | `lint.i18n.enOnlyTmpl` → `[en only] {key}` |
766
+
767
+ - No problems → one line `lint.i18n.okTmpl` → `✅ {n} key(s) consistent`, exit 0.
768
+ - Problems → `lint.i18n.failTitleTmpl` → `❌ {m} problem(s)`, then each problem indented with two spaces, then `lint.i18n.consistentTmpl` → `({n} consistent / {total} total keys)`; **`process.exitCode = 1`**.
769
+ - `consistentCount` = keys present in both maps with non-empty values; `total` = `zhMap.size`.
770
+
771
+ ### 9.2 `lint kb`
772
+
773
+ Scans `.openfeel/kb/*.md` (top level, `.md` only) for file references and reports references whose target no longer exists.
774
+
775
+ - Missing directory → `lint.kb.noDir` → `(.openfeel/kb/ directory does not exist)`, exit 0 (nothing to check).
776
+ - Otherwise prints `lint.kb.scanTmpl` → `Scanning file references in {n} kb file(s)...`.
777
+ - **Extraction** per line, two sources:
778
+ - markdown links — `/\[[^\]]*\]\(([^)]+)\)/g` (group trimmed);
779
+ - inline code — /`` `([^`]+)` ``/g (group trimmed).
780
+ - **Resolution** (`resolveRef`) — returns `null` (skipped, not counted) when: empty; starts with `https?:`, `mailto:`, `#`, `/`, `\`, `~`; contains any of `{}*<>(),\`; contains `..`; contains whitespace; has `/` but no known prefix from `src/ test/ docs/ scripts/ bin/ schemas/ .openfeel/ .opencode/ kb/ ./`. `kb/…` maps to `<project>/.openfeel/kb/…`; `./…` maps to `<kbDir>/…`; other prefixed paths map to `<project>/…`. Fragment `#…` and markdown `"title"` suffixes are stripped first. Without a separator: try `<kbDir>/<p>`; if it exists → that path; if the source was a **link**, still return `<kbDir>/<p>` (so a missing target is reported); if the source was inline **code**, also try `<project>/<p>` and only then give up (`null` — avoids false positives for bare filenames).
781
+ - Every non-null resolution increments the checked counter; a nonexistent target is recorded as stale.
782
+ - No stale → `lint.kb.okTmpl` → `✅ No stale references (checked {n} reference(s))`, exit 0.
783
+ - Stale → `lint.kb.staleTmpl` → `❌ Found {n} stale reference(s)`, then one line per entry via `lint.kb.staleItemTmpl` → ` {file} L{line}: "{ref}" → file not found`; **`process.exitCode = 1`**.
784
+
785
+ Neither subcommand writes anything.
786
+
787
+ ---
788
+
789
+ ## 10. Appendix
790
+
791
+ ### A. Enumerations (single-source constants — never re-hardcode)
792
+
793
+ | Constant | Values | Defined in |
794
+ |---|---|---|
795
+ | `PIPELINE_PHASES` (15) | `plan_pending, plan_review, plan_passed, scheme_pending, scheme_review, scheme_passed, exec_running, review_pending, review_failed, review_passed, test_pending, test_failed, test_passed, archiving, done` | `core/pipeline-schema.ts` |
796
+ | `META_PHASES` | `active, paused, done` | `core/pipeline-schema.ts` |
797
+ | `REVIEW_TIERS` | `full, lean, skip` | `core/pipeline-schema.ts` |
798
+ | `TIER_PRUNED_PHASES` | full→`[]`, lean→`[scheme_review]`, skip→`[plan_review, scheme_review]` | `core/pipeline-schema.ts` |
799
+ | `MODE_DERIVED_TIER` | full→`full`, fast→`lean`, general→`full` | `core/flow-manager.ts` |
800
+ | `STAGE_STATUS_VALUES` | `planned, review_failed, review_passed, testing, archiving, done` | `core/flow-manager.ts` |
801
+ | `WORKFLOW_MODES` | `full, fast, general` (default `fast`) | `core/config.ts` |
802
+ | `REQ_STATUSES` (7) | `draft, scheduled, in_progress, done, blocked, cancelled, rejected` | `core/req/types.ts` |
803
+ | `REQ_PRIORITIES` | `P0, P1, P2` | `core/req/types.ts` |
804
+ | `REQ_ID_PATTERN` | `/^REQ-(\d{3,})$/` | `core/req/types.ts` |
805
+ | Review status | `open, resolved, closed` | flow.json data |
806
+ | Review priority | `high, medium, low` | flow.json data |
807
+ | `PHASE_PATH_MAX_DEPTH` | `8` | `core/flow-manager.ts` |
808
+ | `ARCHIVE_SCHEMA_VERSION` | `1` | `core/flow-archive/archive-file.ts` |
809
+ | Tool ids | `opencode` (enabled), `claude` | `core/update.ts` |
810
+ | KB categories | `architecture, patterns, troubleshooting, setup` | `core/workspace/knowledge.ts` |
811
+ | Config sources | `status.md, config.yaml, profile.yaml, builtin, requirement` | `core/flow-manager.ts` |
812
+ | public-log event types | `状态变更, 审查事件, 测试事件, 里程碑` | `core/public-logger.ts` |
813
+
814
+ ### B. Filesystem layout touched by the CLI
815
+
816
+ ```
817
+ <project>/
818
+ .openfeel/
819
+ flow.json # pipeline state, reviews, requirements, log[] (meta.revision = optimistic lock)
820
+ config.yaml # defaults: execution_mode | auto_advance | merge_mode | workflow_mode
821
+ pipeline.yaml # optional phases/transitions/checkpoint_mapping/phase_corrections/fast_transitions
822
+ .info.json # { user, lang, ... } — source of public-log username
823
+ metrics.json # agent aggregates
824
+ plan/<series>/<stageDir>/{overview.md,status.md,ops/op-NNN.md,ops/deps.yaml}
825
+ requirements/REQ-NNN.md # human-readable requirement body (flow.json is authoritative)
826
+ checkpoints/<stage>-<ts>-<phase>.json
827
+ log/{yyyy}/{MM}/{dd}/{date}-{user}-{NNN}.md + day_index.md ; log/index.md ; log/log.md ; log/archive-<stage>.md
828
+ log/{yyyy-MM-dd}/ # LEGACY flat layout — read-only, never migrated
829
+ flow-archive/<version>.json # flow.json version shards (lock: tmp/locks/flow-archive.lock)
830
+ kb/{index.md,architecture.md,patterns.md,troubleshooting.md,setup.md}
831
+ roadmap/v<version>.md
832
+ bugs/index.md
833
+ dev/{dev_core.md,current.md,decisions.md}
834
+ tmp/{session.json,status.<stage>.bak,locks/*.lock}
835
+ backup/ # migrate rollback manifests
836
+ opencode.jsonc
837
+ AGENTS.md
838
+ .opencode/{agents,skills}/...
839
+ ~/.openfeel/
840
+ cli/logs/openfeel-YYYY-MM-DD.log
841
+ config.json ; update_state.json ; update_infos.md ; schemas/ ; backup/ ; locks/{runtime-log,log,...}.lock
842
+ ~/.config/openfeel/profile.yaml
843
+ ~/.config/opencode/{AGENTS.md,opencode.jsonc,agents/,skills/,openfeel/core.md(legacy)}
844
+ ~/.claude/{CLAUDE.md,settings.json,agents/,skills/}
845
+ ```
846
+
847
+ ### C. Locking & atomicity constants
848
+
849
+ - Advisory file lock: `openSync(path,'wx')` + `{pid,time,token}`; stale reclaim by atomic rename after `staleMs`; release only when the token matches.
850
+ - `timeoutMs = 5000`, `staleMs = 3000`, `initialBackoffMs = 10`, `maxBackoffMs = 500`, backoff ×2 with ±20 % jitter.
851
+ - Project locks: `.openfeel/tmp/locks/{log,status-<stageDir>,flow,...}.lock`; global locks: `~/.openfeel/locks/*.lock`.
852
+ - `flow.json` uses **optimistic concurrency** on `meta.revision`: mismatch throws the concurrent-write error → CLI exit **2** (the retryable signal).
853
+ - Writes are atomic (`atomicWriteFileSync`); backups: `status.<stage>.bak`, `flow.json.bak`, `flow.json.v4.0.bak`, migrate `backup/` manifests.
854
+
855
+ ### D. i18n resolution (affects every message)
856
+
857
+ `t(key, lang, vars)` looks up the `lang`-specific map (`zh` field for `zh-CN`, `en` field for `en`), substitutes `{name}` placeholders, and on a miss prints `[i18n] Missing key: "<key>" for lang="<lang>"` to stderr, retries `zh-CN`, then returns the key itself. `hasKey(key, lang)` is the warning-free probe used by `applyHelpI18n`. `getCliLang(project)` = `.info.json` `lang` > `~/.openfeel/config.json` `lang` > `zh-CN`; values outside `{zh-CN, en}` are ignored. `applyHelpI18n` rewrites `description()` for commands, `help.<cmdPath>` keys, `help.<cmdPath>.arg<name>` for arguments, and `help.<cmdPath>.<camelCaseFlag>` for options — **only when the key exists**, and it skips negated (`--no-*`) options.
858
+
859
+ ---
860
+
861
+ ## 11. Reproduction priority for a DSH tool surface
862
+
863
+ Ranked by how much of the product's behavior a reimplementation must own:
864
+
865
+ 1. **`flow` read commands** — `status --json`, `current --json`, `overview --json`, `phases --json`, `mode --json`, `ops list --json`, `log`, `recover`, `health --json`. These are the agent-facing "what is happening now" surface; all are read-only, all have stable `schemaVersion:1` shapes.
866
+ 2. **`flow advance` / `flow attempt` / `flow stage reset`** — the state machine. Unique-path BFS (depth 8), REV blocking that `--force` cannot override, tier pruning, multi-step partial-failure semantics, metrics windowing, `--dry-run`, `--quiet`. This is the hardest and most behavior-defining command.
867
+ 3. **`req` group** — 11 coherent verbs over one entity with a frozen transition table, deterministic `next` ordering, zero-write read verbs (`list/show/board/next --json`), and a strict "command layer validates nothing, engine decides" split.
868
+ 4. **`stage` group + `plan scheme`** — the status.md/ops file-level write path (probe-before-write, `.bak` before mutation, anchors, `--dry-run`).
869
+ 5. **Exit-code + JSON-envelope contract** (§5, §6) — exit 0/1/2 semantics and "one JSON document, never mixed with prose" are what automation depends on; get these before any output cosmetics.
870
+ 6. **Runtime log** (§7.1) — cheap to implement, and the `[iso][LEVEL][pid] message` + `duration_ms`/`project`/`session_id` triple is the whole cross-project diagnosability story.
871
+ 7. **`view list` / `view accept` / `req board`** — the rendering layer (§8); small, text-only, and needed for human inspection.
872
+ 8. **`lint i18n` / `lint kb`** — self-check tooling; useful but orthogonal to the product surface.
873
+ 9. **`config`, `knowledge`, `model`, `instructions`, `migrate`, `archive`, `roadmap`, `project`, `init`, `setup`, `update`** — deployment/administration. Reproduce the flag surface and exit codes; their internal templates are large and rarely agent-driven.