@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,377 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Domain command factory. Dependencies are injected once by the thin CLI composition root,
5
+ * keeping this module acyclic and independently reviewable.
6
+ * @param {Record<string, any>} deps
7
+ */
8
+ export function createCommands(deps) {
9
+ const {
10
+ randomUUID,
11
+ loadRepositoryProfile,
12
+ WorktreeTraceError,
13
+ SUBMIT_PUSH_TIMEOUT_MS,
14
+ git,
15
+ gitTry,
16
+ commandFailureReason,
17
+ removeWatcherHeartbeat,
18
+ refreshTargetRef,
19
+ log,
20
+ die,
21
+ flag,
22
+ rejectUnknownFlags,
23
+ oneLine,
24
+ loadRecords,
25
+ selectRecord,
26
+ liveGitSnapshot,
27
+ updateRecord,
28
+ assertHistoryOperationIdle,
29
+ gitOperationState,
30
+ isAncestor,
31
+ } = deps;
32
+
33
+ function stackParentForRef(records, ref, baseSha) {
34
+ const normalized = ref.replace(/^refs\/heads\//u, '');
35
+ const candidates = records.filter((record) =>
36
+ record.worktree_state !== 'reclaimed' &&
37
+ record.branch &&
38
+ (record.branch === normalized || normalized.endsWith(`/${record.branch}`)),
39
+ );
40
+ if (candidates.length > 1) die(`base ref ${ref} 同时匹配多个 tracked parent;请先清理歧义。`, 2);
41
+ const parent = candidates[0];
42
+ if (!parent) return null;
43
+ return {
44
+ worktree_id: parent.worktree_id,
45
+ task: parent.task,
46
+ branch: parent.branch,
47
+ base_ref: ref,
48
+ parent_head_sha: baseSha,
49
+ recorded_at: new Date().toISOString(),
50
+ };
51
+ }
52
+
53
+ /** @param {Record<string,any>} record @param {string} command */
54
+ function historyChangeSnapshot(record, command) {
55
+ if (!['active', 'blocked', 'ready_for_review'].includes(record.task_status)) {
56
+ die(`${command} 只接受 active/blocked/ready_for_review,当前为 ${record.task_status}。`, 2);
57
+ }
58
+ if (record.worktree_state !== 'present') die(`${command} 要求 worktree_state=present。`, 2);
59
+ const snapshot = liveGitSnapshot(record);
60
+ if (!snapshot.present) die(`${command} 要求 worktree 仍存在。`, 2);
61
+ if (snapshot.dirty !== false) die(`${command} 要求工作树干净(含 untracked)。`, 2);
62
+ if (!snapshot.head || !record.branch) die(`${command} 不支持 detached HEAD。`, 2);
63
+ const currentBranch = gitTry(['branch', '--show-current'], record.path);
64
+ if (!currentBranch.ok || currentBranch.out !== record.branch) {
65
+ die(`${command} 检测到 branch 漂移:live=${currentBranch.out || '(detached)'} record=${record.branch}`, 2);
66
+ }
67
+ const operation = gitOperationState(record.path);
68
+ if (operation) die(`${command} 检测到未完成 Git 操作 ${operation}。`, 2);
69
+ if (record.auto_reclaim?.state === 'merge_detected') {
70
+ die(`${command} 拒绝改写已进入 merge_detected 的冻结边界。`, 2);
71
+ }
72
+ return snapshot;
73
+ }
74
+
75
+ /** @param {Record<string,any>} next @param {string} reason */
76
+ function disarmHistoryWatcher(next, reason) {
77
+ const watch = next.auto_reclaim;
78
+ if (!watch || ['disarmed', 'reclaimed'].includes(watch.state)) return null;
79
+ watch.state = 'disarmed';
80
+ watch.disarmed_at = new Date().toISOString();
81
+ watch.disarm_reason = reason;
82
+ return watch.token ?? null;
83
+ }
84
+
85
+ /** @param {string} cwd @param {string} fromExclusive @param {string} through */
86
+ function revisionList(cwd, fromExclusive, through) {
87
+ const result = gitTry(['rev-list', '--reverse', `${fromExclusive}..${through}`], cwd);
88
+ return result.ok && result.out ? result.out.split('\n').filter(Boolean) : [];
89
+ }
90
+
91
+ /** @param {Record<string,any>[]} records @param {string} ref @param {string} cwd */
92
+ function resolveManagedBase(records, ref, cwd) {
93
+ const refreshed = refreshTargetRef(ref, cwd);
94
+ if (!refreshed.ok || !refreshed.target_sha) die(`无法解析或刷新 base ref: ${ref}`, 2);
95
+ return {
96
+ ref,
97
+ sha: refreshed.target_sha,
98
+ parent: stackParentForRef(records, ref, refreshed.target_sha),
99
+ };
100
+ }
101
+
102
+ /** @param {Record<string,any>[]} records @param {Record<string,any>} record @param {Record<string,any>|null} parent @param {string} command */
103
+ function assertStackParentAcyclic(records, record, parent, command) {
104
+ if (!parent) return;
105
+ const byId = new Map(records.map((candidate) => [candidate.worktree_id, candidate]));
106
+ let cursor = parent.worktree_id;
107
+ const seen = new Set();
108
+ while (cursor) {
109
+ if (cursor === record.worktree_id) die(`${command} 会形成 stack parent 环,拒绝更新。`, 2);
110
+ if (seen.has(cursor)) die(`${command} 检测到已有 stack parent 环,拒绝扩散。`, 2);
111
+ seen.add(cursor);
112
+ cursor = byId.get(cursor)?.stack_parent?.worktree_id ?? null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 只改验证/MR 目标,不改 Git 历史。新 base 必须已经是 live HEAD 的祖先;否则必须走受控 rebase。
118
+ * @param {{positionals:string[],flags:Map<string,unknown>}} args
119
+ */
120
+ function cmdRetarget(args) {
121
+ rejectUnknownFlags(args.flags, ['base', 'reason', 'expected-head', 'id', 'config']);
122
+ const baseRef = oneLine(flag(args.flags, 'base') ?? '', 'base', 240);
123
+ const reason = oneLine(flag(args.flags, 'reason') ?? '', 'reason', 240);
124
+ const expectedHead = oneLine(flag(args.flags, 'expected-head') ?? '', 'expected-head', 128);
125
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
126
+ const records = loadRecords(loaded.context.common_dir);
127
+ const record = selectRecord(records, args.positionals[0] ?? null, flag(args.flags, 'id'));
128
+ assertHistoryOperationIdle(record, 'retarget');
129
+ const snapshot = historyChangeSnapshot(record, 'retarget');
130
+ if (snapshot.head !== expectedHead) die(`retarget HEAD CAS 失败:expected=${expectedHead}, actual=${snapshot.head}`, 2);
131
+ const base = resolveManagedBase(records, baseRef, record.path);
132
+ assertStackParentAcyclic(records, record, base.parent, 'retarget');
133
+ if (!isAncestor(record.path, base.sha, snapshot.head)) {
134
+ die(`retarget 不改写历史,但 ${base.sha} 不是 live HEAD 的祖先;请改用 rebase --onto ${baseRef}。`, 2);
135
+ }
136
+ const oldWatch = record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)
137
+ ? structuredClone(record.auto_reclaim)
138
+ : null;
139
+ const updated = updateRecord(record, 'base_retargeted', (next) => {
140
+ const live = liveGitSnapshot(next);
141
+ if (live.head !== expectedHead) throw new WorktreeTraceError('RETARGET_HEAD_CHANGED', `retarget 期间 HEAD 已变为 ${live.head}`);
142
+ const watcherToken = disarmHistoryWatcher(next, 'base_retargeted');
143
+ next.base_ref = base.ref;
144
+ next.base_sha = base.sha;
145
+ next.base_reason = reason;
146
+ next.stack_parent = base.parent;
147
+ next.last_head = live.head;
148
+ next.last_seen_at = new Date().toISOString();
149
+ if (next.change_request) {
150
+ const targetBranch = base.ref.includes('/') ? base.ref.slice(base.ref.indexOf('/') + 1) : base.ref;
151
+ next.change_request.target_branch = targetBranch;
152
+ next.change_request.target_ref = base.ref;
153
+ next.change_request.target_sync = 'record_only';
154
+ next.change_request.retargeted_at = new Date().toISOString();
155
+ }
156
+ if (watcherToken) next.task_status = 'active';
157
+ }, {
158
+ old_base_ref: record.base_ref,
159
+ old_base_sha: record.base_sha,
160
+ new_base_ref: base.ref,
161
+ new_base_sha: base.sha,
162
+ stack_parent_worktree_id: base.parent?.worktree_id ?? null,
163
+ reason,
164
+ expected_head: expectedHead,
165
+ }, loaded.context.common_dir);
166
+ if (oldWatch?.token) removeWatcherHeartbeat(loaded.context.common_dir, record.worktree_id, oldWatch.token);
167
+ log(`已 retarget ${record.task}: ${record.base_ref}@${String(record.base_sha).slice(0, 12)} -> ${base.ref}@${base.sha.slice(0, 12)}`);
168
+ if (oldWatch) log('旧 watcher 已失效;确认远端 MR 目标后重新 touch ready_for_review 以武装新 target。');
169
+ if (updated.change_request?.target_sync === 'record_only') log('MR target 仅更新本地记录;portable core 未调用远端 provider。');
170
+ }
171
+
172
+ /** @param {ReturnType<typeof loadRepositoryProfile>} loaded @param {Record<string,any>} record @param {Record<string,any>} operation */
173
+ function finalizeManagedRebase(loaded, record, operation) {
174
+ const live = liveGitSnapshot(record);
175
+ if (!live.present || live.dirty !== false || !live.head) die('rebase 完成后的 worktree 状态不可冻结。', 2);
176
+ if (!isAncestor(record.path, operation.onto_sha, live.head)) die('rebase 后 onto SHA 不是 live HEAD 的祖先。', 2);
177
+ const newCommits = revisionList(record.path, operation.onto_sha, live.head);
178
+ return updateRecord(record, 'history_rebase_completed', (next) => {
179
+ if (next.history_operation?.token !== operation.token) {
180
+ throw new WorktreeTraceError('REBASE_OPERATION_CHANGED', 'rebase finalize 时 operation token 已变化。');
181
+ }
182
+ const epoch = next.ownership_epochs?.at(-1);
183
+ if (epoch && !epoch.ended_at) {
184
+ epoch.ended_at = new Date().toISOString();
185
+ epoch.end_sha = operation.old_head;
186
+ }
187
+ next.ownership_epochs ??= [];
188
+ next.ownership_epochs.push({ agent: next.agent, started_at: new Date().toISOString(), start_sha: operation.onto_sha, end_sha: null, ended_at: null, source: 'managed_rebase' });
189
+ next.history_rewrites ??= [];
190
+ next.history_rewrites.push({
191
+ kind: 'rebase',
192
+ token: operation.token,
193
+ old_base_ref: operation.old_base_ref,
194
+ old_base_sha: operation.old_base_sha,
195
+ old_head: operation.old_head,
196
+ old_commits: operation.old_commits,
197
+ new_base_ref: operation.onto_ref,
198
+ new_base_sha: operation.onto_sha,
199
+ new_head: live.head,
200
+ new_commits: newCommits,
201
+ reason: operation.reason,
202
+ completed_at: new Date().toISOString(),
203
+ });
204
+ next.base_ref = operation.onto_ref;
205
+ next.base_sha = operation.onto_sha;
206
+ next.base_reason = operation.reason;
207
+ next.stack_parent = operation.stack_parent;
208
+ next.last_head = live.head;
209
+ next.last_seen_at = new Date().toISOString();
210
+ next.history_operation = null;
211
+ if (next.task_status === 'ready_for_review') next.task_status = 'active';
212
+ if (next.change_request) {
213
+ next.change_request.source_head_stale = true;
214
+ next.change_request.rewritten_head_sha = live.head;
215
+ next.change_request.rewritten_at = new Date().toISOString();
216
+ }
217
+ }, {
218
+ token: operation.token,
219
+ old_head: operation.old_head,
220
+ new_head: live.head,
221
+ old_base_sha: operation.old_base_sha,
222
+ new_base_sha: operation.onto_sha,
223
+ old_commit_count: operation.old_commits.length,
224
+ new_commit_count: newCommits.length,
225
+ }, loaded.context.common_dir);
226
+ }
227
+
228
+ /**
229
+ * manager-owned history rewrite:先落 intent 并撤防,再执行 Git;崩溃后重跑同命令可 finalize。
230
+ * @param {{positionals:string[],flags:Map<string,unknown>}} args
231
+ */
232
+ function cmdRebase(args) {
233
+ rejectUnknownFlags(args.flags, ['onto', 'reason', 'expected-head', 'id', 'config', 'abort', 'continue']);
234
+ if (args.flags.get('abort') && args.flags.get('continue')) die('rebase --abort 与 --continue 互斥。', 2);
235
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
236
+ const records = loadRecords(loaded.context.common_dir);
237
+ let record = selectRecord(records, args.positionals[0] ?? null, flag(args.flags, 'id'));
238
+ const pending = record.history_operation;
239
+ if (args.flags.get('abort')) {
240
+ if (!pending || pending.kind !== 'rebase') die('当前没有可 abort 的 managed rebase。', 2);
241
+ const operationState = gitOperationState(record.path);
242
+ if (operationState?.startsWith('rebase')) {
243
+ const aborted = gitTry(['rebase', '--abort'], record.path);
244
+ if (!aborted.ok) die(`git rebase --abort 失败: ${commandFailureReason(aborted, 'unknown error')}`);
245
+ }
246
+ const live = liveGitSnapshot(record);
247
+ if (live.head !== pending.old_head || live.dirty !== false) die('rebase abort 后未恢复到原始 clean HEAD,保留 pending 供 doctor。', 2);
248
+ updateRecord(record, 'history_rebase_aborted', (next) => {
249
+ if (next.history_operation?.token !== pending.token) throw new WorktreeTraceError('REBASE_OPERATION_CHANGED', 'abort 时 operation token 已变化。');
250
+ next.history_operation = null;
251
+ next.last_head = live.head;
252
+ next.last_seen_at = new Date().toISOString();
253
+ }, { token: pending.token, restored_head: live.head }, loaded.context.common_dir);
254
+ log(`已 abort managed rebase,HEAD 恢复为 ${live.head.slice(0, 12)};watcher 保持关闭。`);
255
+ return;
256
+ }
257
+
258
+ const ontoInput = flag(args.flags, 'onto');
259
+ const reasonInput = flag(args.flags, 'reason');
260
+ const expectedHeadInput = flag(args.flags, 'expected-head');
261
+ let ontoRef = ontoInput ? oneLine(ontoInput, 'onto', 240) : null;
262
+ let reason = reasonInput ? oneLine(reasonInput, 'reason', 240) : null;
263
+ let expectedHead = expectedHeadInput ? oneLine(expectedHeadInput, 'expected-head', 128) : null;
264
+ if (pending) {
265
+ if (
266
+ pending.kind !== 'rebase' ||
267
+ (ontoRef && pending.onto_ref !== ontoRef) ||
268
+ (expectedHead && pending.old_head !== expectedHead) ||
269
+ (reason && pending.reason !== reason)
270
+ ) {
271
+ die('已有 managed history operation 与本次参数不一致;先按原参数恢复或使用 rebase --abort。', 2);
272
+ }
273
+ ontoRef = pending.onto_ref;
274
+ expectedHead = pending.old_head;
275
+ reason = pending.reason;
276
+ let operationState = gitOperationState(record.path);
277
+ if (operationState?.startsWith('rebase') && args.flags.get('continue')) {
278
+ const continued = gitTry(['rebase', '--continue'], record.path, {
279
+ timeoutMs: SUBMIT_PUSH_TIMEOUT_MS,
280
+ env: { ...process.env, GIT_EDITOR: 'true' },
281
+ });
282
+ operationState = gitOperationState(record.path);
283
+ if (!continued.ok || operationState?.startsWith('rebase')) {
284
+ updateRecord(record, 'history_rebase_conflicted', (next) => {
285
+ if (next.history_operation?.token !== pending.token) throw new WorktreeTraceError('REBASE_OPERATION_CHANGED', 'continue 时 operation token 已变化。');
286
+ next.history_operation.state = 'conflicted';
287
+ next.history_operation.last_error = commandFailureReason(continued, 'rebase conflict remains');
288
+ }, { token: pending.token, git_state: operationState ?? 'unknown', phase: 'continue' }, loaded.context.common_dir);
289
+ die('managed rebase --continue 未完成;继续解决冲突后重跑相同 manager 命令,或使用 rebase --abort。');
290
+ }
291
+ } else if (operationState?.startsWith('rebase')) {
292
+ die('managed rebase 停在冲突态;解决文件并 git add 后,用本命令加 --continue,或使用 rebase --abort。', 2);
293
+ }
294
+ const live = liveGitSnapshot(record);
295
+ if (live.head !== pending.old_head && live.head && isAncestor(record.path, pending.onto_sha, live.head)) {
296
+ const completed = finalizeManagedRebase(loaded, record, pending);
297
+ log(`已恢复并 finalize rebase ${record.task}: ${pending.old_head.slice(0, 12)} -> ${completed.last_head.slice(0, 12)}`);
298
+ return;
299
+ }
300
+ if (live.head !== pending.old_head) die('managed rebase pending,但 live HEAD 既不是 old_head 也不是可验证的新链。', 2);
301
+ }
302
+
303
+ if (!ontoRef || !reason || !expectedHead) {
304
+ die('新建 managed rebase 需要 --onto、--expected-head 与 --reason;恢复 pending 操作只需 selector + --continue。', 2);
305
+ }
306
+
307
+ const snapshot = historyChangeSnapshot(record, 'rebase');
308
+ if (snapshot.head !== expectedHead) die(`rebase HEAD CAS 失败:expected=${expectedHead}, actual=${snapshot.head}`, 2);
309
+ if (!record.base_sha || !isAncestor(record.path, record.base_sha, snapshot.head)) {
310
+ die('record.base_sha 不是 live HEAD 的祖先;历史已在 manager 外改写,拒绝继续。', 2);
311
+ }
312
+ const base = resolveManagedBase(records, ontoRef, record.path);
313
+ assertStackParentAcyclic(records, record, base.parent, 'rebase');
314
+ if (base.sha === record.base_sha) die('onto SHA 与当前 base_sha 相同,无需 rebase。', 2);
315
+ let operation = pending;
316
+ if (!operation) {
317
+ operation = {
318
+ kind: 'rebase',
319
+ state: 'prepared',
320
+ token: randomUUID(),
321
+ old_base_ref: record.base_ref,
322
+ old_base_sha: record.base_sha,
323
+ old_head: snapshot.head,
324
+ old_commits: revisionList(record.path, record.base_sha, snapshot.head),
325
+ onto_ref: base.ref,
326
+ onto_sha: base.sha,
327
+ stack_parent: base.parent,
328
+ reason,
329
+ prepared_at: new Date().toISOString(),
330
+ };
331
+ const oldWatch = record.auto_reclaim && !['disarmed', 'reclaimed'].includes(record.auto_reclaim.state)
332
+ ? structuredClone(record.auto_reclaim)
333
+ : null;
334
+ record = updateRecord(record, 'history_rebase_prepared', (next) => {
335
+ const live = liveGitSnapshot(next);
336
+ if (live.head !== expectedHead || live.dirty !== false) throw new WorktreeTraceError('REBASE_PREPARE_DRIFT', 'rebase prepare 前 Git 状态已变化。');
337
+ disarmHistoryWatcher(next, 'history_rebase_prepared');
338
+ next.history_operation = operation;
339
+ next.last_head = live.head;
340
+ next.last_seen_at = new Date().toISOString();
341
+ }, { ...operation, old_commits: operation.old_commits.length }, loaded.context.common_dir);
342
+ if (oldWatch?.token) removeWatcherHeartbeat(loaded.context.common_dir, record.worktree_id, oldWatch.token);
343
+ }
344
+
345
+ const rebased = gitTry(['rebase', '--onto', operation.onto_sha, operation.old_base_sha, record.branch], record.path, { timeoutMs: SUBMIT_PUSH_TIMEOUT_MS });
346
+ if (!rebased.ok) {
347
+ const state = gitOperationState(record.path);
348
+ if (state?.startsWith('rebase')) {
349
+ updateRecord(record, 'history_rebase_conflicted', (next) => {
350
+ if (next.history_operation?.token !== operation.token) throw new WorktreeTraceError('REBASE_OPERATION_CHANGED', 'conflict 记录时 token 已变化。');
351
+ next.history_operation.state = 'conflicted';
352
+ next.history_operation.last_error = commandFailureReason(rebased, 'rebase conflict');
353
+ }, { token: operation.token, git_state: state }, loaded.context.common_dir);
354
+ die('managed rebase 发生冲突;解决文件并 git add 后,用相同 manager 命令加 --continue,或使用 rebase --abort。');
355
+ }
356
+ const live = liveGitSnapshot(record);
357
+ if (live.head === operation.old_head && live.dirty === false) {
358
+ updateRecord(record, 'history_rebase_failed', (next) => {
359
+ if (next.history_operation?.token !== operation.token) throw new WorktreeTraceError('REBASE_OPERATION_CHANGED', 'failure 记录时 token 已变化。');
360
+ next.history_operation = null;
361
+ }, { token: operation.token, reason: commandFailureReason(rebased, 'rebase failed before applying changes') }, loaded.context.common_dir);
362
+ }
363
+ die(`managed rebase 失败: ${commandFailureReason(rebased, 'unknown error')}`);
364
+ }
365
+ const completed = finalizeManagedRebase(loaded, record, operation);
366
+ log(`已 rebase ${record.task}: ${operation.old_head.slice(0, 12)} -> ${completed.last_head.slice(0, 12)},base=${operation.onto_ref}@${operation.onto_sha.slice(0, 12)}`);
367
+ log('旧 Artifact/MR head/watcher 已失效;push 新 HEAD 后重新登记评审边界。');
368
+ }
369
+
370
+
371
+ return {
372
+ stackParentForRef,
373
+ historyChangeSnapshot,
374
+ cmdRetarget,
375
+ cmdRebase,
376
+ };
377
+ }
@@ -0,0 +1,110 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Domain command factory. Dependencies are injected once by the thin CLI composition root,
5
+ * keeping this module acyclic and independently reviewable.
6
+ * @param {Record<string, any>} deps
7
+ */
8
+ export function createCommands(deps) {
9
+ const {
10
+ randomUUID,
11
+ existsSync,
12
+ mkdirSync,
13
+ writeFileSync,
14
+ join,
15
+ resolve,
16
+ loadRepositoryProfile,
17
+ readEventChain,
18
+ traceLayout,
19
+ DIGEST_PATTERN,
20
+ contentDigest,
21
+ worktreeSkillDigest,
22
+ die,
23
+ flag,
24
+ readJsonFileOrDie,
25
+ rejectUnknownFlags,
26
+ oneLine,
27
+ canonicalJson,
28
+ worktreeEnvelopeContext,
29
+ } = deps;
30
+
31
+ function sanitizeLearningText(value) {
32
+ return oneLine(String(value ?? '').replace(/\bBearer\s+\S+/giu, 'Bearer [REDACTED]').replace(/\/(?:Users|home)\/[^\s"']+/gu, '[LOCAL_PATH]'), 'reflection text', 1000);
33
+ }
34
+
35
+ function learningRoot(commonDir) {
36
+ return join(traceLayout(commonDir).root, 'learning');
37
+ }
38
+
39
+ function cmdIncident(args) {
40
+ rejectUnknownFlags(args.flags, ['input', 'id', 'config']);
41
+ const inputPath = flag(args.flags, 'input');
42
+ if (!inputPath) die('incident 需要 --input <json>。', 2);
43
+ const { loaded, record } = worktreeEnvelopeContext(args);
44
+ const input = readJsonFileOrDie(resolve(inputPath), 'incident --input 文件');
45
+ if (!DIGEST_PATTERN.test(String(input.contract_digest ?? ''))) die('incident contract_digest 无效。', 2);
46
+ if (!['contract_gap', 'skill_gap', 'verification_gap', 'tool_gap', 'environment_gap', 'false_positive', 'false_negative', 'inefficiency'].includes(input.classification)) die('incident classification 无效。', 2);
47
+ const chain = readEventChain(loaded.context.common_dir, record.worktree_id);
48
+ const latest = chain.at(-1);
49
+ if (!latest) die('worktree record 没有可引用事件。', 2);
50
+ const eventDigest = contentDigest(Buffer.from(JSON.stringify(canonicalJson(latest))));
51
+ const reflection = {
52
+ schema_version: 1,
53
+ reflection_id: randomUUID(),
54
+ trigger: input.trigger ?? 'unexpected_outcome',
55
+ scope: { contract_digest: input.contract_digest },
56
+ affected_skill: { name: 'manage-worktrees', version: 'unversioned', content_digest: worktreeSkillDigest() },
57
+ classification: input.classification,
58
+ observation: sanitizeLearningText(input.observation),
59
+ evidence_refs: [{ type: 'event', id: `${record.worktree_id}:${latest.event_id}`, digest: eventDigest }],
60
+ impact: input.impact ?? 'medium',
61
+ confidence: input.confidence ?? 'high',
62
+ recommended_disposition: input.recommended_disposition ?? 'continue',
63
+ recorded_at: new Date().toISOString(),
64
+ };
65
+ reflection.reflection_digest = contentDigest(Buffer.from(JSON.stringify(canonicalJson(reflection))));
66
+ const directory = join(learningRoot(loaded.context.common_dir), 'reflections');
67
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
68
+ const path = join(directory, `${reflection.reflection_id}.json`);
69
+ writeFileSync(path, `${JSON.stringify(reflection, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
70
+ console.log(JSON.stringify({ reflection, ref: path }, null, 2));
71
+ }
72
+
73
+ function cmdProposeImprovement(args) {
74
+ rejectUnknownFlags(args.flags, ['reflection', 'input', 'config']);
75
+ const reflectionId = flag(args.flags, 'reflection');
76
+ const inputPath = flag(args.flags, 'input');
77
+ if (!reflectionId || !inputPath || !/^[0-9a-f-]{36}$/iu.test(reflectionId)) die('propose-improvement 需要 --reflection <uuid> --input <json>。', 2);
78
+ const loaded = loadRepositoryProfile({ explicitConfigPath: flag(args.flags, 'config') });
79
+ const reflectionPath = join(learningRoot(loaded.context.common_dir), 'reflections', `${reflectionId}.json`);
80
+ if (!existsSync(reflectionPath)) die('Reflection 不存在。', 2);
81
+ const reflection = readJsonFileOrDie(reflectionPath, 'Reflection 文件');
82
+ const input = readJsonFileOrDie(resolve(inputPath), 'propose-improvement --input 文件');
83
+ if (!reflection.evidence_refs?.length) die('Proposal 必须来自有证据的 Reflection。', 2);
84
+ const proposal = {
85
+ schema_version: 1,
86
+ proposal_id: randomUUID(),
87
+ target_skill: { name: 'manage-worktrees', based_on_version: 'unversioned', based_on_digest: reflection.affected_skill.content_digest },
88
+ source_reflections: [{ reflection_id: reflection.reflection_id, reflection_digest: reflection.reflection_digest }],
89
+ problem: { type: input.problem_type ?? 'skill_gap', evidence_refs: reflection.evidence_refs },
90
+ proposed_change: sanitizeLearningText(input.proposed_change),
91
+ affected_scope: (input.affected_scope ?? []).map(sanitizeLearningText),
92
+ counterexamples: (input.counterexamples ?? []).map(sanitizeLearningText),
93
+ validation_plan: { replay_cases: input.validation_plan?.replay_cases ?? [], regression_suites: input.validation_plan?.regression_suites ?? [], independent_review: 'required', ...(input.validation_plan?.canary ? { canary: input.validation_plan.canary } : {}) },
94
+ lifecycle: 'proposed',
95
+ };
96
+ proposal.proposal_digest = contentDigest(Buffer.from(JSON.stringify(canonicalJson(proposal))));
97
+ const directory = join(learningRoot(loaded.context.common_dir), 'proposals');
98
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
99
+ const path = join(directory, `${proposal.proposal_id}.json`);
100
+ writeFileSync(path, `${JSON.stringify(proposal, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
101
+ console.log(JSON.stringify({ proposal, ref: path }, null, 2));
102
+ }
103
+
104
+
105
+ return {
106
+ learningRoot,
107
+ cmdIncident,
108
+ cmdProposeImprovement,
109
+ };
110
+ }