@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -9,6 +9,23 @@ export type SandboxWorkspaceView = Readonly<{
|
|
|
9
9
|
taskMountPath: string | null;
|
|
10
10
|
}>;
|
|
11
11
|
|
|
12
|
+
export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
|
|
13
|
+
'active',
|
|
14
|
+
'completed',
|
|
15
|
+
'blocked',
|
|
16
|
+
'archive'
|
|
17
|
+
] as const);
|
|
18
|
+
|
|
19
|
+
export function sandboxWorkspaceViewStatePaths(root: string): ReadonlyArray<Readonly<{
|
|
20
|
+
state: typeof SANDBOX_WORKSPACE_VIEW_STATES[number];
|
|
21
|
+
hostPath: string;
|
|
22
|
+
}>> {
|
|
23
|
+
return SANDBOX_WORKSPACE_VIEW_STATES.map((state) => ({
|
|
24
|
+
state,
|
|
25
|
+
hostPath: path.join(root, state)
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export function sandboxWorkspaceViewPaths(params: Readonly<{
|
|
13
30
|
base: string;
|
|
14
31
|
project: string;
|
|
@@ -31,8 +48,10 @@ export function sandboxWorkspaceViewPaths(params: Readonly<{
|
|
|
31
48
|
|
|
32
49
|
export type SandboxControlSetup = Readonly<{
|
|
33
50
|
channelDir: string;
|
|
51
|
+
statusDir: string;
|
|
34
52
|
manifestPath: string;
|
|
35
53
|
token: string;
|
|
54
|
+
generation: string;
|
|
36
55
|
}>;
|
|
37
56
|
|
|
38
57
|
export function sandboxControlPaths(params: Readonly<{
|
|
@@ -40,13 +59,19 @@ export function sandboxControlPaths(params: Readonly<{
|
|
|
40
59
|
project: string;
|
|
41
60
|
container: string;
|
|
42
61
|
identity: SandboxWorkspaceIdentity;
|
|
43
|
-
}>): Readonly<{ root: string; channelDir: string; manifestPath: string }> {
|
|
62
|
+
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; manifestPath: string }> {
|
|
44
63
|
const identityKey = params.identity.mode === 'task-bound'
|
|
45
64
|
? `task-bound:${params.identity.taskId}`
|
|
46
65
|
: 'branch-only';
|
|
47
66
|
const digest = createHash('sha256').update(identityKey).digest('hex').slice(0, 16);
|
|
48
67
|
const root = path.resolve(params.base, params.project, params.container, digest);
|
|
49
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
root,
|
|
70
|
+
channelDir: path.join(root, 'channel'),
|
|
71
|
+
statusDir: path.join(root, 'public'),
|
|
72
|
+
processingDir: path.join(root, 'processing'),
|
|
73
|
+
manifestPath: path.join(root, 'manifest.json')
|
|
74
|
+
};
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
@@ -59,6 +84,44 @@ function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
|
|
87
|
+
function assertSafeMountTargetPath(target: string, worktreeRoot: string, canonicalWorktreeRoot: string): void {
|
|
88
|
+
const relative = path.relative(worktreeRoot, target);
|
|
89
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
90
|
+
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
91
|
+
}
|
|
92
|
+
let current = worktreeRoot;
|
|
93
|
+
for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
94
|
+
current = segment ? path.join(current, segment) : current;
|
|
95
|
+
try {
|
|
96
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
97
|
+
throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
|
|
98
|
+
}
|
|
99
|
+
const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
|
|
100
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
101
|
+
throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void {
|
|
110
|
+
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
111
|
+
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
112
|
+
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
113
|
+
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
114
|
+
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
115
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
116
|
+
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
117
|
+
}
|
|
118
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
119
|
+
for (const { hostPath } of statePaths) {
|
|
120
|
+
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
121
|
+
}
|
|
122
|
+
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
123
|
+
}
|
|
124
|
+
|
|
62
125
|
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
63
126
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
64
127
|
const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
|
|
@@ -85,11 +148,10 @@ export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
|
85
148
|
assertSafeDirectory(projectRoot, path.resolve(params.base));
|
|
86
149
|
assertSafeDirectory(root, projectRoot);
|
|
87
150
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
88
|
-
for (const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
fs.
|
|
92
|
-
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
151
|
+
for (const { hostPath } of sandboxWorkspaceViewStatePaths(root)) {
|
|
152
|
+
assertSafeDirectory(hostPath, root);
|
|
153
|
+
fs.rmSync(hostPath, { recursive: true, force: true });
|
|
154
|
+
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
93
155
|
}
|
|
94
156
|
|
|
95
157
|
const active = path.join(root, 'active');
|
|
@@ -113,13 +175,22 @@ export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
|
113
175
|
export function materializeSandboxControl(params: Readonly<{
|
|
114
176
|
base: string;
|
|
115
177
|
repoRoot: string;
|
|
178
|
+
worktreeRoot: string;
|
|
116
179
|
project: string;
|
|
117
180
|
container: string;
|
|
118
181
|
branch: string;
|
|
119
182
|
identity: SandboxWorkspaceIdentity;
|
|
120
183
|
}>): SandboxControlSetup {
|
|
121
|
-
const { root, channelDir, manifestPath } = sandboxControlPaths(params);
|
|
184
|
+
const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
|
|
122
185
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
186
|
+
const consumedDir = path.join(root, 'consumed');
|
|
187
|
+
assertSafeDirectory(consumedDir, root);
|
|
188
|
+
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
189
|
+
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
190
|
+
for (const directory of [statusDir, processingDir]) {
|
|
191
|
+
assertSafeDirectory(directory, root);
|
|
192
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
193
|
+
}
|
|
123
194
|
for (const queue of ['requests', 'responses']) {
|
|
124
195
|
const directory = path.join(channelDir, queue);
|
|
125
196
|
assertSafeDirectory(directory, root);
|
|
@@ -127,17 +198,23 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
127
198
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
128
199
|
}
|
|
129
200
|
const token = randomBytes(32).toString('hex');
|
|
201
|
+
const generation = randomBytes(16).toString('hex');
|
|
202
|
+
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
130
203
|
const manifest: SandboxControlManifest = {
|
|
131
|
-
version:
|
|
132
|
-
repoRoot
|
|
204
|
+
version: 3,
|
|
205
|
+
repoRoot,
|
|
206
|
+
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
133
207
|
project: params.project,
|
|
134
208
|
container: params.container,
|
|
135
209
|
branch: params.branch,
|
|
136
210
|
mode: params.identity.mode,
|
|
137
211
|
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
138
212
|
token,
|
|
139
|
-
|
|
213
|
+
generation,
|
|
214
|
+
channelDir,
|
|
215
|
+
publicStatusDir: statusDir,
|
|
216
|
+
processingDir
|
|
140
217
|
};
|
|
141
218
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
142
|
-
return { channelDir, manifestPath, token };
|
|
219
|
+
return { channelDir, statusDir, manifestPath, token, generation };
|
|
143
220
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runProbe } from './shell.ts';
|
|
5
|
+
|
|
6
|
+
type WorktreeChange = {
|
|
7
|
+
indexStatus: string;
|
|
8
|
+
worktreeStatus: string;
|
|
9
|
+
path: string;
|
|
10
|
+
originalPath?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type WorktreeSnapshot = {
|
|
14
|
+
worktree: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
head: string;
|
|
17
|
+
changes: readonly WorktreeChange[];
|
|
18
|
+
identity: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type WorktreeInspection =
|
|
22
|
+
| { status: 'clean'; snapshot: WorktreeSnapshot }
|
|
23
|
+
| { status: 'dirty'; snapshot: WorktreeSnapshot }
|
|
24
|
+
| { status: 'failed'; worktree: string; message: string };
|
|
25
|
+
|
|
26
|
+
type WorktreeRemovalPermit = {
|
|
27
|
+
mode: 'clean' | 'discard';
|
|
28
|
+
snapshot: WorktreeSnapshot;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Probe = typeof runProbe;
|
|
32
|
+
|
|
33
|
+
function probeText(probe: Probe, cwd: string, args: string[]): string {
|
|
34
|
+
const result = probe('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
35
|
+
if (result.error) throw result.error;
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
|
|
38
|
+
throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
|
|
39
|
+
}
|
|
40
|
+
return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseStatus(raw: string): WorktreeChange[] {
|
|
44
|
+
const records = raw.split('\0');
|
|
45
|
+
const changes: WorktreeChange[] = [];
|
|
46
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
47
|
+
const record = records[index] ?? '';
|
|
48
|
+
if (!record || record.startsWith('# ')) continue;
|
|
49
|
+
if (record.startsWith('? ')) {
|
|
50
|
+
changes.push({ indexStatus: '?', worktreeStatus: '?', path: record.slice(2) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (record.startsWith('1 ')) {
|
|
54
|
+
const match = /^1 (..) \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
|
|
55
|
+
if (!match) throw new Error('Unable to parse git porcelain v2 ordinary record');
|
|
56
|
+
changes.push({ indexStatus: match[1]![0]!, worktreeStatus: match[1]![1]!, path: match[2]! });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (record.startsWith('2 ')) {
|
|
60
|
+
const match = /^2 (..) \S+ \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
|
|
61
|
+
const originalPath = records[++index];
|
|
62
|
+
if (!match || originalPath === undefined || originalPath === '') {
|
|
63
|
+
throw new Error('Unable to parse git porcelain v2 rename record');
|
|
64
|
+
}
|
|
65
|
+
changes.push({
|
|
66
|
+
indexStatus: match[1]![0]!,
|
|
67
|
+
worktreeStatus: match[1]![1]!,
|
|
68
|
+
path: match[2]!,
|
|
69
|
+
originalPath
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (record.startsWith('u ')) {
|
|
74
|
+
const match = /^u (..) \S+ \S+ \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
|
|
75
|
+
if (!match) throw new Error('Unable to parse git porcelain v2 unmerged record');
|
|
76
|
+
changes.push({ indexStatus: match[1]![0]!, worktreeStatus: match[1]![1]!, path: match[2]! });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported git porcelain v2 record '${record[0] ?? ''}'`);
|
|
80
|
+
}
|
|
81
|
+
return changes;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updatePart(hash: ReturnType<typeof createHash>, label: string, value: string | Buffer): void {
|
|
85
|
+
const data = Buffer.isBuffer(value) ? value : Buffer.from(value, 'utf8');
|
|
86
|
+
hash.update(`${label.length}:${label}:${data.length}:`, 'utf8');
|
|
87
|
+
hash.update(data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveSnapshotPath(worktree: string, relativePath: string): string {
|
|
91
|
+
const resolved = path.resolve(worktree, relativePath);
|
|
92
|
+
const relative = path.relative(worktree, resolved);
|
|
93
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
94
|
+
throw new Error(`Git reported a path outside the worktree: ${JSON.stringify(relativePath)}`);
|
|
95
|
+
}
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hashPath(hash: ReturnType<typeof createHash>, worktree: string, relativePath: string, probe: Probe): void {
|
|
100
|
+
updatePart(hash, 'path', relativePath);
|
|
101
|
+
const target = resolveSnapshotPath(worktree, relativePath);
|
|
102
|
+
let stat: fs.Stats;
|
|
103
|
+
try {
|
|
104
|
+
stat = fs.lstatSync(target);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
107
|
+
updatePart(hash, 'kind', 'absent');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updatePart(hash, 'mode', String(stat.mode & 0o7777));
|
|
114
|
+
if (stat.isFile()) {
|
|
115
|
+
updatePart(hash, 'kind', 'file');
|
|
116
|
+
updatePart(hash, 'content', fs.readFileSync(target));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (stat.isSymbolicLink()) {
|
|
120
|
+
updatePart(hash, 'kind', 'symlink');
|
|
121
|
+
updatePart(hash, 'target', fs.readlinkSync(target));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (stat.isDirectory()) {
|
|
125
|
+
updatePart(hash, 'kind', 'git-directory');
|
|
126
|
+
const nestedHead = probeText(probe, target, ['rev-parse', '--verify', 'HEAD']).trim();
|
|
127
|
+
const nestedStatus = probeText(probe, target, [
|
|
128
|
+
'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all', '--ignore-submodules=none'
|
|
129
|
+
]);
|
|
130
|
+
const nestedIndex = probeText(probe, target, ['ls-files', '--stage', '-z']);
|
|
131
|
+
updatePart(hash, 'nested-head', nestedHead);
|
|
132
|
+
updatePart(hash, 'nested-status', nestedStatus);
|
|
133
|
+
updatePart(hash, 'nested-index', nestedIndex);
|
|
134
|
+
for (const change of parseStatus(nestedStatus)) {
|
|
135
|
+
hashPath(hash, target, change.path, probe);
|
|
136
|
+
if (change.originalPath) hashPath(hash, target, change.originalPath, probe);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Unsupported file type in dirty snapshot: ${JSON.stringify(relativePath)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function captureOnce(worktree: string, probe: Probe): WorktreeSnapshot {
|
|
144
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
145
|
+
const branch = probeText(probe, resolvedWorktree, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
146
|
+
const head = probeText(probe, resolvedWorktree, ['rev-parse', '--verify', 'HEAD']).trim();
|
|
147
|
+
const status = probeText(probe, resolvedWorktree, [
|
|
148
|
+
'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all', '--ignore-submodules=none'
|
|
149
|
+
]);
|
|
150
|
+
const index = probeText(probe, resolvedWorktree, ['ls-files', '--stage', '-z']);
|
|
151
|
+
if (!branch) throw new Error('Unable to determine worktree branch');
|
|
152
|
+
if (!head) throw new Error('Unable to determine worktree HEAD');
|
|
153
|
+
const changes = parseStatus(status);
|
|
154
|
+
const hash = createHash('sha256');
|
|
155
|
+
updatePart(hash, 'worktree', resolvedWorktree);
|
|
156
|
+
updatePart(hash, 'branch', branch);
|
|
157
|
+
updatePart(hash, 'head', head);
|
|
158
|
+
updatePart(hash, 'status', status);
|
|
159
|
+
updatePart(hash, 'index', index);
|
|
160
|
+
for (const change of changes) {
|
|
161
|
+
hashPath(hash, resolvedWorktree, change.path, probe);
|
|
162
|
+
if (change.originalPath) hashPath(hash, resolvedWorktree, change.originalPath, probe);
|
|
163
|
+
}
|
|
164
|
+
return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function inspectWorktree(worktree: string, { probe = runProbe }: { probe?: Probe } = {}): WorktreeInspection {
|
|
168
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
169
|
+
try {
|
|
170
|
+
const first = captureOnce(resolvedWorktree, probe);
|
|
171
|
+
const second = captureOnce(resolvedWorktree, probe);
|
|
172
|
+
if (first.identity !== second.identity) {
|
|
173
|
+
return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while inspecting worktree' };
|
|
174
|
+
}
|
|
175
|
+
return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
status: 'failed',
|
|
179
|
+
worktree: resolvedWorktree,
|
|
180
|
+
message: error instanceof Error ? error.message : String(error)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inspectWorktrees(worktrees: readonly string[]): WorktreeInspection[] {
|
|
186
|
+
return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
|
|
187
|
+
.map((worktree) => inspectWorktree(worktree));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createCleanPermit(snapshot: WorktreeSnapshot): WorktreeRemovalPermit {
|
|
191
|
+
if (snapshot.changes.length > 0) throw new Error('Cannot create a clean permit for a dirty worktree');
|
|
192
|
+
return { mode: 'clean', snapshot };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createDiscardPermit(snapshot: WorktreeSnapshot): WorktreeRemovalPermit {
|
|
196
|
+
if (snapshot.changes.length === 0) throw new Error('Cannot create a discard permit for a clean worktree');
|
|
197
|
+
return { mode: 'discard', snapshot };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function verifyWorktreePermit(
|
|
201
|
+
permit: WorktreeRemovalPermit,
|
|
202
|
+
options: { probe?: Probe } = {}
|
|
203
|
+
): WorktreeSnapshot {
|
|
204
|
+
const current = inspectWorktree(permit.snapshot.worktree, options);
|
|
205
|
+
if (current.status === 'failed') throw new Error(`Unable to verify worktree permit: ${current.message}`);
|
|
206
|
+
if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
|
|
207
|
+
throw new Error(`Worktree changed after authorization: ${permit.snapshot.worktree}`);
|
|
208
|
+
}
|
|
209
|
+
return current.snapshot;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function formatWorktreeSnapshot(snapshot: WorktreeSnapshot): string {
|
|
213
|
+
const lines = snapshot.changes.map((change) => {
|
|
214
|
+
const rename = change.originalPath ? ` <- ${JSON.stringify(change.originalPath)}` : '';
|
|
215
|
+
return ` ${change.indexStatus}${change.worktreeStatus} ${JSON.stringify(change.path)}${rename}`;
|
|
216
|
+
});
|
|
217
|
+
return [
|
|
218
|
+
`Worktree: ${JSON.stringify(snapshot.worktree)}`,
|
|
219
|
+
`Branch: ${snapshot.branch}`,
|
|
220
|
+
`HEAD: ${snapshot.head}`,
|
|
221
|
+
...lines,
|
|
222
|
+
`Snapshot identity: ${snapshot.identity}`
|
|
223
|
+
].join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export {
|
|
227
|
+
createCleanPermit,
|
|
228
|
+
createDiscardPermit,
|
|
229
|
+
formatWorktreeSnapshot,
|
|
230
|
+
inspectWorktree,
|
|
231
|
+
inspectWorktrees,
|
|
232
|
+
verifyWorktreePermit
|
|
233
|
+
};
|
|
234
|
+
export type { WorktreeChange, WorktreeInspection, WorktreeRemovalPermit, WorktreeSnapshot };
|
|
@@ -24,6 +24,17 @@ export type StopCommand =
|
|
|
24
24
|
| { kind: 'signal'; signal: NodeJS.Signals }
|
|
25
25
|
| { kind: 'exec'; command: string; args: string[] };
|
|
26
26
|
|
|
27
|
+
export type ProcessTreeStopCommand =
|
|
28
|
+
| { kind: 'group-signal'; pid: number; signal: NodeJS.Signals }
|
|
29
|
+
| { kind: 'exec'; command: string; args: string[] };
|
|
30
|
+
|
|
31
|
+
export function buildProcessTreeStopCommand(pid: number, platform: NodeJS.Platform): ProcessTreeStopCommand {
|
|
32
|
+
if (platform === 'win32') {
|
|
33
|
+
return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
|
|
34
|
+
}
|
|
35
|
+
return { kind: 'group-signal', pid: -pid, signal: 'SIGTERM' };
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function buildStopCommand(pid: number, platform: NodeJS.Platform): StopCommand {
|
|
28
39
|
if (platform === 'win32') {
|
|
29
40
|
return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
|
package/lib/task/activity-log.ts
CHANGED
|
@@ -2,10 +2,14 @@ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
|
2
2
|
const NEXT_H2_RE = /^##\s/;
|
|
3
3
|
const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
|
|
4
4
|
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
5
|
+
const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
|
|
6
|
+
const ATTEMPT_RE = /(?:^|;\s*)attempt=([A-Za-z0-9-]{8,64})(?:;|$)/;
|
|
7
|
+
const COMMIT_STARTED_RE = /^started; attempt=([A-Za-z0-9-]{8,64}); baseline=([a-f0-9]{40,64}); agent=([a-z-]+)$/;
|
|
5
8
|
|
|
6
9
|
type LogEntry = { time: string; step: string; agent: string; note: string };
|
|
7
|
-
type StepRow = { step: string; agent: string; started: string; done: string; note: string };
|
|
10
|
+
type StepRow = { step: string; agent: string; started: string; done: string; note: string; attempt?: string };
|
|
8
11
|
type ActivityLogSection = { heading: string; body: string; entries: LogEntry[] };
|
|
12
|
+
type CommitAttempt = Readonly<{ attempt: string; baseline: string; agent: string }>;
|
|
9
13
|
|
|
10
14
|
function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
|
|
11
15
|
const section = locateActivityLog(content);
|
|
@@ -37,26 +41,61 @@ function appendActivityEntry(section: ActivityLogSection, entry: LogEntry): stri
|
|
|
37
41
|
return section.body ? `${section.body}\n${line}` : line;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
function activityAttempt(note: string): string | null {
|
|
45
|
+
return ATTEMPT_RE.exec(note)?.[1] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCommitAttemptStarted(note: string): CommitAttempt | null {
|
|
49
|
+
const match = COMMIT_STARTED_RE.exec(note);
|
|
50
|
+
return match ? { attempt: match[1]!, baseline: match[2]!, agent: match[3]! } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function commitAttemptStartedNote(attempt: CommitAttempt): string {
|
|
54
|
+
return `started; attempt=${attempt.attempt}; baseline=${attempt.baseline}; agent=${attempt.agent}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
function pairEntries(entries: LogEntry[]): StepRow[] {
|
|
41
58
|
const rows: StepRow[] = [];
|
|
42
59
|
const open = new Map<string, StepRow[]>();
|
|
43
60
|
for (const entry of entries) {
|
|
44
61
|
const started = STARTED_SUFFIX_RE.test(entry.step);
|
|
45
|
-
const
|
|
62
|
+
const aborted = ABORTED_SUFFIX_RE.test(entry.step);
|
|
63
|
+
const base = entry.step.replace(STARTED_SUFFIX_RE, '').replace(ABORTED_SUFFIX_RE, '');
|
|
46
64
|
if (started) {
|
|
47
|
-
const
|
|
65
|
+
const attempt = activityAttempt(entry.note);
|
|
66
|
+
const row: StepRow = {
|
|
67
|
+
step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note,
|
|
68
|
+
...(attempt ? { attempt } : {})
|
|
69
|
+
};
|
|
48
70
|
rows.push(row);
|
|
49
71
|
const queue = open.get(base) ?? [];
|
|
50
72
|
queue.push(row);
|
|
51
73
|
open.set(base, queue);
|
|
52
74
|
} else {
|
|
53
|
-
const
|
|
75
|
+
const queue = open.get(base) ?? [];
|
|
76
|
+
const terminalAttempt = aborted ? activityAttempt(entry.note) : null;
|
|
77
|
+
const pendingIndex = terminalAttempt === null
|
|
78
|
+
? 0
|
|
79
|
+
: queue.findIndex((row) => row.attempt === terminalAttempt);
|
|
80
|
+
const pending = pendingIndex >= 0 ? queue.splice(pendingIndex, 1)[0] : undefined;
|
|
54
81
|
if (pending) Object.assign(pending, { done: entry.time, agent: entry.agent, note: entry.note });
|
|
55
|
-
else rows.push({
|
|
82
|
+
else rows.push({
|
|
83
|
+
step: aborted ? entry.step : base,
|
|
84
|
+
agent: entry.agent, started: '', done: entry.time, note: entry.note,
|
|
85
|
+
...(terminalAttempt ? { attempt: terminalAttempt } : {})
|
|
86
|
+
});
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
return rows;
|
|
59
90
|
}
|
|
60
91
|
|
|
61
|
-
export {
|
|
62
|
-
|
|
92
|
+
export {
|
|
93
|
+
activityAttempt,
|
|
94
|
+
appendActivityEntry,
|
|
95
|
+
commitAttemptStartedNote,
|
|
96
|
+
locateActivityLog,
|
|
97
|
+
pairEntries,
|
|
98
|
+
parseActivityLog,
|
|
99
|
+
parseCommitAttemptStarted
|
|
100
|
+
};
|
|
101
|
+
export type { ActivityLogSection, CommitAttempt, LogEntry, StepRow };
|
|
@@ -16,6 +16,7 @@ const artifactFamilyCatalog = [
|
|
|
16
16
|
{ family: 'code', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['实现报告', 'Implementation Report'] },
|
|
17
17
|
{ family: 'review-code', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['代码审查', 'Code Review'] },
|
|
18
18
|
{ family: 'manual-validation', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['人工验证', 'Manual Validation'] },
|
|
19
|
+
{ family: 'validation-run', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['验证运行证据', 'Validation Run Evidence'] },
|
|
19
20
|
{ family: 'pr-review', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['PR 审查报告', 'PR Review Report'] }
|
|
20
21
|
] as const;
|
|
21
22
|
|
|
@@ -257,7 +258,8 @@ const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily;
|
|
|
257
258
|
analysis: { family: 'review-analysis', requireReference: true },
|
|
258
259
|
plan: { family: 'review-plan', requireReference: true },
|
|
259
260
|
'review-code': { family: 'review-plan' },
|
|
260
|
-
'manual-validation': { family: 'review-code' }
|
|
261
|
+
'manual-validation': { family: 'review-code' },
|
|
262
|
+
'validation-run': { family: 'review-code' }
|
|
261
263
|
};
|
|
262
264
|
|
|
263
265
|
function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
|
|
@@ -376,12 +378,11 @@ function resolveDecisionImplementationInput(
|
|
|
376
378
|
if (pending.length === 0) return { input: null };
|
|
377
379
|
const activity = locateActivityLog(content);
|
|
378
380
|
if (!activity) return { error: 'task has no unique Activity Log section' };
|
|
379
|
-
const
|
|
381
|
+
const completed = activity.entries.some((entry) =>
|
|
380
382
|
/^Review Code \(Round \d+\)$/.test(entry.step) && entry.note.includes(`→ ${review.name}`)
|
|
381
383
|
);
|
|
382
|
-
const completed = reviewEntries.at(-1);
|
|
383
384
|
if (!completed) return { error: `cannot find completed Activity Log identity for ${review.name}` };
|
|
384
|
-
try { return { input: selectPendingImplementationInput(rows
|
|
385
|
+
try { return { input: selectPendingImplementationInput(rows) }; }
|
|
385
386
|
catch (error) { return { error: error instanceof Error ? error.message : String(error) }; }
|
|
386
387
|
}
|
|
387
388
|
|