@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,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import { hostname } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { ensureRepositoryIdentity, readRepositoryIdentity } from './worktree-profile.mjs';
|
|
20
|
+
|
|
21
|
+
export const TRACE_SCHEMA_VERSION = 1;
|
|
22
|
+
export const LOCK_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
23
|
+
export const LOCK_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
24
|
+
export const LOCK_RETRY_MS = 10;
|
|
25
|
+
const MALFORMED_LOCK_GRACE_MS = 250;
|
|
26
|
+
const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
27
|
+
|
|
28
|
+
export class WorktreeTraceError extends Error {
|
|
29
|
+
/** @param {string} code @param {string} message */
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'WorktreeTraceError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {string} commonDir */
|
|
38
|
+
export function traceLayout(commonDir) {
|
|
39
|
+
const root = join(commonDir, 'worktree-trace', 'v1');
|
|
40
|
+
return {
|
|
41
|
+
root,
|
|
42
|
+
records: join(root, 'records'),
|
|
43
|
+
events: join(root, 'events'),
|
|
44
|
+
locks: join(root, 'locks'),
|
|
45
|
+
repository: join(root, 'repository.json'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {{common_dir:string}} context */
|
|
50
|
+
export function initializeTraceStore(context) {
|
|
51
|
+
const repository = ensureRepositoryIdentity(context);
|
|
52
|
+
const layout = traceLayout(context.common_dir);
|
|
53
|
+
mkdirSync(layout.records, { recursive: true });
|
|
54
|
+
mkdirSync(layout.events, { recursive: true });
|
|
55
|
+
mkdirSync(layout.locks, { recursive: true });
|
|
56
|
+
return { layout, repository };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {number} milliseconds */
|
|
60
|
+
function sleep(milliseconds) {
|
|
61
|
+
Atomics.wait(SLEEP_CELL, 0, 0, milliseconds);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {number} pid */
|
|
65
|
+
function processIsAlive(pid) {
|
|
66
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return error && typeof error === 'object' && 'code' in error && error.code === 'EPERM';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {string} commonDir @param {string} worktreeId */
|
|
76
|
+
export function inspectRecordLock(commonDir, worktreeId) {
|
|
77
|
+
const lockDir = join(traceLayout(commonDir).locks, `${worktreeId}.lock`);
|
|
78
|
+
if (!existsSync(lockDir)) return { state: 'absent', lock_dir: lockDir };
|
|
79
|
+
let ageMs = Number.POSITIVE_INFINITY;
|
|
80
|
+
try {
|
|
81
|
+
ageMs = Math.max(0, Date.now() - statSync(lockDir).mtimeMs);
|
|
82
|
+
} catch {
|
|
83
|
+
return { state: 'raced', lock_dir: lockDir };
|
|
84
|
+
}
|
|
85
|
+
const ownerPath = join(lockDir, 'owner.json');
|
|
86
|
+
if (!existsSync(ownerPath)) return { state: 'malformed', lock_dir: lockDir, age_ms: ageMs };
|
|
87
|
+
try {
|
|
88
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
89
|
+
if (
|
|
90
|
+
!owner ||
|
|
91
|
+
typeof owner !== 'object' ||
|
|
92
|
+
!Number.isInteger(owner.pid) ||
|
|
93
|
+
typeof owner.hostname !== 'string' ||
|
|
94
|
+
typeof owner.created_at !== 'string' ||
|
|
95
|
+
typeof owner.token !== 'string'
|
|
96
|
+
) {
|
|
97
|
+
return { state: 'malformed', lock_dir: lockDir, age_ms: ageMs, owner };
|
|
98
|
+
}
|
|
99
|
+
const sameHost = owner.hostname === hostname();
|
|
100
|
+
const alive = sameHost ? processIsAlive(owner.pid) : null;
|
|
101
|
+
const stale = sameHost ? !alive : ageMs > LOCK_STALE_AFTER_MS;
|
|
102
|
+
return {
|
|
103
|
+
state: stale ? 'stale' : 'held',
|
|
104
|
+
lock_dir: lockDir,
|
|
105
|
+
age_ms: ageMs,
|
|
106
|
+
owner,
|
|
107
|
+
same_host: sameHost,
|
|
108
|
+
pid_alive: alive,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return { state: 'malformed', lock_dir: lockDir, age_ms: ageMs };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} commonDir
|
|
117
|
+
* @param {string} worktreeId
|
|
118
|
+
* @param {{waitTimeoutMs?:number, command?:string}} [options]
|
|
119
|
+
*/
|
|
120
|
+
function acquireRecordLock(commonDir, worktreeId, options = {}) {
|
|
121
|
+
const layout = traceLayout(commonDir);
|
|
122
|
+
mkdirSync(layout.locks, { recursive: true });
|
|
123
|
+
const lockDir = join(layout.locks, `${worktreeId}.lock`);
|
|
124
|
+
const startedAt = Date.now();
|
|
125
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? LOCK_WAIT_TIMEOUT_MS;
|
|
126
|
+
let recoveredOwner = null;
|
|
127
|
+
|
|
128
|
+
for (;;) {
|
|
129
|
+
const token = randomUUID();
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(lockDir);
|
|
132
|
+
const owner = {
|
|
133
|
+
pid: process.pid,
|
|
134
|
+
hostname: hostname(),
|
|
135
|
+
created_at: new Date().toISOString(),
|
|
136
|
+
command: options.command ?? process.argv.join(' '),
|
|
137
|
+
token,
|
|
138
|
+
};
|
|
139
|
+
writeFileSync(join(lockDir, 'owner.json'), `${JSON.stringify(owner, null, 2)}\n`, {
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
flag: 'wx',
|
|
142
|
+
});
|
|
143
|
+
return { lock_dir: lockDir, owner, recovered_owner: recoveredOwner };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
|
|
146
|
+
if (code !== 'EEXIST') {
|
|
147
|
+
if (existsSync(lockDir) && !existsSync(join(lockDir, 'owner.json'))) {
|
|
148
|
+
// mkdir 成功、owner 写入失败:由显式 recover 处理,避免误删未知锁。
|
|
149
|
+
}
|
|
150
|
+
throw new WorktreeTraceError('LOCK_ACQUIRE_FAILED', `无法创建 record lock ${lockDir}: ${String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const finding = inspectRecordLock(commonDir, worktreeId);
|
|
155
|
+
if (finding.state === 'stale') {
|
|
156
|
+
const quarantine = `${lockDir}.stale.${randomUUID()}`;
|
|
157
|
+
try {
|
|
158
|
+
renameSync(lockDir, quarantine);
|
|
159
|
+
recoveredOwner = finding.owner ?? null;
|
|
160
|
+
rmSync(quarantine, { recursive: true, force: true });
|
|
161
|
+
continue;
|
|
162
|
+
} catch {
|
|
163
|
+
// 另一进程可能先完成接管,重新观察。
|
|
164
|
+
}
|
|
165
|
+
} else if (finding.state === 'malformed' && (finding.age_ms ?? 0) > MALFORMED_LOCK_GRACE_MS) {
|
|
166
|
+
throw new WorktreeTraceError(
|
|
167
|
+
'LOCK_MALFORMED',
|
|
168
|
+
`record lock 缺少有效 owner.json: ${lockDir};请 doctor 后显式 recover。`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
173
|
+
throw new WorktreeTraceError('LOCK_BUSY', `record lock 等待超时: ${lockDir}`);
|
|
174
|
+
}
|
|
175
|
+
sleep(LOCK_RETRY_MS);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @param {{lock_dir:string, owner:{token:string}}} lock */
|
|
180
|
+
function releaseRecordLock(lock) {
|
|
181
|
+
const ownerPath = join(lock.lock_dir, 'owner.json');
|
|
182
|
+
try {
|
|
183
|
+
const current = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
184
|
+
if (current.token !== lock.owner.token) {
|
|
185
|
+
throw new WorktreeTraceError('LOCK_OWNERSHIP_LOST', `record lock token 已变化: ${lock.lock_dir}`);
|
|
186
|
+
}
|
|
187
|
+
rmSync(lock.lock_dir, { recursive: true });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof WorktreeTraceError) throw error;
|
|
190
|
+
if (existsSync(lock.lock_dir)) {
|
|
191
|
+
throw new WorktreeTraceError('LOCK_RELEASE_FAILED', `无法释放 record lock: ${lock.lock_dir}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @param {string} commonDir @param {string} worktreeId @param {{forceMalformed?:boolean}} [options] */
|
|
197
|
+
export function recoverRecordLock(commonDir, worktreeId, options = {}) {
|
|
198
|
+
const finding = inspectRecordLock(commonDir, worktreeId);
|
|
199
|
+
if (finding.state === 'absent' || finding.state === 'raced') return finding;
|
|
200
|
+
if (finding.state === 'held') {
|
|
201
|
+
throw new WorktreeTraceError('LOCK_STILL_HELD', `lock 仍由存活进程持有: ${finding.lock_dir}`);
|
|
202
|
+
}
|
|
203
|
+
if (finding.state === 'malformed' && !options.forceMalformed) {
|
|
204
|
+
throw new WorktreeTraceError('LOCK_MALFORMED', 'malformed lock 需要显式 forceMalformed。');
|
|
205
|
+
}
|
|
206
|
+
const quarantine = `${finding.lock_dir}.recovered.${randomUUID()}`;
|
|
207
|
+
renameSync(finding.lock_dir, quarantine);
|
|
208
|
+
rmSync(quarantine, { recursive: true, force: true });
|
|
209
|
+
return { ...finding, recovered: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @param {string} commonDir @param {string} worktreeId */
|
|
213
|
+
function eventDirectory(commonDir, worktreeId) {
|
|
214
|
+
return join(traceLayout(commonDir).events, worktreeId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @param {unknown} event @param {string} source */
|
|
218
|
+
function validateEvent(event, source) {
|
|
219
|
+
if (
|
|
220
|
+
!event ||
|
|
221
|
+
typeof event !== 'object' ||
|
|
222
|
+
event.schema_version !== TRACE_SCHEMA_VERSION ||
|
|
223
|
+
typeof event.event_id !== 'string' ||
|
|
224
|
+
typeof event.worktree_id !== 'string' ||
|
|
225
|
+
typeof event.event_type !== 'string' ||
|
|
226
|
+
typeof event.occurred_at !== 'string' ||
|
|
227
|
+
!(event.previous_event_id === null || typeof event.previous_event_id === 'string')
|
|
228
|
+
) {
|
|
229
|
+
throw new WorktreeTraceError('EVENT_INVALID', `event 格式无效: ${source}`);
|
|
230
|
+
}
|
|
231
|
+
return event;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @param {string} commonDir @param {string} worktreeId */
|
|
235
|
+
export function readEventChain(commonDir, worktreeId) {
|
|
236
|
+
const dir = eventDirectory(commonDir, worktreeId);
|
|
237
|
+
if (!existsSync(dir)) return [];
|
|
238
|
+
const events = [];
|
|
239
|
+
for (const name of readdirSync(dir)) {
|
|
240
|
+
if (!name.endsWith('.json')) continue;
|
|
241
|
+
const path = join(dir, name);
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
245
|
+
} catch {
|
|
246
|
+
throw new WorktreeTraceError('EVENT_INVALID', `event JSON 损坏: ${path}`);
|
|
247
|
+
}
|
|
248
|
+
const event = validateEvent(parsed, path);
|
|
249
|
+
if (event.worktree_id !== worktreeId) {
|
|
250
|
+
throw new WorktreeTraceError('EVENT_WRONG_RECORD', `event worktree_id 与目录不符: ${path}`);
|
|
251
|
+
}
|
|
252
|
+
events.push(event);
|
|
253
|
+
}
|
|
254
|
+
if (events.length === 0) return [];
|
|
255
|
+
|
|
256
|
+
const byPrevious = new Map();
|
|
257
|
+
for (const event of events) {
|
|
258
|
+
const previous = event.previous_event_id ?? '__GENESIS__';
|
|
259
|
+
const followers = byPrevious.get(previous) ?? [];
|
|
260
|
+
followers.push(event);
|
|
261
|
+
byPrevious.set(previous, followers);
|
|
262
|
+
}
|
|
263
|
+
const genesis = byPrevious.get('__GENESIS__') ?? [];
|
|
264
|
+
if (genesis.length !== 1) {
|
|
265
|
+
throw new WorktreeTraceError('EVENT_CHAIN_FORKED', `event chain genesis 数量=${genesis.length},应为 1。`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const chain = [];
|
|
269
|
+
const visited = new Set();
|
|
270
|
+
let current = genesis[0];
|
|
271
|
+
for (;;) {
|
|
272
|
+
if (visited.has(current.event_id)) {
|
|
273
|
+
throw new WorktreeTraceError('EVENT_CHAIN_CYCLE', `event chain 出现环: ${current.event_id}`);
|
|
274
|
+
}
|
|
275
|
+
visited.add(current.event_id);
|
|
276
|
+
chain.push(current);
|
|
277
|
+
const followers = byPrevious.get(current.event_id) ?? [];
|
|
278
|
+
if (followers.length > 1) {
|
|
279
|
+
throw new WorktreeTraceError('EVENT_CHAIN_FORKED', `event ${current.event_id} 有多个后继。`);
|
|
280
|
+
}
|
|
281
|
+
if (followers.length === 0) break;
|
|
282
|
+
current = followers[0];
|
|
283
|
+
}
|
|
284
|
+
if (chain.length !== events.length) {
|
|
285
|
+
throw new WorktreeTraceError('EVENT_CHAIN_DISCONNECTED', '存在不属于权威 chain 的 event。');
|
|
286
|
+
}
|
|
287
|
+
return chain;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @param {string} commonDir @param {string} worktreeId */
|
|
291
|
+
export function readRecordCache(commonDir, worktreeId) {
|
|
292
|
+
const path = join(traceLayout(commonDir).records, `${worktreeId}.json`);
|
|
293
|
+
if (!existsSync(path)) return null;
|
|
294
|
+
try {
|
|
295
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
296
|
+
} catch {
|
|
297
|
+
throw new WorktreeTraceError('RECORD_CACHE_INVALID', `record cache 损坏: ${path}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @param {string} commonDir */
|
|
302
|
+
export function listRecordCacheEntries(commonDir) {
|
|
303
|
+
const recordsDir = traceLayout(commonDir).records;
|
|
304
|
+
if (!existsSync(recordsDir)) return [];
|
|
305
|
+
return readdirSync(recordsDir)
|
|
306
|
+
.filter((name) => name.endsWith('.json'))
|
|
307
|
+
.map((name) => {
|
|
308
|
+
const path = join(recordsDir, name);
|
|
309
|
+
try {
|
|
310
|
+
return { path, record: JSON.parse(readFileSync(path, 'utf8')), error: null };
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
path,
|
|
314
|
+
record: null,
|
|
315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {string} commonDir @param {string} worktreeId @param {unknown} snapshot @param {unknown} tail @param {number} count */
|
|
322
|
+
function writeRecordCache(commonDir, worktreeId, snapshot, tail, count) {
|
|
323
|
+
const layout = traceLayout(commonDir);
|
|
324
|
+
mkdirSync(layout.records, { recursive: true });
|
|
325
|
+
const target = join(layout.records, `${worktreeId}.json`);
|
|
326
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
327
|
+
const cache = {
|
|
328
|
+
...structuredClone(snapshot),
|
|
329
|
+
last_event_id: tail.event_id,
|
|
330
|
+
event_count: count,
|
|
331
|
+
materialized_at: new Date().toISOString(),
|
|
332
|
+
};
|
|
333
|
+
writeFileSync(temp, `${JSON.stringify(cache, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
334
|
+
try {
|
|
335
|
+
renameSync(temp, target);
|
|
336
|
+
} finally {
|
|
337
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
338
|
+
}
|
|
339
|
+
return cache;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {string} commonDir
|
|
344
|
+
* @param {string} repositoryId
|
|
345
|
+
* @param {string} worktreeId
|
|
346
|
+
* @param {string} eventType
|
|
347
|
+
* @param {{host:string,id:string}|null} actor
|
|
348
|
+
* @param {unknown} snapshot
|
|
349
|
+
* @param {Record<string, unknown>} details
|
|
350
|
+
* @param {string|null} previousEventId
|
|
351
|
+
*/
|
|
352
|
+
function appendEventFile(
|
|
353
|
+
commonDir,
|
|
354
|
+
repositoryId,
|
|
355
|
+
worktreeId,
|
|
356
|
+
eventType,
|
|
357
|
+
actor,
|
|
358
|
+
snapshot,
|
|
359
|
+
details,
|
|
360
|
+
previousEventId,
|
|
361
|
+
) {
|
|
362
|
+
const dir = eventDirectory(commonDir, worktreeId);
|
|
363
|
+
mkdirSync(dir, { recursive: true });
|
|
364
|
+
const eventId = randomUUID();
|
|
365
|
+
const occurredAt = new Date().toISOString();
|
|
366
|
+
const event = {
|
|
367
|
+
schema_version: TRACE_SCHEMA_VERSION,
|
|
368
|
+
repository_id: repositoryId,
|
|
369
|
+
event_id: eventId,
|
|
370
|
+
worktree_id: worktreeId,
|
|
371
|
+
event_type: eventType,
|
|
372
|
+
occurred_at: occurredAt,
|
|
373
|
+
previous_event_id: previousEventId,
|
|
374
|
+
actor: actor ? structuredClone(actor) : null,
|
|
375
|
+
details: structuredClone(details),
|
|
376
|
+
snapshot: snapshot === null ? null : structuredClone(snapshot),
|
|
377
|
+
};
|
|
378
|
+
const filename = `${occurredAt.replaceAll(':', '-')}-${eventId}.json`;
|
|
379
|
+
writeFileSync(join(dir, filename), `${JSON.stringify(event, null, 2)}\n`, {
|
|
380
|
+
encoding: 'utf8',
|
|
381
|
+
flag: 'wx',
|
|
382
|
+
});
|
|
383
|
+
return event;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {{
|
|
388
|
+
* commonDir:string,
|
|
389
|
+
* worktreeId:string,
|
|
390
|
+
* eventType:string,
|
|
391
|
+
* actor?:{host:string,id:string}|null,
|
|
392
|
+
* details?:Record<string, unknown>,
|
|
393
|
+
* mutate:(current:Record<string, unknown>|null)=>Record<string, unknown>,
|
|
394
|
+
* waitTimeoutMs?:number,
|
|
395
|
+
* }} options
|
|
396
|
+
*/
|
|
397
|
+
export function appendTraceEvent(options) {
|
|
398
|
+
if (!/^[0-9a-f-]{36}$/i.test(options.worktreeId)) {
|
|
399
|
+
throw new WorktreeTraceError('WORKTREE_ID_INVALID', `worktreeId 不是 UUID: ${options.worktreeId}`);
|
|
400
|
+
}
|
|
401
|
+
if (!/^[a-z][a-z0-9_]*$/.test(options.eventType)) {
|
|
402
|
+
throw new WorktreeTraceError('EVENT_TYPE_INVALID', `eventType 非法: ${options.eventType}`);
|
|
403
|
+
}
|
|
404
|
+
if (
|
|
405
|
+
options.actor &&
|
|
406
|
+
(typeof options.actor.host !== 'string' ||
|
|
407
|
+
options.actor.host.trim() === '' ||
|
|
408
|
+
typeof options.actor.id !== 'string' ||
|
|
409
|
+
options.actor.id.trim() === '')
|
|
410
|
+
) {
|
|
411
|
+
throw new WorktreeTraceError('ACTOR_INVALID', 'actor.host / actor.id 必须是非空字符串。');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const repository = readRepositoryIdentity({ common_dir: options.commonDir });
|
|
415
|
+
if (!repository) {
|
|
416
|
+
throw new WorktreeTraceError('TRACE_STORE_UNINITIALIZED', 'trace store 尚未初始化。');
|
|
417
|
+
}
|
|
418
|
+
const lock = acquireRecordLock(options.commonDir, options.worktreeId, {
|
|
419
|
+
waitTimeoutMs: options.waitTimeoutMs,
|
|
420
|
+
command: `append:${options.eventType}`,
|
|
421
|
+
});
|
|
422
|
+
try {
|
|
423
|
+
let chain = readEventChain(options.commonDir, options.worktreeId);
|
|
424
|
+
let tail = chain.at(-1) ?? null;
|
|
425
|
+
const current = tail?.snapshot ? structuredClone(tail.snapshot) : null;
|
|
426
|
+
|
|
427
|
+
if (lock.recovered_owner) {
|
|
428
|
+
tail = appendEventFile(
|
|
429
|
+
options.commonDir,
|
|
430
|
+
repository.repository_id,
|
|
431
|
+
options.worktreeId,
|
|
432
|
+
'stale_lock_recovered',
|
|
433
|
+
options.actor ?? null,
|
|
434
|
+
current,
|
|
435
|
+
{ recovered_owner: lock.recovered_owner },
|
|
436
|
+
tail?.event_id ?? null,
|
|
437
|
+
);
|
|
438
|
+
chain = [...chain, tail];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const next = options.mutate(current);
|
|
442
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
|
443
|
+
throw new WorktreeTraceError('SNAPSHOT_INVALID', 'mutate 必须返回 record snapshot object。');
|
|
444
|
+
}
|
|
445
|
+
if (next.worktree_id !== options.worktreeId) {
|
|
446
|
+
throw new WorktreeTraceError('SNAPSHOT_INVALID', 'snapshot.worktree_id 必须等于目标 UUID。');
|
|
447
|
+
}
|
|
448
|
+
const event = appendEventFile(
|
|
449
|
+
options.commonDir,
|
|
450
|
+
repository.repository_id,
|
|
451
|
+
options.worktreeId,
|
|
452
|
+
options.eventType,
|
|
453
|
+
options.actor ?? null,
|
|
454
|
+
next,
|
|
455
|
+
options.details ?? {},
|
|
456
|
+
tail?.event_id ?? null,
|
|
457
|
+
);
|
|
458
|
+
const cache = writeRecordCache(options.commonDir, options.worktreeId, next, event, chain.length + 1);
|
|
459
|
+
return { event, record: cache };
|
|
460
|
+
} finally {
|
|
461
|
+
releaseRecordLock(lock);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @param {string} commonDir @param {string} worktreeId */
|
|
466
|
+
export function rebuildRecordCache(commonDir, worktreeId) {
|
|
467
|
+
const lock = acquireRecordLock(commonDir, worktreeId, { command: 'rebuild' });
|
|
468
|
+
try {
|
|
469
|
+
const chain = readEventChain(commonDir, worktreeId);
|
|
470
|
+
const tail = chain.at(-1);
|
|
471
|
+
if (!tail || !tail.snapshot) {
|
|
472
|
+
throw new WorktreeTraceError('REBUILD_NO_SNAPSHOT', `record ${worktreeId} 没有可物化 snapshot。`);
|
|
473
|
+
}
|
|
474
|
+
return writeRecordCache(commonDir, worktreeId, tail.snapshot, tail, chain.length);
|
|
475
|
+
} finally {
|
|
476
|
+
releaseRecordLock(lock);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-worktrees
|
|
3
|
+
description: "管理 Git worktree 隔离与生命周期,包括碰撞扫描、创建或接管、堆叠分支、批量集成候选、交接和回收。仅在明确需要 worktree,或同仓多写入者存在覆盖风险时使用;只读并行和普通单写入者不适用。"
|
|
4
|
+
metadata:
|
|
5
|
+
requires:
|
|
6
|
+
bins: ["agentkit"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# manage-worktrees:Git Worktree 隔离与生命周期
|
|
10
|
+
|
|
11
|
+
在改文件之前选择最小安全隔离,并让每棵 worktree 都能回答:谁创建、为何创建、正在做什么、谁接手、哪些提交属于谁、为何尚未回收。
|
|
12
|
+
|
|
13
|
+
## 组合位置
|
|
14
|
+
|
|
15
|
+
本 skill 是环境适配层,不决定是否派生 Agent,也不判断全局任务是否完成:
|
|
16
|
+
|
|
17
|
+
- 由 `orchestrate-subagents` 调用时,接收已确认的写入者和并发关系,或已裁决的归属不明 / 路径相交风险;只负责返回目标仓的隔离位置与生命周期记录。
|
|
18
|
+
- `run-agent-verify-loop` 中只有 implementer 写、verifier 只读时不需要 worktree;多个 implementer 并行写或 controller 同时写时才使用本 skill。
|
|
19
|
+
- 用户可以直接要求创建、接管、审计或回收 worktree,此时无需加载其他 skill。
|
|
20
|
+
- 与 `verify-agent-output` 组合时,由本 Skill 输出 Artifact Ref;verifier 只消费冻结身份,不接管 worktree 生命周期。
|
|
21
|
+
|
|
22
|
+
运行时是 `PATH` 上的 `agentkit` 命令(`agentkit worktree <command>`),要求 `git` 可执行。
|
|
23
|
+
下文多行示例采用 POSIX 续行语法;PowerShell/cmd 保留相同 argv 即可。不要把 `python3`、`bash`、
|
|
24
|
+
`~` 展开或 `/tmp` 视为跨平台常量。
|
|
25
|
+
|
|
26
|
+
不要假设目标仓库有同名脚本。仓库可以通过根目录 `.worktree-trace.json` 提供 Profile,但 portable core 不执行 Profile 中的任意 shell command。配置 schema 和项目适配边界见 [Profile 配置](../docs/worktree/profile.md)。
|
|
27
|
+
|
|
28
|
+
所有命令都必须从**目标 Git 仓库的任意 worktree 内**执行:manager 以当前工作目录解析 repository
|
|
29
|
+
identity、primary worktree 和 Profile。
|
|
30
|
+
|
|
31
|
+
### 跨 Skill 稳定产物
|
|
32
|
+
|
|
33
|
+
需要交给 verifier 或 orchestrator 时,不手抄 SHA 和 owner:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
agentkit worktree capabilities --json
|
|
37
|
+
agentkit worktree binding <selector> --json
|
|
38
|
+
agentkit worktree artifact <selector> --json
|
|
39
|
+
agentkit worktree verify-artifact <artifact.json> --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`artifact` 只接受存在、clean 的 worktree,绑定 repository identity、完整 commit、worktree ID 和
|
|
43
|
+
ownership epoch。`verify-artifact` 重算 object format、commit ancestry、owner epoch 与 live HEAD;
|
|
44
|
+
任何漂移都要求重新冻结 Artifact,不能沿用旧验证结果。Binding 只表达隔离与归属,`task_status:
|
|
45
|
+
done` 不代表验证通过或全局任务完成。
|
|
46
|
+
|
|
47
|
+
碰撞、漂移、交接或回收异常可用 `incident <selector> --input <json>` 记录带 trace event 摘要的
|
|
48
|
+
Reflection,再用 `propose-improvement --reflection <uuid> --input <json>` 形成 `proposed` 候选。
|
|
49
|
+
这些文件写入 Git common-dir 下的运行状态,不进入业务源码,也不自动修改 Profile 或 Skill。
|
|
50
|
+
|
|
51
|
+
manager 的 trace/common-dir 状态只应由受信 operator/controller 与 runtime 写入。event digest 链用于
|
|
52
|
+
发现部分、追加、乱序和意外损坏,不是签名或外部不可变审计锚;拥有该状态完整写权限的进程可重写
|
|
53
|
+
一条自洽历史。Artifact/owner epoch 的机械保证以诚实 runtime 和受信状态 writer 为权限边界。
|
|
54
|
+
|
|
55
|
+
## 强制流程
|
|
56
|
+
|
|
57
|
+
按下表顺序执行;只在进入对应阶段时读取链接文档:
|
|
58
|
+
|
|
59
|
+
| 阶段 | 最小命令面 | 硬门禁与按需文档 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| 恢复/盘点 | `resume-all` → `list` → `doctor` | `list/doctor` 只读;`UNTRACKED` 不等于无人使用;`EPHEMERAL` 优先 commit+push;任何 error 都暂停 `spawn/adopt` |
|
|
62
|
+
| 碰撞扫描 | `scan --target <paths>` | `COLLIDE`、范围不确定或重做昂贵时隔离;`CLEAR` 仅在 owner 明确且改动小时共享;低置信后缀匹配先人工核对路径基准 |
|
|
63
|
+
| 交付身份 | 选择复用/并存/替代 | branch、MR 或目录名不同不能单独证明独立交付;同一 agent 有存量树或需要替代时读[交付身份](../docs/worktree/delivery-identity.md) |
|
|
64
|
+
| 创建/接管 | `spawn <semantic-slug>` / `adopt <path>` | 使用真实 agent-id,不猜身份;task 至少两个 lowercase 语义分词;base、root、堆叠、rebase/retarget 读[创建与堆叠](../docs/worktree/spawn-and-stack.md) |
|
|
65
|
+
| 更新/交接 | `touch` / `handoff` / `refresh-review` | `ready_for_review` 默认武装冻结 HEAD;handoff 只接受 clean、已 push 边界,禁止 stash 搬运;读[评审生命周期](../docs/worktree/review-lifecycle.md) |
|
|
66
|
+
| 审计/提交 | `audit` / `submit` | portable core 不决定 MR/PR 或直推策略;先服从目标仓规则,再原子登记 change request 与 watcher target |
|
|
67
|
+
| 批量集成 | `plan-batch` → `batch-integrate` → `batch-step` → `batch-result` | 同仓同 target:≥3 默认聚合,2 个仅在碰撞时;只合成冻结 SHA,不代跑门禁;合同/顺序/target 变化即重规划;读[批量集成](../docs/worktree/batch-integration.md) |
|
|
68
|
+
| 回收 | `reclaim` / `archive` | 必须有远端/其他 ref 可达证明或明确 archive evidence;dirty、stash、submodule、权限或 branch-tip 异常一律 `KEEP`;替代树默认先归档,`--discard` 需人工明确授权;读[回收与看护](../docs/worktree/reclaim-and-watch.md) |
|
|
69
|
+
|
|
70
|
+
不得按目录年龄、名称或“看起来干净”删除。禁止 `rm -rf`、`git worktree remove --force`、`branch -D`
|
|
71
|
+
和 feature owner 手工 rebase。watcher 的 `merge-tree` 结果只是目标前进后的只读成本预判,不保证逐
|
|
72
|
+
commit rebase 成功;`batch-result` 只冻结 controller 已完成的验收结论,不替代验证。
|
|
73
|
+
|
|
74
|
+
## 并发安全底线
|
|
75
|
+
|
|
76
|
+
- 当前树并发时只改明确归属文件,提交必须带 pathspec:`git commit -m "type(scope): message" -- <owned-files...>`。
|
|
77
|
+
- 小步 commit、频繁 push;worktree 只隔离 working directory/index,仍共享 refs、objects 和 stash。
|
|
78
|
+
- feature owner 禁止自行 stash、清理、重置、合并或 rebase;历史刷新只走 manager 的 `rebase` / `refresh-review`。批次候选仅当前 owner 可执行 `batch-integrate`。跨会话先 handoff,非干净树拒绝改写。
|
|
79
|
+
- manager 只拥有它创建或接管的 worktree、branch 和 trace metadata,不拥有项目凭证、SDK license、个人 token 或共享运行态。
|
|
80
|
+
- 秘密放在仓库外的 credential store;不得复制、软链或写入 trace。
|
|
81
|
+
- event 是真源,record 是可重建缓存;`doctor` 永远只报告,不自动修复或删除。
|
|
82
|
+
- trace 是本机审计,不替代跨机器任务看板、代码评审或文件锁。
|
|
83
|
+
|
|
84
|
+
所有命令支持 `--config <path>`,默认只读取 Git primary worktree 的 Profile,linked worktree 副本不能
|
|
85
|
+
改变仓库约定。使用 `capabilities --json` 发现 provider 与协议;命令按上述操作场景读取对应 reference。
|
|
86
|
+
|
|
87
|
+
显式 `--config` 是人工核对后的恢复出口:它不会刷新 primary `default_base` 或执行 drift gate,只使用本地已有 ref。离线使用前,调用者必须自行确认 Profile 与 base ref 已同步。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 1.x 兼容入口。本 Skill 的运行时已迁入 domains/worktree/,此处只做固定转发,不含实现。
|
|
3
|
+
// 直接执行时行为与 domains/worktree/worktree-mgr.mjs 逐字节一致;被 import 时只透传导出。
|
|
4
|
+
import { forwardLegacyEntry } from '../../core/legacy-entry.mjs';
|
|
5
|
+
import { runCli } from '../../domains/worktree/worktree-mgr.mjs';
|
|
6
|
+
|
|
7
|
+
export * from '../../domains/worktree/worktree-mgr.mjs';
|
|
8
|
+
|
|
9
|
+
const status = forwardLegacyEntry(import.meta.url, runCli);
|
|
10
|
+
if (status !== undefined) process.exitCode = status;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 1.x 兼容入口。本 Skill 的运行时已迁入 domains/worktree/,此处只做固定转发,不含实现。
|
|
3
|
+
// 直接执行时行为与 domains/worktree/worktree-scan.mjs 逐字节一致;被 import 时只透传导出。
|
|
4
|
+
import { forwardLegacyEntry } from '../../core/legacy-entry.mjs';
|
|
5
|
+
import { runCli } from '../../domains/worktree/worktree-scan.mjs';
|
|
6
|
+
|
|
7
|
+
export * from '../../domains/worktree/worktree-scan.mjs';
|
|
8
|
+
|
|
9
|
+
const status = forwardLegacyEntry(import.meta.url, runCli);
|
|
10
|
+
if (status !== undefined) process.exitCode = status;
|