@cheongfish/bouncer 0.0.0-stage → 0.1.13

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 (156) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/bin/bouncer +26 -0
  4. package/bin/bouncer-root +25 -0
  5. package/bin/build-stamp.js +78 -0
  6. package/bin/register-path.js +71 -0
  7. package/dist/.build-stamp +1 -0
  8. package/dist/approval-snapshot.js +99 -0
  9. package/dist/bouncer-root.js +80 -0
  10. package/dist/canonical-json.js +37 -0
  11. package/dist/cli-commit-guard-command.js +74 -0
  12. package/dist/cli-config-command.js +47 -0
  13. package/dist/cli-context-review-command.js +273 -0
  14. package/dist/cli-current-command.js +374 -0
  15. package/dist/cli-dispatch-command.js +119 -0
  16. package/dist/cli-doc-commands.js +352 -0
  17. package/dist/cli-epic-command.js +130 -0
  18. package/dist/cli-flags.js +50 -0
  19. package/dist/cli-git-commands.js +931 -0
  20. package/dist/cli-intent-command.js +491 -0
  21. package/dist/cli-metrics-command.js +134 -0
  22. package/dist/cli-ontology-command.js +109 -0
  23. package/dist/cli-project-commands.js +580 -0
  24. package/dist/cli-retro-command.js +138 -0
  25. package/dist/cli-review-command.js +251 -0
  26. package/dist/cli-review-dispatch-command.js +338 -0
  27. package/dist/cli-usage.js +72 -0
  28. package/dist/cli.js +70 -0
  29. package/dist/codex-agents.js +189 -0
  30. package/dist/command-argv.js +125 -0
  31. package/dist/commit-guard.js +72 -0
  32. package/dist/commit-hook.js +470 -0
  33. package/dist/commit-message.js +77 -0
  34. package/dist/commit-sha.js +107 -0
  35. package/dist/commit.js +519 -0
  36. package/dist/composite-gates.js +129 -0
  37. package/dist/comprehension.js +127 -0
  38. package/dist/config.js +298 -0
  39. package/dist/context-ids.js +288 -0
  40. package/dist/context-migration-rewrite.js +421 -0
  41. package/dist/context-ontology.js +746 -0
  42. package/dist/context-review-contracts.js +2 -0
  43. package/dist/context-review-projection.js +147 -0
  44. package/dist/context-review-state.js +824 -0
  45. package/dist/context-review-store.js +602 -0
  46. package/dist/context-review-transitions.js +412 -0
  47. package/dist/coordinate-advance.js +253 -0
  48. package/dist/coordinate-next.js +998 -0
  49. package/dist/coordinate-output.js +57 -0
  50. package/dist/coordinator-command-contracts.js +2 -0
  51. package/dist/coordinator-commands.js +1908 -0
  52. package/dist/coordinator-fanin.js +1021 -0
  53. package/dist/coordinator-git.js +121 -0
  54. package/dist/coordinator-input.js +37 -0
  55. package/dist/coordinator-ledger.js +381 -0
  56. package/dist/coordinator-prepare.js +1047 -0
  57. package/dist/coordinator-repair.js +1416 -0
  58. package/dist/coordinator-verification.js +42 -0
  59. package/dist/coordinator-worker.js +388 -0
  60. package/dist/coordinator.js +7 -0
  61. package/dist/cross-drive.js +85 -0
  62. package/dist/current.js +1148 -0
  63. package/dist/dag-shape.js +93 -0
  64. package/dist/doc-shape-contracts.js +241 -0
  65. package/dist/drive-metrics.js +691 -0
  66. package/dist/epic-index.js +347 -0
  67. package/dist/epic-registry.js +453 -0
  68. package/dist/execute-prepare.js +262 -0
  69. package/dist/execution-contracts.js +46 -0
  70. package/dist/execution-dag.js +148 -0
  71. package/dist/finalize-checks.js +177 -0
  72. package/dist/finalize-collect.js +343 -0
  73. package/dist/finalize-comprehension.js +116 -0
  74. package/dist/finalize-digest-collect.js +388 -0
  75. package/dist/finalize-digest-model.js +595 -0
  76. package/dist/finalize-digest.js +164 -0
  77. package/dist/finalize-explain.js +176 -0
  78. package/dist/finalize-pr.js +610 -0
  79. package/dist/finalize-release-main.js +652 -0
  80. package/dist/finalize.js +714 -0
  81. package/dist/frontmatter.js +22 -0
  82. package/dist/git-ops.js +49 -0
  83. package/dist/graph-exec.js +179 -0
  84. package/dist/graph-scope.js +268 -0
  85. package/dist/graphify.js +995 -0
  86. package/dist/guards.js +82 -0
  87. package/dist/import-git.js +78 -0
  88. package/dist/import-history.js +392 -0
  89. package/dist/import-render.js +59 -0
  90. package/dist/import-types.js +2 -0
  91. package/dist/init.js +571 -0
  92. package/dist/intent-bundle.js +936 -0
  93. package/dist/intent-provenance.js +927 -0
  94. package/dist/layout.js +131 -0
  95. package/dist/lease.js +145 -0
  96. package/dist/legacy-comments.js +71 -0
  97. package/dist/migrate-context-slugs.js +262 -0
  98. package/dist/migrate-merge-legacy-epics.js +392 -0
  99. package/dist/migrate-review-scope.js +144 -0
  100. package/dist/migrate-task-layout.js +194 -0
  101. package/dist/path-overlap.js +38 -0
  102. package/dist/paths.js +45 -0
  103. package/dist/plan-approve.js +218 -0
  104. package/dist/plan-inspect.js +575 -0
  105. package/dist/plan-prerequisites.js +197 -0
  106. package/dist/plan-snapshot-contract.js +98 -0
  107. package/dist/plan-snapshot.js +138 -0
  108. package/dist/plugin-root.js +237 -0
  109. package/dist/posix-path.js +14 -0
  110. package/dist/pre-commit-hook.js +170 -0
  111. package/dist/print-dispatch.js +285 -0
  112. package/dist/render.js +8 -0
  113. package/dist/retention-migration.js +595 -0
  114. package/dist/retro-read.js +259 -0
  115. package/dist/retro-sources.js +318 -0
  116. package/dist/review-dispatch.js +1054 -0
  117. package/dist/review-policy.js +28 -0
  118. package/dist/review-record.js +447 -0
  119. package/dist/review-repair-state.js +52 -0
  120. package/dist/review-snapshot.js +659 -0
  121. package/dist/run-detach.js +165 -0
  122. package/dist/run-preflight.js +368 -0
  123. package/dist/runtime-state.js +1038 -0
  124. package/dist/scaffold.js +533 -0
  125. package/dist/schema.js +294 -0
  126. package/dist/scope.js +1073 -0
  127. package/dist/seed-worktree.js +926 -0
  128. package/dist/session-graph.js +337 -0
  129. package/dist/subagents.js +86 -0
  130. package/dist/symbol-index.js +1390 -0
  131. package/dist/task-brief-hash.js +34 -0
  132. package/dist/task-commits.js +124 -0
  133. package/dist/tasks-docs.js +192 -0
  134. package/dist/templates.js +469 -0
  135. package/dist/time.js +21 -0
  136. package/dist/validate-docs.js +302 -0
  137. package/dist/validate-gates-finalize.js +303 -0
  138. package/dist/validate-gates-plan.js +720 -0
  139. package/dist/validate-gates-run.js +420 -0
  140. package/dist/validate-gates-shared.js +124 -0
  141. package/dist/validate-gates.js +87 -0
  142. package/dist/validate-sections.js +654 -0
  143. package/dist/validate-structural.js +347 -0
  144. package/dist/validate.js +417 -0
  145. package/dist/verification-contracts.js +26 -0
  146. package/dist/verification-evidence.js +233 -0
  147. package/dist/verification-execution.js +163 -0
  148. package/dist/verification-identity.js +413 -0
  149. package/dist/verification-target.js +51 -0
  150. package/dist/verification.js +383 -0
  151. package/dist/windows-npm.js +38 -0
  152. package/package.json +32 -4
  153. package/resources/graphify-compat.json +9 -0
  154. package/vendor/README.md +23 -0
  155. package/vendor/js-yaml.LICENSE +21 -0
  156. package/vendor/js-yaml.js +3022 -0
