@cr1992/agentkit 1.0.0

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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +107 -0
  4. package/README.md +103 -0
  5. package/bin/agentkit.mjs +4 -0
  6. package/bin/cli.mjs +273 -0
  7. package/core/atomic-fs.mjs +23 -0
  8. package/core/cli-help.mjs +54 -0
  9. package/core/content-digest.mjs +66 -0
  10. package/core/digest.mjs +67 -0
  11. package/core/json-schema-lite.mjs +60 -0
  12. package/core/legacy-entry.mjs +37 -0
  13. package/core/reflection.mjs +142 -0
  14. package/core/runtime-bundle.mjs +101 -0
  15. package/docs/loop/embedded-review-adapter.md +41 -0
  16. package/docs/loop/loop-state-machine.md +43 -0
  17. package/docs/loop/recovery-and-fuses.md +34 -0
  18. package/docs/orchestrate/dispatch-contract.md +92 -0
  19. package/docs/orchestrate/failure-routing-and-recovery.md +46 -0
  20. package/docs/orchestrate/host-capability-cache.md +170 -0
  21. package/docs/orchestrate/isolation-fallback.md +18 -0
  22. package/docs/orchestrate/model-routing-config.md +186 -0
  23. package/docs/orchestrate/orchestration-runtime.md +261 -0
  24. package/docs/orchestrate/review-budget.md +90 -0
  25. package/docs/orchestrate/task-playbooks.md +85 -0
  26. package/docs/orchestrate/user-facing-reporting.md +14 -0
  27. package/docs/verify/evidence-schema.md +167 -0
  28. package/docs/verify/input-preparation.md +44 -0
  29. package/docs/verify/verification-protocol.md +76 -0
  30. package/docs/worktree/batch-integration.md +176 -0
  31. package/docs/worktree/delivery-identity.md +41 -0
  32. package/docs/worktree/profile.md +107 -0
  33. package/docs/worktree/reclaim-and-watch.md +96 -0
  34. package/docs/worktree/review-lifecycle.md +92 -0
  35. package/docs/worktree/spawn-and-stack.md +74 -0
  36. package/domains/loop/loop-runtime.mjs +1056 -0
  37. package/domains/orchestrate/contract-tool.mjs +169 -0
  38. package/domains/orchestrate/host_capability_cache.mjs +437 -0
  39. package/domains/orchestrate/orchestration-ledger.mjs +332 -0
  40. package/domains/orchestrate/orchestration-metadata.mjs +4 -0
  41. package/domains/orchestrate/orchestration-reflection.mjs +119 -0
  42. package/domains/orchestrate/resolve_model_policy.mjs +311 -0
  43. package/domains/orchestrate/review-budget.mjs +162 -0
  44. package/domains/orchestrate/worker-capability-preflight.mjs +227 -0
  45. package/domains/verify/verification-runtime.mjs +1638 -0
  46. package/domains/worktree/worktree-archive.mjs +135 -0
  47. package/domains/worktree/worktree-artifact.mjs +123 -0
  48. package/domains/worktree/worktree-batch-integrate.mjs +713 -0
  49. package/domains/worktree/worktree-batch-plan.mjs +198 -0
  50. package/domains/worktree/worktree-batch-result.mjs +241 -0
  51. package/domains/worktree/worktree-core.mjs +908 -0
  52. package/domains/worktree/worktree-doctor.mjs +493 -0
  53. package/domains/worktree/worktree-history.mjs +377 -0
  54. package/domains/worktree/worktree-learning.mjs +110 -0
  55. package/domains/worktree/worktree-lifecycle.mjs +786 -0
  56. package/domains/worktree/worktree-merge-preview.mjs +409 -0
  57. package/domains/worktree/worktree-mgr.mjs +261 -0
  58. package/domains/worktree/worktree-process.mjs +55 -0
  59. package/domains/worktree/worktree-profile.mjs +800 -0
  60. package/domains/worktree/worktree-provider-gitlab.mjs +59 -0
  61. package/domains/worktree/worktree-reclaim.mjs +683 -0
  62. package/domains/worktree/worktree-review-refresh.mjs +574 -0
  63. package/domains/worktree/worktree-review-watch.mjs +661 -0
  64. package/domains/worktree/worktree-scan.mjs +510 -0
  65. package/domains/worktree/worktree-trace-test-worker.mjs +23 -0
  66. package/domains/worktree/worktree-trace.mjs +478 -0
  67. package/manage-worktrees/SKILL.md +87 -0
  68. package/manage-worktrees/agents/openai.yaml +4 -0
  69. package/manage-worktrees/scripts/worktree-mgr.mjs +10 -0
  70. package/manage-worktrees/scripts/worktree-scan.mjs +10 -0
  71. package/orchestrate-subagents/SKILL.md +173 -0
  72. package/orchestrate-subagents/agents/openai.yaml +4 -0
  73. package/orchestrate-subagents/scripts/contract-tool.mjs +10 -0
  74. package/orchestrate-subagents/scripts/host_capability_cache.mjs +10 -0
  75. package/orchestrate-subagents/scripts/orchestration-ledger.mjs +10 -0
  76. package/orchestrate-subagents/scripts/orchestration-reflection.mjs +10 -0
  77. package/orchestrate-subagents/scripts/resolve_model_policy.mjs +10 -0
  78. package/orchestrate-subagents/scripts/review-budget.mjs +10 -0
  79. package/orchestrate-subagents/scripts/worker-capability-preflight.mjs +10 -0
  80. package/package.json +48 -0
  81. package/run-agent-verify-loop/SKILL.md +127 -0
  82. package/run-agent-verify-loop/agents/openai.yaml +4 -0
  83. package/run-agent-verify-loop/scripts/loop-runtime.mjs +10 -0
  84. package/schemas/artifact-ref-v1.schema.json +23 -0
  85. package/schemas/batch-result-v1.schema.json +138 -0
  86. package/schemas/controller-recheck-record-v1.schema.json +22 -0
  87. package/schemas/convergence-report-v1.schema.json +9 -0
  88. package/schemas/effective-worker-capability-v1.schema.json +36 -0
  89. package/schemas/embedded-verification-record-v1.schema.json +32 -0
  90. package/schemas/evidence-package-v1.schema.json +41 -0
  91. package/schemas/improvement-proposal-v1.schema.json +18 -0
  92. package/schemas/loop-state-v1.schema.json +34 -0
  93. package/schemas/model-policy-resolution-v1.schema.json +41 -0
  94. package/schemas/orchestration-ledger-v1.schema.json +110 -0
  95. package/schemas/reflection-record-v1.schema.json +24 -0
  96. package/schemas/review-result-v1.schema.json +37 -0
  97. package/schemas/task-contract-v1.schema.json +83 -0
  98. package/schemas/verification-profile-v1.schema.json +60 -0
  99. package/schemas/worker-capability-requirements-v1.schema.json +21 -0
  100. package/schemas/worktree-binding-v1.schema.json +14 -0
  101. package/shell-manifest.json +79 -0
  102. package/verify-agent-output/SKILL.md +119 -0
  103. package/verify-agent-output/agents/openai.yaml +4 -0
  104. package/verify-agent-output/scripts/verification-runtime.mjs +10 -0
