@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.
- package/LICENSE +202 -0
- package/README.md +31 -2
- package/bin/bouncer +26 -0
- package/bin/bouncer-root +25 -0
- package/bin/build-stamp.js +78 -0
- package/bin/register-path.js +71 -0
- package/dist/.build-stamp +1 -0
- package/dist/approval-snapshot.js +99 -0
- package/dist/bouncer-root.js +80 -0
- package/dist/canonical-json.js +37 -0
- package/dist/cli-commit-guard-command.js +74 -0
- package/dist/cli-config-command.js +47 -0
- package/dist/cli-context-review-command.js +273 -0
- package/dist/cli-current-command.js +374 -0
- package/dist/cli-dispatch-command.js +119 -0
- package/dist/cli-doc-commands.js +352 -0
- package/dist/cli-epic-command.js +130 -0
- package/dist/cli-flags.js +50 -0
- package/dist/cli-git-commands.js +931 -0
- package/dist/cli-intent-command.js +491 -0
- package/dist/cli-metrics-command.js +134 -0
- package/dist/cli-ontology-command.js +109 -0
- package/dist/cli-project-commands.js +580 -0
- package/dist/cli-retro-command.js +138 -0
- package/dist/cli-review-command.js +251 -0
- package/dist/cli-review-dispatch-command.js +338 -0
- package/dist/cli-usage.js +72 -0
- package/dist/cli.js +70 -0
- package/dist/codex-agents.js +189 -0
- package/dist/command-argv.js +125 -0
- package/dist/commit-guard.js +72 -0
- package/dist/commit-hook.js +470 -0
- package/dist/commit-message.js +77 -0
- package/dist/commit-sha.js +107 -0
- package/dist/commit.js +519 -0
- package/dist/composite-gates.js +129 -0
- package/dist/comprehension.js +127 -0
- package/dist/config.js +298 -0
- package/dist/context-ids.js +288 -0
- package/dist/context-migration-rewrite.js +421 -0
- package/dist/context-ontology.js +746 -0
- package/dist/context-review-contracts.js +2 -0
- package/dist/context-review-projection.js +147 -0
- package/dist/context-review-state.js +824 -0
- package/dist/context-review-store.js +602 -0
- package/dist/context-review-transitions.js +412 -0
- package/dist/coordinate-advance.js +253 -0
- package/dist/coordinate-next.js +998 -0
- package/dist/coordinate-output.js +57 -0
- package/dist/coordinator-command-contracts.js +2 -0
- package/dist/coordinator-commands.js +1908 -0
- package/dist/coordinator-fanin.js +1021 -0
- package/dist/coordinator-git.js +121 -0
- package/dist/coordinator-input.js +37 -0
- package/dist/coordinator-ledger.js +381 -0
- package/dist/coordinator-prepare.js +1047 -0
- package/dist/coordinator-repair.js +1416 -0
- package/dist/coordinator-verification.js +42 -0
- package/dist/coordinator-worker.js +388 -0
- package/dist/coordinator.js +7 -0
- package/dist/cross-drive.js +85 -0
- package/dist/current.js +1148 -0
- package/dist/dag-shape.js +93 -0
- package/dist/doc-shape-contracts.js +241 -0
- package/dist/drive-metrics.js +691 -0
- package/dist/epic-index.js +347 -0
- package/dist/epic-registry.js +453 -0
- package/dist/execute-prepare.js +262 -0
- package/dist/execution-contracts.js +46 -0
- package/dist/execution-dag.js +148 -0
- package/dist/finalize-checks.js +177 -0
- package/dist/finalize-collect.js +343 -0
- package/dist/finalize-comprehension.js +116 -0
- package/dist/finalize-digest-collect.js +388 -0
- package/dist/finalize-digest-model.js +595 -0
- package/dist/finalize-digest.js +164 -0
- package/dist/finalize-explain.js +176 -0
- package/dist/finalize-pr.js +610 -0
- package/dist/finalize-release-main.js +652 -0
- package/dist/finalize.js +714 -0
- package/dist/frontmatter.js +22 -0
- package/dist/git-ops.js +49 -0
- package/dist/graph-exec.js +179 -0
- package/dist/graph-scope.js +268 -0
- package/dist/graphify.js +995 -0
- package/dist/guards.js +82 -0
- package/dist/import-git.js +78 -0
- package/dist/import-history.js +392 -0
- package/dist/import-render.js +59 -0
- package/dist/import-types.js +2 -0
- package/dist/init.js +571 -0
- package/dist/intent-bundle.js +936 -0
- package/dist/intent-provenance.js +927 -0
- package/dist/layout.js +131 -0
- package/dist/lease.js +145 -0
- package/dist/legacy-comments.js +71 -0
- package/dist/migrate-context-slugs.js +262 -0
- package/dist/migrate-merge-legacy-epics.js +392 -0
- package/dist/migrate-review-scope.js +144 -0
- package/dist/migrate-task-layout.js +194 -0
- package/dist/path-overlap.js +38 -0
- package/dist/paths.js +45 -0
- package/dist/plan-approve.js +218 -0
- package/dist/plan-inspect.js +575 -0
- package/dist/plan-prerequisites.js +197 -0
- package/dist/plan-snapshot-contract.js +98 -0
- package/dist/plan-snapshot.js +138 -0
- package/dist/plugin-root.js +237 -0
- package/dist/posix-path.js +14 -0
- package/dist/pre-commit-hook.js +170 -0
- package/dist/print-dispatch.js +285 -0
- package/dist/render.js +8 -0
- package/dist/retention-migration.js +595 -0
- package/dist/retro-read.js +259 -0
- package/dist/retro-sources.js +318 -0
- package/dist/review-dispatch.js +1054 -0
- package/dist/review-policy.js +28 -0
- package/dist/review-record.js +447 -0
- package/dist/review-repair-state.js +52 -0
- package/dist/review-snapshot.js +659 -0
- package/dist/run-detach.js +165 -0
- package/dist/run-preflight.js +368 -0
- package/dist/runtime-state.js +1038 -0
- package/dist/scaffold.js +533 -0
- package/dist/schema.js +294 -0
- package/dist/scope.js +1073 -0
- package/dist/seed-worktree.js +926 -0
- package/dist/session-graph.js +337 -0
- package/dist/subagents.js +86 -0
- package/dist/symbol-index.js +1390 -0
- package/dist/task-brief-hash.js +34 -0
- package/dist/task-commits.js +124 -0
- package/dist/tasks-docs.js +192 -0
- package/dist/templates.js +469 -0
- package/dist/time.js +21 -0
- package/dist/validate-docs.js +302 -0
- package/dist/validate-gates-finalize.js +303 -0
- package/dist/validate-gates-plan.js +720 -0
- package/dist/validate-gates-run.js +420 -0
- package/dist/validate-gates-shared.js +124 -0
- package/dist/validate-gates.js +87 -0
- package/dist/validate-sections.js +654 -0
- package/dist/validate-structural.js +347 -0
- package/dist/validate.js +417 -0
- package/dist/verification-contracts.js +26 -0
- package/dist/verification-evidence.js +233 -0
- package/dist/verification-execution.js +163 -0
- package/dist/verification-identity.js +413 -0
- package/dist/verification-target.js +51 -0
- package/dist/verification.js +383 -0
- package/dist/windows-npm.js +38 -0
- package/package.json +32 -4
- package/resources/graphify-compat.json +9 -0
- package/vendor/README.md +23 -0
- package/vendor/js-yaml.LICENSE +21 -0
- package/vendor/js-yaml.js +3022 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const cliUsage = require("./cli-usage");
|
|
3
|
+
function cmdCommit(rest, io) {
|
|
4
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
5
|
+
const { parseFlags } = require('./cli-flags');
|
|
6
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
7
|
+
const { commitTask, commitExceptionResult } = require('./commit');
|
|
8
|
+
const f = parseFlags(rest);
|
|
9
|
+
// 커밋 대상이 없으면 commit-safety가 빈 범위를 검사하게 되므로 형식(2)으로 거절.
|
|
10
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
11
|
+
io.err('commit: --blueprint is required\n');
|
|
12
|
+
return 2;
|
|
13
|
+
}
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = commitTask({
|
|
17
|
+
repoRoot: (f.repo || process.cwd()),
|
|
18
|
+
blueprintDir: f.blueprint,
|
|
19
|
+
// === true: parseFlags가 값 없는 --yes만 boolean으로 둔다. `--yes 1` 같은
|
|
20
|
+
// 문자열은 동의로 치지 않아 실수 커밋을 막는다.
|
|
21
|
+
yes: f.yes === true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// git commit hook 거절 등 모든 commitTask 예외를 stdout JSON 실패로 바꾼다.
|
|
26
|
+
// 그렇지 않으면 stdout이 비어 advance가 unclear-result로 원인을 잃는다.
|
|
27
|
+
// 플래그 오류는 위에서 이미 반환했고, 예외 종류와 무관하게 사용자에게
|
|
28
|
+
// 보이는 결과가 같아야 하므로(report-and-stop) 구분 없이 흡수한다.
|
|
29
|
+
// stage된 경로는 건드리지 않는다.
|
|
30
|
+
const failure = commitExceptionResult(error);
|
|
31
|
+
io.err(`commit: ${failure.cause}\n`);
|
|
32
|
+
io.out(`${JSON.stringify(failure, null, 2)}\n`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
36
|
+
// 스코프 실패·훅 거절은 게이트 결과(1). 플래그는 이미 통과했으므로 2가 아니다.
|
|
37
|
+
return result.ok ? 0 : 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* finalize 서브커맨드를 분기한다. release-main은 메인 정리라 --yes 커밋 경로와
|
|
41
|
+
* 인자를 공유하면 닫힌 사본을 커밋 스코프로 오인하므로 앞 토큰으로만 가른다.
|
|
42
|
+
*
|
|
43
|
+
* @param {string[]} rest - `finalize` 다음 argv
|
|
44
|
+
* @param {CliIo} io - stdout/stderr 콜백
|
|
45
|
+
* @returns {number} 0 성공, 1 실행 거절, 2 사용법(`--blueprint` 누락)
|
|
46
|
+
*/
|
|
47
|
+
function cmdFinalize(rest, io) {
|
|
48
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
49
|
+
const { parseFlags } = require('./cli-flags');
|
|
50
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
51
|
+
const { finalize } = require('./finalize');
|
|
52
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
53
|
+
const { prepareFinalizeDigest } = require('./finalize-digest');
|
|
54
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
55
|
+
const { resolveExplainLinks, renderPrBody } = require('./finalize-pr');
|
|
56
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
57
|
+
const { releaseMain } = require('./finalize-release-main');
|
|
58
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
59
|
+
const { recordComprehension } = require('./finalize-comprehension');
|
|
60
|
+
// worktree-path는 문서가 내부 모듈(runtime-state)을 require하지 않도록 두는 읽기 전용 조회다.
|
|
61
|
+
// 파일·pointer·worktree를 만들지 않으며, 앞 토큰으로 가르지 않으면 기존 finalize 경로로 흘러
|
|
62
|
+
// blueprint 문서를 요구한다.
|
|
63
|
+
if (rest[0] === 'worktree-path') {
|
|
64
|
+
const f = parseFlags(rest.slice(1));
|
|
65
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
66
|
+
io.err('usage: bouncer finalize worktree-path --blueprint <dir> [--repo <dir>]\n');
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
// `--repo`(boolean true)를 경로로 쓰지 않는다. 다른 서브커맨드와 같은 규칙.
|
|
70
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
71
|
+
const { worktreePathFor } = require('./runtime-state');
|
|
72
|
+
try {
|
|
73
|
+
io.out(`${worktreePathFor({ repoRoot, blueprint: f.blueprint })}\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// worktreePathFor는 blueprint id 파싱 실패·git 부재에서만 던지고 쓰기가 없다.
|
|
78
|
+
// 두 경우 모두 사용자에게 보일 결과가 같은 거절(1)이라 메시지를 그대로 전한다.
|
|
79
|
+
io.err(`finalize worktree-path: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// diff-sha는 explain.md의 diff_sha를 구할 때 쓰는 읽기 전용 조회다. computeDiffSha는 throw하지
|
|
84
|
+
// 않고 닫힌 reason 집합을 돌려주므로 결과를 그대로 JSON 한 줄로 낸다.
|
|
85
|
+
if (rest[0] === 'diff-sha') {
|
|
86
|
+
const f = parseFlags(rest.slice(1));
|
|
87
|
+
if (typeof f.base !== 'string' || f.base === '') {
|
|
88
|
+
io.err('usage: bouncer finalize diff-sha --base <ref> [--repo <dir>]\n');
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
92
|
+
const { computeDiffSha } = require('./comprehension');
|
|
93
|
+
const result = computeDiffSha({ repoRoot, base: f.base });
|
|
94
|
+
io.out(`${JSON.stringify(result)}\n`);
|
|
95
|
+
return result.ok ? 0 : 1;
|
|
96
|
+
}
|
|
97
|
+
// prepare는 읽기 전용 digest. 기존 finalize --yes 경로와 인자를 섞지 않는다.
|
|
98
|
+
if (rest[0] === 'prepare') {
|
|
99
|
+
const f = parseFlags(rest.slice(1));
|
|
100
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
101
|
+
io.err('finalize: --blueprint is required\n');
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
// `--repo`(boolean true)를 경로로 쓰지 않는다. cmdImport와 같은 규칙 —
|
|
105
|
+
// truthy 비문자열이 cwd 대신 들어가면 digest가 잘못된 checkout을 읽는다.
|
|
106
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
107
|
+
const result = prepareFinalizeDigest({
|
|
108
|
+
repoRoot,
|
|
109
|
+
blueprintDir: f.blueprint,
|
|
110
|
+
});
|
|
111
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
112
|
+
return result.ok ? 0 : 1;
|
|
113
|
+
}
|
|
114
|
+
// pr-body는 저장한 digest와 요약 파일만 읽어 stdout에 Markdown을 낸다. 원장·Git·task 문서를
|
|
115
|
+
// 읽거나 파일을 쓰지 않아 정리 뒤에도 같은 입력이 같은 출력을 낸다.
|
|
116
|
+
if (rest[0] === 'pr-body') {
|
|
117
|
+
const f = parseFlags(rest.slice(1));
|
|
118
|
+
if (typeof f.digest !== 'string' || f.digest === '') {
|
|
119
|
+
io.err('finalize: --digest is required\n');
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
if (typeof f.summary !== 'string' || f.summary === '') {
|
|
123
|
+
io.err('finalize: --summary is required\n');
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
// --flow는 선택이지만 값 없이 주어지면 사용법 오류다(조용히 무시하면 흐름이 빠진 채 통과한다).
|
|
127
|
+
if (f.flow !== undefined && (typeof f.flow !== 'string' || f.flow === '')) {
|
|
128
|
+
io.err('finalize: --flow requires a file\n');
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const fs = require('node:fs');
|
|
132
|
+
try {
|
|
133
|
+
const digest = JSON.parse(fs.readFileSync(f.digest, 'utf8'));
|
|
134
|
+
const summary = fs.readFileSync(f.summary, 'utf8');
|
|
135
|
+
const flow = typeof f.flow === 'string' ? fs.readFileSync(f.flow, 'utf8') : undefined;
|
|
136
|
+
io.out(renderPrBody({ digest, summary, flow }));
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// 파일 읽기·JSON 파싱·입력 검증 실패는 모두 사용자 입력 문제이며 같은 형식으로 거절한다.
|
|
141
|
+
// 쓰기가 없어 어느 경우든 남는 상태가 없으므로 구분 없이 흡수해도 안전하다.
|
|
142
|
+
io.err(`finalize pr-body: ${error.message || String(error)}\n`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// release-main은 메인 checkout 전용 정리. finalize --yes(integration 커밋)와
|
|
147
|
+
// 섞이면 닫힌 사본을 커밋 대상으로 오인하므로, prepare/links와 같이 앞 토큰으로 가른다.
|
|
148
|
+
if (rest[0] === 'release-main') {
|
|
149
|
+
const f = parseFlags(rest.slice(1));
|
|
150
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
151
|
+
io.err('finalize: --blueprint is required\n');
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
// `--repo`(boolean true)를 경로로 쓰지 않는다. prepare와 같은 규칙 —
|
|
155
|
+
// truthy 비문자열이 cwd 대신 들어가면 메인 판정이 잘못된 checkout을 본다.
|
|
156
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
157
|
+
const result = releaseMain({
|
|
158
|
+
repoRoot,
|
|
159
|
+
cwd: process.cwd(),
|
|
160
|
+
blueprintDir: f.blueprint,
|
|
161
|
+
allowPartialMerge: f['allow-partial-merge'] === true,
|
|
162
|
+
confirmPartialMerge: f['confirm-partial-merge'] === true,
|
|
163
|
+
});
|
|
164
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
165
|
+
return result.ok ? 0 : 1;
|
|
166
|
+
}
|
|
167
|
+
// comprehension은 사용자 quiz 확인 뒤 explain.md 기록만 쓴다. 앞 토큰으로 가르지 않으면
|
|
168
|
+
// 기존 finalize(--yes 커밋 경로)로 흘러 확인 없는 호출이 엉뚱한 결과를 낸다.
|
|
169
|
+
if (rest[0] === 'comprehension') {
|
|
170
|
+
const f = parseFlags(rest.slice(1));
|
|
171
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
172
|
+
io.err('finalize: --blueprint is required\n');
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
if (typeof f.digest !== 'string' || f.digest === '') {
|
|
176
|
+
io.err('finalize: --digest is required\n');
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
// 두 flag는 상호배타: 최초 기록과 갱신은 서로 다른 전제를 가진다.
|
|
180
|
+
if (f['confirm-quiz'] === true && f.refresh === true) {
|
|
181
|
+
io.err('finalize: --confirm-quiz and --refresh are mutually exclusive\n');
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
185
|
+
let digest;
|
|
186
|
+
try {
|
|
187
|
+
digest = JSON.parse(require('node:fs').readFileSync(f.digest, 'utf8'));
|
|
188
|
+
}
|
|
189
|
+
catch (_e) {
|
|
190
|
+
// 읽기·JSON 파싱 실패는 digest 없음과 같다. 기록 모듈이 동의 → 입력 순서로 거절한다.
|
|
191
|
+
digest = undefined;
|
|
192
|
+
}
|
|
193
|
+
let result;
|
|
194
|
+
try {
|
|
195
|
+
result = recordComprehension({
|
|
196
|
+
repoRoot,
|
|
197
|
+
blueprintDir: f.blueprint,
|
|
198
|
+
digest,
|
|
199
|
+
// === true: 값이 붙은 `--confirm-quiz 1`은 동의로 치지 않는다.
|
|
200
|
+
confirmed: f['confirm-quiz'] === true,
|
|
201
|
+
refresh: f.refresh === true,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// 쓰기 실패(디스크·권한)는 거절 reason이 아니라 실행 실패다. 원본은 rename 전이라 온전하다.
|
|
206
|
+
io.err(`finalize: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
210
|
+
return result.ok ? 0 : 1;
|
|
211
|
+
}
|
|
212
|
+
if (rest[0] === 'partial-finalize') {
|
|
213
|
+
const f = parseFlags(rest.slice(1));
|
|
214
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
215
|
+
io.err('finalize: --blueprint is required\n');
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
218
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
219
|
+
const result = finalize({
|
|
220
|
+
repoRoot,
|
|
221
|
+
cwd: process.cwd(),
|
|
222
|
+
blueprintDir: f.blueprint,
|
|
223
|
+
yes: f.yes === true,
|
|
224
|
+
partialFinalize: true,
|
|
225
|
+
confirmPartial: f['confirm-partial-finalize'] === true,
|
|
226
|
+
});
|
|
227
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
228
|
+
return result.ok ? 0 : 1;
|
|
229
|
+
}
|
|
230
|
+
// links는 push 뒤 Explain URL 후보만 돌려 준다. digest·task 문서를 읽지
|
|
231
|
+
// 않으므로 --yes 뒤에도 동작하고, 실패는 ok:true + reason으로만 알린다.
|
|
232
|
+
if (rest[0] === 'links') {
|
|
233
|
+
const f = parseFlags(rest.slice(1));
|
|
234
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
235
|
+
io.err('finalize: --blueprint is required\n');
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
239
|
+
const result = resolveExplainLinks({
|
|
240
|
+
repoRoot,
|
|
241
|
+
blueprintDir: f.blueprint,
|
|
242
|
+
});
|
|
243
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
const f = parseFlags(rest);
|
|
247
|
+
// commit과 같은 2: 대상 없이 --yes를 받으면 빈 스코프로 커밋을 시도한다.
|
|
248
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
249
|
+
io.err('finalize: --blueprint is required\n');
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
const result = finalize({
|
|
253
|
+
repoRoot: (f.repo || process.cwd()),
|
|
254
|
+
blueprintDir: f.blueprint,
|
|
255
|
+
// commit과 같은 동의 규칙. truthy 문자열을 통과시키면 dry-run 기본이 깨진다.
|
|
256
|
+
yes: f.yes === true,
|
|
257
|
+
});
|
|
258
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
259
|
+
// commit과 같은 0/1. remainder 실패도 사용법이 아니라 실행 결과다.
|
|
260
|
+
return result.ok ? 0 : 1;
|
|
261
|
+
}
|
|
262
|
+
function cmdSeedWorktree(rest, io) {
|
|
263
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
264
|
+
const { catchMessage } = require('./guards');
|
|
265
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
266
|
+
const { parseFlags } = require('./cli-flags');
|
|
267
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
268
|
+
const { seedWorktree } = require('./seed-worktree');
|
|
269
|
+
const f = parseFlags(rest);
|
|
270
|
+
// blueprint를 먼저 묻는다. help 나열 순서와 같고, 대상 없이 --to만 있으면
|
|
271
|
+
// 빈 worktree로 문서를 옮기려다 실패 원인을 숨긴다.
|
|
272
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
273
|
+
io.err('seed-worktree: --blueprint is required\n');
|
|
274
|
+
return 2;
|
|
275
|
+
}
|
|
276
|
+
if (typeof f.to !== 'string' || f.to === '') {
|
|
277
|
+
io.err('seed-worktree: --to is required\n');
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
// --repo(기본 cwd)는 plan 문서가 있는 base checkout;
|
|
282
|
+
// --to는 committed HEAD에서 막 만든 worktree.
|
|
283
|
+
const result = seedWorktree({
|
|
284
|
+
repoRoot: (f.repo || process.cwd()),
|
|
285
|
+
blueprintDir: f.blueprint,
|
|
286
|
+
worktreePath: f.to,
|
|
287
|
+
});
|
|
288
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
289
|
+
return result.ok ? 0 : 1;
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// git 실패 등 런타임. 플래그는 이미 통과했으므로 사용법(2)이 아니라 1.
|
|
293
|
+
io.err(`seed-worktree: ${catchMessage(error)}\n`);
|
|
294
|
+
return 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function cmdImport(rest, io) {
|
|
298
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
299
|
+
const { parseFlags } = require('./cli-flags');
|
|
300
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
301
|
+
const { planImport, applyImport } = require('./import-history');
|
|
302
|
+
const f = parseFlags(rest);
|
|
303
|
+
// `--repo`(boolean true)를 경로로 쓰지 않는다. 다른 명령의 `f.repo || cwd`와
|
|
304
|
+
// 달리 여기만 문자열을 요구해 온 동작을 유지한다.
|
|
305
|
+
const repoRoot = typeof f.repo === 'string' && f.repo ? f.repo : process.cwd();
|
|
306
|
+
const yes = f.yes === true;
|
|
307
|
+
let limit;
|
|
308
|
+
if (typeof f.limit === 'string' && f.limit !== '') {
|
|
309
|
+
const n = Number(f.limit);
|
|
310
|
+
// NaN/Infinity를 planImport에 넘기지 않는다. 깨진 --limit은 "무제한"과
|
|
311
|
+
// 같게 두어 거절 대신 기본 계획으로 떨어지게 한다(기존 동작).
|
|
312
|
+
if (Number.isFinite(n))
|
|
313
|
+
limit = n;
|
|
314
|
+
}
|
|
315
|
+
const plan = planImport({
|
|
316
|
+
repoRoot,
|
|
317
|
+
source: typeof f.source === 'string' ? f.source : undefined,
|
|
318
|
+
since: typeof f.since === 'string' ? f.since : undefined,
|
|
319
|
+
limit,
|
|
320
|
+
epicId: typeof f['epic-id'] === 'string' ? f['epic-id'] : undefined,
|
|
321
|
+
epicName: typeof f['epic-name'] === 'string' ? f['epic-name'] : undefined,
|
|
322
|
+
});
|
|
323
|
+
// --yes 없으면 dry-run. --message 만 있어도 무시하고 계획만 낸다.
|
|
324
|
+
if (!yes) {
|
|
325
|
+
io.out(`${JSON.stringify(plan, null, 2)}\n`);
|
|
326
|
+
// 거절·한도 실패는 게이트 실패(1)가 아니라 호출 조건(2). apply도 같다.
|
|
327
|
+
return plan.ok ? 0 : 2;
|
|
328
|
+
}
|
|
329
|
+
const result = applyImport({
|
|
330
|
+
repoRoot,
|
|
331
|
+
plan,
|
|
332
|
+
message: typeof f.message === 'string' ? f.message : undefined,
|
|
333
|
+
});
|
|
334
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
335
|
+
return result.ok ? 0 : 2;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 반복 `--paths`를 순서대로 모은다.
|
|
339
|
+
*
|
|
340
|
+
* parseFlags는 같은 플래그의 마지막 값만 남기므로(cli-flags.ts) 여러 번 준
|
|
341
|
+
* 경로가 하나로 접힌다. cli-project-commands의 `--for`와 같은 방식으로 rest를
|
|
342
|
+
* 직접 훑는다. 값이 없거나 다음 토큰이 또 다른 플래그면 건너뛴다 — 빈 문자열을
|
|
343
|
+
* 경로로 넘기면 reviseTaskScope가 그것을 out-of-bounds로 바꿔 거절 코드가
|
|
344
|
+
* `scope-paths-required`에서 갈라진다.
|
|
345
|
+
*/
|
|
346
|
+
function collectPathValues(rest, flag = '--paths') {
|
|
347
|
+
const out = [];
|
|
348
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
349
|
+
if (rest[i] !== flag)
|
|
350
|
+
continue;
|
|
351
|
+
const value = rest[i + 1];
|
|
352
|
+
if (value === undefined || value.startsWith('--'))
|
|
353
|
+
continue;
|
|
354
|
+
out.push(value);
|
|
355
|
+
i += 1;
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* `--findings`도 반복 플래그라 parseFlags의 마지막 값 보존을 우회한다.
|
|
361
|
+
*
|
|
362
|
+
* 값이 없는 occurrence도 빈 문자열로 남긴다. 유효한 다른 finding만 모으면 한 번의
|
|
363
|
+
* malformed flag가 성공 요청에 섞여 ledger write까지 통과하므로, coordinator의
|
|
364
|
+
* findings-required 검사가 요청 전체를 원자적으로 거절할 수 있게 한다.
|
|
365
|
+
*/
|
|
366
|
+
function collectFindingValues(rest) {
|
|
367
|
+
const out = [];
|
|
368
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
369
|
+
if (rest[i] !== '--findings')
|
|
370
|
+
continue;
|
|
371
|
+
const value = rest[i + 1];
|
|
372
|
+
if (value === undefined || value.startsWith('--')) {
|
|
373
|
+
out.push('');
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
out.push(value);
|
|
377
|
+
i += 1;
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 반복 `--review-finding`을 순서대로 모은다.
|
|
383
|
+
* parseFlags는 마지막 값만 남겨 두 번째 finding이 사라지므로 rest를 직접 훑는다.
|
|
384
|
+
* 플래그가 한 번도 없으면 undefined를 돌려 CI 원인 repair와 구분한다. 값이 빈
|
|
385
|
+
* 문자열이거나 빠진 occurrence는 ''로 남겨 coordinator가
|
|
386
|
+
* `failure-evidence-required`로 원자 거절하게 한다.
|
|
387
|
+
*
|
|
388
|
+
* @param {string[]} rest - coordinate 서브커맨드 뒤 argv
|
|
389
|
+
* @returns {string[] | undefined} 등장 순서의 finding id, 없으면 undefined
|
|
390
|
+
*/
|
|
391
|
+
function collectReviewFindingValues(rest) {
|
|
392
|
+
const out = [];
|
|
393
|
+
let seen = false;
|
|
394
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
395
|
+
if (rest[i] !== '--review-finding')
|
|
396
|
+
continue;
|
|
397
|
+
seen = true;
|
|
398
|
+
const value = rest[i + 1];
|
|
399
|
+
if (value === undefined || (value.startsWith('--') && value !== '')) {
|
|
400
|
+
out.push('');
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
out.push(value);
|
|
404
|
+
i += 1;
|
|
405
|
+
}
|
|
406
|
+
return seen ? out : undefined;
|
|
407
|
+
}
|
|
408
|
+
function cmdExecute(rest, io) {
|
|
409
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
410
|
+
const { catchMessage } = require('./guards');
|
|
411
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
412
|
+
const { parseFlags } = require('./cli-flags');
|
|
413
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
414
|
+
const { executePrepare } = require('./execute-prepare');
|
|
415
|
+
const command = rest[0];
|
|
416
|
+
const f = parseFlags(rest.slice(1));
|
|
417
|
+
// prepare만 공개한다. 다른 서브커맨드를 받으면 사용법(2) — 런타임 거절(1)과
|
|
418
|
+
// 구분해, 없는 동사를 worktree 쓰기로 착각하지 않게 한다.
|
|
419
|
+
if (command !== 'prepare') {
|
|
420
|
+
io.err('execute: command must be prepare\n');
|
|
421
|
+
return 2;
|
|
422
|
+
}
|
|
423
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
424
|
+
io.err('execute: --blueprint is required\n');
|
|
425
|
+
return 2;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const result = executePrepare({
|
|
429
|
+
repoRoot: (f.repo || process.cwd()),
|
|
430
|
+
blueprintDir: f.blueprint,
|
|
431
|
+
});
|
|
432
|
+
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
433
|
+
return result.ok ? 0 : 1;
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
io.err(`execute prepare: ${catchMessage(error)}\n`);
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const COORDINATE_COMMANDS = [
|
|
441
|
+
'bootstrap', 'prepare', 'ready', 'dispatch', 'report', 'record', 'rerecord', 'integrate',
|
|
442
|
+
'status', 'revise', 'repair', 'reverify', 'partial-close', 'critical-recovery', 'revoke', 'next',
|
|
443
|
+
'advance', 'promote-stop', 'session-open', 'worker-model', 'worker-tokens',
|
|
444
|
+
'note',
|
|
445
|
+
];
|
|
446
|
+
/**
|
|
447
|
+
* 전역 help는 static usage 표를 이용해 핸들러 선택 전에 출력 가능하게 한다.
|
|
448
|
+
*
|
|
449
|
+
* @returns {string} 전역 coordinate usage
|
|
450
|
+
*/
|
|
451
|
+
function coordinateRegistryUsage() {
|
|
452
|
+
return cliUsage.COMMAND_USAGE.coordinate;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* usage(2) 한 줄 뒤에 같은 stderr로 블록을 붙인다. 알 수 없는 서브커맨드는
|
|
456
|
+
* 전체 registry usage, 알면 그 서브커맨드 `--help` 블록이다.
|
|
457
|
+
*
|
|
458
|
+
* @param {CliIo} io - stderr 싱크
|
|
459
|
+
* @param {string} message - 첫 줄(개행 포함)
|
|
460
|
+
* @param {string} [command] - 알려진 서브커맨드. 없으면 전체 usage
|
|
461
|
+
* @returns {2} usage 종료 코드
|
|
462
|
+
*/
|
|
463
|
+
function failCoordinateUsage(io, message, command) {
|
|
464
|
+
io.err(message);
|
|
465
|
+
if (command && COORDINATE_COMMANDS.includes(command)) {
|
|
466
|
+
io.err(cliUsage.coordinateUsage(command));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
io.err(coordinateRegistryUsage());
|
|
470
|
+
}
|
|
471
|
+
return 2;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* status 성공 뒤 coordinator print 입력 파일을 쓴다. 값은 원장(`base`)과
|
|
475
|
+
* main worktree 설정(autonomy)에서 모은다 — lib status는 읽기 전용으로 두려고
|
|
476
|
+
* 파일 쓰기를 CLI 계층에 둔다. 부모 디렉터리가 없으면 만든다.
|
|
477
|
+
*
|
|
478
|
+
* @param {object} args - 쓰기 입력
|
|
479
|
+
* @param {string} args.file - 출력 경로. 상대 경로는 cwd 기준
|
|
480
|
+
* @param {string} args.repoRoot - status에 쓴 저장소 루트
|
|
481
|
+
* @param {string} args.blueprint - blueprint 디렉터리
|
|
482
|
+
* @param {Record<string, unknown>} args.checkpoint - status가 돌려준 checkpoint
|
|
483
|
+
* @returns {string} 쓴 파일의 절대 경로
|
|
484
|
+
*/
|
|
485
|
+
function writeCoordinatorInput(args) {
|
|
486
|
+
const fs = require('node:fs');
|
|
487
|
+
const nodePath = require('node:path');
|
|
488
|
+
const { coordinatorPathsFor, runtimePaths } = require('./runtime-state');
|
|
489
|
+
const { loadLedgerBytes } = require('./coordinator-ledger');
|
|
490
|
+
const { readAutonomy } = require('./run-preflight');
|
|
491
|
+
const { buildCoordinatorInput } = require('./coordinator-input');
|
|
492
|
+
const paths = coordinatorPathsFor({ repoRoot: args.repoRoot, blueprint: args.blueprint });
|
|
493
|
+
const loaded = loadLedgerBytes(paths.ledgerFile);
|
|
494
|
+
if (!loaded)
|
|
495
|
+
throw new Error('ledger missing while writing coordinator input');
|
|
496
|
+
// autonomy 설정은 gitignore라 main checkout에만 있다. integration cwd가 아니라
|
|
497
|
+
// git common dir 부모(projectRoot)에서 읽는다.
|
|
498
|
+
const { projectRoot } = runtimePaths({ repoRoot: args.repoRoot });
|
|
499
|
+
const text = buildCoordinatorInput({
|
|
500
|
+
integrationPath: paths.integrationPath,
|
|
501
|
+
blueprint: args.blueprint,
|
|
502
|
+
base: loaded.ledger.base,
|
|
503
|
+
checkpoint: args.checkpoint,
|
|
504
|
+
autonomy: readAutonomy(projectRoot).value,
|
|
505
|
+
projectRoot,
|
|
506
|
+
});
|
|
507
|
+
const target = nodePath.resolve(process.cwd(), args.file);
|
|
508
|
+
fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
509
|
+
fs.writeFileSync(target, text);
|
|
510
|
+
return target;
|
|
511
|
+
}
|
|
512
|
+
const hasFlag = (f, name) => Object.prototype.hasOwnProperty.call(f, name);
|
|
513
|
+
const nonEmpty = (v) => typeof v === 'string' && v !== '';
|
|
514
|
+
/**
|
|
515
|
+
* 단순 문자열 플래그 → `coordinate()` 인자 필드 대응 표. 값이 문자열일 때만 넘기고
|
|
516
|
+
* 아니면 undefined다. 명령에 따라 달라지는 필드(decision·reason·model·kind·done)와
|
|
517
|
+
* 반복 플래그(paths·findings)는 `coordinateCall`에서 따로 채운다.
|
|
518
|
+
*/
|
|
519
|
+
const COORDINATE_STRING_FLAG_FIELDS = {
|
|
520
|
+
task: 'task',
|
|
521
|
+
sha: 'sha',
|
|
522
|
+
failureCommand: 'failure-command',
|
|
523
|
+
summary: 'summary',
|
|
524
|
+
verify: 'verify',
|
|
525
|
+
resumeTask: 'resume-task',
|
|
526
|
+
disposition: 'disposition',
|
|
527
|
+
outcome: 'outcome',
|
|
528
|
+
reason: 'reason',
|
|
529
|
+
role: 'role',
|
|
530
|
+
perspective: 'perspective',
|
|
531
|
+
tokens: 'tokens',
|
|
532
|
+
taskBriefHash: 'task-brief-hash',
|
|
533
|
+
text: 'text',
|
|
534
|
+
};
|
|
535
|
+
/** `--write-input`은 status 전용이고 값이 있어야 한다. 아니면 파일을 쓰기 전에 usage(2). */
|
|
536
|
+
function checkWriteInput(f, command, io) {
|
|
537
|
+
if (hasFlag(f, 'write-input') && (command !== 'status' || !nonEmpty(f['write-input']))) {
|
|
538
|
+
return failCoordinateUsage(io, 'coordinate: --write-input <file> is accepted by status only and needs a value\n', command);
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* fence 플래그(--ledger-path·--ledger-hash) 누락 처리. worker-model·worker-tokens는
|
|
544
|
+
* usage(2), 나머지 fenced 명령은 다른 coordinate 거절과 같은 JSON reason + exit 1이다.
|
|
545
|
+
* argv 문법 오류와 stale/invalid fence를 같은 채널로 모아 자동화 클라이언트가 파싱한다.
|
|
546
|
+
*/
|
|
547
|
+
function checkFenceFlags(f, command, io) {
|
|
548
|
+
// bootstrap·status(ready 별칭)만 원장 fence 예외. 그 외 mutation은 path/hash 쌍이
|
|
549
|
+
// 있어야 stale checkpoint로 원장·Git이 갈라지는 쓰기를 막는다.
|
|
550
|
+
const fenced = [
|
|
551
|
+
'prepare', 'dispatch', 'report', 'record', 'rerecord', 'integrate', 'revise',
|
|
552
|
+
'repair', 'reverify', 'partial-close', 'critical-recovery', 'revoke', 'promote-stop', 'session-open',
|
|
553
|
+
'worker-model', 'worker-tokens', 'note',
|
|
554
|
+
];
|
|
555
|
+
const missing = !nonEmpty(f['ledger-path']) || !nonEmpty(f['ledger-hash']);
|
|
556
|
+
if (!missing)
|
|
557
|
+
return undefined;
|
|
558
|
+
if (command === 'worker-model' || command === 'worker-tokens') {
|
|
559
|
+
return failCoordinateUsage(io, `coordinate ${command}: --ledger-path and --ledger-hash are required\n`, command);
|
|
560
|
+
}
|
|
561
|
+
if (!fenced.includes(command))
|
|
562
|
+
return undefined;
|
|
563
|
+
const coordinatorMod = require('./coordinator');
|
|
564
|
+
// 다른 coordinate 거절과 같이 hint 표를 거쳐 cause·next를 붙인다.
|
|
565
|
+
const hinted = coordinatorMod.withCoordinateFailureHints({ ok: false, reason: 'ledger-checkpoint-invalid' });
|
|
566
|
+
io.out(`${JSON.stringify(hinted)}\n`);
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
569
|
+
/** 양의 정수 플래그(--generation·--max-steps)를 검사한다. 잘못되면 usage(2)용 메시지를 돌려준다. */
|
|
570
|
+
function positiveIntFlag(f, name) {
|
|
571
|
+
if (!hasFlag(f, name))
|
|
572
|
+
return {};
|
|
573
|
+
const raw = f[name];
|
|
574
|
+
if (typeof raw !== 'string' || !/^[1-9]\d*$/.test(raw))
|
|
575
|
+
return { bad: true };
|
|
576
|
+
return { value: Number(raw) };
|
|
577
|
+
}
|
|
578
|
+
/** report 필수 flag 부재는 usage(2)로 돌려 argv 누락과 stale mismatch(1)를 구분한다. */
|
|
579
|
+
function checkReportFlags(f, io) {
|
|
580
|
+
const required = [
|
|
581
|
+
['attempt', '--attempt'], ['task-brief-hash', '--task-brief-hash'],
|
|
582
|
+
['outcome', '--outcome'], ['summary', '--summary'],
|
|
583
|
+
];
|
|
584
|
+
for (const [name, label] of required) {
|
|
585
|
+
if (!nonEmpty(f[name])) {
|
|
586
|
+
return failCoordinateUsage(io, `coordinate report: ${label} is required\n`, 'report');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* repair 전용 플래그(--kind·--done·--resume-task·--disposition·--verify) 검사.
|
|
593
|
+
* 값이 잘못되면 core로 넘기지 않고 usage(2)로 끝내 오타가 조용히 기존 경로로 접히지 않게 한다.
|
|
594
|
+
*/
|
|
595
|
+
function checkRepairFlags(f, io) {
|
|
596
|
+
const usage = (message) => failCoordinateUsage(io, `coordinate repair: ${message}\n`, 'repair');
|
|
597
|
+
if (hasFlag(f, 'kind') && f.kind !== 'product' && f.kind !== 'supplement') {
|
|
598
|
+
return usage('--kind must be product or supplement');
|
|
599
|
+
}
|
|
600
|
+
if (hasFlag(f, 'done') && (f.done !== true || f.kind !== 'supplement')) {
|
|
601
|
+
return usage('--done is a flag accepted only with --kind supplement');
|
|
602
|
+
}
|
|
603
|
+
// --resume-task/--disposition은 값이 필요하다. 값 없는 플래그(true)나 둘의 동시 입력이 조용히
|
|
604
|
+
// 기존 dynamic repair 경로로 접히면 새 task가 생기므로 core에 보내기 전에 usage(2)로 끝낸다.
|
|
605
|
+
for (const name of ['resume-task', 'disposition']) {
|
|
606
|
+
if (hasFlag(f, name) && !nonEmpty(f[name]))
|
|
607
|
+
return usage(`--${name} requires a value`);
|
|
608
|
+
}
|
|
609
|
+
// --verify는 새 repair task 문서에 쓸 단일 argv다. 값 없는 플래그가 생략으로 접히면
|
|
610
|
+
// scoped_terminal 거절이 엉뚱한 사유로 보이므로 usage(2). --resume-task와는 함께 쓸 수 없다.
|
|
611
|
+
if (hasFlag(f, 'verify') && (typeof f.verify !== 'string' || f.verify.trim() === '')) {
|
|
612
|
+
return usage('--verify requires a value');
|
|
613
|
+
}
|
|
614
|
+
if (hasFlag(f, 'verify') && hasFlag(f, 'resume-task')) {
|
|
615
|
+
return usage('--verify cannot be combined with --resume-task');
|
|
616
|
+
}
|
|
617
|
+
if (hasFlag(f, 'resume-task') && hasFlag(f, 'disposition')) {
|
|
618
|
+
return usage('--resume-task and --disposition are mutually exclusive');
|
|
619
|
+
}
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
/** worker-model·worker-tokens 전용 플래그 검사. 값 형식은 core가 model-invalid·tokens-invalid(1)로 거절한다. */
|
|
623
|
+
function checkWorkerFlags(f, command, io) {
|
|
624
|
+
const usage = (message) => failCoordinateUsage(io, `coordinate ${command}: ${message}\n`, command);
|
|
625
|
+
// 플래그 자체가 없는 경우만 usage(2). 값 없는 --model(true)은 core가 model-invalid(1)로 거절한다.
|
|
626
|
+
if (!nonEmpty(f.role))
|
|
627
|
+
return usage('--role is required');
|
|
628
|
+
if (command === 'worker-model' && !hasFlag(f, 'model'))
|
|
629
|
+
return usage('--model is required');
|
|
630
|
+
// --tokens는 worker-tokens에서 필수, worker-model에서 선택이다.
|
|
631
|
+
if (command === 'worker-tokens' && !hasFlag(f, 'tokens'))
|
|
632
|
+
return usage('--tokens is required');
|
|
633
|
+
// 값 없는 --task·--perspective(true)를 undefined로 접으면 task·관점 없는 decision이
|
|
634
|
+
// 조용히 기록된다. 누락과 달리 "있으나 값 없음"이므로 usage(2)로 막는다.
|
|
635
|
+
for (const name of ['task', 'perspective', 'tokens']) {
|
|
636
|
+
if (hasFlag(f, name) && !nonEmpty(f[name]))
|
|
637
|
+
return usage(`--${name} requires a value`);
|
|
638
|
+
}
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
/** 서브커맨드별 필수·형식 플래그 검사. 통과하면 undefined, 아니면 usage(2). */
|
|
642
|
+
function checkCommandFlags(f, command, rest, io) {
|
|
643
|
+
if (command === 'report') {
|
|
644
|
+
const bad = checkReportFlags(f, io);
|
|
645
|
+
if (bad !== undefined)
|
|
646
|
+
return bad;
|
|
647
|
+
}
|
|
648
|
+
if (command === 'repair') {
|
|
649
|
+
const bad = checkRepairFlags(f, io);
|
|
650
|
+
if (bad !== undefined)
|
|
651
|
+
return bad;
|
|
652
|
+
}
|
|
653
|
+
// --new-path는 반복 플래그라 값 없는 occurrence가 유효한 다른 값 뒤에 숨을 수 있다.
|
|
654
|
+
// 발생 횟수와 수집된 값 수가 다르면 조용히 버리지 않고 usage(2)로 끝낸다.
|
|
655
|
+
if ((command === 'repair' || command === 'revise')
|
|
656
|
+
&& rest.slice(1).filter((tok) => tok === '--new-path').length
|
|
657
|
+
!== collectPathValues(rest.slice(1), '--new-path').length) {
|
|
658
|
+
return failCoordinateUsage(io, `coordinate ${command}: --new-path requires a value\n`, command);
|
|
659
|
+
}
|
|
660
|
+
// --release-do-not-touch도 반복 플래그다. 값 없는 발생은 해제 의도를 알 수 없으므로
|
|
661
|
+
// 조용히 버리면 충돌 거절로 위장되어 원인이 흐려진다. usage(2)로 끝낸다.
|
|
662
|
+
if (command === 'revise'
|
|
663
|
+
&& rest.slice(1).filter((tok) => tok === '--release-do-not-touch').length
|
|
664
|
+
!== collectPathValues(rest.slice(1), '--release-do-not-touch').length) {
|
|
665
|
+
return failCoordinateUsage(io, 'coordinate revise: --release-do-not-touch requires a value\n', command);
|
|
666
|
+
}
|
|
667
|
+
if (command === 'worker-model' || command === 'worker-tokens') {
|
|
668
|
+
const bad = checkWorkerFlags(f, command, io);
|
|
669
|
+
if (bad !== undefined)
|
|
670
|
+
return bad;
|
|
671
|
+
}
|
|
672
|
+
if (command === 'note') {
|
|
673
|
+
// 내용 검사(trim·2000자)는 core 몫이라 여기서는 플래그 존재·값 유무만 본다.
|
|
674
|
+
// 값 없는 --task(true)를 undefined로 접으면 task 없는 note가 조용히 기록되므로 usage(2)로 막는다.
|
|
675
|
+
if (!nonEmpty(f.text))
|
|
676
|
+
return failCoordinateUsage(io, 'coordinate note: --text is required\n', command);
|
|
677
|
+
if (hasFlag(f, 'task') && !nonEmpty(f.task)) {
|
|
678
|
+
return failCoordinateUsage(io, 'coordinate note: --task requires a value\n', command);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// reverify는 terminal task id 없이는 core가 대상을 알 수 없다. core 호출 전에 usage(2)로 거절한다.
|
|
682
|
+
if (command === 'reverify' && !nonEmpty(f.task)) {
|
|
683
|
+
return failCoordinateUsage(io, 'coordinate reverify: --task is required\n', command);
|
|
684
|
+
}
|
|
685
|
+
if (command === 'revoke') {
|
|
686
|
+
if (!nonEmpty(f.task))
|
|
687
|
+
return failCoordinateUsage(io, 'coordinate revoke: --task is required\n', command);
|
|
688
|
+
if (!nonEmpty(f.reason))
|
|
689
|
+
return failCoordinateUsage(io, 'coordinate revoke: --reason is required\n', command);
|
|
690
|
+
}
|
|
691
|
+
if (command === 'promote-stop') {
|
|
692
|
+
// 빈 summary·reason은 usage(2). enum 밖 reason은 core가 promote-reason-invalid(1).
|
|
693
|
+
// whitespace-only는 core summary-required(1)로 가지 않게 trim 후 검사한다.
|
|
694
|
+
if (!nonEmpty(f.reason)) {
|
|
695
|
+
return failCoordinateUsage(io, 'coordinate promote-stop: --reason is required\n', command);
|
|
696
|
+
}
|
|
697
|
+
if (typeof f.summary !== 'string' || f.summary.trim() === '') {
|
|
698
|
+
return failCoordinateUsage(io, 'coordinate promote-stop: --summary is required\n', command);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* revise 갈래. `revise`는 `coordinate()`가 아니라 scope.reviseTaskScope가 처리하므로 CLI가
|
|
705
|
+
* `process.cwd()` 원장에 대해 fence를 직접 확인한다. scope 판정은 coordinator의 것이고 여기서는
|
|
706
|
+
* 인자만 모아 넘긴다. reviseTaskScope는 `repoRoot`를 write boundary로 쓰므로(main checkout·미할당
|
|
707
|
+
* worktree 거절) `--repo`를 받지 않고 실제 cwd만 넘긴다. fence도 같은 cwd 원장에 묶는다.
|
|
708
|
+
*/
|
|
709
|
+
function coordinateRevise(rest, f, io) {
|
|
710
|
+
const coordinatorMod = require('./coordinator');
|
|
711
|
+
const { reviseTaskScope } = require('./scope');
|
|
712
|
+
const { coordinatorPathsFor } = require('./runtime-state');
|
|
713
|
+
const { assertLedgerFence, projectCheckpoint, loadLedgerBytes } = require('./coordinator-ledger');
|
|
714
|
+
const reviseRoot = process.cwd();
|
|
715
|
+
const paths = coordinatorPathsFor({ repoRoot: reviseRoot, blueprint: f.blueprint });
|
|
716
|
+
// load bytes → fence → (이후 revise가 같은 경로를 씀). 디스크를 두 번 읽어
|
|
717
|
+
// hash와 변조 대상이 어긋나지 않게 한 snapshot만 검사한다.
|
|
718
|
+
const loaded = loadLedgerBytes(paths.ledgerFile);
|
|
719
|
+
if (!loaded) {
|
|
720
|
+
const hinted = coordinatorMod.withCoordinateFailureHints({ ok: false, reason: 'ledger-checkpoint-invalid' });
|
|
721
|
+
io.out(`${JSON.stringify(hinted)}\n`);
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
const fenced = assertLedgerFence({
|
|
725
|
+
ledgerPath: f['ledger-path'], ledgerHash: f['ledger-hash'], ledgerBytes: loaded.bytes,
|
|
726
|
+
});
|
|
727
|
+
if (!fenced.ok) {
|
|
728
|
+
io.out(`${JSON.stringify(coordinatorMod.withCoordinateFailureHints(fenced))}\n`);
|
|
729
|
+
return 1;
|
|
730
|
+
}
|
|
731
|
+
const result = reviseTaskScope({
|
|
732
|
+
repoRoot: reviseRoot,
|
|
733
|
+
blueprint: f.blueprint,
|
|
734
|
+
task: typeof f.task === 'string' ? f.task : undefined,
|
|
735
|
+
paths: collectPathValues(rest.slice(1)),
|
|
736
|
+
newPaths: collectPathValues(rest.slice(1), '--new-path'),
|
|
737
|
+
releaseDoNotTouch: collectPathValues(rest.slice(1), '--release-do-not-touch'),
|
|
738
|
+
reason: typeof f.reason === 'string' ? f.reason : undefined,
|
|
739
|
+
});
|
|
740
|
+
if (!result.ok) {
|
|
741
|
+
// 거절도 hint를 거친 stdout JSON(reason·paths·cause·next)으로 낸다. coordinator가 stderr
|
|
742
|
+
// 문자열을 파싱하지 않고 다음 행동을 고르게 하려는 것이다. stderr의 reason 한 줄은 기존 신호라 둔다.
|
|
743
|
+
io.out(`${JSON.stringify(coordinatorMod.withCoordinateFailureHints(result))}\n`);
|
|
744
|
+
io.err(`coordinate revise: ${result.reason}\n`);
|
|
745
|
+
return 1;
|
|
746
|
+
}
|
|
747
|
+
// 성공 시 다음 fencing token이 될 checkpoint를 돌려 status→revise 루프를 잇는다.
|
|
748
|
+
const after = loadLedgerBytes(paths.ledgerFile);
|
|
749
|
+
const payload = after
|
|
750
|
+
? { ...result, checkpoint: projectCheckpoint(after.ledger, paths.ledgerFile, after.bytes) }
|
|
751
|
+
: result;
|
|
752
|
+
io.out(`${JSON.stringify(payload)}\n`);
|
|
753
|
+
return 0;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* next·advance 갈래 공통 실행. 둘 다 fence를 요구하지 않는 읽기 중심 명령이고, 값 없는
|
|
757
|
+
* `--task`(parseFlags가 true)를 undefined로 접으면 blueprint 전체가 열려 잘못된 범위가 되므로
|
|
758
|
+
* usage(2)로 막는다. `advance`는 `--max-steps`도 양의 정수만 받는다.
|
|
759
|
+
*/
|
|
760
|
+
function coordinateNextOrAdvance(command, f, io) {
|
|
761
|
+
const { catchMessage } = require('./guards');
|
|
762
|
+
const { compactCoordinateOutput } = require('./coordinate-output');
|
|
763
|
+
if (hasFlag(f, 'task') && !nonEmpty(f.task)) {
|
|
764
|
+
return failCoordinateUsage(io, `coordinate ${command}: --task requires a ddd value\n`, command);
|
|
765
|
+
}
|
|
766
|
+
const task = typeof f.task === 'string' ? f.task : undefined;
|
|
767
|
+
const repoRoot = (f.repo || process.cwd());
|
|
768
|
+
let maxSteps;
|
|
769
|
+
if (command === 'advance') {
|
|
770
|
+
const steps = positiveIntFlag(f, 'max-steps');
|
|
771
|
+
if (steps.bad) {
|
|
772
|
+
return failCoordinateUsage(io, 'coordinate advance: --max-steps must be a positive integer\n', command);
|
|
773
|
+
}
|
|
774
|
+
maxSteps = steps.value;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
let result;
|
|
778
|
+
if (command === 'next') {
|
|
779
|
+
const { coordinateNext } = require('./coordinate-next');
|
|
780
|
+
result = coordinateNext({
|
|
781
|
+
repoRoot, blueprint: f.blueprint, cwd: process.cwd(), task,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
const { advance } = require('./coordinate-advance');
|
|
786
|
+
result = advance({ repoRoot, blueprint: f.blueprint, task, maxSteps });
|
|
787
|
+
}
|
|
788
|
+
io.out(`${JSON.stringify(compactCoordinateOutput(command, result))}\n`);
|
|
789
|
+
return result.ok ? 0 : 1;
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
io.err(`coordinate: ${catchMessage(error)}\n`);
|
|
793
|
+
return 1;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/** 공통 `coordinate()` 호출 갈래. status의 `--write-input`은 성공 뒤에만 파일을 쓴다. */
|
|
797
|
+
function coordinateCall(command, rest, f, generation, io) {
|
|
798
|
+
const { catchMessage } = require('./guards');
|
|
799
|
+
const { coordinate } = require('./coordinator');
|
|
800
|
+
const { compactCoordinateOutput } = require('./coordinate-output');
|
|
801
|
+
try {
|
|
802
|
+
// --repo는 main checkout을 가리키고 cwd는 실제 write boundary 검증에 쓴다.
|
|
803
|
+
const attemptRaw = typeof f.attempt === 'string' ? f.attempt : undefined;
|
|
804
|
+
const fields = {};
|
|
805
|
+
for (const [field, flag] of Object.entries(COORDINATE_STRING_FLAG_FIELDS)) {
|
|
806
|
+
fields[field] = typeof f[flag] === 'string' ? f[flag] : undefined;
|
|
807
|
+
}
|
|
808
|
+
const decisionFlag = command === 'rerecord' ? f.reason : f.decision;
|
|
809
|
+
const result = coordinate({
|
|
810
|
+
...fields,
|
|
811
|
+
command: command === 'ready' ? 'status' : command,
|
|
812
|
+
repoRoot: (f.repo || process.cwd()),
|
|
813
|
+
blueprint: f.blueprint,
|
|
814
|
+
cwd: process.cwd(),
|
|
815
|
+
decision: typeof decisionFlag === 'string' ? decisionFlag : undefined,
|
|
816
|
+
paths: collectPathValues(rest.slice(1)),
|
|
817
|
+
newPaths: collectPathValues(rest.slice(1), '--new-path'),
|
|
818
|
+
findings: collectFindingValues(rest.slice(1)),
|
|
819
|
+
reviewFindings: collectReviewFindingValues(rest.slice(1)),
|
|
820
|
+
kind: typeof f.kind === 'string' && command === 'repair' ? f.kind : undefined,
|
|
821
|
+
done: command === 'repair' && f.done === true,
|
|
822
|
+
attempt: attemptRaw !== undefined && /^\d+$/.test(attemptRaw) ? Number(attemptRaw) : undefined,
|
|
823
|
+
// 값 없는 --model(parseFlags가 true)은 ''로 넘겨 core가 model-invalid로 거절하게 한다.
|
|
824
|
+
// 플래그가 없으면 undefined라 기록하지 않는다.
|
|
825
|
+
model: hasFlag(f, 'model') ? (typeof f.model === 'string' ? f.model : '') : undefined,
|
|
826
|
+
leaseId: typeof f['lease-id'] === 'string' ? f['lease-id'] : undefined,
|
|
827
|
+
generation,
|
|
828
|
+
ledgerPath: typeof f['ledger-path'] === 'string' ? f['ledger-path'] : undefined,
|
|
829
|
+
ledgerHash: typeof f['ledger-hash'] === 'string' ? f['ledger-hash'] : undefined,
|
|
830
|
+
userConfirmed: f['user-confirmed'] === true,
|
|
831
|
+
promoteLight: f['promote-light'] === true,
|
|
832
|
+
});
|
|
833
|
+
const compact = compactCoordinateOutput(command, result);
|
|
834
|
+
if (result.ok && hasFlag(f, 'write-input')) {
|
|
835
|
+
// status가 모든 검사를 통과한 뒤에만 쓴다. 쓰기 오류는 아래 catch가
|
|
836
|
+
// `coordinate: <msg>` exit 1로 옮기고, 이 시점에는 stdout이 비어 있다.
|
|
837
|
+
compact.input_file = writeCoordinatorInput({
|
|
838
|
+
file: f['write-input'],
|
|
839
|
+
repoRoot: (f.repo || process.cwd()),
|
|
840
|
+
blueprint: f.blueprint,
|
|
841
|
+
checkpoint: result.checkpoint,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
io.out(`${JSON.stringify(compact)}\n`);
|
|
845
|
+
return result.ok ? 0 : 1;
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
io.err(`coordinate: ${catchMessage(error)}\n`);
|
|
849
|
+
return 1;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* coordinate 서브커맨드를 CLI 경계에서 해석한다.
|
|
854
|
+
* `--help`/`-h`는 인자·fence·core보다 먼저 stdout으로 끝내고, 허용 목록 밖
|
|
855
|
+
* 이름은 core에 넘기지 않고 usage(2)로 끝낸다 — JSON 거절은 알려진 명령의
|
|
856
|
+
* 런타임 실패에만 쓴다. `next`의 값 없는 `--task`는 생략이 아니라 usage(2)다.
|
|
857
|
+
* 검사 순서(help → 명령명 → blueprint → write-input → fence → generation → 명령별)를
|
|
858
|
+
* 통과하면 revise·next·advance·공통 호출 갈래 중 하나에 위임한다.
|
|
859
|
+
*
|
|
860
|
+
* @param {string[]} rest - `coordinate` 뒤 argv
|
|
861
|
+
* @param {CliIo} io - stdout/stderr
|
|
862
|
+
* @returns {number} 성공 0, 런타임 거절 1, usage 오류 2
|
|
863
|
+
*/
|
|
864
|
+
function cmdCoordinate(rest, io) {
|
|
865
|
+
// 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
|
|
866
|
+
const { parseFlags, argvRequestsHelp } = require('./cli-flags');
|
|
867
|
+
// 1. 도움말은 잘못된 플래그보다 앞. 소스 역산 대신 --help를 보게 한다.
|
|
868
|
+
if (argvRequestsHelp(rest)) {
|
|
869
|
+
const helpTarget = rest[0];
|
|
870
|
+
if (typeof helpTarget === 'string'
|
|
871
|
+
&& COORDINATE_COMMANDS.includes(helpTarget)) {
|
|
872
|
+
io.out(cliUsage.coordinateUsage(helpTarget));
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
io.out(coordinateRegistryUsage());
|
|
876
|
+
}
|
|
877
|
+
return 0;
|
|
878
|
+
}
|
|
879
|
+
const command = rest[0];
|
|
880
|
+
const f = parseFlags(rest.slice(1));
|
|
881
|
+
if (!COORDINATE_COMMANDS.includes(command)) {
|
|
882
|
+
return failCoordinateUsage(io, 'coordinate: command must be bootstrap, prepare, ready, dispatch, report, record, rerecord, '
|
|
883
|
+
+ 'integrate, status, revise, repair, reverify, partial-close, critical-recovery, revoke, '
|
|
884
|
+
+ 'next, advance, promote-stop, session-open, worker-model, or worker-tokens\n');
|
|
885
|
+
}
|
|
886
|
+
if (typeof f.blueprint !== 'string' || f.blueprint === '') {
|
|
887
|
+
return failCoordinateUsage(io, 'coordinate: --blueprint is required\n', command);
|
|
888
|
+
}
|
|
889
|
+
// --generation은 양의 정수만. abc·0은 usage(2) — core의 lease-required와 구분한다.
|
|
890
|
+
const generation = positiveIntFlag(f, 'generation');
|
|
891
|
+
const early = checkWriteInput(f, command, io) ?? checkFenceFlags(f, command, io);
|
|
892
|
+
if (early !== undefined)
|
|
893
|
+
return early;
|
|
894
|
+
if (generation.bad) {
|
|
895
|
+
return failCoordinateUsage(io, 'coordinate: --generation must be a positive integer\n', command);
|
|
896
|
+
}
|
|
897
|
+
const invalid = checkCommandFlags(f, command, rest, io);
|
|
898
|
+
if (invalid !== undefined)
|
|
899
|
+
return invalid;
|
|
900
|
+
if (command === 'revise')
|
|
901
|
+
return coordinateRevise(rest, f, io);
|
|
902
|
+
if (command === 'next' || command === 'advance')
|
|
903
|
+
return coordinateNextOrAdvance(command, f, io);
|
|
904
|
+
return coordinateCall(command, rest, f, generation.value, io);
|
|
905
|
+
}
|
|
906
|
+
module.exports = {
|
|
907
|
+
commit: {
|
|
908
|
+
run: cmdCommit,
|
|
909
|
+
usage: cliUsage.COMMAND_USAGE['commit'],
|
|
910
|
+
},
|
|
911
|
+
finalize: {
|
|
912
|
+
run: cmdFinalize,
|
|
913
|
+
usage: cliUsage.COMMAND_USAGE['finalize'],
|
|
914
|
+
},
|
|
915
|
+
'seed-worktree': {
|
|
916
|
+
run: cmdSeedWorktree,
|
|
917
|
+
usage: cliUsage.COMMAND_USAGE['seed-worktree'],
|
|
918
|
+
},
|
|
919
|
+
execute: {
|
|
920
|
+
run: cmdExecute,
|
|
921
|
+
usage: cliUsage.COMMAND_USAGE['execute'],
|
|
922
|
+
},
|
|
923
|
+
coordinate: {
|
|
924
|
+
run: cmdCoordinate,
|
|
925
|
+
usage: cliUsage.COMMAND_USAGE['coordinate'],
|
|
926
|
+
},
|
|
927
|
+
import: {
|
|
928
|
+
run: cmdImport,
|
|
929
|
+
usage: cliUsage.COMMAND_USAGE['import'],
|
|
930
|
+
},
|
|
931
|
+
};
|