@@ -0,0 +1,338 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ // 표식 확인용 가벼운 모듈. review-dispatch 그래프는 끌어오지 않는다(lazy require 계약).
6
+ const frontmatter = require("./frontmatter");
7
+ const schema = require("./schema");
8
+ const cliFlags = require("./cli-flags");
9
+ const { parseFlags, argvRequestsHelp } = cliFlags;
10
+ // validate-sections는 ./paths 계열만 정적으로 끌어오므로(intent 모듈 없음) help 문자열을
11
+ // 검증기 상수에서 만들어도 ./review-dispatch lazy require 계약이 깨지지 않는다.
12
+ const validateSections = require("./validate-sections");
13
+ const { CONTEXT_REVIEW_PERSPECTIVE, CONTEXT_REVIEW_STATUS, REVIEW_SEVERITY, findingFingerprint, REPORT_FINDING_STATUS, FINDING_ACTIONABILITY, FINDING_ORIGIN, CONTEXT_ROUND_MODES, } = validateSections;
14
+ const USAGE = `usage: bouncer review-dispatch <plan|execute> [options]
15
+
16
+ review-dispatch plan --blueprint <dir> [--previous <file>]
17
+ Classify plan context-review strategy (read-only JSON).
18
+ review-dispatch execute --blueprint <dir> [--task <ddd>] --base <sha> --head <sha> [--mode discovery|delta]
19
+ Classify execute review strategy from the frozen final worktree state
20
+ (JSON). Writes only the snapshot artifact under .bouncer/runtime/
21
+ review-snapshots/ and returns target.snapshot_id/snapshot_path; --head
22
+ must be the current HEAD. On failure it prints reason/cause/next, exit 1.
23
+ When that blueprint's scale is light, returns single/combined
24
+ regardless of diff size. A blueprint with review_contract brief gets the
25
+ brief-compliance perspectives; only it accepts --mode delta without --task.
26
+ `;
27
+ // 예시 finding의 구성 요소. fingerprint는 아래에서 검증기와 같은 함수로 계산해
28
+ // 손으로 쓴 값이 공식과 어긋나는 일(fingerprint mismatch)을 없앤다.
29
+ const EXAMPLE_FINDING = {
30
+ category: 'scope',
31
+ brief_clause: 'tasks/001 touch',
32
+ file: '.bouncer/context/epics/014-auth/blueprints/001-signup/tasks/001/tasks.md',
33
+ symbol: 'touch',
34
+ };
35
+ /**
36
+ * `review-dispatch --help`에 실리는 `bouncer.context_review` 예시. 들여쓰기는
37
+ * `bouncer:` 아래에 붙인 `context_review:` 블록 그대로이고, `<target.digest>` 두 곳만
38
+ * `review-dispatch plan` 출력 값으로 바꾸면 기록 검증을 통과한다. fence는 하나만 둔다.
39
+ */
40
+ const HELP_CONTEXT_REVIEW_EXAMPLE = `context_review:
41
+ rounds:
42
+ - round: 1
43
+ mode: discovery
44
+ target: { digest: <target.digest> }
45
+ perspectives:
46
+ - { name: combined, target_digest: <target.digest> }
47
+ severity_changes: []
48
+ findings:
49
+ - id: CR-1
50
+ severity: minor
51
+ status: accepted
52
+ note: Wording only; the clause is still unambiguous.
53
+ category: ${EXAMPLE_FINDING.category}
54
+ brief_clause: ${EXAMPLE_FINDING.brief_clause}
55
+ file: ${EXAMPLE_FINDING.file}
56
+ symbol: ${EXAMPLE_FINDING.symbol}
57
+ fingerprint: ${findingFingerprint(EXAMPLE_FINDING, 'context')}
58
+ actionability: advisory
59
+ origin: discovery
60
+ first_seen_round: 1
61
+ last_seen_round: 1`;
62
+ /**
63
+ * `review-dispatch --help` stdout 본문. plan 에이전트가 검증기 소스를 열지 않고
64
+ * 기록 검증을 통과하는 기록을 쓰도록 형식·enum·digest 출처를 한곳에 모은다.
65
+ */
66
+ const HELP = `${USAGE}
67
+ Context review record shape (plan). Reports are recorded with
68
+ \`bouncer context-review record\`; the projection it returns on --status accepted has
69
+ this shape and goes under \`bouncer:\` in context-review.md. Do not hand-write rounds.
70
+
71
+ \`\`\`yaml
72
+ ${HELP_CONTEXT_REVIEW_EXAMPLE}
73
+ \`\`\`
74
+
75
+ Enums:
76
+ severity: ${REVIEW_SEVERITY.join('|')}
77
+ finding status (context-review.md): ${CONTEXT_REVIEW_STATUS.join('|')} (accepted requires note)
78
+ finding status (record input): ${REPORT_FINDING_STATUS.join('|')} (open is in-progress only)
79
+ actionability: ${FINDING_ACTIONABILITY.join('|')}
80
+ origin: ${FINDING_ORIGIN.join('|')}
81
+ round mode: ${CONTEXT_ROUND_MODES.join('|')}
82
+ perspective name (also finding category): ${CONTEXT_REVIEW_PERSPECTIVE.join('|')}
83
+
84
+ fingerprint formula: context:lower(category):lower(brief_clause):posix(file)#symbol
85
+
86
+ Do not compute the digest. Copy \`target.digest\` from the JSON that
87
+ \`review-dispatch plan --blueprint <dir>\` prints into both <target.digest>
88
+ places. When it prints \`ok: false\` there is no digest: do not call a reviewer.
89
+
90
+ Plan fields (\`single\` or \`perspectives\`; older stored \`clustered\` payloads are still read;
91
+ absent on \`skip\` and failures):
92
+ A blueprint with review_contract brief gets strategy \`perspectives\`: full runs consistency, scope_minimality,
93
+ verifiability and risk_security in parallel; light runs verifiability only (one reviewer call per perspective).
94
+ snapshot_version 2. target.digest hashes the ordered {file, body, structure} of
95
+ every plan document; status/title/timestamp/commit evidence are excluded
96
+ parts per-document body sha256 (epic index, blueprint index, each tasks.md)
97
+ structure_parts per-document sha256 of the normalized judgement structure
98
+ (task: affected_paths, depends_on, parallel_safe, dependency_gate,
99
+ exclusive_resources, execution_kind; blueprint: scale, review_scope)
100
+ scope_parts observation only (Interface/Touch/affected_paths/depends_on hash);
101
+ never used to choose follow_up
102
+ documents_content path -> {body, structure}; the immutable content a later
103
+ \`--previous\` run diffs against (no git or working-tree lookup)
104
+ follow_up \`full\` | \`partial\` — compare with \`--previous <file>\` (the
105
+ stored prior plan JSON). Omit \`--previous\` only for a first
106
+ classification with no review evidence → \`full\`, every current
107
+ document in changed_documents, before null.
108
+ changed_documents sorted union of added/removed/body-changed/structure-changed paths
109
+ related_documents contract references for the delta reviewer: changed tasks' previous
110
+ and current cluster neighbours, DAG predecessors/successors (transitive),
111
+ epic and blueprint; all tasks when epic/blueprint changed; the whole
112
+ current set on \`full\`. Changed documents are not repeated (partial).
113
+ revision_diff [{file, before, after}] with {body, structure}; added/removed
114
+ documents null their missing side
115
+
116
+ \`full\` when the document set or any document's normalized structure differs (task
117
+ added/removed, affected_paths, depends_on, parallel_safe, exclusive_resources,
118
+ execution_kind, scale/review_scope) or when \`--previous\` is a digest-only legacy
119
+ payload (before null, reason legacy-payload-no-content, counts as a restart);
120
+ otherwise every prose edit, including epic/blueprint, Interface and Touch, is
121
+ \`partial\`. Once a cycle or recorded rounds exist, \`--previous\` is required and
122
+ must equal the cycle latest_dispatch. Failures print
123
+ \`{ ok: false, reason, cause, next }\` (exit 1) with reason
124
+ previous-payload-required | previous-payload-invalid | previous-payload-stale.
125
+ `;
126
+ /**
127
+ * review-dispatch argv를 plan/execute로 분기한다. 잘못된 사용법은 resolver를
128
+ * 열기 전에 거절해 exit 2와 운영 실패(exit 1)를 구분한다.
129
+ *
130
+ * @param {string[]} rest - `review-dispatch` 뒤 argv
131
+ * @returns {ParsedArgs} 성공 시 mode별 필드, 실패 시 error
132
+ */
133
+ function parseReviewDispatchArgs(rest) {
134
+ const sub = rest[0];
135
+ if (sub !== 'plan' && sub !== 'execute') {
136
+ return { mode: 'usage', error: `review-dispatch: command must be plan or execute\n${USAGE}` };
137
+ }
138
+ if (sub === 'plan')
139
+ return parsePlanArgs(rest.slice(1));
140
+ return parseExecuteArgs(rest.slice(1));
141
+ }
142
+ /**
143
+ * plan 하위 명령 인자. --blueprint는 필수, --previous는 이전 payload 경로다.
144
+ * 분류기는 문서를 읽기만 한다.
145
+ *
146
+ * @param {string[]} rest - `plan` 뒤 argv
147
+ * @returns {ParsedPlan} 성공 필드 또는 error
148
+ */
149
+ function parsePlanArgs(rest) {
150
+ const f = parseFlags(rest);
151
+ const fail = (message) => ({
152
+ mode: 'plan',
153
+ error: `review-dispatch plan: ${message}\n`,
154
+ blueprint: null,
155
+ previous: null,
156
+ });
157
+ if (typeof f.blueprint !== 'string' || f.blueprint === '') {
158
+ return fail('--blueprint is required');
159
+ }
160
+ // --previous만 있고 경로가 없으면 boolean true가 된다. 파일 부재(exit 1)와
161
+ // 구분하기 위해 사용법 거절(exit 2)로 돌린다.
162
+ if (Object.prototype.hasOwnProperty.call(f, 'previous')
163
+ && (typeof f.previous !== 'string' || f.previous === '')) {
164
+ return fail('--previous requires a file');
165
+ }
166
+ // parseFlags는 중복 플래그에서 마지막 값을 조용히 택한다. 어느 previous가 대조되는지
167
+ // 모호해지므로 분류기 전에 사용법 오류(exit 2)로 막는다.
168
+ for (const flag of ['--blueprint', '--previous']) {
169
+ if (rest.filter((tok) => tok === flag).length > 1)
170
+ return fail(`${flag} may be given only once`);
171
+ }
172
+ if (f.repo !== undefined && (typeof f.repo !== 'string' || f.repo === '')) {
173
+ return fail('--repo requires a directory');
174
+ }
175
+ return {
176
+ mode: 'plan',
177
+ blueprint: f.blueprint,
178
+ previous: typeof f.previous === 'string' ? f.previous : null,
179
+ repo: typeof f.repo === 'string' ? f.repo : undefined,
180
+ };
181
+ }
182
+ /**
183
+ * execute 하위 명령 인자. frozen base/head가 빠지면 분류를 시작하지 않는다.
184
+ * `--task`는 한 commit task 범위일 때만 필수이고, 생략하면 blueprint 전체
185
+ * numstat·위험 합집합으로 간다 — 최종 리뷰가 task 하나를 강제하면 CLI가
186
+ * 분류기보다 좁아진다. `--mode`는 discovery|delta만 받고, 없으면 discovery다.
187
+ *
188
+ * @param {string[]} rest - `execute` 뒤 argv
189
+ * @returns {ParsedExecute} 성공 필드 또는 error
190
+ */
191
+ function parseExecuteArgs(rest) {
192
+ const f = parseFlags(rest);
193
+ const fail = (message) => ({
194
+ mode: 'execute',
195
+ error: `review-dispatch execute: ${message}\n`,
196
+ blueprint: null,
197
+ task: null,
198
+ base: null,
199
+ head: null,
200
+ reviewMode: 'discovery',
201
+ });
202
+ if (typeof f.blueprint !== 'string' || f.blueprint === '') {
203
+ return fail('--blueprint is required');
204
+ }
205
+ if (Object.prototype.hasOwnProperty.call(f, 'task')
206
+ && (typeof f.task !== 'string' || f.task === '')) {
207
+ return fail('--task <ddd> is required');
208
+ }
209
+ if (typeof f.base !== 'string' || f.base === '') {
210
+ return fail('--base <sha> is required');
211
+ }
212
+ if (typeof f.head !== 'string' || f.head === '') {
213
+ return fail('--head <sha> is required');
214
+ }
215
+ if (f.repo !== undefined && (typeof f.repo !== 'string' || f.repo === '')) {
216
+ return fail('--repo requires a directory');
217
+ }
218
+ // --mode만 있으면 parseFlags가 boolean true를 준다. 분류기 전에 거절해
219
+ // require.cache에 review-dispatch가 남지 않게 한다(usage exit 2 계약).
220
+ if (Object.prototype.hasOwnProperty.call(f, 'mode')) {
221
+ if (f.mode !== 'discovery' && f.mode !== 'delta') {
222
+ return fail('--mode must be discovery or delta');
223
+ }
224
+ }
225
+ // task 없는 delta는 여기서 거절하지 않는다. review_contract 표식이 있는 blueprint만
226
+ // 받으므로 표식 확인이 필요하고, 그 확인은 분류기 적재 전 cmdReviewDispatch가 맡는다.
227
+ return {
228
+ mode: 'execute',
229
+ blueprint: f.blueprint,
230
+ task: typeof f.task === 'string' ? f.task : null,
231
+ base: f.base,
232
+ head: f.head,
233
+ reviewMode: f.mode === 'delta' ? 'delta' : 'discovery',
234
+ repo: typeof f.repo === 'string' ? f.repo : undefined,
235
+ };
236
+ }
237
+ const DELTA_NEEDS_TASK = 'review-dispatch execute: --mode delta requires --task <ddd>'
238
+ + ' (the blueprint final review has no delta round)\n';
239
+ /**
240
+ * task 없는 `--mode delta`를 분류기 적재 전에 거절해야 하는지 판정한다.
241
+ * review_contract brief 표식 blueprint만 이 호출을 받는다. 표식 확인은 분류기를
242
+ * require하지 않도록 `readDoc`과 `reviewContractOf`만 쓴다.
243
+ *
244
+ * @param {ParsedExecute} parsed - task 없는 delta로 해석된 execute 인자
245
+ * @returns {string | null} 거절 시 stderr 메시지(exit 2), 표식이 brief면 null
246
+ */
247
+ function blueprintDeltaRefusal(parsed) {
248
+ const repoRoot = parsed.repo ? parsed.repo : process.cwd();
249
+ let bouncer;
250
+ try {
251
+ const doc = frontmatter.readDoc(path.join(repoRoot, parsed.blueprint, 'index.md'));
252
+ bouncer = doc.data?.bouncer;
253
+ }
254
+ catch {
255
+ // index 부재·파싱 실패를 모두 기존 --task 사용법 메시지로 접는다. 표식을 읽지
256
+ // 못한 호출은 legacy와 구분할 수 없고, legacy 출력을 보존하는 것이 이 경로의 계약이다.
257
+ return DELTA_NEEDS_TASK;
258
+ }
259
+ const contract = schema.reviewContractOf(bouncer);
260
+ if (contract === 'brief')
261
+ return null;
262
+ if (contract === null)
263
+ return `review-dispatch execute: ${schema.REVIEW_CONTRACT_INVALID_MESSAGE}\n`;
264
+ return DELTA_NEEDS_TASK;
265
+ }
266
+ /**
267
+ * 공개 review-dispatch 핸들러. 분류 모듈과 stdout JSON·exit 계약만 연결한다.
268
+ *
269
+ * @param {string[]} rest - 서브커맨드와 플래그
270
+ * @param {CliIo} io - stdout/stderr 싱크
271
+ * @returns {number} 도움말·성공 0, 분류 거절 1, 사용법 2
272
+ */
273
+ function cmdReviewDispatch(rest, io) {
274
+ // help는 argv 해석·분류기 적재보다 앞에서 끝낸다. 도움말이 파일·git을 읽지 않게 함.
275
+ if (argvRequestsHelp(rest)) {
276
+ io.out(HELP);
277
+ return 0;
278
+ }
279
+ const parsed = parseReviewDispatchArgs(rest);
280
+ if (parsed.mode === 'usage' || parsed.error) {
281
+ io.err(parsed.error || USAGE);
282
+ return 2;
283
+ }
284
+ if (parsed.mode === 'execute' && parsed.reviewMode === 'delta' && parsed.task === null) {
285
+ const refusal = blueprintDeltaRefusal(parsed);
286
+ if (refusal !== null) {
287
+ io.err(refusal);
288
+ return 2;
289
+ }
290
+ }
291
+ // argv를 통과한 뒤에만 분류기를 적재한다. 사용법 거절이 review-dispatch·
292
+ // validate 그래프를 require.cache에 남기지 않게 하기 위함.
293
+ const reviewDispatch = require('./review-dispatch');
294
+ const repoRoot = (parsed.repo ? parsed.repo : process.cwd());
295
+ if (parsed.mode === 'plan') {
296
+ let previous;
297
+ if (parsed.previous) {
298
+ // 파일 부재·비JSON은 분류기 전에 거절한다. parts 부재는 분류기가
299
+ // previous-payload-invalid로 돌린다 — 읽기 실패와 같은 reason으로 맞춘다.
300
+ try {
301
+ const raw = fs.readFileSync(parsed.previous, 'utf8');
302
+ previous = JSON.parse(raw);
303
+ }
304
+ catch {
305
+ // ENOENT·SyntaxError·권한 오류를 모두 같은 reason으로 접는다. 운영자가
306
+ // 경로/형식을 고치면 되고, 세부 errno를 strategy 실패와 섞지 않기 위함.
307
+ io.out(`${JSON.stringify({
308
+ ok: false,
309
+ reason: 'previous-payload-invalid',
310
+ cause: 'previous file is missing, unreadable, or not JSON',
311
+ next: 'pass the unmodified JSON that review-dispatch plan printed (the stored cycle latest_dispatch)',
312
+ }, null, 2)}\n`);
313
+ return 1;
314
+ }
315
+ }
316
+ const result = reviewDispatch.classifyPlanReview({
317
+ repoRoot,
318
+ blueprintDir: parsed.blueprint,
319
+ ...(previous !== undefined ? { previous } : {}),
320
+ });
321
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
322
+ return result.ok ? 0 : 1;
323
+ }
324
+ const result = reviewDispatch.classifyExecuteReview({
325
+ repoRoot,
326
+ blueprintDir: parsed.blueprint,
327
+ ...(parsed.task ? { taskId: parsed.task } : {}),
328
+ base: parsed.base,
329
+ head: parsed.head,
330
+ reviewMode: parsed.reviewMode,
331
+ });
332
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
333
+ return result.ok ? 0 : 1;
334
+ }
335
+ module.exports = {
336
+ run: cmdReviewDispatch,
337
+ usage: cliUsage.COMMAND_USAGE['review-dispatch'],
338
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+ /* eslint-disable max-len -- Static usage text intentionally preserves terminal output lines. */
3
+ // 도움말은 기능 모듈 없이 출력되어야 하므로 문자열만 정적 보유한다.
4
+ const COMMAND_USAGE = Object.freeze({
5
+ 'validate': ' validate --blueprint <dir> --gate <plan|execute|commit|finalize>\n Run the structural checks and one gate. Reports failure codes.\n',
6
+ 'verify': ' verify --blueprint <dir>\n Run the configured verify command and record its evidence.\n',
7
+ 'scaffold': ' scaffold epic --id <ddd> --name <slug> --description <text>\n epic --name <slug> --description <text> (v2 registry: slug epic, no --id)\n blueprint --epic-dir <dir> --id <ddd> --name <slug> [--scale light|full]\n [--area <a>] [--attribution <text>]\n blueprint --epic-dir <slug-epic> --order <n> --name <slug> [--scale light|full]\n [--area <a>] [--attribution <text>]\n task --blueprint <dir> --id <ddd> [--execution-kind commit|verification]\n [--depends-on TASKS-NNN[,TASKS-NNN...]] [--verify <command>]\n explain --blueprint <dir>\n context-review --blueprint <dir>\n Create a document set with correct frontmatter.\n blueprint writes index.md, review.md, and (full) context-review.md;\n a new task writes tasks.md and verification.md (legacy blueprints\n without review_scope still get task review.md).\n (explain is for finalize; epic/blueprint scaffold omit it.)\n',
8
+ 'commit': ' commit --blueprint <dir> [--yes]\n Check task commit scope and, with --yes, commit one task.\n',
9
+ 'commit-guard': ' commit-guard --staged [--repo <dir>]\n commit-guard --command-stdin [--repo <dir>]\n Allow or block a commit from the current index (git pre-commit) or a hook-supplied command.\n',
10
+ 'finalize': ' finalize prepare --blueprint <dir>\n Print a read-only finalize digest JSON for Explain, Quiz, and PR.\n finalize links --blueprint <dir>\n Print Explain URL candidates for a pushed GitHub head (read-only).\n finalize pr-body --digest <json-file> --summary <markdown-file> [--flow <mermaid-file>]\n Print a draft PR body from a saved digest and a Korean summary (read-only, stdout).\n finalize comprehension --blueprint <dir> --digest <json-file> (--confirm-quiz | --refresh) [--repo <dir>]\n Record the single explain.md comprehension entry after the user answered the quiz.\n --refresh only updates an existing valid entry; the flags are mutually exclusive.\n finalize partial-finalize --blueprint <dir> [--yes --confirm-partial-finalize]\n Review a partial_closed drive; explicit confirmation clears its pointer and worktrees.\n finalize release-main --blueprint <dir> [--allow-partial-merge --confirm-partial-merge]\n Run from main; partial merge requires both independent flags.\n finalize worktree-path --blueprint <dir> [--repo <dir>]\n Print the execute worktree path for a blueprint (read-only, one line).\n finalize diff-sha --base <ref> [--repo <dir>]\n Print the explain diff_sha as JSON (read-only); exit 1 when ok is false.\n finalize --blueprint <dir> [--yes]\n Check the commit scope and, with --yes, commit the blueprint.\n',
11
+ 'seed-worktree': ' seed-worktree --blueprint <dir> --to <worktree>\n Move the plan context documents into a freshly created worktree.\n',
12
+ 'execute': ' execute prepare --blueprint <dir>\n Create or reuse the execute worktree, seed plan documents, and print JSON.\n',
13
+ 'plan': ' plan inspect [--epic-dir <dir>] [--blueprint <dir>]\n Print epic registry candidates, next domain epic id, verify signals, pointer state,\n and optional advisory light routing as JSON.\n plan approve --blueprint <dir> [--repo <dir>]\n Move epic, blueprint, and task status to approved/ready in one\n all-or-nothing step. Run only after the user approves the plan.\n',
14
+ 'run': " run preflight --blueprint <dir>\n Print pointer, open tasks, DAG, ready wave, and autonomy as JSON.\n run detach --blueprint <dir> --yes [--reason <text>]\n After approval, preserve the ledger, mark the run detached, and remove this blueprint's pointer.\n",
15
+ 'coordinate': ' coordinate <bootstrap|prepare|ready|dispatch|report|record|rerecord|integrate|status|revoke> --blueprint <dir>\n [--task <ddd>] [--sha <sha>] [--lease-id <id>] [--generation <n>]\n Operate the coordinator ledger and isolated integration worktrees.\n coordinate integrate --blueprint <dir> [--task <ddd>]\n --ledger-path <path> --ledger-hash <sha256>\n Fan-in recorded commit tasks via a candidate worktree (omit --task for the wave).\n coordinate dispatch --blueprint <dir> --task <ddd> --ledger-path <path> --ledger-hash <sha256>\n [--repo <main>] [--lease-id <id> --generation <n>] [--model <value>]\n Open one dispatch attempt on the assigned worker and return brief/HEAD metadata.\n coordinate report --blueprint <dir> --task <ddd> --attempt <n>\n --task-brief-hash <sha256> --outcome <accepted|rework|scope_revision|task_change|blocked>\n --summary <text> --ledger-path <path> --ledger-hash <sha256> [--repo <main>]\n [--lease-id <id> --generation <n>]\n Record a worker report against the active attempt, or append stale-report evidence.\n coordinate revoke --blueprint <dir> --task <ddd> --reason <text>\n --ledger-path <path> --ledger-hash <sha256>\n Revoke an active lease, clear dispatch/sha, and return the task to pending.\n coordinate rerecord --blueprint <dir> --task <ddd> --reason <text>\n --ledger-path <path> --ledger-hash <sha256> [--sha <sha>]\n Replace a recorded worker SHA with its direct-child HEAD and preserve the decision.\n coordinate repair --blueprint <dir> --task <ddd> --failure-command <cmd>\n --summary <text> --paths <p> --decision <reason>\n --ledger-path <path> --ledger-hash <sha256>\n Add one audited repair task and move the terminal CI dependency.\n coordinate repair --blueprint <dir> [--task <ddd>] --review-finding <id>\n [--review-finding <id>]... --summary <text> --paths <p> --decision <reason>\n --ledger-path <path> --ledger-hash <sha256>\n Open a repair task from final-review must_fix findings (omit --task\n when the blueprint has no terminal verification).\n coordinate repair --blueprint <dir> --kind supplement --review-finding <id>\n [--review-finding <id>]... --summary <text> --paths <test-path>\n --ledger-path <path> --ledger-hash <sha256>\n Accept an in-scope test-evidence gap in place (no new task or wave).\n coordinate repair --blueprint <dir> --kind supplement --done --summary <text>\n --ledger-path <path> --ledger-hash <sha256>\n Verify the finished supplement; then run the one delta review.\n coordinate reverify --blueprint <dir> --task <terminal ddd>\n --ledger-path <path> --ledger-hash <sha256>\n Re-run the terminal verification after an environment-only fix (no repair wave).\n coordinate partial-close --blueprint <dir> --user-confirmed\n --ledger-path <path> --ledger-hash <sha256>\n Preserve the failed drive and mark it partial_closed after two repair waves.\n coordinate critical-recovery --blueprint <dir> --task <ddd> --findings <id>\n [--findings <id>]... --reason <text>\n --ledger-path <path> --ledger-hash <sha256>\n Record the one permitted blocker or major recovery for a prepared task.\n coordinate critical-recovery --blueprint <dir> --task <ddd> --outcome <resolved|blocked>\n --reason <text> --ledger-path <path> --ledger-hash <sha256>\n Record the outcome without permitting another recovery.\n coordinate revise --blueprint <dir> --task <ddd> --paths <p> [--paths <p>]...\n [--release-do-not-touch <p>]... --reason <text> --ledger-path <path> --ledger-hash <sha256>\n Record one scope decision in the task document and ledger.\n --paths replaces the whole scope with exactly the given set.\n --release-do-not-touch <p> drops one overlapping Do not touch entry from the brief.\n Takes no --repo: the write boundary is the current directory, so\n run it from the assigned task worktree, not the main checkout.\n coordinate next --blueprint <dir> [--task <ddd>] [--repo <dir>]\n Print the next coordinator action and filled argv without mutating the ledger.\n coordinate advance --blueprint <dir> [--task <ddd>] [--max-steps <n>] [--repo <dir>]\n Run deterministic next argv actions until judge, worker, terminal, or error.\n coordinate promote-stop --blueprint <dir> --reason <enum> --summary <text>\n --ledger-path <path> --ledger-hash <sha256>\n Stop a light ledger for promotion; records reason and state (no resume).\n coordinate session-open --blueprint <dir> --ledger-path <path> --ledger-hash <sha256>\n Record a coordinator session boundary before dispatching a new coordinator.\n coordinate worker-model --blueprint <dir> --role <implementer|reviewer|debugger> --model <value>\n --ledger-path <path> --ledger-hash <sha256> [--task <ddd>] [--perspective <name>]\n [--tokens <n>]\n Record the model argument (and host-reported tokens) of a worker dispatch.\n coordinate worker-tokens --blueprint <dir> --role <implementer|reviewer|debugger> --tokens <n>\n --ledger-path <path> --ledger-hash <sha256> [--task <ddd>] [--perspective <name>]\n Append host-reported subagent tokens that arrived after the worker-model record.\n coordinate note --blueprint <dir> --text <text> --ledger-path <path> --ledger-hash <sha256> [--task <ddd>]\n Leave a handoff note for the next coordinator session (no dispatch, lease, or status change).\n Mutations (except bootstrap/status) require --ledger-path and --ledger-hash from\n the latest status checkpoint so stale ledger writes are rejected before mutation.\n',
16
+ 'init': ' init [--upgrade-graphify] [--pre-commit-hook]\n [--profile <standard|domain-only>] [--domain-epic-limit <n>]\n Bootstrap .bouncer/ for this project. Never overwrites.\n',
17
+ 'graph-sync': ' graph-sync Rebuild stale graphify source + test graphs (SessionStart / plan).\n',
18
+ 'intent': ' intent --symbol <function-name> [--candidate <qualified-ref>] [--limit <1..5>]\n Print function intent provenance JSON (read-only).\n intent bundle --task <tasks.md> --symbol <name> [--candidate <qualified-ref>]...\n Create or reuse a task intent bundle JSON (read/write cache).\n intent bundle --task <tasks.md> --no-symbols\n Same, with an empty function list for tasks without function symbols.\n intent sections --task <tasks.md> --role <implementer|reviewer|debugger>\n Print role-specific intent section bodies JSON (read-only).\n',
19
+ 'ontology': ' ontology entity --entity <token> [--repo <dir>] [--limit <1..100>]\n file --file <path> [--repo <dir>] [--limit <1..100>]\n impact --file <path> [--repo <dir>] [--limit <1..100>]\n Search canonical Bouncer context in memory (read-only JSON).\n',
20
+ 'graphify-bin': ' graphify-bin\n Print the resolved graphify executable path (one line).\n',
21
+ 'project-root': " project-root\n Print the consuming project's main worktree absolute path (one line).\n",
22
+ 'subagent-model': " subagent-model --agent <name> [--provider <name>]\n Print the named agent's model slug, or inherit (one line).\n",
23
+ 'codex-agents': ' codex-agents check --agent <name>\n Compare local Codex TOML to generated bytes (read-only JSON).\n',
24
+ 'current': ' current [--set <blueprint dir> [--base <branch>] [--task <NNN|TASKS-NNN>] [--replace] [--keep-others] [--reapprove]]\n [--clear]\n Show the active blueprint pointer, or set / clear it.\n --task picks a task doc; without it, first ready/in_progress wins.\n --replace deletes the uniquely selected key then writes the target; omitted, --set adds a parallel key.\n --replace at a multi-pointer base exits 1 with CURRENT_AMBIGUOUS.\n At a base checkout, a second key is refused unless --replace or --keep-others is given.\n --reapprove rewrites the approval snapshot after the user explicitly approves a scope change.\n',
25
+ 'migrate': ' migrate task-layout [--dry-run]\n Move legacy task files into tasks/<NNN>/ units.\n migrate context-slugs [--yes]\n Move numbered epic and blueprint directories to slugs, rewrite ids and hashes, record\n aliases (dry-run plan unless --yes; refuses a dirty tree or in-progress runs).\n migrate merge-legacy-epics [--yes]\n Move legacy epic blueprints into domain epics, rewrite ids and hashes, record moved_to\n (dry-run plan unless --yes; refuses a dirty tree or in-progress runs).\n migrate review-scope --blueprint <dir>\n Set review_scope: blueprint and create the root pending review.md (idempotent).\n migrate retention\n Audit closed blueprints for retention (dry-run, JSON, no writes).\n migrate retention --apply --blueprint <dir>\n Promote Explain then delete transients for one eligible closed blueprint.\n',
26
+ 'import': ' import [--source merges|commits] [--since <ref>] [--limit <n>]\n [--epic-id <ddd>] [--epic-name <slug>] [--yes --message <msg>]\n Transcribe git history into imported epic/blueprint documents.\n',
27
+ 'review-dispatch': ' review-dispatch plan --blueprint <dir> [--previous <file>]\n Classify plan context-review strategy (read-only JSON).\n',
28
+ 'review': " review-dispatch execute --blueprint <dir> [--task <ddd>] --base <sha> --head <sha> [--mode discovery|delta]\n Classify execute review strategy from the frozen final worktree state\n (JSON). Writes only the snapshot artifact under .bouncer/runtime/\n review-snapshots/ and returns target.snapshot_id/snapshot_path; --head\n must be the current HEAD. On failure it prints reason/cause/next, exit 1.\n When that blueprint's scale is light, returns single/combined\n regardless of diff size.\n review record --blueprint <dir> [--task <ddd>] --round <json-file> [--status <requested|addressed|accepted>]\n Record one review round and finding updates (JSON).\n review accept-risk --blueprint <dir> --finding <id> --note <text> --yes\n Close one open blueprint final-review finding as a user-accepted risk.\n",
29
+ 'context-review': ' context-review <start|record|close|restart|route> --blueprint <dir>\n Track plan context-review cycles; limit full restarts to one.\n',
30
+ 'dispatch': ' dispatch print --role <implementer|reviewer|debugger|context-reviewer|coordinator> --cwd <dir> --input <file> --out <dir>\n Run one Cursor print dispatch (JSON).\n',
31
+ 'config': ' config --help\n Show .bouncer/config.json "subagents" keys, allowed values, and defaults (read-only).\n',
32
+ 'retro': ' retro sources [--repo <dir>] [--limit <1..200>] [--claude-root <dir>]\n [--codex-root <dir>] [--cursor-root <dir>]\n List Claude Code, Codex, and Cursor transcript candidates for this project (read-only JSON).\n retro read --path <file> [--max-chars <n>]\n Normalize one transcript into role/text messages with a character cap (read-only JSON).\n',
33
+ 'epic': ' epic add-domain --slug <s> --reason <text> --yes\n add-area --name <a> --yes\n Record a user-approved domain epic or area in .bouncer/epics.json.\n',
34
+ 'metrics': ' metrics start --kind <coordinator-session|context-review|root-direct-session> --blueprint <dir> [--label <text>]\n [--target-digest <64hex>]\n Record a coordinator, root-direct, or plan context-review session start in drive-metrics.json.\n metrics end --id <m-id> [--tokens <n>] [--input-tokens <n>] [--output-tokens <n>]\n [--cache-read-tokens <n>] [--cache-write-tokens <n>] [--model <slug>] [--host-duration-ms <n>]\n Record host-reported tokens and time; missing values are stored as null with a reason.\n metrics summary --blueprint <dir>\n Sum sessions per kind (null tokens and costs are counted as missing, never as 0).\n worker_tokens_source says whether worker totals came from the ledger, a saved snapshot, or are unavailable.\n metrics snapshot --blueprint <dir>\n Save worker token totals and the ledger SHA-256 to the Git common dir before cleanup removes the ledger.\n\nEvery command accepts --repo <dir> to run against another repository.\n',
35
+ });
36
+ const COORDINATE_USAGE = Object.freeze({
37
+ 'bootstrap': 'usage: bouncer coordinate bootstrap\n --blueprint <dir> [--repo <dir>] [--promote-light]\n Seed the integration worktree and ledger. Not ledger-fenced.\n --promote-light archives a promotion_stopped light ledger and starts a full ledger.\n',
38
+ 'prepare': 'usage: bouncer coordinate prepare\n --blueprint <dir> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Create or reuse assigned worker worktrees and refresh leases.\n',
39
+ 'ready': 'usage: bouncer coordinate ready\n --blueprint <dir> [--repo <dir>]\n Alias of coordinate status. Prints the checkpoint; not ledger-fenced.\n',
40
+ 'dispatch': 'usage: bouncer coordinate dispatch\n --blueprint <dir> --task <ddd> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--repo <main>] [--lease-id <id>] [--generation <n>] [--model <value>]\n Open one dispatch attempt on the assigned worker and return brief/HEAD metadata.\n --model records the dispatch model argument (`bouncer subagent-model` output) on the ledger.\n',
41
+ 'report': 'usage: bouncer coordinate report\n --blueprint <dir> --task <ddd> --attempt <n> --task-brief-hash <sha256>\n --outcome <accepted|rework|scope_revision|task_change|blocked> --summary <text>\n --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <main>] [--lease-id <id>] [--generation <n>]\n Record a worker report against the active attempt, or append stale-report evidence.\n',
42
+ 'record': 'usage: bouncer coordinate record\n --blueprint <dir> --task <ddd> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--sha <sha>] [--repo <dir>] [--lease-id <id>] [--generation <n>]\n Record the worker HEAD after an accepted report.\n',
43
+ 'rerecord': 'usage: bouncer coordinate rerecord\n --blueprint <dir> --task <ddd> --reason <text> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--sha <sha>] [--repo <dir>]\n Replace a recorded worker SHA with its direct-child HEAD and preserve the decision.\n',
44
+ 'integrate': 'usage: bouncer coordinate integrate\n --blueprint <dir> [--task <ddd>] --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--sha <sha>] [--repo <dir>] [--lease-id <id>] [--generation <n>]\n Fan-in recorded commit tasks via a candidate worktree (omit --task for the wave).\n',
45
+ 'status': 'usage: bouncer coordinate status\n --blueprint <dir> [--repo <dir>] [--write-input <file>]\n Print the checkpoint (including checkpoint.ledger). Not ledger-fenced.\n --write-input writes the coordinator print-dispatch input text to <file>\n (status only; reports input_file) for `dispatch print --role coordinator --input`.\n',
46
+ 'revoke': 'usage: bouncer coordinate revoke\n --blueprint <dir> --task <ddd> --reason <text> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Revoke an active lease, clear dispatch/sha, and return the task to pending.\n',
47
+ 'advance': "usage: bouncer coordinate advance\n --blueprint <dir> [--task <ddd>] [--max-steps <n>] [--repo <dir>]\n Not ledger-fenced. Calls coordinate next and runs argv for prepare, integrate,\n verification_node, verify, and commit until a stop or failure.\n Stop reasons (ok: true): judge, worker, blocked, done, none, max-steps, yield\n Failure reasons (ok: false): repeated-failure, unclear-result, advance-argv-invalid,\n or the automatic action's failure reason.\n Response fields: executed, stop.next (success) or reason, cause, next, executed (failure)\n",
48
+ 'critical-recovery': 'usage: bouncer coordinate critical-recovery\n --blueprint <dir> --task <ddd> --findings <id> [--findings <id>]... --reason <text>\n --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Record the one permitted blocker or major recovery for a prepared task.\n --blueprint <dir> --task <ddd> --outcome <resolved|blocked> --reason <text>\n --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Record the outcome without permitting another recovery.\n',
49
+ 'next': 'usage: bouncer coordinate next\n --blueprint <dir> [--task <ddd>] [--repo <dir>]\n Read-only. Ignores --ledger-path and --ledger-hash if given (not ledger-fenced).\n Blueprint actions: prepare, yield, drive_tasks, integrate, verification_node, supplement, final_review, done, blocked\n Task actions: dispatch, implement, verify, review, commit, report, record, revise, none, blocked\n Response fields: action, cwd, argv, judge, task_ids, payload, card, checkpoint\n card {id,body} only on: dispatch, implement, verify, review, report, revise, record, supplement, final_review, blocked\n',
50
+ 'reverify': 'usage: bouncer coordinate reverify\n --blueprint <dir> --task <terminal ddd> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Re-run the terminal verification after an environment-only fix. Uses no repair wave;\n refused with reverify-source-moved or reverify-worktree-dirty when the source changed.\n',
51
+ 'partial-close': 'usage: bouncer coordinate partial-close\n --blueprint <dir> --user-confirmed --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Preserve the failed drive and mark it partial_closed after two repair waves.\n',
52
+ 'promote-stop': "usage: bouncer coordinate promote-stop\n --blueprint <dir> --reason <security-risk|out-of-scope|task-split|interface-semantics|reviewer-wider-scope>\n --summary <text> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Record promotion_stopped on a light ledger with the open task's diff and evidence ids.\n Same reason+summary is idempotent; other mutations then fail with promotion-stopped.\n",
53
+ 'repair': 'usage: bouncer coordinate repair\n --blueprint <dir> --task <ddd> --failure-command <cmd> --summary <text>\n --paths <p> --decision <reason> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Add one audited repair task and move the terminal CI dependency.\n --blueprint <dir> [--task <ddd>] --review-finding <id> [--review-finding <id>]...\n --summary <text> --paths <p> --decision <reason> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Open a repair task from final-review must_fix findings (omit --task\n when the blueprint has no terminal verification).\n --kind <product|supplement> (default product, the forms above)\n --blueprint <dir> --kind supplement --review-finding <id> [--review-finding <id>]...\n --summary <text> --paths <test-path> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Accept an in-scope test-evidence gap in place: no new task, no repair wave.\n --blueprint <dir> --kind supplement --done --summary <text> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Verify the finished supplement; the next final review is the one delta round.\n Product-behavior fixes, new dependencies, and public interfaces are not supplements.\n',
54
+ 'revise': 'usage: bouncer coordinate revise\n --blueprint <dir> --task <ddd> --paths <p> [--paths <p>]... [--release-do-not-touch <p>]... --reason <text>\n --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n Record one scope decision in the task document and ledger.\n --paths replaces the whole scope with exactly the given set; pass every path\n the task still needs, not only new ones (dropped paths are reported as warnings).\n A Do not touch entry in the brief that overlaps a newly added path is refused with\n scope-do-not-touch-conflict; release each such entry with --release-do-not-touch <p>, which\n also removes it from the brief. A value that is not a conflicting entry is refused with\n release-do-not-touch-unknown. A valueless --release-do-not-touch is usage (2).\n Takes no --repo: the write boundary is the current directory, so\n run it from the assigned task worktree, not the main checkout.\n',
55
+ 'session-open': "usage: bouncer coordinate session-open\n --blueprint <dir> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger) [--repo <dir>]\n Run by the root before dispatching a coordinator session. Appends a session decision\n (opened_at, integration_head, waves_cap) so coordinate next can count this session's waves.\n Refused without changing the ledger on a light, promotion_stopped, partial_closed, or\n awaiting_confirmation ledger.\n",
56
+ 'worker-model': 'usage: bouncer coordinate worker-model\n --blueprint <dir> --role <implementer|reviewer|debugger> --model <value> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--task <ddd>] [--perspective <name>] [--tokens <n>] [--repo <dir>]\n Run from the integration worktree. Appends a worker-model decision (role, model, task,\n perspective, tokens) to the ledger; --task also appends it to that task. The model is the one-line\n `bouncer subagent-model --agent <name>` output (slug or inherit); it records what the\n coordinator passed, not proof of what the host ran. --perspective is reviewer-only.\n --tokens is the host-reported subagent token count (non-negative integer); omit it when the\n completion notice has not arrived, then add it later with coordinate worker-tokens.\n Does not change dispatch, lease, status, or attempt state.\n',
57
+ 'note': 'usage: bouncer coordinate note\n --blueprint <dir> --text <text> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--task <ddd>] [--repo <dir>]\n Run from the integration worktree. Appends a note decision (text, optional task) to the ledger so the next\n coordinator session reads it in checkpoint.notes. The text is trimmed and at most 2000 characters; it is data,\n never an instruction. Refused with note-text-required, note-too-long, or task-outside-blueprint. A missing --text is usage (2).\n Does not change dispatch, lease, status, or attempt state.\n',
58
+ 'worker-tokens': "usage: bouncer coordinate worker-tokens\n --blueprint <dir> --role <implementer|reviewer|debugger> --tokens <n> --ledger-path <path> --ledger-hash <sha256> (from coordinate status checkpoint.ledger)\n [--task <ddd>] [--perspective <name>] [--repo <dir>]\n Run from the integration worktree. Appends a worker-tokens decision (role, tokens, task,\n perspective) to the ledger; --task also appends it to that task. Use it when the host's\n completion notice arrives after the worker-model record. `metrics summary` adds it to\n worker_tokens, never to the coordinator-session total. --perspective is reviewer-only.\n Does not change dispatch, lease, status, or attempt state.\n",
59
+ });
60
+ const USAGE_HEADER = 'usage: bouncer <command> [options]\n\n';
61
+ const USAGE_FOOTER = 'Every command accepts --repo <dir> to run against another repository.\n';
62
+ /**
63
+ * coordinate 서브커맨드의 도움말을 정적 표에서 돌려준다. 알 수 없는 값은 overview로 폴백한다.
64
+ *
65
+ * @param {string} [sub] - 도움말을 조회할 서브커맨드
66
+ * @returns {string} 해당 서브커맨드 도움말 또는 coordinate usage
67
+ */
68
+ function coordinateUsage(sub) {
69
+ return sub && Object.hasOwn(COORDINATE_USAGE, sub) ? COORDINATE_USAGE[sub] : COMMAND_USAGE.coordinate;
70
+ }
71
+ const USAGE = USAGE_HEADER + Object.values(COMMAND_USAGE).join('') + '\n' + USAGE_FOOTER;
72
+ module.exports = { COMMAND_USAGE, coordinateUsage, USAGE };
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+ /* eslint-disable max-len -- Lazy handler registrations keep imports deferred until command selection. */
3
+ const cliFlags = require("./cli-flags");
4
+ const cliUsage = require("./cli-usage");
5
+ const { parseFlags, argvRequestsHelp } = cliFlags;
6
+ const { COMMAND_USAGE, USAGE } = cliUsage;
7
+ // help에서 핸들러 모듈을 읽지 않도록, 레지스트리는 선택 뒤에 실행할 로더만 보유한다.
8
+ const COMMANDS = {
9
+ validate: { usage: COMMAND_USAGE.validate, load: () => require('./cli-doc-commands').validate.run },
10
+ verify: { usage: COMMAND_USAGE.verify, load: () => require('./cli-doc-commands').verify.run },
11
+ scaffold: { usage: COMMAND_USAGE.scaffold, load: () => require('./cli-doc-commands').scaffold.run },
12
+ commit: { usage: COMMAND_USAGE.commit, load: () => require('./cli-git-commands').commit.run },
13
+ 'commit-guard': { usage: COMMAND_USAGE['commit-guard'], load: () => require('./cli-commit-guard-command').run },
14
+ finalize: { usage: COMMAND_USAGE.finalize, load: () => require('./cli-git-commands').finalize.run },
15
+ 'seed-worktree': { usage: COMMAND_USAGE['seed-worktree'], load: () => require('./cli-git-commands')['seed-worktree'].run },
16
+ execute: { usage: COMMAND_USAGE.execute, load: () => require('./cli-git-commands').execute.run },
17
+ plan: { usage: COMMAND_USAGE.plan, load: () => require('./cli-doc-commands').plan.run },
18
+ run: { usage: COMMAND_USAGE.run, load: () => require('./cli-project-commands').run.run },
19
+ coordinate: { usage: COMMAND_USAGE.coordinate, ownHelp: true, load: () => require('./cli-git-commands').coordinate.run },
20
+ init: { usage: COMMAND_USAGE.init, load: () => require('./cli-project-commands').init.run },
21
+ 'graph-sync': { usage: COMMAND_USAGE['graph-sync'], load: () => require('./cli-project-commands')['graph-sync'].run },
22
+ intent: { usage: COMMAND_USAGE.intent, load: () => require('./cli-project-commands').intent.run },
23
+ 'graphify-bin': { usage: COMMAND_USAGE['graphify-bin'], load: () => require('./cli-project-commands')['graphify-bin'].run },
24
+ 'project-root': { usage: COMMAND_USAGE['project-root'], load: () => require('./cli-project-commands')['project-root'].run },
25
+ 'subagent-model': { usage: COMMAND_USAGE['subagent-model'], load: () => require('./cli-project-commands')['subagent-model'].run },
26
+ 'codex-agents': { usage: COMMAND_USAGE['codex-agents'], load: () => require('./cli-project-commands')['codex-agents'].run },
27
+ current: { usage: COMMAND_USAGE.current, load: () => require('./cli-current-command').current.run },
28
+ migrate: { usage: COMMAND_USAGE.migrate, load: () => require('./cli-project-commands').migrate.run },
29
+ import: { usage: COMMAND_USAGE.import, load: () => require('./cli-git-commands').import.run },
30
+ 'review-dispatch': { usage: COMMAND_USAGE['review-dispatch'], ownHelp: true, load: () => require('./cli-review-dispatch-command').run },
31
+ review: { usage: COMMAND_USAGE.review, ownHelp: true, load: () => require('./cli-review-command').run },
32
+ 'context-review': { usage: COMMAND_USAGE['context-review'], ownHelp: true, load: () => require('./cli-context-review-command').run },
33
+ dispatch: { usage: COMMAND_USAGE.dispatch, ownHelp: true, load: () => require('./cli-dispatch-command').run },
34
+ config: { usage: COMMAND_USAGE.config, ownHelp: true, load: () => require('./cli-config-command').run },
35
+ retro: { usage: COMMAND_USAGE.retro, load: () => require('./cli-retro-command').run },
36
+ metrics: { usage: COMMAND_USAGE.metrics, load: () => require('./cli-metrics-command').run },
37
+ epic: { usage: COMMAND_USAGE.epic, ownHelp: true, load: () => require('./cli-epic-command').run },
38
+ ontology: { usage: COMMAND_USAGE.ontology, load: () => require('./cli-project-commands').ontology.run },
39
+ };
40
+ /**
41
+ * CLI argv를 처리하고 선택한 명령의 handler만 적재한다.
42
+ * help와 unknown 경로는 static usage만 출력하며, 주입된 IO가 있으면 프로세스 스트림을 쓰지 않는다.
43
+ *
44
+ * @param {string[]} argv - bouncer 명령과 인자
45
+ * @param {Partial<CliIo> | null} [io] - 선택적 stdout/stderr 싱크
46
+ * @returns {number} handler 또는 help/unknown 경로의 종료 코드
47
+ */
48
+ function runCli(argv, io) {
49
+ const out = io && io.out ? io.out : (s) => process.stdout.write(s);
50
+ const err = io && io.err ? io.err : (s) => process.stderr.write(s);
51
+ const sink = { out, err };
52
+ const [cmd, ...rest] = argv;
53
+ if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
54
+ out(USAGE);
55
+ return 0;
56
+ }
57
+ const entry = Object.hasOwn(COMMANDS, cmd) ? COMMANDS[cmd] : undefined;
58
+ if (!entry) {
59
+ err(`unknown command: ${cmd}\n\n${USAGE}`);
60
+ return 2;
61
+ }
62
+ // handler를 적재하기 전에 help를 끝내 `<cmd> --help`가 명령을 실행하거나 저장소를 바꾸지 못하게 한다.
63
+ // ownHelp 명령은 하위 명령별 usage를 내야 하므로 handler가 판정한다.
64
+ if (!entry.ownHelp && argvRequestsHelp(rest)) {
65
+ out(COMMAND_USAGE[cmd]);
66
+ return 0;
67
+ }
68
+ return entry.load()(rest, sink);
69
+ }
70
+ module.exports = { runCli, parseFlags };