@@ -0,0 +1,198 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {Record<string, any>} deps
5
+ */
6
+ export function createCommands(deps) {
7
+ const {
8
+ createHash,
9
+ computeConflictScan,
10
+ printConflictScan,
11
+ loadRepositoryProfile,
12
+ readRepositoryIdentity,
13
+ resolveBaseRef,
14
+ gitTry,
15
+ isAncestor,
16
+ log,
17
+ die,
18
+ flag,
19
+ rejectUnknownFlags,
20
+ loadRecords,
21
+ selectRecord,
22
+ liveGitSnapshot,
23
+ canonicalJson,
24
+ gitOperationState,
25
+ } = deps;
26
+
27
+ /** @param {string} targetSha @param {string[]} orderedInputShas */
28
+ function batchFingerprint(targetSha, orderedInputShas) {
29
+ const input = { schema_version: 1, target_sha: targetSha, ordered_input_shas: orderedInputShas };
30
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonicalJson(input))).digest('hex')}`;
31
+ }
32
+
33
+ /** @param {Record<string,any>} item @param {string} code @param {string} detail */
34
+ function blockBatchItem(item, code, detail) {
35
+ item.state = 'blocked';
36
+ item.reasons.push({ code, detail });
37
+ }
38
+
39
+ /**
40
+ * 为同一 repository identity 内的多个 feature 生成只读、可复现的集成候选计划。
41
+ * 不 fetch、不 merge、不创建 worktree;调用者必须把 target SHA 和有序输入 SHA 当作冻结契约。
42
+ *
43
+ * 与 cmdPlanBatch 分离是为了让 batch-integrate 能用同一套规则重算计划做新鲜度校验,
44
+ * 避免「规划口径」和「合成口径」各写一份而悄悄漂移。
45
+ * @param {ReturnType<typeof loadRepositoryProfile>} loaded
46
+ * @param {string[]} selectors
47
+ * @param {string|null} targetOverride
48
+ */
49
+ function computeBatchPlan(loaded, selectors, targetOverride) {
50
+ const records = loadRecords(loaded.context.common_dir);
51
+ const target = resolveBaseRef(
52
+ loaded.context.current_worktree,
53
+ loaded.profile.default_base,
54
+ targetOverride,
55
+ );
56
+ const targetCommit = gitTry(['rev-parse', '--verify', `${target.ref}^{commit}`], loaded.context.current_worktree);
57
+ if (!targetCommit.ok || !targetCommit.out) die(`无法解析批次 target commit: ${target.ref}`, 2);
58
+
59
+ const seenRecords = new Set();
60
+ const requested = selectors.map((selector) => {
61
+ const record = selectRecord(records, selector, null);
62
+ if (seenRecords.has(record.worktree_id)) die(`重复 selector 指向同一 record: ${selector}`, 2);
63
+ seenRecords.add(record.worktree_id);
64
+ const snapshot = liveGitSnapshot(record);
65
+ const item = {
66
+ selector,
67
+ worktree_id: record.worktree_id,
68
+ task: record.task,
69
+ branch: record.branch ?? null,
70
+ path: record.path,
71
+ task_status: record.task_status,
72
+ worktree_state: record.worktree_state,
73
+ base_ref: record.base_ref ?? null,
74
+ base_sha: record.base_sha ?? null,
75
+ head: snapshot.head ?? null,
76
+ upstream_ref: snapshot.upstream ?? null,
77
+ upstream_sha: null,
78
+ state: 'candidate',
79
+ reasons: [],
80
+ };
81
+
82
+ if (!snapshot.head) {
83
+ blockBatchItem(item, 'HEAD_UNREADABLE', '无法读取 feature HEAD。');
84
+ return item;
85
+ }
86
+ if (isAncestor(loaded.context.current_worktree, snapshot.head, targetCommit.out)) {
87
+ item.state = 'already_integrated';
88
+ item.reasons.push({ code: 'ALREADY_IN_TARGET', detail: `${snapshot.head} 已被 ${target.ref} 包含。` });
89
+ return item;
90
+ }
91
+ if (!snapshot.present || record.worktree_state === 'reclaimed') {
92
+ blockBatchItem(item, 'WORKTREE_NOT_PRESENT', '尚未进入 target 的批次输入必须来自仍存在的 tracked worktree。');
93
+ return item;
94
+ }
95
+ if (!['ready_for_review', 'integrating'].includes(record.task_status)) {
96
+ blockBatchItem(item, 'NOT_READY_FOR_REVIEW', `task_status=${record.task_status}`);
97
+ }
98
+ if (snapshot.dirty !== false) {
99
+ blockBatchItem(item, 'DIRTY_WORKTREE', '批次只接受干净 worktree 的固定提交。');
100
+ }
101
+ const operation = gitOperationState(record.path);
102
+ if (operation) blockBatchItem(item, 'GIT_OPERATION_IN_PROGRESS', `检测到 ${operation}。`);
103
+ if (record.last_head && record.last_head !== snapshot.head) {
104
+ blockBatchItem(item, 'HEAD_DRIFT', `trace=${record.last_head} live=${snapshot.head}`);
105
+ }
106
+ if (!snapshot.upstream) {
107
+ blockBatchItem(item, 'UPSTREAM_MISSING', 'feature branch 尚未登记 upstream,无法证明已推送。');
108
+ return item;
109
+ }
110
+ const upstreamCommit = gitTry(['rev-parse', '--verify', `${snapshot.upstream}^{commit}`], record.path);
111
+ item.upstream_sha = upstreamCommit.ok ? upstreamCommit.out : null;
112
+ if (!upstreamCommit.ok || upstreamCommit.out !== snapshot.head) {
113
+ blockBatchItem(
114
+ item,
115
+ 'UPSTREAM_NOT_AT_HEAD',
116
+ upstreamCommit.ok ? `upstream=${upstreamCommit.out} live=${snapshot.head}` : `无法解析 ${snapshot.upstream}`,
117
+ );
118
+ }
119
+ return item;
120
+ });
121
+
122
+ const candidates = requested.filter((item) => item.state === 'candidate');
123
+ for (let index = 0; index < candidates.length; index++) {
124
+ const item = candidates[index];
125
+ const duplicate = candidates.slice(0, index).find((other) => other.state === 'candidate' && other.head === item.head);
126
+ if (duplicate) {
127
+ item.state = 'covered';
128
+ item.reasons.push({ code: 'DUPLICATE_HEAD', detail: `与 ${duplicate.task} 指向同一 HEAD。` });
129
+ continue;
130
+ }
131
+ const covering = candidates.find((other) =>
132
+ other !== item &&
133
+ other.state === 'candidate' &&
134
+ other.head !== item.head &&
135
+ isAncestor(loaded.context.current_worktree, item.head, other.head),
136
+ );
137
+ if (covering) {
138
+ item.state = 'covered';
139
+ item.reasons.push({ code: 'COVERED_BY_DESCENDANT', detail: `${item.task} 已包含在 ${covering.task}。` });
140
+ }
141
+ }
142
+
143
+ const included = requested.filter((item) => item.state === 'candidate');
144
+ const blockers = requested
145
+ .filter((item) => item.state === 'blocked')
146
+ .flatMap((item) => item.reasons.map((reason) => ({ task: item.task, worktree_id: item.worktree_id, ...reason })));
147
+ if (included.length === 0) blockers.push({ task: null, worktree_id: null, code: 'NO_UNIQUE_INPUT', detail: '没有需要合成的唯一 feature HEAD。' });
148
+ const repositoryId = readRepositoryIdentity(loaded.context)?.repository_id ?? null;
149
+ const ready = blockers.length === 0;
150
+ return {
151
+ schema_version: 1,
152
+ generated_at: new Date().toISOString(),
153
+ repository_id: repositoryId,
154
+ // 原始 selector 全集(含随后被折叠或已在 target 的输入)。重验新鲜度必须按这一份重算:
155
+ // 只回算 included 会把「被折叠的父分支后来前进了」这类漂移整个漏掉。
156
+ requested_selectors: [...selectors],
157
+ target: { ref: target.ref, sha: targetCommit.out, source: target.source },
158
+ ready,
159
+ fingerprint: ready ? batchFingerprint(targetCommit.out, included.map((item) => item.head)) : null,
160
+ included,
161
+ excluded: requested.filter((item) => item.state !== 'candidate' && item.state !== 'blocked'),
162
+ blockers,
163
+ };
164
+ }
165
+
166
+
167
+ /** @param {{positionals:string[],flags:Map<string,unknown>}} args */
168
+ function cmdPlanBatch(args) {
169
+ rejectUnknownFlags(args.flags, ['json', 'target', 'config', 'scan-conflicts']);
170
+ if (args.positionals.length < 2) die('plan-batch 至少需要两个 feature selector。', 2);
171
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
172
+ const result = computeBatchPlan(loaded, args.positionals, flag(args.flags, 'target'));
173
+ const included = result.included;
174
+ const blockers = result.blockers;
175
+ // 冲突矩阵是 cmdPlanBatch 的**附加输出**,不进 computeBatchPlan:后者同时是 batch-integrate
176
+ // 的新鲜度重算口径,往里塞扫描结果会让每次合成都白跑一遍 merge-tree,也会把决策辅助信息
177
+ // 混进"必须逐项比对"的冻结契约里。指纹只绑 SHA,加不加 --scan-conflicts 都不变。
178
+ if (args.flags.get('scan-conflicts')) {
179
+ result.conflict_scan = computeConflictScan(loaded.context.current_worktree, result.target, included);
180
+ }
181
+ if (args.flags.get('json')) console.log(JSON.stringify(result, null, 2));
182
+ else {
183
+ log(`batch target=${result.target.ref}@${result.target.sha.slice(0, 12)} ready=${result.ready} inputs=${included.length}`);
184
+ for (const item of included) console.log(` [INCLUDE] ${item.task} ${item.head.slice(0, 12)} upstream=${item.upstream_ref}`);
185
+ for (const item of result.excluded) console.log(` [${item.state.toUpperCase()}] ${item.task} ${item.reasons.map((reason) => reason.code).join(',')}`);
186
+ for (const blocker of blockers) console.log(` [BLOCK] ${blocker.task ?? '-'} ${blocker.code}: ${blocker.detail}`);
187
+ if (result.fingerprint) console.log(` fingerprint=${result.fingerprint}`);
188
+ if (result.conflict_scan) printConflictScan(result.conflict_scan);
189
+ }
190
+ if (!result.ready) process.exitCode = 1;
191
+ }
192
+
193
+ return {
194
+ batchFingerprint,
195
+ computeBatchPlan,
196
+ cmdPlanBatch,
197
+ };
198
+ }
@@ -0,0 +1,241 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {Record<string, any>} deps
5
+ */
6
+ export function createCommands(deps) {
7
+ const {
8
+ existsSync,
9
+ readFileSync,
10
+ resolve,
11
+ loadRepositoryProfile,
12
+ DIGEST_PATTERN,
13
+ contentDigest,
14
+ log,
15
+ die,
16
+ flag,
17
+ rejectUnknownFlags,
18
+ oneLine,
19
+ loadRecords,
20
+ selectRecord,
21
+ liveGitSnapshot,
22
+ updateRecord,
23
+ canonicalJson,
24
+ exactCommitOid,
25
+ gitOperationState,
26
+ isAncestor,
27
+ } = deps;
28
+
29
+ function plainObject(value, label) {
30
+ if (!value || typeof value !== 'object' || Array.isArray(value)) die(`${label} 必须是 JSON object。`, 2);
31
+ return /** @type {Record<string,any>} */ (value);
32
+ }
33
+
34
+ /** @param {Record<string,any>} value @param {string[]} allowed @param {string} label */
35
+ function rejectUnknownJsonKeys(value, allowed, label) {
36
+ const allow = new Set(allowed);
37
+ const unknown = Object.keys(value).filter((key) => !allow.has(key));
38
+ if (unknown.length) die(`${label} 包含未知字段: ${unknown.join(', ')}。`, 2);
39
+ }
40
+
41
+ /**
42
+ * 批次证据只冻结可审计摘要,不接收原始日志、环境变量或凭证。
43
+ * @param {string} path
44
+ */
45
+ function readBatchEvidenceFile(path) {
46
+ if (!existsSync(path)) die(`--evidence 文件不存在: ${path}`, 2);
47
+ const bytes = readFileSync(path);
48
+ if (bytes.length > 128 * 1024) die('--evidence 最大 128 KiB;原始日志应放在外部证据存储。', 2);
49
+ let parsed;
50
+ try { parsed = JSON.parse(bytes.toString('utf8')); } catch (error) {
51
+ die(`--evidence 文件不是合法 JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
52
+ }
53
+ const root = plainObject(parsed, '--evidence 根节点');
54
+ rejectUnknownJsonKeys(root, ['schema_version', 'contract_digest', 'checks'], '--evidence');
55
+ if (root.schema_version !== 1) die('--evidence schema_version 必须是 1。', 2);
56
+ if (root.contract_digest !== undefined && root.contract_digest !== null && !DIGEST_PATTERN.test(root.contract_digest)) {
57
+ die('--evidence contract_digest 必须是 sha256 digest 或 null。', 2);
58
+ }
59
+ if (!Array.isArray(root.checks) || root.checks.length === 0 || root.checks.length > 100) {
60
+ die('--evidence checks 必须包含 1-100 项。', 2);
61
+ }
62
+ const names = new Set();
63
+ const checks = root.checks.map((raw, index) => {
64
+ const check = plainObject(raw, `checks[${index}]`);
65
+ rejectUnknownJsonKeys(check, ['name', 'environment', 'argv', 'outcome', 'exit_code', 'evidence_refs'], `checks[${index}]`);
66
+ const name = oneLine(String(check.name ?? ''), `checks[${index}].name`, 120);
67
+ if (names.has(name)) die(`--evidence check name 重复: ${name}`, 2);
68
+ names.add(name);
69
+ const environment = plainObject(check.environment, `checks[${index}].environment`);
70
+ const environmentEntries = Object.entries(environment);
71
+ if (environmentEntries.length > 50 || environmentEntries.some(([key, value]) => (
72
+ !/^[a-z][a-z0-9_.-]{0,63}$/u.test(key) ||
73
+ /(?:secret|token|password|credential|private[_-]?key|api[_-]?key)/u.test(key) ||
74
+ !['string', 'number', 'boolean'].includes(typeof value) ||
75
+ (typeof value === 'number' && !Number.isFinite(value)) ||
76
+ (typeof value === 'string' && (value.length > 500 || /[\u0000\r\n]/u.test(value)))
77
+ ))) {
78
+ die(`checks[${index}].environment 只接受不含敏感键的有界标量元数据。`, 2);
79
+ }
80
+ if (!Array.isArray(check.argv) || check.argv.length === 0 || check.argv.length > 100 || check.argv.some((item) => typeof item !== 'string' || !item || item.length > 1000 || /[\u0000\r\n]/u.test(item))) {
81
+ die(`checks[${index}].argv 必须是 1-100 个安全字符串组成的 argv。`, 2);
82
+ }
83
+ if (!['passed', 'failed', 'undecidable'].includes(check.outcome)) {
84
+ die(`checks[${index}].outcome 只接受 passed / failed / undecidable。`, 2);
85
+ }
86
+ if (!Number.isInteger(check.exit_code) || check.exit_code < 0 || check.exit_code > 255) {
87
+ die(`checks[${index}].exit_code 必须是 0-255 整数。`, 2);
88
+ }
89
+ if (check.outcome === 'passed' && check.exit_code !== 0) die(`checks[${index}] passed 时 exit_code 必须为 0。`, 2);
90
+ if (check.outcome === 'failed' && check.exit_code === 0) die(`checks[${index}] failed 时 exit_code 不能为 0。`, 2);
91
+ if (!Array.isArray(check.evidence_refs) || check.evidence_refs.length === 0 || check.evidence_refs.length > 20) {
92
+ die(`checks[${index}].evidence_refs 必须包含 1-20 项。`, 2);
93
+ }
94
+ const evidenceRefs = check.evidence_refs.map((rawRef, refIndex) => {
95
+ const ref = plainObject(rawRef, `checks[${index}].evidence_refs[${refIndex}]`);
96
+ rejectUnknownJsonKeys(ref, ['kind', 'id', 'digest'], `checks[${index}].evidence_refs[${refIndex}]`);
97
+ const kind = oneLine(String(ref.kind ?? ''), 'evidence kind', 40);
98
+ const id = oneLine(String(ref.id ?? ''), 'evidence id', 1000);
99
+ if (!DIGEST_PATTERN.test(ref.digest ?? '')) die(`checks[${index}].evidence_refs[${refIndex}].digest 必须是 sha256 digest。`, 2);
100
+ return { kind, id, digest: ref.digest };
101
+ });
102
+ return { name, environment: canonicalJson(environment), argv: check.argv, outcome: check.outcome, exit_code: check.exit_code, evidence_refs: evidenceRefs };
103
+ });
104
+ const manifest = canonicalJson({ schema_version: 1, contract_digest: root.contract_digest ?? null, checks });
105
+ return { manifest, digest: contentDigest(Buffer.from(JSON.stringify(manifest))) };
106
+ }
107
+
108
+ /**
109
+ * 登记 Profile 声明的合成后步骤的执行结果。工具只记录 controller 的回报,从不代跑。
110
+ * @param {{positionals:string[],flags:Map<string,unknown>}} args
111
+ */
112
+ function cmdBatchStep(args) {
113
+ rejectUnknownFlags(args.flags, ['step', 'state', 'note', 'id', 'json', 'config']);
114
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
115
+ const record = selectRecord(loadRecords(loaded.context.common_dir), args.positionals[0] ?? null, flag(args.flags, 'id'));
116
+ const stepName = flag(args.flags, 'step');
117
+ const state = flag(args.flags, 'state');
118
+ if (!stepName) die('batch-step 需要 --step <name>。', 2);
119
+ if (!['done', 'skipped', 'failed'].includes(state ?? '')) die('--state 只接受 done / skipped / failed。', 2);
120
+ const batch = record.batch_integration;
121
+ if (!batch || batch.state !== 'composed') die(`${record.task} 不是已合成的集成候选,无法登记合成后步骤。`);
122
+ if (record.batch_result) die('batch_result 已冻结,不能再修改合成后步骤。', 2);
123
+ const steps = batch.post_integrate_steps ?? [];
124
+ const target = steps.find((step) => step.name === stepName);
125
+ if (!target) {
126
+ die(
127
+ `未声明的步骤名: ${stepName};当前候选只登记 Profile 声明过的步骤` +
128
+ `${steps.length ? `:${steps.map((step) => step.name).join(', ')}` : '(该 Profile 未声明任何步骤)'}。`,
129
+ 2,
130
+ );
131
+ }
132
+ const note = flag(args.flags, 'note') ? oneLine(flag(args.flags, 'note'), 'note', 240) : null;
133
+ const now = new Date().toISOString();
134
+ const updated = updateRecord(record, 'batch_post_step_recorded', (next) => {
135
+ const step = next.batch_integration.post_integrate_steps.find((item) => item.name === stepName);
136
+ step.state = state;
137
+ step.note = note;
138
+ step.recorded_at = now;
139
+ next.last_seen_at = now;
140
+ }, { step: stepName, state, note, fingerprint: batch.fingerprint }, loaded.context.common_dir);
141
+ const recorded = updated.batch_integration.post_integrate_steps;
142
+ if (args.flags.get('json')) {
143
+ console.log(JSON.stringify({ schema_version: 1, worktree_id: updated.worktree_id, post_integrate_steps: recorded }, null, 2));
144
+ return;
145
+ }
146
+ log(`已登记 ${stepName} -> ${state}`);
147
+ const pending = recorded.filter((step) => step.state === 'pending');
148
+ console.log(pending.length === 0
149
+ ? ' 合成后步骤已全部登记。'
150
+ : ` 仍待登记: ${pending.map((step) => step.name).join(', ')}`);
151
+ }
152
+
153
+ /**
154
+ * 冻结一次性集成候选的终态验收结果。结果不可覆盖;新 target/input/contract 必须另起候选。
155
+ * @param {{positionals:string[],flags:Map<string,unknown>}} args
156
+ */
157
+ function cmdBatchResult(args) {
158
+ rejectUnknownFlags(args.flags, ['state', 'candidate', 'evidence', 'reason', 'id', 'json', 'config']);
159
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
160
+ let record = selectRecord(loadRecords(loaded.context.common_dir), args.positionals[0] ?? null, flag(args.flags, 'id'));
161
+ const outcome = flag(args.flags, 'state');
162
+ if (!['passed', 'failed', 'stale'].includes(outcome ?? '')) die('--state 只接受 passed / failed / stale。', 2);
163
+ const candidateInput = flag(args.flags, 'candidate');
164
+ if (!candidateInput) die('batch-result 需要 --candidate <exact-object-id>。', 2);
165
+ const candidateSha = exactCommitOid(loaded.context.current_worktree, candidateInput, '--candidate');
166
+ const evidencePath = flag(args.flags, 'evidence');
167
+ if (outcome !== 'stale' && !evidencePath) die(`${outcome} 结果需要 --evidence <json>。`, 2);
168
+ const evidence = evidencePath ? readBatchEvidenceFile(resolve(evidencePath)) : null;
169
+ const reason = flag(args.flags, 'reason') ? oneLine(flag(args.flags, 'reason'), '--reason', 500) : null;
170
+ if (outcome === 'stale' && !reason) die('stale 结果需要 --reason <原因>。', 2);
171
+ if (outcome !== 'stale' && !evidence.manifest.contract_digest) {
172
+ die(`${outcome} 结果的 Evidence 必须提供非空 contract_digest;合同变化必须另起候选。`, 2);
173
+ }
174
+ if (outcome === 'passed' && evidence.manifest.checks.some((check) => check.outcome !== 'passed')) {
175
+ die('passed 结果要求所有 evidence checks 都为 passed。', 2);
176
+ }
177
+ if (outcome === 'failed' && !evidence.manifest.checks.some((check) => check.outcome === 'failed')) {
178
+ die('failed 结果至少需要一个 failed evidence check。', 2);
179
+ }
180
+ const batch = record.batch_integration;
181
+ if (!batch || batch.state !== 'composed') die(`${record.task} 不是已合成的集成候选。`, 2);
182
+ if (!['integrating', 'done'].includes(record.task_status)) die(`batch-result 不接受 ${record.task_status} 候选。`, 2);
183
+ const requested = canonicalJson({
184
+ schema_version: 1,
185
+ outcome,
186
+ candidate_sha: candidateSha,
187
+ fingerprint: batch.fingerprint,
188
+ target_ref: batch.target_ref,
189
+ target_sha: batch.target_sha,
190
+ ordered_input_shas: (batch.ordered_inputs ?? []).map((item) => item.head),
191
+ evidence_manifest_digest: evidence?.digest ?? null,
192
+ evidence_manifest: evidence?.manifest ?? null,
193
+ reason,
194
+ });
195
+ const requestedDigest = contentDigest(Buffer.from(JSON.stringify(requested)));
196
+ if (record.batch_result) {
197
+ if (record.batch_result.result_digest !== requestedDigest) die('batch_result 已冻结且与本次输入不同;不得覆盖终态结果。', 2);
198
+ if (args.flags.get('json')) console.log(JSON.stringify(record.batch_result, null, 2));
199
+ else log(`批次结果已冻结(幂等) ${record.task} ${outcome} ${candidateSha.slice(0, 12)}`);
200
+ return;
201
+ }
202
+ if (record.worktree_state !== 'present') die(`batch-result 只接受 present 候选;当前 ${record.worktree_state}。`, 2);
203
+ const live = liveGitSnapshot(record);
204
+ if (!live.present) die(`候选 worktree missing: ${record.path}`, 2);
205
+ if (live.dirty !== false) die(`候选 worktree 必须干净:${record.path}`, 2);
206
+ const operation = gitOperationState(record.path);
207
+ if (operation) die(`候选仍处于 ${operation} 中间态,拒绝冻结结果。`, 2);
208
+ if (live.head?.toLowerCase() !== candidateSha) die(`--candidate 与 live HEAD 不一致:expected ${live.head}`, 2);
209
+ if (!isAncestor(record.path, batch.composed_sha, candidateSha)) {
210
+ die(`candidate SHA 不是 composed_sha ${batch.composed_sha} 的后继,拒绝冻结。`, 2);
211
+ }
212
+ if (outcome === 'passed') {
213
+ const incomplete = (batch.post_integrate_steps ?? []).filter((step) => !['done', 'skipped'].includes(step.state));
214
+ if (incomplete.length) die(`passed 前仍有未通过的合成后步骤: ${incomplete.map((step) => `${step.name}=${step.state}`).join(', ')}`, 2);
215
+ }
216
+ const recordedAt = new Date().toISOString();
217
+ record = updateRecord(record, 'batch_result_recorded', (next) => {
218
+ next.batch_result = { ...requested, result_digest: requestedDigest, recorded_at: recordedAt };
219
+ next.task_status = 'done';
220
+ next.last_head = candidateSha;
221
+ next.last_seen_at = recordedAt;
222
+ }, {
223
+ outcome,
224
+ candidate_sha: candidateSha,
225
+ fingerprint: batch.fingerprint,
226
+ result_digest: requestedDigest,
227
+ evidence_manifest_digest: evidence?.digest ?? null,
228
+ reason,
229
+ }, loaded.context.common_dir);
230
+ if (args.flags.get('json')) console.log(JSON.stringify(record.batch_result, null, 2));
231
+ else log(`已冻结批次结果 ${record.task} ${outcome} sha=${candidateSha.slice(0, 12)} evidence=${evidence?.digest ?? 'none'}`);
232
+ }
233
+
234
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded */
235
+
236
+
237
+ return {
238
+ cmdBatchStep,
239
+ cmdBatchResult,
240
+ };
241
+ }