@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,800 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import { execFileSync } from 'node:child_process';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import {
7
+ existsSync,
8
+ linkSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ readdirSync,
12
+ realpathSync,
13
+ statSync,
14
+ unlinkSync,
15
+ writeFileSync,
16
+ } from 'node:fs';
17
+ import { homedir, tmpdir } from 'node:os';
18
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
19
+
20
+ export const PROFILE_FILENAME = '.worktree-trace.json';
21
+ export const PROFILE_SCHEMA_VERSION = 1;
22
+
23
+ const TASK_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
24
+ const WORKTREE_ROOT_MARKER = '.repository.json';
25
+ const TASK_SHORT_MAX = 28;
26
+ const SPAWN_TEMPLATE_PLACEHOLDERS = new Set(['task', 'task_short', 'repo', 'host', 'id8']);
27
+ const ALLOWED_TOP_LEVEL_KEYS = new Set([
28
+ 'schema_version',
29
+ 'default_base',
30
+ 'branch_template',
31
+ 'path_template',
32
+ 'worktree_root',
33
+ 'change_request',
34
+ 'task_naming',
35
+ 'scan',
36
+ 'ephemeral_path_patterns',
37
+ 'post_integrate_steps',
38
+ 'extensions',
39
+ ]);
40
+ const ALLOWED_SCAN_KEYS = new Set(['sources']);
41
+ // 只声明、不执行:portable core 永远不把 Profile 内容当命令跑。这里刻意不提供
42
+ // command/run/script 字段,未知键一律 fail-closed,避免有人把 shell 塞进声明位。
43
+ const ALLOWED_POST_INTEGRATE_STEP_KEYS = new Set(['name', 'hint']);
44
+ const POST_INTEGRATE_STEP_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
45
+ const POST_INTEGRATE_STEPS_MAX = 20;
46
+ const POST_INTEGRATE_HINT_MAX = 240;
47
+ const ALLOWED_CHANGE_REQUEST_KEYS = new Set(['provider', 'remote', 'target_branch', 'remove_source_branch']);
48
+ const ALLOWED_TASK_NAMING_KEYS = new Set(['mode', 'example']);
49
+ const ALLOWED_TASK_SOURCE_KEYS = new Set(['type', 'glob']);
50
+ const PORTABLE_SCAN_SOURCES = new Set(['git_worktrees', 'recent_commits']);
51
+ const CONFIGURED_SCAN_SOURCE_TYPES = new Set(['kiro_tasks']);
52
+ const BUILTIN_EPHEMERAL_PATTERNS = [
53
+ `${resolve(tmpdir()).replaceAll('\\', '/').replace(/\/+$/, '')}/`,
54
+ '/tmp/',
55
+ '/private/tmp/',
56
+ '/var/folders/',
57
+ '/private/var/folders/',
58
+ '/scratchpad/',
59
+ ];
60
+
61
+ const DEFAULT_PROFILE = Object.freeze({
62
+ schema_version: PROFILE_SCHEMA_VERSION,
63
+ default_base: null,
64
+ branch_template: '{host}/{task}',
65
+ path_template: '{host}-{task}',
66
+ worktree_root: '~/.worktrees',
67
+ change_request: {
68
+ provider: 'manual',
69
+ remote: 'origin',
70
+ target_branch: null,
71
+ remove_source_branch: true,
72
+ },
73
+ task_naming: {
74
+ mode: 'semantic',
75
+ example: 'ci-gate-hardening',
76
+ },
77
+ scan: { sources: ['git_worktrees', 'recent_commits'] },
78
+ ephemeral_path_patterns: BUILTIN_EPHEMERAL_PATTERNS,
79
+ post_integrate_steps: [],
80
+ extensions: {},
81
+ });
82
+
83
+ export class WorktreeProfileError extends Error {
84
+ /** @param {string} code @param {string} message */
85
+ constructor(code, message) {
86
+ super(message);
87
+ this.name = 'WorktreeProfileError';
88
+ this.code = code;
89
+ }
90
+ }
91
+
92
+ /** @param {string[]} args @param {string} cwd */
93
+ function git(args, cwd) {
94
+ return execFileSync('git', args, {
95
+ cwd,
96
+ encoding: 'utf8',
97
+ stdio: ['ignore', 'pipe', 'pipe'],
98
+ }).trim();
99
+ }
100
+
101
+ /** @param {string[]} args @param {string} cwd */
102
+ function gitTry(args, cwd) {
103
+ try {
104
+ return { ok: true, out: git(args, cwd) };
105
+ } catch (error) {
106
+ return {
107
+ ok: false,
108
+ out: error && typeof error === 'object' && 'stdout' in error ? String(error.stdout).trim() : '',
109
+ };
110
+ }
111
+ }
112
+
113
+ /** @param {unknown} value */
114
+ function isPlainObject(value) {
115
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
116
+ }
117
+
118
+ /** @param {Record<string, unknown>} value @param {Set<string>} allowed @param {string} location */
119
+ function rejectUnknownKeys(value, allowed, location) {
120
+ for (const key of Object.keys(value)) {
121
+ if (!allowed.has(key)) {
122
+ throw new WorktreeProfileError('PROFILE_UNKNOWN_KEY', `${location}.${key} 是未知配置字段。`);
123
+ }
124
+ }
125
+ }
126
+
127
+ /** @param {unknown} value @param {string} location */
128
+ function requireNonEmptyString(value, location) {
129
+ if (typeof value !== 'string' || value.trim() === '') {
130
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', `${location} 必须是非空字符串。`);
131
+ }
132
+ return value;
133
+ }
134
+
135
+ /** @param {unknown} value @param {string} location */
136
+ function requireSafeRepositoryGlob(value, location) {
137
+ const glob = requireNonEmptyString(value, location).replaceAll('\\', '/');
138
+ const segments = glob.split('/');
139
+ if (
140
+ glob.startsWith('/') ||
141
+ /^[A-Za-z]:\//.test(glob) ||
142
+ segments.some((segment) => segment === '..' || segment === '') ||
143
+ /[\u0000]/.test(glob)
144
+ ) {
145
+ throw new WorktreeProfileError(
146
+ 'PROFILE_INVALID_GLOB',
147
+ `${location} 必须是仓库内的安全相对 glob,不能含绝对路径、空路径段或 ..。`,
148
+ );
149
+ }
150
+ return glob;
151
+ }
152
+
153
+ /** @param {string} template @param {string} location @param {Set<string>} allowed @param {boolean} requireTask */
154
+ function validateTemplate(template, location, allowed, requireTask) {
155
+ const placeholders = [...template.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
156
+ for (const placeholder of placeholders) {
157
+ if (!allowed.has(placeholder)) {
158
+ throw new WorktreeProfileError(
159
+ 'PROFILE_INVALID_TEMPLATE',
160
+ `${location} 含未知占位符 {${placeholder}};只允许 ${[...allowed].map((item) => `{${item}}`).join(' / ')}。`,
161
+ );
162
+ }
163
+ }
164
+ if (requireTask && !placeholders.some((item) => item === 'task' || item === 'task_short')) {
165
+ throw new WorktreeProfileError('PROFILE_INVALID_TEMPLATE', `${location} 必须包含 {task} 或 {task_short}。`);
166
+ }
167
+ const residue = template.replace(/\{[^{}]+\}/g, '');
168
+ if (residue.includes('{') || residue.includes('}')) {
169
+ throw new WorktreeProfileError('PROFILE_INVALID_TEMPLATE', `${location} 含未闭合占位符。`);
170
+ }
171
+ }
172
+
173
+ /** @param {unknown} raw */
174
+ export function validateProfile(raw) {
175
+ if (!isPlainObject(raw)) {
176
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'Profile 根节点必须是 JSON object。');
177
+ }
178
+ rejectUnknownKeys(raw, ALLOWED_TOP_LEVEL_KEYS, 'profile');
179
+ if (raw.schema_version !== PROFILE_SCHEMA_VERSION) {
180
+ throw new WorktreeProfileError(
181
+ 'PROFILE_SCHEMA_UNSUPPORTED',
182
+ `profile.schema_version 必须是 ${PROFILE_SCHEMA_VERSION}。`,
183
+ );
184
+ }
185
+
186
+ const branchTemplate = requireNonEmptyString(
187
+ raw.branch_template ?? DEFAULT_PROFILE.branch_template,
188
+ 'profile.branch_template',
189
+ );
190
+ const pathTemplate = requireNonEmptyString(
191
+ raw.path_template ?? DEFAULT_PROFILE.path_template,
192
+ 'profile.path_template',
193
+ );
194
+ validateTemplate(branchTemplate, 'profile.branch_template', SPAWN_TEMPLATE_PLACEHOLDERS, true);
195
+ validateTemplate(pathTemplate, 'profile.path_template', SPAWN_TEMPLATE_PLACEHOLDERS, true);
196
+
197
+ if (raw.default_base !== undefined && raw.default_base !== null) {
198
+ requireNonEmptyString(raw.default_base, 'profile.default_base');
199
+ }
200
+ const defaultWorktreeRoot = pathTemplate.startsWith('../') ? '..' : DEFAULT_PROFILE.worktree_root;
201
+ const worktreeRoot = requireNonEmptyString(raw.worktree_root ?? defaultWorktreeRoot, 'profile.worktree_root');
202
+ validateTemplate(worktreeRoot, 'profile.worktree_root', new Set(), false);
203
+
204
+ const changeRequest = raw.change_request ?? DEFAULT_PROFILE.change_request;
205
+ if (!isPlainObject(changeRequest)) {
206
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'profile.change_request 必须是 object。');
207
+ }
208
+ rejectUnknownKeys(changeRequest, ALLOWED_CHANGE_REQUEST_KEYS, 'profile.change_request');
209
+ const changeRequestProvider = requireNonEmptyString(
210
+ changeRequest.provider ?? DEFAULT_PROFILE.change_request.provider,
211
+ 'profile.change_request.provider',
212
+ );
213
+ if (!['manual', 'gitlab'].includes(changeRequestProvider)) {
214
+ throw new WorktreeProfileError(
215
+ 'PROFILE_UNKNOWN_CHANGE_REQUEST_PROVIDER',
216
+ `profile.change_request.provider 未知: ${changeRequestProvider}`,
217
+ );
218
+ }
219
+ const changeRequestRemote = requireNonEmptyString(
220
+ changeRequest.remote ?? DEFAULT_PROFILE.change_request.remote,
221
+ 'profile.change_request.remote',
222
+ );
223
+ if (changeRequest.target_branch !== undefined && changeRequest.target_branch !== null) {
224
+ requireNonEmptyString(changeRequest.target_branch, 'profile.change_request.target_branch');
225
+ }
226
+ if (
227
+ changeRequest.remove_source_branch !== undefined &&
228
+ typeof changeRequest.remove_source_branch !== 'boolean'
229
+ ) {
230
+ throw new WorktreeProfileError(
231
+ 'PROFILE_INVALID_TYPE',
232
+ 'profile.change_request.remove_source_branch 必须是 boolean。',
233
+ );
234
+ }
235
+
236
+ const taskNaming = raw.task_naming ?? DEFAULT_PROFILE.task_naming;
237
+ if (!isPlainObject(taskNaming)) {
238
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'profile.task_naming 必须是 object。');
239
+ }
240
+ rejectUnknownKeys(taskNaming, ALLOWED_TASK_NAMING_KEYS, 'profile.task_naming');
241
+ const taskNamingMode = requireNonEmptyString(
242
+ taskNaming.mode ?? DEFAULT_PROFILE.task_naming.mode,
243
+ 'profile.task_naming.mode',
244
+ );
245
+ if (!['slug', 'semantic'].includes(taskNamingMode)) {
246
+ throw new WorktreeProfileError(
247
+ 'PROFILE_UNKNOWN_TASK_NAMING_MODE',
248
+ `profile.task_naming.mode 未知: ${taskNamingMode}`,
249
+ );
250
+ }
251
+ const taskNamingExample = requireNonEmptyString(
252
+ taskNaming.example ?? DEFAULT_PROFILE.task_naming.example,
253
+ 'profile.task_naming.example',
254
+ );
255
+ validateTaskSlug(taskNamingExample);
256
+ if (taskNamingMode === 'semantic') {
257
+ validateTaskNaming(taskNamingExample, { mode: taskNamingMode, example: taskNamingExample });
258
+ const required = [
259
+ ['profile.branch_template', branchTemplate],
260
+ ['profile.path_template', pathTemplate],
261
+ ];
262
+ for (const [location, template] of required) {
263
+ if (!template.includes('{host}') || !template.includes('{task}')) {
264
+ throw new WorktreeProfileError(
265
+ 'PROFILE_NAMING_DOD_FAILED',
266
+ `${location} 在 semantic 模式必须同时包含 {host} 和完整 {task}。`,
267
+ );
268
+ }
269
+ if (template.includes('{id8}') || template.includes('{task_short}')) {
270
+ throw new WorktreeProfileError(
271
+ 'PROFILE_NAMING_DOD_FAILED',
272
+ `${location} 在 semantic 模式禁止 {id8}/{task_short};trace identity 只留在 metadata。`,
273
+ );
274
+ }
275
+ }
276
+ }
277
+
278
+ const scan = raw.scan ?? DEFAULT_PROFILE.scan;
279
+ if (!isPlainObject(scan)) {
280
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'profile.scan 必须是 object。');
281
+ }
282
+ rejectUnknownKeys(scan, ALLOWED_SCAN_KEYS, 'profile.scan');
283
+ if (!Array.isArray(scan.sources)) {
284
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'profile.scan.sources 必须是 array。');
285
+ }
286
+ for (const [index, source] of scan.sources.entries()) {
287
+ const location = `profile.scan.sources[${index}]`;
288
+ if (typeof source === 'string') {
289
+ if (!PORTABLE_SCAN_SOURCES.has(source)) {
290
+ throw new WorktreeProfileError('PROFILE_UNKNOWN_SCAN_SOURCE', `${location} 未知 source: ${source}`);
291
+ }
292
+ continue;
293
+ }
294
+ if (!isPlainObject(source)) {
295
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', `${location} 必须是 string 或 object。`);
296
+ }
297
+ rejectUnknownKeys(source, ALLOWED_TASK_SOURCE_KEYS, location);
298
+ const type = requireNonEmptyString(source.type, `${location}.type`);
299
+ if (!CONFIGURED_SCAN_SOURCE_TYPES.has(type)) {
300
+ throw new WorktreeProfileError('PROFILE_UNKNOWN_SCAN_SOURCE', `${location}.type 未知: ${type}`);
301
+ }
302
+ requireSafeRepositoryGlob(source.glob, `${location}.glob`);
303
+ }
304
+
305
+ const ephemeralPathPatterns = raw.ephemeral_path_patterns ?? [];
306
+ if (!Array.isArray(ephemeralPathPatterns)) {
307
+ throw new WorktreeProfileError(
308
+ 'PROFILE_INVALID_TYPE',
309
+ 'profile.ephemeral_path_patterns 必须是 string array。',
310
+ );
311
+ }
312
+ for (const [index, pattern] of ephemeralPathPatterns.entries()) {
313
+ requireNonEmptyString(pattern, `profile.ephemeral_path_patterns[${index}]`);
314
+ }
315
+ const postIntegrateSteps = raw.post_integrate_steps ?? [];
316
+ if (!Array.isArray(postIntegrateSteps)) {
317
+ throw new WorktreeProfileError(
318
+ 'PROFILE_INVALID_TYPE',
319
+ 'profile.post_integrate_steps 必须是 array。',
320
+ );
321
+ }
322
+ if (postIntegrateSteps.length > POST_INTEGRATE_STEPS_MAX) {
323
+ throw new WorktreeProfileError(
324
+ 'PROFILE_INVALID_TYPE',
325
+ `profile.post_integrate_steps 最多 ${POST_INTEGRATE_STEPS_MAX} 项,当前 ${postIntegrateSteps.length}。`,
326
+ );
327
+ }
328
+ const postIntegrateNames = new Set();
329
+ const normalizedPostIntegrateSteps = postIntegrateSteps.map((step, index) => {
330
+ const location = `profile.post_integrate_steps[${index}]`;
331
+ if (!isPlainObject(step)) {
332
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', `${location} 必须是 object。`);
333
+ }
334
+ rejectUnknownKeys(step, ALLOWED_POST_INTEGRATE_STEP_KEYS, location);
335
+ const name = requireNonEmptyString(step.name, `${location}.name`);
336
+ if (!POST_INTEGRATE_STEP_NAME_PATTERN.test(name)) {
337
+ throw new WorktreeProfileError(
338
+ 'PROFILE_INVALID_TYPE',
339
+ `${location}.name 必须是 kebab-case slug(如 regenerate-golden),当前: ${name}`,
340
+ );
341
+ }
342
+ if (postIntegrateNames.has(name)) {
343
+ throw new WorktreeProfileError(
344
+ 'PROFILE_INVALID_TYPE',
345
+ `${location}.name 重复: ${name};步骤名是登记执行结果的键,必须唯一。`,
346
+ );
347
+ }
348
+ postIntegrateNames.add(name);
349
+ const hint = requireNonEmptyString(step.hint, `${location}.hint`);
350
+ if (hint.length > POST_INTEGRATE_HINT_MAX || /[\u0000-\u001f\u007f\r\n]/u.test(hint)) {
351
+ throw new WorktreeProfileError(
352
+ 'PROFILE_INVALID_TYPE',
353
+ `${location}.hint 必须是 1-${POST_INTEGRATE_HINT_MAX} 字符的单行文本。`,
354
+ );
355
+ }
356
+ return { name, hint };
357
+ });
358
+
359
+ const extensions = raw.extensions ?? {};
360
+ if (!isPlainObject(extensions)) {
361
+ throw new WorktreeProfileError('PROFILE_INVALID_TYPE', 'profile.extensions 必须是 object。');
362
+ }
363
+
364
+ return {
365
+ schema_version: PROFILE_SCHEMA_VERSION,
366
+ default_base: raw.default_base ?? null,
367
+ branch_template: branchTemplate,
368
+ path_template: pathTemplate,
369
+ worktree_root: worktreeRoot,
370
+ change_request: {
371
+ provider: changeRequestProvider,
372
+ remote: changeRequestRemote,
373
+ target_branch: changeRequest.target_branch ?? null,
374
+ remove_source_branch: changeRequest.remove_source_branch ?? true,
375
+ },
376
+ task_naming: {
377
+ mode: taskNamingMode,
378
+ example: taskNamingExample,
379
+ },
380
+ scan: structuredClone(scan),
381
+ ephemeral_path_patterns: [...new Set([...BUILTIN_EPHEMERAL_PATTERNS, ...ephemeralPathPatterns])],
382
+ post_integrate_steps: normalizedPostIntegrateSteps,
383
+ extensions: structuredClone(extensions),
384
+ };
385
+ }
386
+
387
+ /** @param {string} task */
388
+ export function validateTaskSlug(task) {
389
+ if (
390
+ typeof task !== 'string' ||
391
+ !TASK_PATTERN.test(task) ||
392
+ task.includes('..') ||
393
+ task.includes('/') ||
394
+ task.includes('\\') ||
395
+ task.startsWith('.') ||
396
+ task.endsWith('.')
397
+ ) {
398
+ throw new WorktreeProfileError(
399
+ 'INVALID_TASK_SLUG',
400
+ 'task 必须匹配 ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$,且不能含 ..、路径分隔符、空白、控制字符或首尾点号。',
401
+ );
402
+ }
403
+ return task;
404
+ }
405
+
406
+ /**
407
+ * Profile 可把自由 slug 收紧为可读语义 slug。semantic 模式的 DoD:
408
+ * lowercase、至少两个以字母开头的连字符分词;纯编号和 trace identity 不进入可见名称。
409
+ * @param {string} task
410
+ * @param {{mode?:string,example?:string}|null|undefined} taskNaming
411
+ */
412
+ export function validateTaskNaming(task, taskNaming) {
413
+ validateTaskSlug(task);
414
+ const mode = taskNaming?.mode ?? 'slug';
415
+ if (mode === 'slug') return task;
416
+ const semantic = /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+$/.test(task);
417
+ if (!semantic) {
418
+ const example = taskNaming?.example ?? 'ci-gate-hardening';
419
+ throw new WorktreeProfileError(
420
+ 'TASK_NAMING_DOD_FAILED',
421
+ `task "${task}" 未满足 Profile 命名 DoD:必须使用可读的 lowercase semantic slug(例如 ${example}),至少两个以字母开头的连字符分词;纯 Task 编号和 trace ID 不得进入可见名称。`,
422
+ );
423
+ }
424
+ return task;
425
+ }
426
+
427
+ /** @param {string} task */
428
+ export function shortenTaskSlug(task) {
429
+ validateTaskSlug(task);
430
+ if (task.length <= TASK_SHORT_MAX) return task;
431
+ const digest = createHash('sha256').update(task).digest('hex').slice(0, 6);
432
+ const head = task.slice(0, 12).replace(/[._-]+$/, '');
433
+ const tail = task.slice(-8).replace(/^[._-]+/, '');
434
+ return `${head}-${digest}-${tail}`;
435
+ }
436
+
437
+ /** @param {string} host */
438
+ export function normalizeHostSlug(host) {
439
+ const normalized = String(host).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
440
+ if (!normalized) throw new WorktreeProfileError('INVALID_HOST_SLUG', 'Agent host 无法生成安全命名 slug。');
441
+ return normalized;
442
+ }
443
+
444
+ /** @param {string} template @param {Record<string,string>} values */
445
+ export function renderTemplate(template, values) {
446
+ let rendered = template;
447
+ for (const [key, value] of Object.entries(values)) rendered = rendered.replaceAll(`{${key}}`, value);
448
+ return rendered;
449
+ }
450
+
451
+ /** @param {string} configured @param {string} primaryWorktree @param {Record<string,string>} values */
452
+ function resolveRootBase(configured, primaryWorktree, values) {
453
+ const rendered = renderTemplate(configured, values);
454
+ if (rendered === '~') return homedir();
455
+ if (rendered.startsWith('~/')) return resolve(homedir(), rendered.slice(2));
456
+ if (rendered.startsWith('~')) {
457
+ throw new WorktreeProfileError('WORKTREE_ROOT_INVALID', 'worktree_root 只支持 ~ 或 ~/ 前缀,不展开其他用户 home。');
458
+ }
459
+ return resolve(primaryWorktree, rendered);
460
+ }
461
+
462
+ /** @param {string} directory */
463
+ function readRootMarker(directory) {
464
+ const markerPath = join(directory, WORKTREE_ROOT_MARKER);
465
+ if (!existsSync(markerPath)) return null;
466
+ try {
467
+ const marker = JSON.parse(readFileSync(markerPath, 'utf8'));
468
+ return typeof marker?.repository_id === 'string' ? marker : null;
469
+ } catch {
470
+ return null;
471
+ }
472
+ }
473
+
474
+ /** @param {string} directory @param {string} repositoryId */
475
+ function rootAvailability(directory, repositoryId) {
476
+ if (!existsSync(directory)) return 'vacant';
477
+ if (!statSync(directory).isDirectory()) return 'occupied';
478
+ const marker = readRootMarker(directory);
479
+ if (marker) return marker.repository_id === repositoryId ? 'owned' : 'occupied';
480
+ return readdirSync(directory).length === 0 ? 'vacant' : 'occupied';
481
+ }
482
+
483
+ /** @param {string} rootBase @param {string} repoName @param {string} repositoryId */
484
+ function selectRepositoryRoot(rootBase, repoName, repositoryId) {
485
+ const preferred = canonicalizeFuturePath(join(rootBase, repoName));
486
+ const status = rootAvailability(preferred, repositoryId);
487
+ if (status === 'vacant' || status === 'owned') return preferred;
488
+ const fallback = canonicalizeFuturePath(`${preferred}-${repositoryId.slice(0, 8)}`);
489
+ const fallbackStatus = rootAvailability(fallback, repositoryId);
490
+ if (fallbackStatus === 'occupied') {
491
+ throw new WorktreeProfileError('WORKTREE_ROOT_CONFLICT', `worktree repository root 已被其他 identity 占用: ${fallback}`);
492
+ }
493
+ return fallback;
494
+ }
495
+
496
+ /**
497
+ * 在所有纯校验通过后原子认领集中目录;同名仓库冲突时只给冲突方追加 repository id8。
498
+ * @param {{root_base:string,repo_name:string,repository_id:string,primary_worktree:string}} options
499
+ */
500
+ export function claimWorktreeRepositoryRoot(options) {
501
+ mkdirSync(options.root_base, { recursive: true });
502
+ let directory = selectRepositoryRoot(options.root_base, options.repo_name, options.repository_id);
503
+ for (let attempt = 0; attempt < 2; attempt++) {
504
+ mkdirSync(directory, { recursive: true });
505
+ const markerPath = join(directory, WORKTREE_ROOT_MARKER);
506
+ const marker = {
507
+ schema_version: PROFILE_SCHEMA_VERSION,
508
+ repository_id: options.repository_id,
509
+ primary_worktree: options.primary_worktree,
510
+ claimed_at: new Date().toISOString(),
511
+ };
512
+ try {
513
+ writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
514
+ return realpathSync(directory);
515
+ } catch (error) {
516
+ const current = readRootMarker(directory);
517
+ if (current?.repository_id === options.repository_id) return realpathSync(directory);
518
+ if (attempt === 0) {
519
+ directory = canonicalizeFuturePath(join(options.root_base, `${options.repo_name}-${options.repository_id.slice(0, 8)}`));
520
+ continue;
521
+ }
522
+ throw new WorktreeProfileError('WORKTREE_ROOT_CONFLICT', `无法认领 worktree repository root: ${directory}`);
523
+ }
524
+ }
525
+ throw new WorktreeProfileError('WORKTREE_ROOT_CONFLICT', '无法认领 worktree repository root。');
526
+ }
527
+
528
+ /**
529
+ * git worktree list --porcelain 的第一条是 primary worktree;路径按前缀后整行读取,保留空格。
530
+ * @param {string} cwd
531
+ */
532
+ function primaryWorktreePath(cwd) {
533
+ const listed = gitTry(['worktree', 'list', '--porcelain'], cwd);
534
+ if (!listed.ok) return null;
535
+ for (const line of listed.out.split('\n')) {
536
+ if (line.startsWith('worktree ')) return resolve(line.slice('worktree '.length));
537
+ }
538
+ return null;
539
+ }
540
+
541
+ /** @param {string} cwd */
542
+ export function resolveGitContext(cwd = process.cwd()) {
543
+ const top = gitTry(['rev-parse', '--show-toplevel'], cwd);
544
+ if (!top.ok) {
545
+ throw new WorktreeProfileError('NOT_A_GIT_REPOSITORY', `${cwd} 不在 Git 工作树内。`);
546
+ }
547
+ const currentWorktree = resolve(top.out);
548
+ const common = gitTry(['rev-parse', '--git-common-dir'], currentWorktree);
549
+ if (!common.ok) {
550
+ throw new WorktreeProfileError('GIT_COMMON_DIR_UNAVAILABLE', '无法解析 git common-dir。');
551
+ }
552
+ const commonDir = resolve(currentWorktree, common.out);
553
+ const primaryWorktree = primaryWorktreePath(currentWorktree);
554
+ return {
555
+ current_worktree: currentWorktree,
556
+ primary_worktree: primaryWorktree,
557
+ common_dir: commonDir,
558
+ repo_name: basename(primaryWorktree ?? currentWorktree),
559
+ };
560
+ }
561
+
562
+ /** @param {string} profilePath */
563
+ function readProfileFile(profilePath) {
564
+ let parsed;
565
+ try {
566
+ parsed = JSON.parse(readFileSync(profilePath, 'utf8'));
567
+ } catch (error) {
568
+ throw new WorktreeProfileError(
569
+ 'PROFILE_READ_FAILED',
570
+ `无法读取或解析 Profile ${profilePath}: ${error instanceof Error ? error.message : String(error)}`,
571
+ );
572
+ }
573
+ return validateProfile(parsed);
574
+ }
575
+
576
+ /**
577
+ * @param {{cwd?:string, explicitConfigPath?:string|null}} [options]
578
+ */
579
+ export function loadRepositoryProfile(options = {}) {
580
+ const cwd = options.cwd ?? process.cwd();
581
+ const context = resolveGitContext(cwd);
582
+ let profilePath;
583
+ let source;
584
+ if (options.explicitConfigPath) {
585
+ profilePath = resolve(cwd, options.explicitConfigPath);
586
+ source = 'explicit';
587
+ if (!existsSync(profilePath)) {
588
+ throw new WorktreeProfileError('PROFILE_NOT_FOUND', `显式 Profile 不存在: ${profilePath}`);
589
+ }
590
+ } else {
591
+ if (!context.primary_worktree) {
592
+ throw new WorktreeProfileError(
593
+ 'PRIMARY_WORKTREE_UNAVAILABLE',
594
+ '无法定位 primary worktree;请显式传 --config <path>。',
595
+ );
596
+ }
597
+ profilePath = join(context.primary_worktree, PROFILE_FILENAME);
598
+ source = existsSync(profilePath) ? 'primary' : 'defaults';
599
+ }
600
+
601
+ const profile = existsSync(profilePath) ? readProfileFile(profilePath) : structuredClone(DEFAULT_PROFILE);
602
+ return { context, profile, profile_path: profilePath, profile_source: source };
603
+ }
604
+
605
+ /** @param {string} target */
606
+ export function canonicalizeFuturePath(target) {
607
+ const absolute = resolve(target);
608
+ let cursor = absolute;
609
+ const missing = [];
610
+ while (!existsSync(cursor)) {
611
+ const parent = dirname(cursor);
612
+ if (parent === cursor) {
613
+ throw new WorktreeProfileError('PATH_CANONICALIZE_FAILED', `找不到 ${absolute} 的已存在祖先。`);
614
+ }
615
+ missing.unshift(basename(cursor));
616
+ cursor = parent;
617
+ }
618
+ let canonical = realpathSync(cursor);
619
+ for (const segment of missing) canonical = join(canonical, segment);
620
+ return canonical;
621
+ }
622
+
623
+ /** @param {string} parent @param {string} child */
624
+ export function isStrictlyInside(parent, child) {
625
+ const rel = relative(parent, child);
626
+ return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
627
+ }
628
+
629
+ /** @param {string} branch @param {string} cwd */
630
+ export function validateBranchName(branch, cwd) {
631
+ const checked = gitTry(['check-ref-format', '--branch', branch], cwd);
632
+ if (!checked.ok) {
633
+ throw new WorktreeProfileError('INVALID_BRANCH_NAME', `branch template 生成非法 Git ref: ${branch}`);
634
+ }
635
+ return branch;
636
+ }
637
+
638
+ /** @param {string} cwd @param {string|null} profileBase @param {string|null} override */
639
+ export function resolveBaseRef(cwd, profileBase, override) {
640
+ if (override) return { ref: override, source: 'cli' };
641
+ if (profileBase) return { ref: profileBase, source: 'profile' };
642
+
643
+ const remotes = gitTry(['remote'], cwd);
644
+ if (remotes.ok) {
645
+ for (const remote of remotes.out.split('\n').filter(Boolean)) {
646
+ const head = gitTry(['symbolic-ref', '--quiet', '--short', `refs/remotes/${remote}/HEAD`], cwd);
647
+ if (head.ok && head.out) return { ref: head.out, source: 'remote_head' };
648
+ }
649
+ }
650
+ for (const candidate of ['origin/main', 'origin/master']) {
651
+ if (gitTry(['rev-parse', '--verify', '--quiet', `${candidate}^{commit}`], cwd).ok) {
652
+ return { ref: candidate, source: 'well_known_remote' };
653
+ }
654
+ }
655
+ const upstream = gitTry(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], cwd);
656
+ if (upstream.ok && upstream.out) return { ref: upstream.out, source: 'upstream' };
657
+ if (gitTry(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], cwd).ok) {
658
+ return { ref: 'HEAD', source: 'head' };
659
+ }
660
+ throw new WorktreeProfileError('DEFAULT_BASE_UNAVAILABLE', '无法自动探测 default base;请传 --base <ref>。');
661
+ }
662
+
663
+ /**
664
+ * 只解析和校验,不创建 branch/path/trace,供 spawn 在副作用前调用。
665
+ * @param {{cwd?:string, task:string, host?:string, worktreeId?:string, repositoryId?:string, base?:string|null, explicitConfigPath?:string|null, worktreeRootOverride?:string|null}} options
666
+ */
667
+ export function resolveSpawnPlan(options) {
668
+ const loaded = loadRepositoryProfile({
669
+ cwd: options.cwd,
670
+ explicitConfigPath: options.explicitConfigPath,
671
+ });
672
+ const { context, profile } = loaded;
673
+ const task = validateTaskNaming(options.task, profile.task_naming);
674
+ if (!context.primary_worktree) {
675
+ throw new WorktreeProfileError('PRIMARY_WORKTREE_UNAVAILABLE', 'spawn 需要可定位的 primary worktree。');
676
+ }
677
+
678
+ const worktreeId = options.worktreeId ?? randomUUID();
679
+ if (!/^[0-9a-f-]{36}$/i.test(worktreeId)) {
680
+ throw new WorktreeProfileError('INVALID_WORKTREE_ID', 'worktreeId 必须是完整 UUID。');
681
+ }
682
+ const repositoryId = options.repositoryId ?? readRepositoryIdentity(context)?.repository_id ?? randomUUID();
683
+ const values = {
684
+ task,
685
+ task_short: shortenTaskSlug(task),
686
+ repo: context.repo_name,
687
+ host: normalizeHostSlug(options.host ?? 'agent'),
688
+ id8: worktreeId.replaceAll('-', '').slice(0, 8),
689
+ };
690
+ const branch = validateBranchName(renderTemplate(profile.branch_template, values), context.current_worktree);
691
+ const renderedPath = renderTemplate(profile.path_template, values);
692
+ if (isAbsolute(renderedPath)) {
693
+ throw new WorktreeProfileError('SPAWN_PATH_OUTSIDE_ROOT', `path_template 不能生成绝对路径: ${renderedPath}`);
694
+ }
695
+
696
+ const rootCandidate = resolveRootBase(
697
+ options.worktreeRootOverride ?? profile.worktree_root,
698
+ context.primary_worktree,
699
+ values,
700
+ );
701
+ if (existsSync(rootCandidate) && !statSync(rootCandidate).isDirectory()) {
702
+ throw new WorktreeProfileError('WORKTREE_ROOT_INVALID', `worktree_root 已存在但不是目录: ${rootCandidate}`);
703
+ }
704
+ const rootBase = canonicalizeFuturePath(rootCandidate);
705
+ const legacyLayout = renderedPath.startsWith('../');
706
+ const repositoryRoot = legacyLayout
707
+ ? rootBase
708
+ : selectRepositoryRoot(rootBase, context.repo_name, repositoryId);
709
+ const allowedRoot = repositoryRoot;
710
+ const targetCandidate = legacyLayout
711
+ ? resolve(context.primary_worktree, renderedPath)
712
+ : resolve(repositoryRoot, renderedPath);
713
+ const targetPath = canonicalizeFuturePath(targetCandidate);
714
+ if (!isStrictlyInside(allowedRoot, targetPath)) {
715
+ throw new WorktreeProfileError(
716
+ 'SPAWN_PATH_OUTSIDE_ROOT',
717
+ `spawn path 逃出允许根: target=${targetPath} root=${allowedRoot}`,
718
+ );
719
+ }
720
+
721
+ const base = resolveBaseRef(context.current_worktree, profile.default_base, options.base ?? null);
722
+ const branchExists = gitTry(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], context.current_worktree).ok;
723
+ return {
724
+ ...loaded,
725
+ task,
726
+ task_short: values.task_short,
727
+ host_slug: values.host,
728
+ worktree_id: worktreeId,
729
+ repository_id: repositoryId,
730
+ branch,
731
+ path: targetPath,
732
+ worktree_root_base: rootBase,
733
+ repository_root: repositoryRoot,
734
+ legacy_layout: legacyLayout,
735
+ allowed_root: allowedRoot,
736
+ base_ref: base.ref,
737
+ base_source: base.source,
738
+ branch_exists: branchExists,
739
+ path_exists: existsSync(targetPath),
740
+ };
741
+ }
742
+
743
+ /** @param {ReturnType<typeof resolveGitContext>} context */
744
+ export function readRepositoryIdentity(context) {
745
+ const identityPath = join(context.common_dir, 'worktree-trace', 'v1', 'repository.json');
746
+ if (!existsSync(identityPath)) return null;
747
+ try {
748
+ const value = JSON.parse(readFileSync(identityPath, 'utf8'));
749
+ if (
750
+ value &&
751
+ value.schema_version === PROFILE_SCHEMA_VERSION &&
752
+ typeof value.repository_id === 'string'
753
+ ) {
754
+ return value;
755
+ }
756
+ } catch {
757
+ // 由调用者/doctor 决定如何报告损坏;读取函数不覆盖。
758
+ }
759
+ throw new WorktreeProfileError('REPOSITORY_IDENTITY_INVALID', `repository identity 损坏: ${identityPath}`);
760
+ }
761
+
762
+ /** @param {ReturnType<typeof resolveGitContext>} context @param {string|null} [proposedId] */
763
+ export function ensureRepositoryIdentity(context, proposedId = null) {
764
+ const existing = readRepositoryIdentity(context);
765
+ if (existing) return existing;
766
+ const traceRoot = join(context.common_dir, 'worktree-trace', 'v1');
767
+ mkdirSync(traceRoot, { recursive: true });
768
+ const identityPath = join(traceRoot, 'repository.json');
769
+ const value = {
770
+ schema_version: PROFILE_SCHEMA_VERSION,
771
+ repository_id: proposedId ?? randomUUID(),
772
+ created_at: new Date().toISOString(),
773
+ };
774
+ const tempPath = `${identityPath}.${process.pid}.${randomUUID()}.tmp`;
775
+ try {
776
+ writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
777
+ try {
778
+ // hard link 是同一 common-dir 文件系统内的原子「不存在才创建」,不会像 rename 覆盖并发赢家。
779
+ linkSync(tempPath, identityPath);
780
+ unlinkSync(tempPath);
781
+ } catch (error) {
782
+ if (existsSync(identityPath)) {
783
+ unlinkSync(tempPath);
784
+ return readRepositoryIdentity(context);
785
+ }
786
+ throw error;
787
+ }
788
+ } finally {
789
+ if (existsSync(tempPath)) unlinkSync(tempPath);
790
+ }
791
+ return value;
792
+ }
793
+
794
+ /** @param {string} candidate @param {string[]} patterns */
795
+ export function classifyStorage(candidate, patterns = BUILTIN_EPHEMERAL_PATTERNS) {
796
+ const normalized = `${canonicalizeFuturePath(candidate).replaceAll('\\', '/')}/`;
797
+ return patterns.some((pattern) => normalized.includes(pattern.replaceAll('\\', '/')))
798
+ ? 'ephemeral'
799
+ : 'persistent';
800
+ }