@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,510 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * worktree-scan — 本地 git 感知 + 重叠检查(DESIGN §2.1)
5
+ *
6
+ * 动手改某任务的文件之前调用,扫本机 git 汇集「在飞 + 近期改过」的文件集合:
7
+ * ① 遍历每棵 worktree(git worktree list --porcelain,路径可能含空格),
8
+ * git -C <dir> status --porcelain 取改动 + untracked 文件。
9
+ * ② git log --all --since='6 hours ago' --name-only 近期跨所有本地分支 commit 碰过的文件。
10
+ * ③ 扫 .kiro/specs 下各 spec 的 tasks.md 里标 [-](实施中)的任务,提取其「影响文件或目录」字段并入
11
+ * (覆盖「认领了任务但还没动文件」git 扫不到的情况——母规范 race-2)。
12
+ * 输出三者并集。
13
+ *
14
+ * 给了 --target(逗号分隔文件列表)就打印交集 + 结论(COLLIDE / CLEAR);
15
+ * 不给就打印整个在飞集合。
16
+ *
17
+ * 安全 / 范围:暂时只管本机、不 git fetch(DESIGN:本期只管本机并发)。
18
+ * 纯只读,不改任何 git 状态。
19
+ *
20
+ * 用法:
21
+ * node tools/worktree/worktree-scan.mjs scan # 打印整个在飞集合
22
+ * node tools/worktree/worktree-scan.mjs scan --target a.tsx,b.css # 打印交集 + COLLIDE/CLEAR
23
+ * node tools/worktree/worktree-scan.mjs --help
24
+ *
25
+ * 退出码: 0 成功(CLEAR 或纯列举); 0 也用于 COLLIDE(重叠是「需要决策」的信息,不是脚本错误,
26
+ * 决策由调用者/agent 做——脚本职责是如实报告); 1 = 内部错误(不在 git 仓库等)。
27
+ *
28
+ * 纯 Node stdlib,无第三方依赖。中文注释 + 英文术语。
29
+ */
30
+
31
+ import { execFileSync } from 'node:child_process';
32
+ import { readFileSync, readdirSync, realpathSync } from 'node:fs';
33
+ import path from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ import { loadRepositoryProfile } from './worktree-profile.mjs';
37
+
38
+ const PREFIX = '[worktree-scan]';
39
+
40
+ // ───────────────────────────────────────────────────────────────────────────
41
+ // git helper:统一封装 execFileSync('git', ...),失败由调用方按需 try/catch。
42
+ // ───────────────────────────────────────────────────────────────────────────
43
+ /**
44
+ * @param {string[]} gitArgs
45
+ * @param {{cwd?:string}} [opts]
46
+ * @returns {string}
47
+ */
48
+ function git(gitArgs, opts = {}) {
49
+ return execFileSync('git', gitArgs, {
50
+ cwd: opts.cwd,
51
+ encoding: 'utf8',
52
+ maxBuffer: 64 * 1024 * 1024,
53
+ });
54
+ }
55
+
56
+ // ───────────────────────────────────────────────────────────────────────────
57
+ // CLI 解析
58
+ // ───────────────────────────────────────────────────────────────────────────
59
+ function usage() {
60
+ console.log(`${PREFIX} 用法:
61
+ node tools/worktree/worktree-scan.mjs scan
62
+ 汇集本机所有 worktree 在飞改动 + 近期 commit 文件 + tasks.md [-] 任务影响文件,打印并集。
63
+
64
+ node tools/worktree/worktree-scan.mjs scan --target <逗号分隔文件列表>
65
+ 与上面的在飞集合取交集;交集非空 -> COLLIDE,空 -> CLEAR。
66
+ 路径以仓库根为基准(如 apps/web/src/...)。
67
+
68
+ node tools/worktree/worktree-scan.mjs --help
69
+
70
+ 退出码: 0 成功(含 COLLIDE,重叠是需决策的信息非脚本错误); 1 内部错误。`);
71
+ }
72
+
73
+ export function runCli(argv = process.argv.slice(2)) {
74
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
75
+ usage();
76
+ return 0;
77
+ }
78
+
79
+ const sub = argv[0];
80
+ if (sub !== 'scan') {
81
+ console.error(`${PREFIX} 未知子命令 "${sub}"。`);
82
+ usage();
83
+ return 1;
84
+ }
85
+
86
+ // --target <list> 或 --target=<list>
87
+ let targetRaw = null;
88
+ for (let i = 1; i < argv.length; i++) {
89
+ const a = argv[i];
90
+ if (a === '--target') {
91
+ targetRaw = argv[i + 1] ?? '';
92
+ i++;
93
+ } else if (a.startsWith('--target=')) {
94
+ targetRaw = a.slice('--target='.length);
95
+ }
96
+ }
97
+
98
+ // ───────────────────────────────────────────────────────────────────────────
99
+ // 仓库根(脚本可从任意 cwd 调用)
100
+ // ───────────────────────────────────────────────────────────────────────────
101
+ let ROOT;
102
+ let PROFILE;
103
+ try {
104
+ ROOT = git(['rev-parse', '--show-toplevel']).trim();
105
+ PROFILE = loadRepositoryProfile({ cwd: ROOT });
106
+ } catch {
107
+ console.error(`${PREFIX} 无法加载 Git 仓库或 worktree Profile。`);
108
+ return 1;
109
+ }
110
+
111
+ const SCAN_SOURCES = PROFILE.profile.scan.sources;
112
+ const HAS_WORKTREE_SOURCE = SCAN_SOURCES.includes('git_worktrees');
113
+ const HAS_RECENT_SOURCE = SCAN_SOURCES.includes('recent_commits');
114
+ const KIRO_TASK_SOURCE = SCAN_SOURCES.find(
115
+ (source) => typeof source === 'object' && source !== null && source.type === 'kiro_tasks',
116
+ );
117
+
118
+ // ───────────────────────────────────────────────────────────────────────────
119
+ // 路径规范化:把各来源的路径统一成「仓库根为基准的 posix 相对路径」,便于取交集。
120
+ // · worktree status --porcelain 给的是相对该 worktree 根的路径 -> 直接用(worktree 与主树同一仓库视图)。
121
+ // · git log --name-only 给的是相对仓库根的路径 -> 直接用。
122
+ // · tasks.md 影响文件字段写的多是相对 app 的路径(如 src/zones/...),无法可靠定位到仓库根,
123
+ // 原样保留(作为「在飞集合」的成员;交集匹配时调用者 --target 用同样写法即可命中,见 normalizeTarget)。
124
+ // 这里只做轻量清洗:去首尾空白、去引号、统一反斜杠为正斜杠、去 ./ 前缀、去尾部斜杠。
125
+ // ───────────────────────────────────────────────────────────────────────────
126
+ /** @param {string} p */
127
+ function cleanPath(p) {
128
+ let s = p.trim();
129
+ // 去掉包裹的引号(git status 对含特殊字符的路径会加双引号)
130
+ if (
131
+ (s.startsWith('"') && s.endsWith('"')) ||
132
+ (s.startsWith("'") && s.endsWith("'"))
133
+ ) {
134
+ s = s.slice(1, -1);
135
+ }
136
+ if (path.sep === '\\') s = s.replace(/\\/g, '/'); // Windows 分隔符 -> posix;POSIX 保留合法的反斜杠文件名
137
+ s = s.replace(/^\.\//, ''); // 去 ./ 前缀
138
+ s = s.replace(/\/+$/, ''); // 去尾部斜杠(影响目录的情形)
139
+ return s.trim();
140
+ }
141
+
142
+ // ───────────────────────────────────────────────────────────────────────────
143
+ // ① 所有 worktree 在飞改动 + untracked
144
+ // 解析 git worktree list --porcelain:每条记录以 "worktree <path>" 行开头,
145
+ // 路径可能含空格 —— 取 "worktree " 之后的整行剩余(绝不用 awk '{print $2}',M5)。
146
+ // ───────────────────────────────────────────────────────────────────────────
147
+ /** @returns {string[]} 各 worktree 绝对路径 */
148
+ function listWorktreeDirs() {
149
+ let out;
150
+ try {
151
+ out = git(['worktree', 'list', '--porcelain'], { cwd: ROOT });
152
+ } catch {
153
+ return [];
154
+ }
155
+ const dirs = [];
156
+ for (const line of out.split('\n')) {
157
+ if (line.startsWith('worktree ')) {
158
+ // 取前缀之后的全部,路径含空格也完整保留
159
+ const p = line.slice('worktree '.length);
160
+ if (p) dirs.push(p);
161
+ }
162
+ }
163
+ return dirs;
164
+ }
165
+
166
+ /**
167
+ * 解析单个 worktree 的 git status --porcelain,返回改动/untracked 的仓库相对路径集合。
168
+ * porcelain 行格式:`XY <path>` 或重命名 `R <old> -> <new>`。
169
+ * @param {string} dir
170
+ * @returns {string[]}
171
+ */
172
+ function statusFiles(dir) {
173
+ let out;
174
+ try {
175
+ out = git(['status', '--porcelain=v1', '-z', '--untracked-files=all'], { cwd: dir });
176
+ } catch {
177
+ // 某棵 worktree 不可访问(已被删目录但元数据残留等)——跳过,不致命
178
+ return [];
179
+ }
180
+ const files = [];
181
+ const records = out.split('\0');
182
+ for (let index = 0; index < records.length; index++) {
183
+ const record = records[index];
184
+ if (!record) continue;
185
+ const status = record.slice(0, 2);
186
+ const target = record.slice(3);
187
+ const cleaned = cleanPath(target);
188
+ if (cleaned) files.push(cleaned);
189
+ // -z 模式下 rename/copy 的当前路径在本条,下一条是原路径;原路径不属于当前写入目标。
190
+ if (/[RC]/.test(status)) index++;
191
+ }
192
+ return files;
193
+ }
194
+
195
+ // ───────────────────────────────────────────────────────────────────────────
196
+ // ② 近期跨所有本地分支 commit 碰过的文件(git log --all --since='6 hours ago')
197
+ // ───────────────────────────────────────────────────────────────────────────
198
+ /** @returns {string[]} */
199
+ function recentCommitFiles() {
200
+ let out;
201
+ try {
202
+ out = git(
203
+ ['log', '--all', '--since=6 hours ago', '--name-only', '-z', '--pretty=format:'],
204
+ { cwd: ROOT },
205
+ );
206
+ } catch {
207
+ return [];
208
+ }
209
+ const files = [];
210
+ for (const line of out.split('\0')) {
211
+ const cleaned = cleanPath(line);
212
+ if (cleaned) files.push(cleaned);
213
+ }
214
+ return files;
215
+ }
216
+
217
+ // ───────────────────────────────────────────────────────────────────────────
218
+ // ③ tasks.md 中 [-](实施中)任务的「影响文件或目录」字段
219
+ // 逐文件扫 .kiro/specs 下各 spec 的 tasks.md:
220
+ // · 找 `- [-] ...` 任务行(标记实施中)。
221
+ // · 在该任务块内(到下一个同级或更高级任务行为止)找 `**影响文件或目录**:` 字段。
222
+ // · 从字段值里抽取 backtick 包裹的路径 token(真实 tasks.md 一律 `path` 反引号包裹,
223
+ // 含散文与 + / 逗号 / 大括号分隔)。大括号展开(brace expansion)一并处理。
224
+ // ───────────────────────────────────────────────────────────────────────────
225
+
226
+ /** 任务行:`- [x] ID. title`,x ∈ space/-/x。返回 marker 或 null。 */
227
+ function taskMarkerOf(line) {
228
+ const m = line.match(/^\s*-\s*\[([ x\-])\]\s/);
229
+ return m ? m[1] : null;
230
+ }
231
+
232
+ /** 字段行:`- **影响文件或目录**: ...` 或 `**影响文件或目录**: ...`(不同缩进)。 */
233
+ function impactFieldValue(line) {
234
+ const m = line.match(/\*\*影响文件或目录\*\*\s*[::]\s*(.*)$/);
235
+ return m ? m[1] : null;
236
+ }
237
+
238
+ /**
239
+ * 从「影响文件或目录」字段值里抽取 backtick 路径 token。
240
+ * 处理大括号展开:`a/{x,y}.tsx` -> a/x.tsx, a/y.tsx。
241
+ * @param {string} value
242
+ * @returns {string[]}
243
+ */
244
+ function extractImpactPaths(value) {
245
+ const out = [];
246
+ // 匹配所有 `...` 反引号片段
247
+ const segments = value.match(/`[^`]+`/g) || [];
248
+ for (const seg of segments) {
249
+ const inner = seg.slice(1, -1).trim();
250
+ // 一个反引号片段里可能有多个路径(逗号 / + 分隔);先按这些分隔切,再各自 brace 展开
251
+ const parts = inner
252
+ .split(/[,+]/)
253
+ .map((s) => s.trim())
254
+ .filter(Boolean);
255
+ for (const part of parts) {
256
+ // 只收看起来像路径的(含 / 或常见源码扩展名),过滤掉散文性反引号(如 `N/A`、`[-]`)
257
+ for (const expanded of expandBraces(part)) {
258
+ const cleaned = cleanPath(expanded);
259
+ if (!cleaned) continue;
260
+ if (cleaned.includes('/') || /\.(tsx?|jsx?|css|mjs|cjs|json|go|py|md|ya?ml|proto)$/i.test(cleaned)) {
261
+ out.push(cleaned);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+
269
+ /**
270
+ * 简易 brace expansion:仅展开单层 `{a,b,c}`。
271
+ * `src/{A,B}.tsx` -> [src/A.tsx, src/B.tsx];无大括号原样返回。
272
+ * 多组 / 嵌套不展开(tasks.md 实际用法只有单层),原样返回避免误展开。
273
+ * @param {string} s
274
+ * @returns {string[]}
275
+ */
276
+ function expandBraces(s) {
277
+ const m = s.match(/^(.*?)\{([^{}]+)\}(.*)$/);
278
+ if (!m) return [s];
279
+ const [, pre, body, post] = m;
280
+ const opts = body.split(',').map((o) => o.trim()).filter(Boolean);
281
+ if (opts.length === 0) return [s];
282
+ // 只递归展开 post 里可能的第二组(保守:post 再有一层也处理)
283
+ const result = [];
284
+ for (const o of opts) {
285
+ for (const tail of expandBraces(post)) {
286
+ result.push(`${pre}${o}${tail}`);
287
+ }
288
+ }
289
+ return result;
290
+ }
291
+
292
+ /** @param {string} segment */
293
+ function globSegmentPattern(segment) {
294
+ const escaped = segment.replace(/[.+^${}()|[\]\\]/g, '\\$&');
295
+ return new RegExp(`^${escaped.replaceAll('*', '[^/]*').replaceAll('?', '[^/]')}$`);
296
+ }
297
+
298
+ /** @param {string} root @param {string} glob */
299
+ function expandRepositoryGlob(root, glob) {
300
+ const segments = glob.replaceAll('\\', '/').split('/');
301
+ const found = [];
302
+ const visit = (directory, index) => {
303
+ if (index === segments.length) {
304
+ found.push(directory);
305
+ return;
306
+ }
307
+ const segment = segments[index];
308
+ if (segment === '**') {
309
+ visit(directory, index + 1);
310
+ let entries = [];
311
+ try { entries = readdirSync(directory, { withFileTypes: true }); } catch { return; }
312
+ for (const entry of entries) if (entry.isDirectory()) visit(path.join(directory, entry.name), index);
313
+ return;
314
+ }
315
+ const pattern = globSegmentPattern(segment);
316
+ let entries = [];
317
+ try { entries = readdirSync(directory, { withFileTypes: true }); } catch { return; }
318
+ for (const entry of entries) {
319
+ if (!pattern.test(entry.name)) continue;
320
+ const candidate = path.join(directory, entry.name);
321
+ if (index === segments.length - 1) {
322
+ if (entry.isFile()) found.push(candidate);
323
+ } else if (entry.isDirectory()) {
324
+ visit(candidate, index + 1);
325
+ }
326
+ }
327
+ };
328
+ visit(root, 0);
329
+ return found;
330
+ }
331
+
332
+ /** @returns {string[]} 与 Profile glob 匹配的 tasks.md 路径 */
333
+ function findTasksFiles() {
334
+ if (!KIRO_TASK_SOURCE) return [];
335
+ return expandRepositoryGlob(
336
+ PROFILE.context.primary_worktree ?? ROOT,
337
+ String(KIRO_TASK_SOURCE.glob),
338
+ );
339
+ }
340
+
341
+ /** @returns {string[]} 所有 [-] 任务的影响文件 token */
342
+ function inProgressTaskFiles() {
343
+ const out = [];
344
+ for (const tasksPath of findTasksFiles()) {
345
+ let content;
346
+ try {
347
+ content = readFileSync(tasksPath, 'utf8');
348
+ } catch {
349
+ continue;
350
+ }
351
+ const lines = content.split('\n');
352
+ let inProgressBlock = false;
353
+ for (const line of lines) {
354
+ const marker = taskMarkerOf(line);
355
+ if (marker !== null) {
356
+ // 遇到任意任务行就切换块状态:是 [-] 则进入收集,否则退出收集
357
+ inProgressBlock = marker === '-';
358
+ continue;
359
+ }
360
+ if (!inProgressBlock) continue;
361
+ const fieldVal = impactFieldValue(line);
362
+ if (fieldVal !== null) {
363
+ out.push(...extractImpactPaths(fieldVal));
364
+ }
365
+ }
366
+ }
367
+ return out;
368
+ }
369
+
370
+ // ───────────────────────────────────────────────────────────────────────────
371
+ // 汇集并集
372
+ // ───────────────────────────────────────────────────────────────────────────
373
+ /** @typedef {{file:string, sources:Set<string>}} Entry */
374
+
375
+ /** @returns {Map<string,Set<string>>} file -> 来源标签集合 */
376
+ function buildInFlightSet() {
377
+ /** @type {Map<string,Set<string>>} */
378
+ const map = new Map();
379
+ const add = (file, source) => {
380
+ if (!file) return;
381
+ if (!map.has(file)) map.set(file, new Set());
382
+ map.get(file).add(source);
383
+ };
384
+
385
+ // ① worktree status
386
+ if (HAS_WORKTREE_SOURCE) {
387
+ for (const dir of listWorktreeDirs()) {
388
+ const label = `worktree:${path.basename(dir) || dir}`;
389
+ for (const f of statusFiles(dir)) add(f, label);
390
+ }
391
+ }
392
+ // ② 近期 commit
393
+ if (HAS_RECENT_SOURCE) for (const f of recentCommitFiles()) add(f, 'recent-commit');
394
+ // ③ tasks.md [-]
395
+ if (KIRO_TASK_SOURCE) for (const f of inProgressTaskFiles()) add(f, 'task:[-]');
396
+
397
+ return map;
398
+ }
399
+
400
+ // ───────────────────────────────────────────────────────────────────────────
401
+ // target 规范化 + 交集
402
+ // target 路径按仓库根为基准(与 worktree status / git log 同基准)。
403
+ // 只有 tasks.md adapter 来源可能是 app 相对路径,允许低置信度「后缀包含」兜底;Git 来源已经以
404
+ // 仓库根为基准,只做精确或祖先/后代匹配,避免 example/lib/main.dart 与 lib/main.dart 误报。
405
+ // ───────────────────────────────────────────────────────────────────────────
406
+ /** @param {string} raw @returns {string[]} */
407
+ function parseTargets(raw) {
408
+ return raw
409
+ .split(',')
410
+ .map((s) => cleanPath(s))
411
+ .filter(Boolean);
412
+ }
413
+
414
+ /**
415
+ * 判定 target 是否与某在飞路径重叠并返回匹配依据。
416
+ * @param {string} target
417
+ * @param {string} inflight
418
+ * @param {boolean} allowRelative
419
+ */
420
+ function pathMatch(target, inflight, allowRelative) {
421
+ if (target === inflight) return { kind: 'exact', confidence: 'high' };
422
+ const prefixMatch = (parent, child) => child.startsWith(`${parent}/`);
423
+ if (prefixMatch(target, inflight) || prefixMatch(inflight, target)) return { kind: 'ancestor', confidence: 'high' };
424
+ if (!allowRelative) return null;
425
+
426
+ // adapter 可能给 app 相对路径;尝试把短路径对齐到长路径的任一目录边界,再做祖先/后代判断。
427
+ const relativeMatch = (long, short) => {
428
+ const segments = long.split('/');
429
+ for (let index = 1; index < segments.length; index++) {
430
+ const suffix = segments.slice(index).join('/');
431
+ if (suffix === short || prefixMatch(short, suffix) || prefixMatch(suffix, short)) return true;
432
+ }
433
+ return false;
434
+ };
435
+ if ((target.length > inflight.length && relativeMatch(target, inflight)) || (inflight.length > target.length && relativeMatch(inflight, target))) return { kind: 'suffix-relative', confidence: 'low' };
436
+ return null;
437
+ }
438
+
439
+ // ───────────────────────────────────────────────────────────────────────────
440
+ // 主流程
441
+ // ───────────────────────────────────────────────────────────────────────────
442
+ const inFlight = buildInFlightSet();
443
+ const sortedFiles = [...inFlight.keys()].sort();
444
+
445
+ if (targetRaw === null) {
446
+ // 纯列举模式
447
+ console.log(`${PREFIX} 本机在飞 + 近期改过的文件集合(共 ${sortedFiles.length} 项):`);
448
+ if (sortedFiles.length === 0) {
449
+ console.log(`${PREFIX} (空)`);
450
+ }
451
+ for (const f of sortedFiles) {
452
+ const sources = [...inFlight.get(f)].sort().join(', ');
453
+ console.log(` ${f} [${sources}]`);
454
+ }
455
+ return 0;
456
+ }
457
+
458
+ // --target 交集模式
459
+ const targets = parseTargets(targetRaw);
460
+ if (targets.length === 0) {
461
+ console.error(`${PREFIX} --target 为空,未给出任何文件。`);
462
+ return 1;
463
+ }
464
+
465
+ /** @type {{target:string, inflight:string, sources:string, match_kind:string, confidence:string}[]} */
466
+ const collisions = [];
467
+ for (const t of targets) {
468
+ for (const f of sortedFiles) {
469
+ const sourceSet = inFlight.get(f);
470
+ const match = pathMatch(t, f, [...sourceSet].some((source) => source === 'task:[-]'));
471
+ if (match) {
472
+ collisions.push({
473
+ target: t,
474
+ inflight: f,
475
+ sources: [...sourceSet].sort().join(', '),
476
+ match_kind: match.kind,
477
+ confidence: match.confidence,
478
+ });
479
+ }
480
+ }
481
+ }
482
+
483
+ console.log(`${PREFIX} 检查 ${targets.length} 个目标文件 vs 在飞集合(${sortedFiles.length} 项):`);
484
+ if (collisions.length === 0) {
485
+ console.log(`${PREFIX} 结论: CLEAR — 目标文件无人在动,可走当前分支并发。`);
486
+ return 0;
487
+ }
488
+
489
+ console.log(`${PREFIX} 重叠明细:`);
490
+ for (const c of collisions) {
491
+ if (c.target === c.inflight) {
492
+ console.log(` ${c.target} [${c.sources}; match=${c.match_kind}; confidence=${c.confidence}]`);
493
+ } else {
494
+ console.log(` ${c.target} ~ ${c.inflight} [${c.sources}; match=${c.match_kind}; confidence=${c.confidence}]`);
495
+ }
496
+ }
497
+ console.log(
498
+ `${PREFIX} 结论: COLLIDE — 目标文件有别人在动,建议开 worktree 隔离(DESIGN §2.1:会撞=开 worktree)。`,
499
+ );
500
+ // COLLIDE 仍以 0 退出:重叠是「需决策」的信息,不是脚本错误(决策由调用者/agent 做)。
501
+ return 0;
502
+ }
503
+
504
+ export function isCliEntry(argv1 = process.argv[1], selfUrl = import.meta.url) {
505
+ if (!argv1) return false;
506
+ try { return realpathSync(argv1) === realpathSync(fileURLToPath(selfUrl)); }
507
+ catch { return path.resolve(argv1) === path.resolve(fileURLToPath(selfUrl)); }
508
+ }
509
+
510
+ if (isCliEntry()) process.exitCode = runCli();
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendTraceEvent } from './worktree-trace.mjs';
4
+
5
+ const [commonDir, worktreeId, countRaw, workerId] = process.argv.slice(2);
6
+ const count = Number.parseInt(countRaw, 10);
7
+ if (!commonDir || !worktreeId || !Number.isInteger(count) || count < 1 || !workerId) {
8
+ process.exit(2);
9
+ }
10
+
11
+ for (let index = 0; index < count; index++) {
12
+ appendTraceEvent({
13
+ commonDir,
14
+ worktreeId,
15
+ eventType: 'worker_tick',
16
+ actor: { host: 'node-test-worker', id: workerId },
17
+ details: { worker_id: workerId, index },
18
+ mutate(current) {
19
+ if (!current) throw new Error('missing initial snapshot');
20
+ return { ...current, counter: Number(current.counter ?? 0) + 1 };
21
+ },
22
+ });
23
+ }