@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,374 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const cliFlags = require("./cli-flags");
4
+ const { parseFlags } = cliFlags;
5
+ const { execFileSync } = require('node:child_process');
6
+ const validate = require("./validate");
7
+ const { validateBlueprint } = validate;
8
+ const current = require("./current");
9
+ const { writeCurrent, clearCurrent, listReadyBlueprints, baseNamespaceKeys, resolvePointerTask, presentCurrent, resolveCurrent, CurrentSelectionError, } = current;
10
+ const config = require("./config");
11
+ const { readConfig } = config;
12
+ const runtimeState = require("./runtime-state");
13
+ const { listNamespacePointers, pointerKeyFromBlueprint, writeApprovalSnapshot, } = runtimeState;
14
+ const approvalSnapshot = require("./approval-snapshot");
15
+ const { computeApprovalDigest } = approvalSnapshot;
16
+ /**
17
+ * 교체 전 포인터를 파일 본문과 같은 `{ blueprint, base, task }`로 고정한다.
18
+ * presentCurrent의 path+id·scale은 넣지 않는다. 진단 payload를 파일과
19
+ * 1:1로 맞춰 복사·바이트 비교가 가능하게 한다.
20
+ *
21
+ * @param {{ blueprint: string, base: string, task: string | null }} stored - readCurrent 결과
22
+ * @returns {{ blueprint: string, base: string, task: string | null }} stdout/stderr 공용 previous
23
+ */
24
+ function previousPayload(stored) {
25
+ return {
26
+ blueprint: stored.blueprint,
27
+ base: stored.base,
28
+ task: stored.task == null ? null : stored.task,
29
+ };
30
+ }
31
+ /**
32
+ * 기존 포인터를 stderr에 JSON 한 줄로 남겨 셸에서 복사할 수 있게 한다.
33
+ * pretty stdout과 키는 같고 개행만 다르다.
34
+ *
35
+ * @param {CliIo} io - stderr 싱크
36
+ * @param {PreviousPointer} previous - previousPayload 결과
37
+ * @returns {void}
38
+ */
39
+ function emitPrevious(io, previous) {
40
+ io.err(`previous: ${JSON.stringify(previous)}\n`);
41
+ }
42
+ function storedCandidate(pointer) {
43
+ return previousPayload(pointer);
44
+ }
45
+ function isSelectionError(error) {
46
+ return error instanceof CurrentSelectionError
47
+ || (typeof error === 'object'
48
+ && error !== null
49
+ && 'code' in error
50
+ && typeof error.code === 'string'
51
+ && String(error.code).startsWith('CURRENT_'));
52
+ }
53
+ /**
54
+ * 모호성·충돌·이관 미완료를 종료 코드 1로 내고 상태는 그대로 둔다.
55
+ * candidates는 저장 구조(task 문자열|null)이며 presentCurrent의 path+id·scale을 넣지 않는다.
56
+ *
57
+ * @param {CliIo} io - stdout 싱크
58
+ * @param {InstanceType<typeof CurrentSelectionError>} error - 선택/이관 오류
59
+ * @returns {number} 항상 1
60
+ */
61
+ function emitSelectionFailure(io, error) {
62
+ const payload = { ok: false, reason: error.code };
63
+ if (error.candidates) {
64
+ payload.candidates = error.candidates.map(storedCandidate);
65
+ }
66
+ if (error.issues)
67
+ payload.issues = error.issues;
68
+ // 모호성은 사용자가 풀어야 하는 상태라 다음 행동을 같이 낸다. INVALID는 파일 수리가
69
+ // 필요해 일반 안내가 맞지 않으므로 next 없이 둔다.
70
+ if (error.code === 'CURRENT_AMBIGUOUS') {
71
+ payload.next = [
72
+ 'run `bouncer current` from the candidate worktree (.worktrees/<epic>/<bp>)',
73
+ 'or pick one candidate blueprint and run `bouncer current --set <blueprint dir> --replace`',
74
+ ];
75
+ }
76
+ io.out(`${JSON.stringify(payload, null, 2)}\n`);
77
+ return 1;
78
+ }
79
+ function emitResolutionFailure(io, resolution) {
80
+ const err = resolution.status === 'ambiguous'
81
+ ? new CurrentSelectionError({ code: 'CURRENT_AMBIGUOUS', candidates: resolution.candidates })
82
+ : new CurrentSelectionError({
83
+ code: 'CURRENT_INVALID',
84
+ issues: resolution.issues,
85
+ candidates: resolution.candidates,
86
+ });
87
+ return emitSelectionFailure(io, err);
88
+ }
89
+ /**
90
+ * base checkout에서 새 key를 더하면 선택이 모호해지는 `--set`을 쓰기 전에 거절한다.
91
+ * 포인터 파일은 건드리지 않는다. `--replace`는 기존 key가 정확히 하나일 때만 next에
92
+ * 싣는다 — 둘 이상이면 어느 key를 지울지 추측할 수 없어 cmdCurrent도 거절하기 때문.
93
+ *
94
+ * @param {CliIo} io - stdout 싱크
95
+ * @param {string} blueprintDir - `--set` 대상
96
+ * @param {string | undefined} taskArg - 사용자가 준 `--task` 값(있으면 명령 문자열에 보존)
97
+ * @param {PreviousPointer[]} existing - 이미 저장된 base key들의 저장 형태(blueprint 순)
98
+ * @returns {number} 항상 1
99
+ */
100
+ function emitWouldBeAmbiguous(io, blueprintDir, taskArg, existing) {
101
+ const base = `bouncer current --set ${blueprintDir}${taskArg ? ` --task ${taskArg}` : ''}`;
102
+ const next = [
103
+ ...(existing.length === 1 ? [`${base} --replace`] : []),
104
+ `${base} --keep-others`,
105
+ ];
106
+ const payload = {
107
+ ok: false,
108
+ reason: 'CURRENT_WOULD_BE_AMBIGUOUS',
109
+ cause: 'The base already stores another pointer key, so this --set would leave the active pointer ambiguous.',
110
+ candidates: existing,
111
+ next,
112
+ };
113
+ io.out(`${JSON.stringify(payload, null, 2)}\n`);
114
+ return 1;
115
+ }
116
+ /**
117
+ * 활성 포인터를 읽거나 `--set`/`--clear`로 바꾼다.
118
+ * namespace 전환 뒤 기본 `--set`은 대상 키만 추가·갱신하고 다른 키를 보존한다.
119
+ * `--replace`는 현재 위치에서 유일하게 선택된 키를 지운 뒤 대상을 쓰며,
120
+ * 다중 후보에서는 대상을 추측하지 않고 종료 코드 1이다.
121
+ * base checkout에서 새 key가 더해져 선택이 모호해질 `--set`은 plan gate 전에 거절하며
122
+ * (`CURRENT_WOULD_BE_AMBIGUOUS`), `--replace` 또는 `--keep-others`로 의도를 밝혀야 한다.
123
+ * 대상 namespace 포인터가 없을 때와 `--reapprove`일 때만 승인 파일을 기록한다.
124
+ * 이미 있는 포인터에 task만 전진하면 승인 파일을 건드리지 않아 `/bouncer-commit`의
125
+ * `--set --task`가 매번 재승인되지 않게 한다.
126
+ *
127
+ * @param {string[]} rest - `current` 다음 CLI 인자
128
+ * @param {CliIo} io - stdout/stderr 싱크
129
+ * @returns {number} 성공 0, plan/base/모호성 실패 1, 사용법 거절 2
130
+ */
131
+ function cmdCurrent(rest, io) {
132
+ const f = parseFlags(rest);
133
+ // hasOwnProperty: --set/--task/--replace 가 boolean true(값 없음)여도 "요청함"으로 본다.
134
+ // truthy 문자열만 보면 `--set` 단독이 아래 분기를 건너뛴다.
135
+ const wantsSet = Object.prototype.hasOwnProperty.call(f, 'set');
136
+ const wantsClear = f.clear === true;
137
+ const wantsTask = Object.prototype.hasOwnProperty.call(f, 'task');
138
+ const wantsReplace = Object.prototype.hasOwnProperty.call(f, 'replace');
139
+ const wantsKeepOthers = Object.prototype.hasOwnProperty.call(f, 'keep-others');
140
+ const wantsReapprove = Object.prototype.hasOwnProperty.call(f, 'reapprove');
141
+ // 모순을 값 검증보다 먼저: 어느 값을 고쳐야 하는지 알 수 없는 채
142
+ // "--set requires a blueprint directory"로 떨어지지 않게 한다.
143
+ if (wantsSet && wantsClear) {
144
+ io.err('current: --set and --clear are mutually exclusive\n');
145
+ return 2;
146
+ }
147
+ if (wantsClear && wantsTask) {
148
+ io.err('current: --clear and --task are mutually exclusive\n');
149
+ return 2;
150
+ }
151
+ if (wantsClear && wantsReplace) {
152
+ io.err('current: --clear and --replace are mutually exclusive\n');
153
+ return 2;
154
+ }
155
+ if (wantsKeepOthers && wantsClear) {
156
+ io.err('current: --clear and --keep-others are mutually exclusive\n');
157
+ return 2;
158
+ }
159
+ if (wantsKeepOthers && wantsReplace) {
160
+ io.err('current: --replace and --keep-others are mutually exclusive\n');
161
+ return 2;
162
+ }
163
+ if (wantsKeepOthers && !wantsSet) {
164
+ io.err('current: --keep-others requires --set\n');
165
+ return 2;
166
+ }
167
+ if (wantsTask && !wantsSet) {
168
+ io.err('current: --task requires --set\n');
169
+ return 2;
170
+ }
171
+ if (wantsReplace && !wantsSet) {
172
+ io.err('current: --replace requires --set\n');
173
+ return 2;
174
+ }
175
+ if (wantsReapprove && !wantsSet) {
176
+ io.err('current: --reapprove requires --set\n');
177
+ return 2;
178
+ }
179
+ if (wantsSet && (typeof f.set !== 'string' || f.set === '')) {
180
+ io.err('current: --set requires a blueprint directory\n');
181
+ return 2;
182
+ }
183
+ const repoRoot = (f.repo || process.cwd());
184
+ if (wantsClear) {
185
+ // set보다 먼저 return. 모순은 위에서 이미 거절했으므로 여기선 선택된 키만 지운다.
186
+ try {
187
+ clearCurrent({ repoRoot });
188
+ }
189
+ catch (error) {
190
+ if (isSelectionError(error))
191
+ return emitSelectionFailure(io, error);
192
+ throw error;
193
+ }
194
+ io.out(`${JSON.stringify({ ok: true, current: null }, null, 2)}\n`);
195
+ return 0;
196
+ }
197
+ if (wantsSet) {
198
+ const blueprintDir = f.set;
199
+ let previous = null;
200
+ let replaceKey;
201
+ const selection = resolveCurrent({ repoRoot });
202
+ if (selection.status === 'invalid') {
203
+ return emitResolutionFailure(io, selection);
204
+ }
205
+ if (wantsReplace) {
206
+ // 기준 checkout의 다중 후보는 어느 키를 지울지 추측하지 않는다.
207
+ if (selection.status === 'ambiguous') {
208
+ return emitResolutionFailure(io, selection);
209
+ }
210
+ if (selection.status === 'selected' && selection.current.blueprint !== blueprintDir) {
211
+ previous = previousPayload(selection.current);
212
+ replaceKey = selection.key;
213
+ }
214
+ }
215
+ // plan gate보다 먼저: 거절될 요청에 gate 비용·부수효과를 쓰지 않는다. key는
216
+ // writeCurrent와 같은 pointerKeyFromBlueprint로 계산해 판정이 쓰기와 어긋나지 않는다.
217
+ // 같은 key 갱신·nested/flat worktree·--replace는 새 모호성을 만들지 않으므로 통과한다.
218
+ const targetKey = pointerKeyFromBlueprint(blueprintDir).key;
219
+ const baseKeys = wantsReplace ? null : baseNamespaceKeys({ repoRoot });
220
+ const existingEntries = baseKeys
221
+ ? listNamespacePointers({ repoRoot })
222
+ .filter((e) => e.pointer != null && e.key !== targetKey)
223
+ : [];
224
+ // 쓰기 뒤 key가 2개 이상이 되는 경우(= 기존 key가 있고 대상이 새 key)만 거절한다.
225
+ if (baseKeys && !wantsKeepOthers && !baseKeys.includes(targetKey) && baseKeys.length >= 1) {
226
+ const existing = existingEntries
227
+ .map((e) => previousPayload(e.pointer))
228
+ .sort((x, y) => (x.blueprint < y.blueprint ? -1 : x.blueprint > y.blueprint ? 1 : 0));
229
+ return emitWouldBeAmbiguous(io, blueprintDir, typeof f.task === 'string' ? f.task : undefined, existing);
230
+ }
231
+ const result = validateBlueprint({
232
+ repoRoot,
233
+ blueprintDir,
234
+ gate: 'plan',
235
+ });
236
+ if (!result.ok) {
237
+ // failures를 그대로 전달 — plan gate가 권위; 실패한 brief에는 pointer를
238
+ // 쓰지 않음.
239
+ // gates는 validate가 G 실패를 요약했을 때만 있다 — 있을 때만 그대로 싣는다.
240
+ const payload = result.gates ? { ok: false, failures: result.failures, gates: result.gates }
241
+ : { ok: false, failures: result.failures };
242
+ io.out(`${JSON.stringify(payload, null, 2)}\n`);
243
+ return 1;
244
+ }
245
+ // plan 통과 뒤에만 task 해석·포인터 기록. 해석 실패는 사용법 오류(2).
246
+ const taskSpec = wantsTask ? f.task : undefined;
247
+ if (wantsTask && (typeof taskSpec !== 'string' || taskSpec === '')) {
248
+ io.err('current: --task requires a task id (NNN or TASKS-NNN)\n');
249
+ return 2;
250
+ }
251
+ const resolved = resolvePointerTask({
252
+ repoRoot,
253
+ blueprintDir,
254
+ task: typeof taskSpec === 'string' ? taskSpec : undefined,
255
+ });
256
+ if (!resolved.ok) {
257
+ const ids = (resolved.available || [])
258
+ .map((t) => t.id)
259
+ .filter(Boolean)
260
+ .join(', ');
261
+ io.err(`current: cannot resolve --task ${JSON.stringify(taskSpec)}`
262
+ + (ids ? ` (available: ${ids})` : '')
263
+ + '\n');
264
+ return 2;
265
+ }
266
+ let base = typeof f.base === 'string' ? f.base : undefined;
267
+ if (!base) {
268
+ // 부재·깨진 JSON을 {}로 삼킨다. --set은 base_branch만 읽고, 없으면
269
+ // 현재 체크아웃 브랜치. init이 탐지 실패 시 키를 비우므로 이 폴백이
270
+ // 기본 경로다. develop/main을 추측하지 않는다.
271
+ const config = (readConfig(repoRoot) ?? {});
272
+ if (config && typeof config.base_branch === 'string' && config.base_branch) {
273
+ base = config.base_branch;
274
+ }
275
+ else {
276
+ try {
277
+ const out = execFileSync('git', ['symbolic-ref', '--short', 'HEAD'], {
278
+ cwd: repoRoot,
279
+ encoding: 'utf8',
280
+ stdio: ['ignore', 'pipe', 'pipe'],
281
+ });
282
+ base = String(out).trim();
283
+ }
284
+ catch (_e) {
285
+ base = '';
286
+ }
287
+ }
288
+ }
289
+ // init과 같이 실패한 HEAD를 빈 문자열·develop으로 바꾸지 않는다.
290
+ // 포인터에 빈 base를 쓰면 finalize PR이 없는 브랜치를 향한다.
291
+ if (!base) {
292
+ io.err('current: cannot resolve base (no config.base_branch and HEAD is not a branch)\n');
293
+ return 1;
294
+ }
295
+ const pointerKey = pointerKeyFromBlueprint(blueprintDir).key;
296
+ // baseKeys를 읽은 뒤 writeCurrent 전까지 포인터를 바꾸는 코드가 없으므로 같은 목록을 재사용한다.
297
+ // baseKeys가 null(--replace 또는 base checkout 아님)일 때만 다시 읽는다.
298
+ const pointerExisted = baseKeys
299
+ ? baseKeys.includes(pointerKey)
300
+ : listNamespacePointers({ repoRoot }).some((entry) => entry.key === pointerKey);
301
+ // 포인터가 이미 있으면 task 전진만으로 승인 파일을 덮지 않는다.
302
+ // `--reapprove`만 사용자가 명시한 재승인이다 (AGENTS.md hard rule 3).
303
+ const recordApproval = wantsReapprove || !pointerExisted;
304
+ const computed = recordApproval
305
+ ? computeApprovalDigest({ repoRoot, blueprintDir })
306
+ : null;
307
+ try {
308
+ writeCurrent({
309
+ repoRoot,
310
+ blueprint: blueprintDir,
311
+ base,
312
+ task: resolved.task || undefined,
313
+ replace: wantsReplace,
314
+ replaceKey,
315
+ });
316
+ }
317
+ catch (error) {
318
+ if (isSelectionError(error))
319
+ return emitSelectionFailure(io, error);
320
+ throw error;
321
+ }
322
+ let approval = 'unchanged';
323
+ if (computed) {
324
+ writeApprovalSnapshot({
325
+ repoRoot,
326
+ blueprintDir,
327
+ digest: computed.digest,
328
+ parts: computed.parts,
329
+ });
330
+ approval = 'recorded';
331
+ }
332
+ // 방금 쓴 키를 보여 준다. 위치 기반 재해석은 병렬 추가 뒤 base에서
333
+ // CURRENT_AMBIGUOUS가 되어 성공한 --set을 실패로 뒤집는다.
334
+ const stored = {
335
+ blueprint: blueprintDir,
336
+ base,
337
+ task: typeof resolved.task === 'string' ? resolved.task : null,
338
+ };
339
+ const current = presentCurrent(stored, { repoRoot });
340
+ // --keep-others로 병렬 key를 남겼다는 사실을 호출자가 볼 수 있게 한다.
341
+ const others = wantsKeepOthers && existingEntries.length > 0
342
+ ? existingEntries.map((e) => ({ blueprint: e.pointer.blueprint }))
343
+ : undefined;
344
+ if (previous) {
345
+ emitPrevious(io, previous);
346
+ io.out(`${JSON.stringify({ ok: true, current, previous, approval }, null, 2)}\n`);
347
+ }
348
+ else {
349
+ io.out(`${JSON.stringify({ ok: true, current, ...(others ? { others } : {}), approval }, null, 2)}\n`);
350
+ }
351
+ return 0;
352
+ }
353
+ // 없음도 오류가 아닌 상태: unset이면 ready list를 붙여 execute가
354
+ // "planned but unset"과 "nothing planned"를 구분하게 함.
355
+ // 다중 후보·충돌은 상태를 바꾸지 않고 종료 코드 1.
356
+ const shown = resolveCurrent({ repoRoot });
357
+ if (shown.status === 'selected') {
358
+ const current = presentCurrent(shown.current, { repoRoot });
359
+ io.out(`${JSON.stringify({ ok: true, current }, null, 2)}\n`);
360
+ return 0;
361
+ }
362
+ if (shown.status === 'empty') {
363
+ const ready = listReadyBlueprints({ repoRoot });
364
+ io.out(`${JSON.stringify({ ok: true, current: null, ready }, null, 2)}\n`);
365
+ return 0;
366
+ }
367
+ return emitResolutionFailure(io, shown);
368
+ }
369
+ module.exports = {
370
+ current: {
371
+ run: cmdCurrent,
372
+ usage: cliUsage.COMMAND_USAGE['current'],
373
+ },
374
+ };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const cliFlags = require("./cli-flags");
4
+ const { parseFlags, argvRequestsHelp } = cliFlags;
5
+ const PRINT_ROLES = ['implementer', 'reviewer', 'debugger', 'context-reviewer', 'coordinator'];
6
+ const USAGE_BLOCK = ' dispatch print --role <implementer|reviewer|debugger|context-reviewer|coordinator>'
7
+ + ' --cwd <dir> --input <file> --out <dir>\n'
8
+ + ' Run one Cursor print dispatch (JSON).\n';
9
+ const USAGE = `usage: bouncer dispatch print --role <role> --cwd <dir> --input <file> --out <dir> [--repo <dir>]
10
+
11
+ ${USAGE_BLOCK}`;
12
+ const HELP = `${USAGE}
13
+ --input is a UTF-8 text file, not JSON. The command appends those bytes after the role body.
14
+ Roles: implementer, reviewer, debugger, context-reviewer, coordinator.
15
+ --out keeps bouncer-<role>.prompt.md, bouncer-<role>.jsonl, and bouncer-<role>.log.
16
+ Payload rules: rules/cursor-print-dispatch.md
17
+ `;
18
+ const REQUIRED_FLAGS = ['role', 'cwd', 'input', 'out'];
19
+ /**
20
+ * `--flag` 토큰 횟수. parseFlags는 마지막 값만 남겨 중복 `--role`을 조용히
21
+ * 덮으므로, 덮기 전에 거절해야 잘못된 역할로 실행되지 않는다.
22
+ *
23
+ * @param {string[]} rest - 플래그 argv
24
+ * @param {string} name - `--` 없는 플래그 이름
25
+ * @returns {number} 등장 횟수
26
+ */
27
+ function countFlag(rest, name) {
28
+ let n = 0;
29
+ for (const tok of rest) {
30
+ if (tok === `--${name}`)
31
+ n += 1;
32
+ }
33
+ return n;
34
+ }
35
+ /**
36
+ * `dispatch print` argv. print 밖의 동사와 필수 플래그 누락·중복은 resolver를
37
+ * 열기 전에 거절해 exit 2와 운영 실패(exit 1)를 가른다.
38
+ *
39
+ * @param {string[]} rest - `dispatch` 뒤 argv
40
+ * @returns {ParsedPrint} 성공 필드 또는 error
41
+ */
42
+ function parseDispatchPrintArgs(rest) {
43
+ const fail = (message) => ({
44
+ error: `dispatch print: ${message}\n${USAGE}`,
45
+ role: null,
46
+ cwd: null,
47
+ input: null,
48
+ out: null,
49
+ });
50
+ if (rest[0] !== 'print') {
51
+ return fail('command must be print');
52
+ }
53
+ const flags = rest.slice(1);
54
+ for (const name of REQUIRED_FLAGS) {
55
+ if (countFlag(flags, name) > 1) {
56
+ return fail(`--${name} specified more than once`);
57
+ }
58
+ }
59
+ if (countFlag(flags, 'repo') > 1) {
60
+ return fail('--repo specified more than once');
61
+ }
62
+ const f = parseFlags(flags);
63
+ for (const name of REQUIRED_FLAGS) {
64
+ const value = f[name];
65
+ if (typeof value !== 'string' || value === '') {
66
+ return fail(`--${name} is required`);
67
+ }
68
+ }
69
+ if (f.repo !== undefined && (typeof f.repo !== 'string' || f.repo === '')) {
70
+ return fail('--repo requires a directory');
71
+ }
72
+ const role = f.role;
73
+ if (!PRINT_ROLES.includes(role)) {
74
+ return fail('--role must be implementer, reviewer, debugger, context-reviewer, or coordinator');
75
+ }
76
+ return {
77
+ role,
78
+ cwd: f.cwd,
79
+ input: f.input,
80
+ out: f.out,
81
+ repo: typeof f.repo === 'string' ? f.repo : undefined,
82
+ };
83
+ }
84
+ /**
85
+ * 공개 `dispatch` 핸들러. `--help`는 print 모듈을 열기 전에 stdout으로 끝내고,
86
+ * 그 외는 argv 검증과 print 실행·JSON·exit만 연결한다.
87
+ *
88
+ * @param {string[]} rest - `print`와 플래그
89
+ * @param {CliIo} io - stdout/stderr 싱크
90
+ * @returns {number} 성공 0, 운영 거절 1, 사용법 2
91
+ */
92
+ function cmdDispatch(rest, io) {
93
+ // 도움말은 필수 플래그 검사·lazy require보다 먼저. 입력 형식은 stdout에만 적는다.
94
+ if (argvRequestsHelp(rest)) {
95
+ io.out(HELP);
96
+ return 0;
97
+ }
98
+ const parsed = parseDispatchPrintArgs(rest);
99
+ if (parsed.error) {
100
+ io.err(parsed.error);
101
+ return 2;
102
+ }
103
+ // argv를 통과한 뒤에만 print 모듈을 적재한다. 사용법 거절이 agent spawn
104
+ // 경로를 require.cache에 남기지 않게 하기 위함.
105
+ const printDispatch = require('./print-dispatch');
106
+ const result = printDispatch.runPrintDispatch({
107
+ repoRoot: parsed.repo ? parsed.repo : process.cwd(),
108
+ role: parsed.role,
109
+ cwd: parsed.cwd,
110
+ inputFile: parsed.input,
111
+ outDir: parsed.out,
112
+ });
113
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
114
+ return result.ok ? 0 : 1;
115
+ }
116
+ module.exports = {
117
+ run: cmdDispatch,
118
+ usage: cliUsage.COMMAND_USAGE['dispatch'],
119
+ };