@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.en.md +107 -0
- package/README.md +103 -0
- package/bin/agentkit.mjs +4 -0
- package/bin/cli.mjs +273 -0
- package/core/atomic-fs.mjs +23 -0
- package/core/cli-help.mjs +54 -0
- package/core/content-digest.mjs +66 -0
- package/core/digest.mjs +67 -0
- package/core/json-schema-lite.mjs +60 -0
- package/core/legacy-entry.mjs +37 -0
- package/core/reflection.mjs +142 -0
- package/core/runtime-bundle.mjs +101 -0
- package/docs/loop/embedded-review-adapter.md +41 -0
- package/docs/loop/loop-state-machine.md +43 -0
- package/docs/loop/recovery-and-fuses.md +34 -0
- package/docs/orchestrate/dispatch-contract.md +92 -0
- package/docs/orchestrate/failure-routing-and-recovery.md +46 -0
- package/docs/orchestrate/host-capability-cache.md +170 -0
- package/docs/orchestrate/isolation-fallback.md +18 -0
- package/docs/orchestrate/model-routing-config.md +186 -0
- package/docs/orchestrate/orchestration-runtime.md +261 -0
- package/docs/orchestrate/review-budget.md +90 -0
- package/docs/orchestrate/task-playbooks.md +85 -0
- package/docs/orchestrate/user-facing-reporting.md +14 -0
- package/docs/verify/evidence-schema.md +167 -0
- package/docs/verify/input-preparation.md +44 -0
- package/docs/verify/verification-protocol.md +76 -0
- package/docs/worktree/batch-integration.md +176 -0
- package/docs/worktree/delivery-identity.md +41 -0
- package/docs/worktree/profile.md +107 -0
- package/docs/worktree/reclaim-and-watch.md +96 -0
- package/docs/worktree/review-lifecycle.md +92 -0
- package/docs/worktree/spawn-and-stack.md +74 -0
- package/domains/loop/loop-runtime.mjs +1056 -0
- package/domains/orchestrate/contract-tool.mjs +169 -0
- package/domains/orchestrate/host_capability_cache.mjs +437 -0
- package/domains/orchestrate/orchestration-ledger.mjs +332 -0
- package/domains/orchestrate/orchestration-metadata.mjs +4 -0
- package/domains/orchestrate/orchestration-reflection.mjs +119 -0
- package/domains/orchestrate/resolve_model_policy.mjs +311 -0
- package/domains/orchestrate/review-budget.mjs +162 -0
- package/domains/orchestrate/worker-capability-preflight.mjs +227 -0
- package/domains/verify/verification-runtime.mjs +1638 -0
- package/domains/worktree/worktree-archive.mjs +135 -0
- package/domains/worktree/worktree-artifact.mjs +123 -0
- package/domains/worktree/worktree-batch-integrate.mjs +713 -0
- package/domains/worktree/worktree-batch-plan.mjs +198 -0
- package/domains/worktree/worktree-batch-result.mjs +241 -0
- package/domains/worktree/worktree-core.mjs +908 -0
- package/domains/worktree/worktree-doctor.mjs +493 -0
- package/domains/worktree/worktree-history.mjs +377 -0
- package/domains/worktree/worktree-learning.mjs +110 -0
- package/domains/worktree/worktree-lifecycle.mjs +786 -0
- package/domains/worktree/worktree-merge-preview.mjs +409 -0
- package/domains/worktree/worktree-mgr.mjs +261 -0
- package/domains/worktree/worktree-process.mjs +55 -0
- package/domains/worktree/worktree-profile.mjs +800 -0
- package/domains/worktree/worktree-provider-gitlab.mjs +59 -0
- package/domains/worktree/worktree-reclaim.mjs +683 -0
- package/domains/worktree/worktree-review-refresh.mjs +574 -0
- package/domains/worktree/worktree-review-watch.mjs +661 -0
- package/domains/worktree/worktree-scan.mjs +510 -0
- package/domains/worktree/worktree-trace-test-worker.mjs +23 -0
- package/domains/worktree/worktree-trace.mjs +478 -0
- package/manage-worktrees/SKILL.md +87 -0
- package/manage-worktrees/agents/openai.yaml +4 -0
- package/manage-worktrees/scripts/worktree-mgr.mjs +10 -0
- package/manage-worktrees/scripts/worktree-scan.mjs +10 -0
- package/orchestrate-subagents/SKILL.md +173 -0
- package/orchestrate-subagents/agents/openai.yaml +4 -0
- package/orchestrate-subagents/scripts/contract-tool.mjs +10 -0
- package/orchestrate-subagents/scripts/host_capability_cache.mjs +10 -0
- package/orchestrate-subagents/scripts/orchestration-ledger.mjs +10 -0
- package/orchestrate-subagents/scripts/orchestration-reflection.mjs +10 -0
- package/orchestrate-subagents/scripts/resolve_model_policy.mjs +10 -0
- package/orchestrate-subagents/scripts/review-budget.mjs +10 -0
- package/orchestrate-subagents/scripts/worker-capability-preflight.mjs +10 -0
- package/package.json +48 -0
- package/run-agent-verify-loop/SKILL.md +127 -0
- package/run-agent-verify-loop/agents/openai.yaml +4 -0
- package/run-agent-verify-loop/scripts/loop-runtime.mjs +10 -0
- package/schemas/artifact-ref-v1.schema.json +23 -0
- package/schemas/batch-result-v1.schema.json +138 -0
- package/schemas/controller-recheck-record-v1.schema.json +22 -0
- package/schemas/convergence-report-v1.schema.json +9 -0
- package/schemas/effective-worker-capability-v1.schema.json +36 -0
- package/schemas/embedded-verification-record-v1.schema.json +32 -0
- package/schemas/evidence-package-v1.schema.json +41 -0
- package/schemas/improvement-proposal-v1.schema.json +18 -0
- package/schemas/loop-state-v1.schema.json +34 -0
- package/schemas/model-policy-resolution-v1.schema.json +41 -0
- package/schemas/orchestration-ledger-v1.schema.json +110 -0
- package/schemas/reflection-record-v1.schema.json +24 -0
- package/schemas/review-result-v1.schema.json +37 -0
- package/schemas/task-contract-v1.schema.json +83 -0
- package/schemas/verification-profile-v1.schema.json +60 -0
- package/schemas/worker-capability-requirements-v1.schema.json +21 -0
- package/schemas/worktree-binding-v1.schema.json +14 -0
- package/shell-manifest.json +79 -0
- package/verify-agent-output/SKILL.md +119 -0
- package/verify-agent-output/agents/openai.yaml +4 -0
- package/verify-agent-output/scripts/verification-runtime.mjs +10 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { gitTry, isAncestor } from './worktree-core.mjs';
|
|
4
|
+
import { runFileCapture } from './worktree-process.mjs';
|
|
5
|
+
|
|
6
|
+
const MERGE_TREE_TIMEOUT_MS = 60_000;
|
|
7
|
+
const CONFLICT_SCAN_FILES_CAP = 50;
|
|
8
|
+
const CONFLICT_SCAN_NOTES_CAP = 20;
|
|
9
|
+
const CONFLICT_SCAN_PRINT_CAP = 10;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* "只有在合成态重新生成一次才对"的产物路径。这类文件被多个分支各自重生成后必然冲突,
|
|
13
|
+
* 而且挑任何一边都是错的——所以矩阵命中它们时要提示的是"合成后重跑生成步骤",
|
|
14
|
+
* 不是"谁先合谁后合"。
|
|
15
|
+
*
|
|
16
|
+
* 命中只是**提示**,不是判定:仓库到底该跑什么命令由 Profile 的 `post_integrate_steps`
|
|
17
|
+
* 声明,portable core 既不猜命令也不代跑(与 batch-integrate 是同一条边界)。
|
|
18
|
+
*/
|
|
19
|
+
const REGENERATED_PATH_RULES = [
|
|
20
|
+
{ kind: 'lockfile', pattern: /(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|pubspec\.lock|Podfile\.lock|Gemfile\.lock|poetry\.lock|uv\.lock|composer\.lock|go\.sum|flake\.lock)$/u },
|
|
21
|
+
{ kind: 'golden', pattern: /(^|\/)(goldens?|__snapshots__|snapshots)\//u },
|
|
22
|
+
{ kind: 'golden', pattern: /\.(golden|snap|ambr)$/u },
|
|
23
|
+
{ kind: 'codegen', pattern: /(^|\/)(__generated__|generated)\//u },
|
|
24
|
+
{ kind: 'codegen', pattern: /\.(g|gen|generated|freezed|pb)\.[a-z0-9]+$/u },
|
|
25
|
+
{ kind: 'codegen', pattern: /_pb2(_grpc)?\.pyi?$/u },
|
|
26
|
+
{ kind: 'build_output', pattern: /(^|\/)dist\//u },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 路径是否属于"合成后需重生成"的产物类;返回粗分类别或 null。
|
|
31
|
+
* @param {string} path
|
|
32
|
+
*/
|
|
33
|
+
export function regeneratedPathKind(path) {
|
|
34
|
+
if (typeof path !== 'string' || !path) return null;
|
|
35
|
+
for (const rule of REGENERATED_PATH_RULES) if (rule.pattern.test(path)) return rule.kind;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 解析 `git --version` 的前三段版本号。
|
|
41
|
+
* @param {string|null} text
|
|
42
|
+
*/
|
|
43
|
+
export function parseGitVersion(text) {
|
|
44
|
+
if (typeof text !== 'string') return null;
|
|
45
|
+
const matched = text.match(/(\d+)\.(\d+)(?:\.(\d+))?/u);
|
|
46
|
+
if (!matched) return null;
|
|
47
|
+
return [Number(matched[1]), Number(matched[2]), Number(matched[3] ?? 0)];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 冲突预测要求 Git ≥ 2.39。
|
|
52
|
+
*
|
|
53
|
+
* `merge-tree --write-tree` 本身 2.38 就有,但本扫描依赖的是 `-z` 信息段里那条**结构化 NUL
|
|
54
|
+
* 记录**(`<路径数>NUL<路径>...NUL<conflict-type>NUL<message>NUL`,其中 conflict-type 是
|
|
55
|
+
* git-merge-tree(1) 明说的 stable string)。2.38 的 `-z` 信息段还是自由文本,按结构化格式去解
|
|
56
|
+
* 会把消息当成类型,冲突类型粗分随之错判。
|
|
57
|
+
*
|
|
58
|
+
* 与其写一段本机无法实测的 2.38 兼容分支,不如把支持线抬到 2.39 并 fail-closed 降级:
|
|
59
|
+
* 低版本回报 `supported: false`,计划本身照常冻结,绝不给出一份可能错的矩阵。
|
|
60
|
+
* @param {string|null} versionText
|
|
61
|
+
*/
|
|
62
|
+
export function mergeTreeScanSupported(versionText) {
|
|
63
|
+
const version = parseGitVersion(versionText);
|
|
64
|
+
if (!version) return false;
|
|
65
|
+
const [major, minor] = version;
|
|
66
|
+
return major > 2 || (major === 2 && minor >= 39);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 成对状态分类。**冲突判定只认 merge-tree 退出码**,不认文件条目数量。
|
|
71
|
+
*
|
|
72
|
+
* git-merge-tree(1) MISTAKES TO AVOID 原文:「Do NOT interpret an empty Conflicted file info
|
|
73
|
+
* list as a clean merge; check the exit status.」——目录重命名类冲突可以退出码为 1 却没有任何
|
|
74
|
+
* unmerged 文件条目。按条目数判 clean 会把真冲突报成干净。
|
|
75
|
+
*
|
|
76
|
+
* 相邻面算不出来(diff 失败/超时)时报 `incomplete`,不静默降级成 `clean`。
|
|
77
|
+
* @param {{mergeTreeOk:boolean, conflicted:boolean, adjacentFiles:number|null}} input
|
|
78
|
+
*/
|
|
79
|
+
export function classifyPairState({ mergeTreeOk, conflicted, adjacentFiles }) {
|
|
80
|
+
if (!mergeTreeOk) return 'error';
|
|
81
|
+
if (conflicted) return 'conflict';
|
|
82
|
+
if (adjacentFiles === null) return 'incomplete';
|
|
83
|
+
return adjacentFiles > 0 ? 'adjacent' : 'clean';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 写树式干跑 merge:`git merge-tree --write-tree` 在**对象库**里算出合并结果,
|
|
88
|
+
* 只写未被任何 ref 引用的临时 tree/blob(随 gc 回收),不动工作区、index、HEAD 和任何 ref。
|
|
89
|
+
* 因此它可以在 plan-batch 的只读语义下跑,也不需要先建候选树。
|
|
90
|
+
*
|
|
91
|
+
* 退出码语义(git-merge-tree(1) EXIT STATUS):0=可自动合并,1=有冲突,其他=Git 自身失败。
|
|
92
|
+
* `conflicted` 一律取自退出码;文件条目只是明细,可能为空。
|
|
93
|
+
* @param {string} cwd @param {string} a @param {string} b
|
|
94
|
+
*/
|
|
95
|
+
function mergeTreeDryRun(cwd, a, b) {
|
|
96
|
+
const result = runFileCapture(
|
|
97
|
+
'git',
|
|
98
|
+
['merge-tree', '--write-tree', '-z', '--name-only', a, b],
|
|
99
|
+
{ cwd, timeoutMs: MERGE_TREE_TIMEOUT_MS },
|
|
100
|
+
);
|
|
101
|
+
if (result.status === 0 && result.ok) return { ok: true, conflicted: false, files: [], notes: [], reason: null };
|
|
102
|
+
if (result.status !== 1) {
|
|
103
|
+
return { ok: false, conflicted: null, files: [], notes: [], reason: (result.stderr || result.stdout || 'git merge-tree 执行失败').slice(0, 300) };
|
|
104
|
+
}
|
|
105
|
+
// -z 输出:<冲突树 OID>NUL <冲突文件名>NUL... NUL(段结束)
|
|
106
|
+
// 之后是结构化信息记录:<路径数>NUL <路径>NUL... <conflict-type>NUL <人读 message>NUL
|
|
107
|
+
const fields = result.stdout.split('\0');
|
|
108
|
+
let cursor = 1;
|
|
109
|
+
const paths = [];
|
|
110
|
+
while (cursor < fields.length && fields[cursor] !== '') paths.push(fields[cursor++]);
|
|
111
|
+
cursor += 1;
|
|
112
|
+
const types = new Map();
|
|
113
|
+
const notes = [];
|
|
114
|
+
while (cursor < fields.length) {
|
|
115
|
+
const count = Number.parseInt(fields[cursor++], 10);
|
|
116
|
+
if (!Number.isInteger(count) || count < 0 || cursor + count >= fields.length) break;
|
|
117
|
+
const entryPaths = fields.slice(cursor, cursor + count);
|
|
118
|
+
cursor += count;
|
|
119
|
+
const conflictType = fields[cursor++];
|
|
120
|
+
const message = fields[cursor++] ?? '';
|
|
121
|
+
if (!conflictType || !conflictType.startsWith('CONFLICT')) continue;
|
|
122
|
+
for (const path of entryPaths) if (!types.has(path)) types.set(path, conflictType);
|
|
123
|
+
if (notes.length < CONFLICT_SCAN_NOTES_CAP) {
|
|
124
|
+
notes.push({ paths: entryPaths, conflict_type: conflictType, message: message.trim().slice(0, 300) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
conflicted: true,
|
|
130
|
+
files: paths.map((path) => ({ path, conflict_type: types.get(path) ?? 'CONFLICT (unknown)' })),
|
|
131
|
+
notes,
|
|
132
|
+
reason: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* watcher 的只读 refresh 预判。它回答“把当前 target 与冻结 feature head 做三方合并是否出现
|
|
138
|
+
* 文本/树冲突”,不承诺逐 commit rebase 一定同样成功;调用方必须把 method/limitation 一并展示。
|
|
139
|
+
* @param {string} cwd @param {string|null} recordedBaseSha @param {string} targetSha @param {string} headSha
|
|
140
|
+
*/
|
|
141
|
+
export function predictReviewRefresh(cwd, recordedBaseSha, targetSha, headSha) {
|
|
142
|
+
if (!recordedBaseSha) return { state: 'unknown', method: 'merge-tree', reason: 'recorded base SHA missing' };
|
|
143
|
+
if (recordedBaseSha === targetSha) return { state: 'unchanged', method: 'merge-tree', reason: null };
|
|
144
|
+
if (!isAncestor(cwd, recordedBaseSha, targetSha)) {
|
|
145
|
+
return { state: 'diverged', method: 'ancestry', reason: 'target no longer descends from recorded base' };
|
|
146
|
+
}
|
|
147
|
+
const version = gitTry(['--version'], cwd);
|
|
148
|
+
if (!mergeTreeScanSupported(version.ok ? version.out : null)) {
|
|
149
|
+
return { state: 'unknown', method: 'merge-tree', reason: 'Git 2.39+ required for structured merge-tree prediction' };
|
|
150
|
+
}
|
|
151
|
+
const preview = mergeTreeDryRun(cwd, targetSha, headSha);
|
|
152
|
+
if (!preview.ok) return { state: 'unknown', method: 'merge-tree', reason: preview.reason };
|
|
153
|
+
return {
|
|
154
|
+
state: preview.conflicted ? 'conflict' : 'clean',
|
|
155
|
+
method: 'merge-tree',
|
|
156
|
+
reason: null,
|
|
157
|
+
conflict_files: preview.files.slice(0, CONFLICT_SCAN_PRINT_CAP).map((item) => item.path),
|
|
158
|
+
files_truncated: preview.files.length > CONFLICT_SCAN_PRINT_CAP,
|
|
159
|
+
limitation: 'merge-tree is a merge preview; per-commit rebase may still differ',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {string} cwd @param {string} from @param {string} to */
|
|
164
|
+
function changedPathsBetween(cwd, from, to) {
|
|
165
|
+
const diff = gitTry(['diff', '--name-only', '-z', from, to], cwd, { timeoutMs: MERGE_TREE_TIMEOUT_MS });
|
|
166
|
+
if (!diff.ok) return null;
|
|
167
|
+
return new Set(diff.out.split('\0').filter(Boolean));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 干跑一对提交,产出冲突文件清单和"同文件相邻"清单。
|
|
172
|
+
*
|
|
173
|
+
* 两类必须分开报,因为处置完全不同:
|
|
174
|
+
* - `overlapping` / `structural`:Git 自己合不了,合成时一定要人裁决。
|
|
175
|
+
* - `adjacent`:Git 能自动合,但两支确实改了同一个文件——语义冲突(哨兵语义、
|
|
176
|
+
* 同一常量两种含义)正好躲在这里,机器判不出来,只能靠人看清单。
|
|
177
|
+
* @param {string} cwd @param {string} a @param {string} b
|
|
178
|
+
*/
|
|
179
|
+
function scanCommitPair(cwd, a, b) {
|
|
180
|
+
const base = gitTry(['merge-base', a, b], cwd);
|
|
181
|
+
const dryRun = mergeTreeDryRun(cwd, a, b);
|
|
182
|
+
if (!dryRun.ok) {
|
|
183
|
+
return {
|
|
184
|
+
state: classifyPairState({ mergeTreeOk: false, conflicted: false, adjacentFiles: 0 }),
|
|
185
|
+
merge_base: base.ok ? base.out : null,
|
|
186
|
+
conflict_files: 0,
|
|
187
|
+
adjacent_files: null,
|
|
188
|
+
files: [],
|
|
189
|
+
files_total: 0,
|
|
190
|
+
files_truncated: false,
|
|
191
|
+
conflict_notes: [],
|
|
192
|
+
regenerated: [],
|
|
193
|
+
reason: dryRun.reason,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const conflicted = new Map(dryRun.files.map((item) => [item.path, item.conflict_type]));
|
|
197
|
+
// 相邻面算不出来时保持 null(未知),绝不退化成空数组——空数组会被读成"确认没有相邻文件"。
|
|
198
|
+
let adjacent = null;
|
|
199
|
+
let adjacentReason = null;
|
|
200
|
+
if (!base.ok || !base.out) {
|
|
201
|
+
adjacentReason = '无法解析 merge base,同文件相邻面未知。';
|
|
202
|
+
} else {
|
|
203
|
+
const changedA = changedPathsBetween(cwd, base.out, a);
|
|
204
|
+
const changedB = changedPathsBetween(cwd, base.out, b);
|
|
205
|
+
if (changedA && changedB) adjacent = [...changedA].filter((path) => changedB.has(path) && !conflicted.has(path)).sort();
|
|
206
|
+
else adjacentReason = 'git diff 失败或超时,同文件相邻面未知。';
|
|
207
|
+
}
|
|
208
|
+
// 全量清单:REGEN 汇总、计数都基于它;截断只发生在展示用的 files 上。
|
|
209
|
+
const allFiles = [
|
|
210
|
+
...[...conflicted.entries()]
|
|
211
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
212
|
+
.map(([path, conflictType]) => ({
|
|
213
|
+
path,
|
|
214
|
+
class: conflictType === 'CONFLICT (contents)' ? 'overlapping' : 'structural',
|
|
215
|
+
conflict_type: conflictType,
|
|
216
|
+
regenerated: regeneratedPathKind(path),
|
|
217
|
+
})),
|
|
218
|
+
...(adjacent ?? []).map((path) => ({ path, class: 'adjacent', conflict_type: null, regenerated: regeneratedPathKind(path) })),
|
|
219
|
+
];
|
|
220
|
+
return {
|
|
221
|
+
state: classifyPairState({
|
|
222
|
+
mergeTreeOk: true,
|
|
223
|
+
conflicted: dryRun.conflicted,
|
|
224
|
+
adjacentFiles: adjacent === null ? null : adjacent.length,
|
|
225
|
+
}),
|
|
226
|
+
merge_base: base.ok ? base.out : null,
|
|
227
|
+
conflict_files: conflicted.size,
|
|
228
|
+
adjacent_files: adjacent === null ? null : adjacent.length,
|
|
229
|
+
files: allFiles.slice(0, CONFLICT_SCAN_FILES_CAP),
|
|
230
|
+
files_total: allFiles.length,
|
|
231
|
+
files_truncated: allFiles.length > CONFLICT_SCAN_FILES_CAP,
|
|
232
|
+
conflict_notes: dryRun.notes,
|
|
233
|
+
regenerated: allFiles.filter((file) => file.regenerated).map((file) => ({ path: file.path, kind: file.regenerated, class: file.class })),
|
|
234
|
+
reason: adjacentReason,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 冻结前的冲突矩阵:对 included 输入两两干跑,并各自对 target 干跑一次。
|
|
240
|
+
*
|
|
241
|
+
* 目的是把"哪两支会打架、打在哪些文件上"提前到**规划期**,让合并排序和"同构双生
|
|
242
|
+
* 该不该并支"有依据;否则这些信息只有在 batch-integrate 真的合到冲突时才暴露。
|
|
243
|
+
*
|
|
244
|
+
* 已知边界(不要当成保证):两两干跑预测的是**成对**冲突。实际合成是顺序累积的三方
|
|
245
|
+
* 合并,两两都干净不等于合成一定干净;矩阵是决策输入,不替代 batch-integrate 的实合。
|
|
246
|
+
* @param {string} cwd
|
|
247
|
+
* @param {{ref:string,sha:string}} target
|
|
248
|
+
* @param {Record<string,any>[]} inputs
|
|
249
|
+
*/
|
|
250
|
+
export function computeConflictScan(cwd, target, inputs) {
|
|
251
|
+
const unsupported = (reason) => ({
|
|
252
|
+
schema_version: 1,
|
|
253
|
+
supported: false,
|
|
254
|
+
reason,
|
|
255
|
+
pairs: [],
|
|
256
|
+
against_target: [],
|
|
257
|
+
summary: null,
|
|
258
|
+
});
|
|
259
|
+
const versionText = gitTry(['--version'], cwd);
|
|
260
|
+
if (!mergeTreeScanSupported(versionText.ok ? versionText.out : null)) {
|
|
261
|
+
return unsupported(
|
|
262
|
+
`冲突预测要求 Git ≥ 2.39(本机:${versionText.ok ? versionText.out : '版本不可读'}):` +
|
|
263
|
+
'2.38 的 `merge-tree -z` 信息段还不是结构化 NUL 记录,按新格式解会错判冲突类型。' +
|
|
264
|
+
'本次跳过冲突预测,不影响计划本身。',
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const probe = runFileCapture(
|
|
268
|
+
'git',
|
|
269
|
+
['merge-tree', '--write-tree', '-z', '--name-only', target.sha, target.sha],
|
|
270
|
+
{ cwd, timeoutMs: MERGE_TREE_TIMEOUT_MS },
|
|
271
|
+
);
|
|
272
|
+
if (probe.status !== 0) {
|
|
273
|
+
return unsupported(`本机 \`git merge-tree --write-tree\` 自检未通过:${(probe.stderr || probe.stdout || '原因未知').slice(0, 200)};本次跳过冲突预测,不影响计划本身。`);
|
|
274
|
+
}
|
|
275
|
+
const identify = (item) => ({ task: item.task, worktree_id: item.worktree_id, head: item.head });
|
|
276
|
+
const againstTarget = inputs.map((item) => ({
|
|
277
|
+
input: identify(item),
|
|
278
|
+
target: { ref: target.ref, sha: target.sha },
|
|
279
|
+
...scanCommitPair(cwd, target.sha, item.head),
|
|
280
|
+
}));
|
|
281
|
+
const pairs = [];
|
|
282
|
+
for (let left = 0; left < inputs.length; left++) {
|
|
283
|
+
for (let right = left + 1; right < inputs.length; right++) {
|
|
284
|
+
pairs.push({
|
|
285
|
+
a: identify(inputs[left]),
|
|
286
|
+
b: identify(inputs[right]),
|
|
287
|
+
...scanCommitPair(cwd, inputs[left].head, inputs[right].head),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// 每个输入的"冲突面":直接支撑"冲突面大的压轴"这条排序口径。
|
|
292
|
+
// conflicting_peers 排在 conflict_files 前面,是因为没有文件条目的目录重命名类冲突
|
|
293
|
+
// 同样是真冲突,不能因为 conflict_files=0 被排到干净输入后面。
|
|
294
|
+
const load = inputs.map((item) => {
|
|
295
|
+
const related = pairs.filter((pair) => pair.a.worktree_id === item.worktree_id || pair.b.worktree_id === item.worktree_id);
|
|
296
|
+
return {
|
|
297
|
+
task: item.task,
|
|
298
|
+
worktree_id: item.worktree_id,
|
|
299
|
+
conflicting_peers: related.filter((pair) => pair.state === 'conflict').length,
|
|
300
|
+
conflict_files: related.reduce((sum, pair) => sum + pair.conflict_files, 0),
|
|
301
|
+
adjacent_files: related.reduce((sum, pair) => sum + (pair.adjacent_files ?? 0), 0),
|
|
302
|
+
incomplete_peers: related.filter((pair) => pair.state === 'incomplete' || pair.state === 'error' || pair.adjacent_files === null).length,
|
|
303
|
+
target_conflict_files: againstTarget.find((row) => row.input.worktree_id === item.worktree_id)?.conflict_files ?? 0,
|
|
304
|
+
};
|
|
305
|
+
}).sort((left, right) =>
|
|
306
|
+
right.conflicting_peers - left.conflicting_peers ||
|
|
307
|
+
right.conflict_files - left.conflict_files ||
|
|
308
|
+
right.adjacent_files - left.adjacent_files ||
|
|
309
|
+
left.task.localeCompare(right.task));
|
|
310
|
+
// 汇总跑在**未截断**的全量命中上:先截 50 再统计会让排在后面的 lock/golden 从汇总消失。
|
|
311
|
+
const regenerated = new Map();
|
|
312
|
+
for (const pair of [...pairs, ...againstTarget]) {
|
|
313
|
+
for (const hit of pair.regenerated) {
|
|
314
|
+
const entry = regenerated.get(hit.path) ?? { path: hit.path, kind: hit.kind, pairs: 0, classes: new Set() };
|
|
315
|
+
entry.pairs += 1;
|
|
316
|
+
entry.classes.add(hit.class);
|
|
317
|
+
regenerated.set(hit.path, entry);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const allRows = [...pairs, ...againstTarget];
|
|
321
|
+
return {
|
|
322
|
+
schema_version: 1,
|
|
323
|
+
supported: true,
|
|
324
|
+
reason: null,
|
|
325
|
+
pairs,
|
|
326
|
+
against_target: againstTarget,
|
|
327
|
+
summary: {
|
|
328
|
+
pair_count: pairs.length,
|
|
329
|
+
conflicting_pairs: pairs.filter((pair) => pair.state === 'conflict').length,
|
|
330
|
+
adjacent_only_pairs: pairs.filter((pair) => pair.state === 'adjacent').length,
|
|
331
|
+
incomplete_pairs: pairs.filter((pair) => pair.state === 'incomplete').length,
|
|
332
|
+
failed_pairs: pairs.filter((pair) => pair.state === 'error').length,
|
|
333
|
+
// 相邻面未知的格子:conflict 态也可能相邻面算不出来,单独计数,不许被 state 掩盖。
|
|
334
|
+
unknown_adjacency_rows: allRows.filter((row) => row.adjacent_files === null).length,
|
|
335
|
+
pathless_conflict_pairs: pairs.filter((pair) => pair.state === 'conflict' && pair.conflict_files === 0).length,
|
|
336
|
+
max_pair_conflict_files: pairs.reduce((max, pair) => Math.max(max, pair.conflict_files), 0),
|
|
337
|
+
inputs: load,
|
|
338
|
+
regenerated_paths: [...regenerated.values()]
|
|
339
|
+
.map((hit) => ({ path: hit.path, kind: hit.kind, pairs: hit.pairs, classes: [...hit.classes].sort() }))
|
|
340
|
+
.sort((left, right) => right.pairs - left.pairs || left.path.localeCompare(right.path)),
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** @param {Record<string,any>} row */
|
|
346
|
+
function formatPairCounts(row) {
|
|
347
|
+
const adjacent = row.adjacent_files === null ? '未知' : String(row.adjacent_files);
|
|
348
|
+
return `conflict=${row.conflict_files} adjacent=${adjacent}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @param {Record<string,any>} row @param {string} indent */
|
|
352
|
+
function printPairDetail(row, indent) {
|
|
353
|
+
for (const file of row.files.slice(0, CONFLICT_SCAN_PRINT_CAP)) {
|
|
354
|
+
console.log(`${indent}${file.class.padEnd(12)} ${file.path}${file.conflict_type ? ` ${file.conflict_type}` : ''}${file.regenerated ? ` [${file.regenerated}]` : ''}`);
|
|
355
|
+
}
|
|
356
|
+
if (row.files_total > CONFLICT_SCAN_PRINT_CAP) {
|
|
357
|
+
console.log(`${indent}… 共 ${row.files_total} 项${row.files_truncated ? `(--json 亦按 ${CONFLICT_SCAN_FILES_CAP} 项上限截断;计数与产物类汇总仍是全量)` : ''}`);
|
|
358
|
+
}
|
|
359
|
+
// 冲突但没有文件条目时,信息性记录是唯一线索,必须打出来。
|
|
360
|
+
if (row.state === 'conflict' && row.conflict_files === 0) {
|
|
361
|
+
for (const note of row.conflict_notes) {
|
|
362
|
+
console.log(`${indent}[NOTE] ${note.conflict_type} → ${note.paths.join(', ')}`);
|
|
363
|
+
if (note.message) console.log(`${indent} ${note.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (row.reason) console.log(`${indent}[DEGRADED] ${row.reason}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @param {ReturnType<typeof computeConflictScan>} scan */
|
|
370
|
+
export function printConflictScan(scan) {
|
|
371
|
+
if (!scan.supported) {
|
|
372
|
+
console.log(` [SCAN] 跳过:${scan.reason}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const summary = scan.summary;
|
|
376
|
+
console.log(
|
|
377
|
+
` [SCAN] pairs=${summary.pair_count} conflicting=${summary.conflicting_pairs} adjacent_only=${summary.adjacent_only_pairs}` +
|
|
378
|
+
` incomplete=${summary.incomplete_pairs} failed=${summary.failed_pairs} unknown_adjacency=${summary.unknown_adjacency_rows}`,
|
|
379
|
+
);
|
|
380
|
+
const ordered = [...scan.pairs].sort((left, right) =>
|
|
381
|
+
(right.state === 'conflict' ? 1 : 0) - (left.state === 'conflict' ? 1 : 0) ||
|
|
382
|
+
right.conflict_files - left.conflict_files ||
|
|
383
|
+
(right.adjacent_files ?? 0) - (left.adjacent_files ?? 0));
|
|
384
|
+
for (const pair of ordered) {
|
|
385
|
+
if (pair.state === 'error') {
|
|
386
|
+
console.log(` [PAIR] ${pair.a.task} × ${pair.b.task} 扫描失败:${pair.reason}`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
console.log(` [PAIR] ${pair.a.task} × ${pair.b.task} ${formatPairCounts(pair)} state=${pair.state}`);
|
|
390
|
+
printPairDetail(pair, ' ');
|
|
391
|
+
}
|
|
392
|
+
for (const row of scan.against_target) {
|
|
393
|
+
if (row.state === 'error') {
|
|
394
|
+
console.log(` [TARGET] ${row.input.task} → ${row.target.ref} 扫描失败:${row.reason}`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
console.log(` [TARGET] ${row.input.task} → ${row.target.ref} ${formatPairCounts(row)} state=${row.state}`);
|
|
398
|
+
if (row.state !== 'clean') printPairDetail(row, ' ');
|
|
399
|
+
}
|
|
400
|
+
for (const row of summary.inputs) {
|
|
401
|
+
console.log(` [LOAD] ${row.task} conflict=${row.conflict_files} adjacent=${row.adjacent_files} peers=${row.conflicting_peers} vs_target=${row.target_conflict_files}`);
|
|
402
|
+
}
|
|
403
|
+
for (const hit of summary.regenerated_paths) {
|
|
404
|
+
console.log(` [REGEN] ${hit.path} (${hit.kind}) 命中 ${hit.pairs} 处;这类产物只能在合成态重新生成,见 Profile post_integrate_steps`);
|
|
405
|
+
}
|
|
406
|
+
if (summary.incomplete_pairs > 0 || summary.failed_pairs > 0 || summary.unknown_adjacency_rows > 0) {
|
|
407
|
+
console.log(' [WARN] 存在未完成的格子:矩阵不完整,不能据它断言"没有冲突"。');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import * as core from './worktree-core.mjs';
|
|
12
|
+
import * as mergePreview from './worktree-merge-preview.mjs';
|
|
13
|
+
import * as profile from './worktree-profile.mjs';
|
|
14
|
+
import * as provider from './worktree-provider-gitlab.mjs';
|
|
15
|
+
import * as trace from './worktree-trace.mjs';
|
|
16
|
+
import { createCommands as createArchiveCommands } from './worktree-archive.mjs';
|
|
17
|
+
import { createCommands as createArtifactCommands } from './worktree-artifact.mjs';
|
|
18
|
+
import { createCommands as createBatchIntegrateCommands } from './worktree-batch-integrate.mjs';
|
|
19
|
+
import { createCommands as createBatchPlanCommands } from './worktree-batch-plan.mjs';
|
|
20
|
+
import { createCommands as createBatchResultCommands } from './worktree-batch-result.mjs';
|
|
21
|
+
import { createCommands as createDoctorCommands } from './worktree-doctor.mjs';
|
|
22
|
+
import { createCommands as createHistoryCommands } from './worktree-history.mjs';
|
|
23
|
+
import { createCommands as createLearningCommands } from './worktree-learning.mjs';
|
|
24
|
+
import { createCommands as createLifecycleCommands } from './worktree-lifecycle.mjs';
|
|
25
|
+
import { createCommands as createReclaimCommands } from './worktree-reclaim.mjs';
|
|
26
|
+
import { createCommands as createReviewRefreshCommands } from './worktree-review-refresh.mjs';
|
|
27
|
+
import { createCommands as createReviewWatchCommands } from './worktree-review-watch.mjs';
|
|
28
|
+
|
|
29
|
+
export { runFileCapture, runFileTry } from './worktree-process.mjs';
|
|
30
|
+
export {
|
|
31
|
+
classifyPairState,
|
|
32
|
+
mergeTreeScanSupported,
|
|
33
|
+
parseGitVersion,
|
|
34
|
+
predictReviewRefresh,
|
|
35
|
+
regeneratedPathKind,
|
|
36
|
+
} from './worktree-merge-preview.mjs';
|
|
37
|
+
export {
|
|
38
|
+
canonicalJson,
|
|
39
|
+
codegraphStdio,
|
|
40
|
+
deliverReclaimNotification,
|
|
41
|
+
normalizeCodegraphMode,
|
|
42
|
+
processIsAlive,
|
|
43
|
+
refreshTargetRefCached,
|
|
44
|
+
worktreeSkillDigest,
|
|
45
|
+
} from './worktree-core.mjs';
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
PREFIX,
|
|
49
|
+
die,
|
|
50
|
+
parseArgs,
|
|
51
|
+
rejectUnknownFlags,
|
|
52
|
+
worktreeSkillDigest,
|
|
53
|
+
} = core;
|
|
54
|
+
const { WorktreeProfileError } = profile;
|
|
55
|
+
const { GitlabSubmitError } = provider;
|
|
56
|
+
const { WorktreeTraceError } = trace;
|
|
57
|
+
const managerScript = fileURLToPath(import.meta.url);
|
|
58
|
+
|
|
59
|
+
const dependencies = {
|
|
60
|
+
spawn,
|
|
61
|
+
createHash,
|
|
62
|
+
randomUUID,
|
|
63
|
+
existsSync,
|
|
64
|
+
lstatSync,
|
|
65
|
+
mkdirSync,
|
|
66
|
+
readFileSync,
|
|
67
|
+
readdirSync,
|
|
68
|
+
realpathSync,
|
|
69
|
+
renameSync,
|
|
70
|
+
rmSync,
|
|
71
|
+
statSync,
|
|
72
|
+
writeFileSync,
|
|
73
|
+
tmpdir,
|
|
74
|
+
basename,
|
|
75
|
+
dirname,
|
|
76
|
+
join,
|
|
77
|
+
resolve,
|
|
78
|
+
fileURLToPath,
|
|
79
|
+
managerScript,
|
|
80
|
+
...mergePreview,
|
|
81
|
+
...profile,
|
|
82
|
+
...provider,
|
|
83
|
+
...trace,
|
|
84
|
+
...core,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/// 依赖袋按构造顺序累积,而各工厂在被调用的瞬间就把依赖解构完毕:下面的构造顺序与键名
|
|
88
|
+
/// 都是承重契约(例如 lifecycle 依赖 history/reclaim/review-watch 先注入)。assignNew 把
|
|
89
|
+
/// 「后来的工厂静默覆盖既有同名依赖」变成加载期硬错误,避免同名 helper 在不同模块间
|
|
90
|
+
/// 悄悄分叉;初始字面量里 core 有意最后展开、覆盖同名基础导出,不受此守卫约束。
|
|
91
|
+
function assignNew(bag, additions, source) {
|
|
92
|
+
for (const key of Object.keys(additions)) {
|
|
93
|
+
if (Object.hasOwn(bag, key)) throw new Error(`依赖袋键冲突: ${source} 重复提供 ${key}`);
|
|
94
|
+
}
|
|
95
|
+
return Object.assign(bag, additions);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const historyCommands = createHistoryCommands(dependencies);
|
|
99
|
+
assignNew(dependencies, historyCommands, 'worktree-history');
|
|
100
|
+
const reclaimCommands = createReclaimCommands(dependencies);
|
|
101
|
+
assignNew(dependencies, reclaimCommands, 'worktree-reclaim');
|
|
102
|
+
const reviewWatchCommands = createReviewWatchCommands(dependencies);
|
|
103
|
+
assignNew(dependencies, reviewWatchCommands, 'worktree-review-watch');
|
|
104
|
+
const reviewRefreshCommands = createReviewRefreshCommands(dependencies);
|
|
105
|
+
assignNew(dependencies, reviewRefreshCommands, 'worktree-review-refresh');
|
|
106
|
+
const reviewCommands = { ...reviewWatchCommands, ...reviewRefreshCommands };
|
|
107
|
+
const lifecycleCommands = createLifecycleCommands(dependencies);
|
|
108
|
+
assignNew(dependencies, lifecycleCommands, 'worktree-lifecycle');
|
|
109
|
+
const archiveCommands = createArchiveCommands(dependencies);
|
|
110
|
+
assignNew(dependencies, archiveCommands, 'worktree-archive');
|
|
111
|
+
const batchPlanCommands = createBatchPlanCommands(dependencies);
|
|
112
|
+
assignNew(dependencies, batchPlanCommands, 'worktree-batch-plan');
|
|
113
|
+
const batchIntegrateCommands = createBatchIntegrateCommands(dependencies);
|
|
114
|
+
assignNew(dependencies, batchIntegrateCommands, 'worktree-batch-integrate');
|
|
115
|
+
const batchResultCommands = createBatchResultCommands(dependencies);
|
|
116
|
+
assignNew(dependencies, batchResultCommands, 'worktree-batch-result');
|
|
117
|
+
const batchCommands = { ...batchPlanCommands, ...batchIntegrateCommands, ...batchResultCommands };
|
|
118
|
+
const artifactCommands = createArtifactCommands(dependencies);
|
|
119
|
+
assignNew(dependencies, artifactCommands, 'worktree-artifact');
|
|
120
|
+
const learningCommands = createLearningCommands(dependencies);
|
|
121
|
+
assignNew(dependencies, learningCommands, 'worktree-learning');
|
|
122
|
+
const doctorCommands = createDoctorCommands(dependencies);
|
|
123
|
+
|
|
124
|
+
export const batchFingerprint = batchCommands.batchFingerprint;
|
|
125
|
+
export const verifyArtifactEnvelope = artifactCommands.verifyArtifactEnvelope;
|
|
126
|
+
|
|
127
|
+
function cmdCapabilities(args) {
|
|
128
|
+
rejectUnknownFlags(args.flags, ['json']);
|
|
129
|
+
console.log(JSON.stringify({ skill: 'manage-worktrees', runtime_version: '1.3.0', contracts: { worktree_binding: [1], artifact_ref: [1], reflection_record: [1], improvement_proposal: [1], batch_result: [1] }, features: ['git-common-dir-ledger', 'ownership-epochs', 'artifact-verification', 'incident-reflection', 'proposed-only-improvement', 'batch-integrate', 'batch-conflict-scan', 'declared-post-integrate-steps', 'batch-result', 'evidence-archive-reclaim', 'durable-pushed-ref-proof', 'auto-armed-review-watch', 'review-target-advance-prediction', 'explicit-review-refresh', 'managed-history-rewrite', 'stack-parent-attribution', 'structured-change-registration'], content_digest: worktreeSkillDigest() }, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function usage() {
|
|
133
|
+
console.log(`${PREFIX} portable multi-Agent worktree manager
|
|
134
|
+
|
|
135
|
+
spawn <task> --agent <host> --agent-id <id> --purpose <text> [--owner <name>] [--base <ref> --base-reason <text>] [--root <path>] [--codegraph auto|on|off]
|
|
136
|
+
同会话已有未回收树时默认拒绝;独立并行加 --parallel-reason <text>;替代加 --supersedes <selector> --replacement-reason <text>
|
|
137
|
+
adopt <path> --agent <host> --agent-id <id> --purpose <text> [--task <slug>] [--base <ref> --base-reason <text>]
|
|
138
|
+
list [--json] [--all] [--present] [--archived]
|
|
139
|
+
--present:只列目录仍然存在的 record(TRACKED/UNTRACKED/MAIN 分类不变),隐藏全部历史记录
|
|
140
|
+
--archived:历史记录里额外显示已 archive 的 record(标 [ARCHIVED]),默认隐藏
|
|
141
|
+
plan-batch <selector> <selector> [...] [--target <ref>] [--scan-conflicts] [--json]
|
|
142
|
+
--scan-conflicts:冻结前两两 merge-tree 干跑,输出冲突矩阵(不动工作区/index/ref,不改指纹)
|
|
143
|
+
batch-integrate --plan <plan.json> | <selector> <selector> [...] --agent <host> --agent-id <id>
|
|
144
|
+
[--candidate-task <slug>] [--target <ref>] [--abort-on-conflict] [--no-rerere]
|
|
145
|
+
[--recompose --recompose-head <exact-current-head>] [--json]
|
|
146
|
+
按冻结顺序合成精确 SHA;冲突 fail-closed 停在冲突处;不执行任何门禁命令
|
|
147
|
+
batch-step <candidate-selector> --step <name> --state done|skipped|failed [--note <text>] [--json]
|
|
148
|
+
batch-result <candidate-selector> --state passed|failed|stale --candidate <exact-object-id>
|
|
149
|
+
[--evidence <json>] [--reason <text>] [--json]
|
|
150
|
+
passed/failed 必须给结构化 evidence;stale 必须给 reason;终态结果不可覆盖
|
|
151
|
+
touch <selector> [--status <status>] [--note <text>] [--mr <http-url>] [--watch-target <ref>] [--id <uuid>]
|
|
152
|
+
--status ready_for_review 默认自动武装 watch;退出用 --no-watch
|
|
153
|
+
rebase <selector> --onto <ref> --expected-head <sha> --reason <text> [--continue|--abort] [--id <uuid>]
|
|
154
|
+
manager 原子登记 intent、撤防旧 watcher、执行/恢复 rebase,并刷新 base、父分支和 ownership epoch;pending 恢复只需 selector + --continue
|
|
155
|
+
refresh-review <selector> [--target <ref>] [--reason <text>] [--pause-before-push] [--continue|--abort] [--id <uuid>]
|
|
156
|
+
显式刷新 ready_for_review 分支:managed rebase、精确 force-with-lease push、重冻结并重启 watcher;--pause-before-push 允许插入项目门禁
|
|
157
|
+
retarget <selector> --base <ref> --expected-head <sha> --reason <text> [--id <uuid>]
|
|
158
|
+
仅当新 base 已是 HEAD 祖先时更新验证/MR 目标;不会改写 Git 历史
|
|
159
|
+
supersede <old-selector> --by <replacement-selector> --reason <text> [--id <uuid>] [--by-id <uuid>]
|
|
160
|
+
handoff <selector> --to-agent <host> --to-agent-id <id> --note <text> [--id <uuid>]
|
|
161
|
+
audit <selector> [--json] [--id <uuid>]
|
|
162
|
+
doctor [--json] [--verbose]
|
|
163
|
+
非 json 文本模式默认把目录已消失 record 的 WORKTREE_MISSING/BASE_OVERRIDE/EPHEMERAL_WORKTREE
|
|
164
|
+
三类 warning 折叠成一行 [summary];--verbose 逐条展开;--json 输出的 findings 始终完整不折叠
|
|
165
|
+
rebuild [<selector>] [--id <uuid>] [--recover-lock]
|
|
166
|
+
watch <selector> [--target <remote/ref>] [--interval-ms <ms>] [--change-ref <text>] [--notify auto|off] [--id <uuid>]
|
|
167
|
+
submit <selector> [--title <text>] [--description <text>] [--target <branch>] [--remote <name>] [--interval-ms <ms>] [--notify auto|off] [--id <uuid>]
|
|
168
|
+
resume-all [--json]
|
|
169
|
+
unwatch <selector> [--id <uuid>]
|
|
170
|
+
reclaim <selector> --pushed <sha> [--id <uuid>]
|
|
171
|
+
reclaim <selector> --superseded-by <replacement-selector> [--discard <exact-old-head>] [--id <uuid>] [--replacement-id <uuid>]
|
|
172
|
+
reclaim <selector> --archive-evidence <exact-candidate-head> --reason <text> [--id <uuid>]
|
|
173
|
+
archive <selector> --reason <text> [--id <uuid>]
|
|
174
|
+
只接受目录已不存在、分支已删除或已合入、且未武装监听的历史 record;只追加一条 archived
|
|
175
|
+
event,不删除分支、不删除目录、不改任何 ref;默认从 list/doctor 隐藏,list --archived 可见
|
|
176
|
+
binding <selector> [--id <uuid>] [--json]
|
|
177
|
+
artifact <selector> [--id <uuid>] [--json]
|
|
178
|
+
verify-artifact <artifact.json> [--json]
|
|
179
|
+
incident <selector> --input <json> [--id <uuid>]
|
|
180
|
+
propose-improvement --reflection <uuid> --input <json>
|
|
181
|
+
capabilities [--json]
|
|
182
|
+
|
|
183
|
+
所有命令支持 --config <path>;默认 Profile 固定从 primary worktree 读取。`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
function main(argv = process.argv.slice(2)) {
|
|
188
|
+
if (!argv[0] || ['--help', '-h', 'help'].includes(argv[0])) {
|
|
189
|
+
usage();
|
|
190
|
+
process.exit(argv[0] ? 0 : 1);
|
|
191
|
+
}
|
|
192
|
+
const subcommand = argv[0];
|
|
193
|
+
const args = parseArgs(argv.slice(1));
|
|
194
|
+
const commands = {
|
|
195
|
+
spawn: lifecycleCommands.cmdSpawn,
|
|
196
|
+
adopt: lifecycleCommands.cmdAdopt,
|
|
197
|
+
list: lifecycleCommands.cmdList,
|
|
198
|
+
'plan-batch': batchCommands.cmdPlanBatch,
|
|
199
|
+
'batch-integrate': batchCommands.cmdBatchIntegrate,
|
|
200
|
+
'batch-step': batchCommands.cmdBatchStep,
|
|
201
|
+
'batch-result': batchCommands.cmdBatchResult,
|
|
202
|
+
touch: lifecycleCommands.cmdTouch,
|
|
203
|
+
rebase: historyCommands.cmdRebase,
|
|
204
|
+
'refresh-review': reviewCommands.cmdRefreshReview,
|
|
205
|
+
retarget: historyCommands.cmdRetarget,
|
|
206
|
+
supersede: lifecycleCommands.cmdSupersede,
|
|
207
|
+
handoff: lifecycleCommands.cmdHandoff,
|
|
208
|
+
audit: lifecycleCommands.cmdAudit,
|
|
209
|
+
doctor: doctorCommands.cmdDoctor,
|
|
210
|
+
rebuild: lifecycleCommands.cmdRebuild,
|
|
211
|
+
submit: reviewCommands.cmdSubmit,
|
|
212
|
+
watch: reviewCommands.cmdWatch,
|
|
213
|
+
'resume-all': reviewCommands.cmdResumeAll,
|
|
214
|
+
unwatch: reviewCommands.cmdUnwatch,
|
|
215
|
+
'watch-worker': reviewCommands.cmdWatchWorker,
|
|
216
|
+
reclaim: reclaimCommands.cmdReclaim,
|
|
217
|
+
archive: archiveCommands.cmdArchive,
|
|
218
|
+
binding: artifactCommands.cmdBinding,
|
|
219
|
+
artifact: artifactCommands.cmdArtifact,
|
|
220
|
+
'verify-artifact': artifactCommands.cmdVerifyArtifact,
|
|
221
|
+
incident: learningCommands.cmdIncident,
|
|
222
|
+
'propose-improvement': learningCommands.cmdProposeImprovement,
|
|
223
|
+
capabilities: cmdCapabilities,
|
|
224
|
+
};
|
|
225
|
+
if (!commands[subcommand]) die(`未知子命令: ${subcommand}`, 2);
|
|
226
|
+
commands[subcommand](args);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// 本文件是否被当作 CLI 入口直接执行。
|
|
230
|
+
///
|
|
231
|
+
/// 必须按 **realpath** 比对:skill 常以软链安装(`~/.claude/skills/<name>` →
|
|
232
|
+
/// 团队仓真实目录)。软链下 `import.meta.url` 是 Node 解析后的真实路径,而
|
|
233
|
+
/// `process.argv[1]` 保留调用时的软链路径,字符串比对永不相等——main() 不跑、
|
|
234
|
+
/// 进程以 0 退出、**stdout/stderr 全空**,调用方只看到「命令成功但没有输出」,
|
|
235
|
+
/// 极难归因。realpath 两端归一后两种装法都成立。
|
|
236
|
+
export function isCliEntry(argv1 = process.argv[1], selfUrl = import.meta.url) {
|
|
237
|
+
if (!argv1) return false;
|
|
238
|
+
const self = fileURLToPath(selfUrl);
|
|
239
|
+
const entry = resolve(argv1);
|
|
240
|
+
if (self === entry) return true;
|
|
241
|
+
try {
|
|
242
|
+
return realpathSync(self) === realpathSync(entry);
|
|
243
|
+
} catch {
|
|
244
|
+
// 任一端不存在(罕见:入口被删/权限)——退回字符串比对结论。
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function runCli(argv = process.argv.slice(2)) {
|
|
250
|
+
try {
|
|
251
|
+
main(argv);
|
|
252
|
+
return Number.isInteger(process.exitCode) ? process.exitCode : 0;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof WorktreeProfileError || error instanceof WorktreeTraceError || error instanceof GitlabSubmitError) {
|
|
255
|
+
die(`${error.code}: ${error.message}`);
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isCliEntry()) process.exitCode = runCli();
|