@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { requestSandboxTaskCreate, SandboxControlClientError } from '../sandbox/control/client.ts';
|
|
5
|
+
import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.ts';
|
|
6
|
+
import { validateTaskCreateCandidate } from '../task/create.ts';
|
|
7
|
+
import { createTask, type TaskCreateResult } from '../task/create-service.ts';
|
|
8
|
+
|
|
9
|
+
function failed(code: string, message: string, retryable = false): TaskCreateResult {
|
|
10
|
+
return {
|
|
11
|
+
status: retryable ? 'blocked' : 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
12
|
+
operations: [], warnings: [], error: { code, message, retryable }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function output(result: TaskCreateResult): void {
|
|
17
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
18
|
+
process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function taskCreate(args: string[]): void {
|
|
22
|
+
const inputIndex = args.indexOf('--input');
|
|
23
|
+
const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
|
|
24
|
+
if (!input || args.length !== 2 || inputIndex !== 0) {
|
|
25
|
+
output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const inputPath = path.resolve(input);
|
|
30
|
+
const stat = fs.lstatSync(inputPath);
|
|
31
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
|
|
32
|
+
if (stat.size > SANDBOX_CONTROL_MAX_BYTES) throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
|
|
33
|
+
const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
|
|
34
|
+
if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
|
|
35
|
+
const response = requestSandboxTaskCreate({ candidate });
|
|
36
|
+
if (response.phase === 'rejected') {
|
|
37
|
+
output(failed(
|
|
38
|
+
response.error?.code ?? 'SANDBOX_CONTROL_REJECTED',
|
|
39
|
+
response.error?.message ?? response.stderr,
|
|
40
|
+
response.error?.retryable ?? false
|
|
41
|
+
));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(response.stdout);
|
|
45
|
+
process.stderr.write(response.stderr);
|
|
46
|
+
process.exitCode = response.exitCode;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
output(createTask(candidate, { repoRoot: process.cwd() }));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof SandboxControlClientError) {
|
|
52
|
+
output(failed(error.detail.code, error.detail.message, error.detail.retryable));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
|
|
57
|
+
output(failed(code, message));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { taskCreate };
|
|
@@ -9,18 +9,20 @@ import {
|
|
|
9
9
|
checkpointCommitIntent,
|
|
10
10
|
completeCommitIntent,
|
|
11
11
|
pauseOrchestration,
|
|
12
|
-
prepareOrchestrationDelegation,
|
|
13
12
|
recoverCommitIntent,
|
|
14
13
|
routeOrchestration,
|
|
15
14
|
sealMatchingOrchestrationDelegation,
|
|
16
15
|
sealOrchestrationDelegation,
|
|
16
|
+
startCommitAttempt,
|
|
17
17
|
statusCommitIntent,
|
|
18
|
-
statusOrchestration
|
|
18
|
+
statusOrchestration,
|
|
19
|
+
terminateCommitAttempt
|
|
19
20
|
} from '../task/orchestration.ts';
|
|
21
|
+
import { prepareCodexOrchestrationDelegation } from '../task/codex-orchestration.ts';
|
|
20
22
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
21
23
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
22
24
|
|
|
23
|
-
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-status> [options]\n';
|
|
25
|
+
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
|
|
24
26
|
|
|
25
27
|
function usageFailure(message: string): void {
|
|
26
28
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -31,7 +33,7 @@ function usageFailure(message: string): void {
|
|
|
31
33
|
process.exitCode = 2;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
function taskOrchestration(args: string[] = []): void {
|
|
36
|
+
async function taskOrchestration(args: string[] = []): Promise<void> {
|
|
35
37
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
36
38
|
process.stdout.write(USAGE);
|
|
37
39
|
return;
|
|
@@ -43,7 +45,7 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
43
45
|
const [taskRef, intent] = args;
|
|
44
46
|
if (![
|
|
45
47
|
'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
|
|
46
|
-
'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
|
|
48
|
+
'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
|
|
47
49
|
].includes(intent!)) {
|
|
48
50
|
usageFailure(`unknown intent '${intent}'`);
|
|
49
51
|
return;
|
|
@@ -67,6 +69,7 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
67
69
|
'--model-fallback-reason', '--reasoning-effort-fallback-reason',
|
|
68
70
|
'--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
|
|
69
71
|
'--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
72
|
+
, '--attempt', '--git-worktree-root'
|
|
70
73
|
].includes(flag)) {
|
|
71
74
|
usageFailure(`unknown option '${flag}'`);
|
|
72
75
|
return;
|
|
@@ -84,12 +87,14 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
84
87
|
values[flag] = value;
|
|
85
88
|
}
|
|
86
89
|
const commitIntentFlags: Record<string, readonly string[]> = {
|
|
87
|
-
'commit-
|
|
88
|
-
'commit-
|
|
89
|
-
'commit-
|
|
90
|
-
'commit-
|
|
91
|
-
'commit-
|
|
92
|
-
'commit-
|
|
90
|
+
'commit-start': ['--agent', '--git-worktree-root'],
|
|
91
|
+
'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
|
|
92
|
+
'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
|
|
93
|
+
'commit-complete': ['--token', '--agent', '--git-worktree-root'],
|
|
94
|
+
'commit-recover': ['--agent', '--git-worktree-root'],
|
|
95
|
+
'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
|
|
96
|
+
'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
|
|
97
|
+
'commit-status': ['--git-worktree-root']
|
|
93
98
|
};
|
|
94
99
|
if (intent! in commitIntentFlags) {
|
|
95
100
|
const allowed = commitIntentFlags[intent!]!;
|
|
@@ -97,24 +102,33 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
97
102
|
if (invalid) { usageFailure(`intent '${intent}' does not accept '${invalid}'`); return; }
|
|
98
103
|
} else {
|
|
99
104
|
const commitOnly = [...seen].find((flag) => [
|
|
100
|
-
'--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
105
|
+
'--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
101
106
|
].includes(flag));
|
|
102
107
|
if (commitOnly) { usageFailure(`intent '${intent}' does not accept '${commitOnly}'`); return; }
|
|
103
108
|
}
|
|
104
109
|
const requireValues = (flags: string[]) => flags.find((flag) => values[flag] === undefined);
|
|
110
|
+
const coreOptions = values['--git-worktree-root'] === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { gitWorktreeRoot: values['--git-worktree-root'] };
|
|
105
113
|
if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
|
|
106
114
|
usageFailure(`unknown client '${values['--client']}'`);
|
|
107
115
|
return;
|
|
108
116
|
}
|
|
109
117
|
let result;
|
|
110
|
-
if (intent === 'commit-
|
|
111
|
-
const missing = requireValues(['--agent'
|
|
118
|
+
if (intent === 'commit-start') {
|
|
119
|
+
const missing = requireValues(['--agent']);
|
|
120
|
+
if (missing) { usageFailure(`intent 'commit-start' requires '${missing}'`); return; }
|
|
121
|
+
const agent = normalizeAgentToken(values['--agent']!);
|
|
122
|
+
if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
|
|
123
|
+
result = startCommitAttempt(taskRef!, { agent }, coreOptions);
|
|
124
|
+
} else if (intent === 'commit-begin') {
|
|
125
|
+
const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
|
|
112
126
|
if (missing) { usageFailure(`intent 'commit-begin' requires '${missing}'`); return; }
|
|
113
127
|
const agent = normalizeAgentToken(values['--agent']!);
|
|
114
128
|
if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
|
|
115
129
|
result = beginCommitIntent(taskRef!, {
|
|
116
|
-
agent, orchestrated, baselineHead: values['--baseline-head']!
|
|
117
|
-
});
|
|
130
|
+
agent, orchestrated, baselineHead: values['--baseline-head']!, attempt: values['--attempt']!
|
|
131
|
+
}, coreOptions);
|
|
118
132
|
} else if (intent === 'commit-checkpoint') {
|
|
119
133
|
const missing = requireValues(['--token', '--kind', '--head']);
|
|
120
134
|
if (missing) { usageFailure(`intent 'commit-checkpoint' requires '${missing}'`); return; }
|
|
@@ -130,25 +144,36 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
130
144
|
result = checkpointCommitIntent(taskRef!, {
|
|
131
145
|
token: values['--token']!, kind: values['--kind'] as 'committed' | 'pushed',
|
|
132
146
|
head: values['--head']!, remote: values['--remote'], ref: values['--ref']
|
|
133
|
-
});
|
|
147
|
+
}, coreOptions);
|
|
134
148
|
} else if (intent === 'commit-complete') {
|
|
135
149
|
const missing = requireValues(['--token', '--agent']);
|
|
136
150
|
if (missing) { usageFailure(`intent 'commit-complete' requires '${missing}'`); return; }
|
|
137
151
|
const agent = normalizeAgentToken(values['--agent']!);
|
|
138
152
|
if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
|
|
139
|
-
result = completeCommitIntent(taskRef!, { token: values['--token']!, agent });
|
|
153
|
+
result = completeCommitIntent(taskRef!, { token: values['--token']!, agent }, coreOptions);
|
|
140
154
|
} else if (intent === 'commit-recover') {
|
|
141
155
|
const missing = requireValues(['--agent']);
|
|
142
156
|
if (missing) { usageFailure(`intent 'commit-recover' requires '${missing}'`); return; }
|
|
143
157
|
const agent = normalizeAgentToken(values['--agent']!);
|
|
144
158
|
if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
|
|
145
|
-
result = recoverCommitIntent(taskRef!, { agent });
|
|
159
|
+
result = recoverCommitIntent(taskRef!, { agent }, coreOptions);
|
|
146
160
|
} else if (intent === 'commit-abort') {
|
|
147
161
|
const missing = requireValues(['--token', '--expected-head']);
|
|
148
162
|
if (missing) { usageFailure(`intent 'commit-abort' requires '${missing}'`); return; }
|
|
149
|
-
result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! });
|
|
163
|
+
result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! }, coreOptions);
|
|
164
|
+
} else if (intent === 'commit-terminate') {
|
|
165
|
+
const missing = requireValues(['--attempt', '--agent', '--code']);
|
|
166
|
+
if (missing) { usageFailure(`intent 'commit-terminate' requires '${missing}'`); return; }
|
|
167
|
+
const agent = normalizeAgentToken(values['--agent']!);
|
|
168
|
+
if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
|
|
169
|
+
if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code']!)) {
|
|
170
|
+
usageFailure('--code must be a stable uppercase identifier'); return;
|
|
171
|
+
}
|
|
172
|
+
result = terminateCommitAttempt(taskRef!, {
|
|
173
|
+
attempt: values['--attempt']!, agent, code: values['--code']!
|
|
174
|
+
}, coreOptions);
|
|
150
175
|
} else if (intent === 'commit-status') {
|
|
151
|
-
result = statusCommitIntent(taskRef
|
|
176
|
+
result = statusCommitIntent(taskRef!, coreOptions);
|
|
152
177
|
} else if (intent === 'begin-or-resume') {
|
|
153
178
|
const missing = requireValues(['--client']);
|
|
154
179
|
if (missing) { usageFailure(`intent 'begin-or-resume' requires '${missing}'`); return; }
|
|
@@ -166,6 +191,7 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
166
191
|
return;
|
|
167
192
|
}
|
|
168
193
|
result = beginOrResumeOrchestration(taskRef!, {
|
|
194
|
+
...coreOptions,
|
|
169
195
|
maxSteps,
|
|
170
196
|
client: values['--client'] as AgentClientId,
|
|
171
197
|
modelPolicy: hasAnyPolicy ? {
|
|
@@ -180,17 +206,17 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
180
206
|
} : undefined
|
|
181
207
|
});
|
|
182
208
|
} else if (intent === 'route') {
|
|
183
|
-
result = routeOrchestration(taskRef
|
|
209
|
+
result = routeOrchestration(taskRef!, coreOptions);
|
|
184
210
|
} else if (intent === 'status') {
|
|
185
|
-
result = statusOrchestration(taskRef
|
|
211
|
+
result = statusOrchestration(taskRef!, coreOptions);
|
|
186
212
|
} else if (intent === 'prepare') {
|
|
187
213
|
const missing = requireValues(['--client']);
|
|
188
214
|
if (missing) { usageFailure(`intent 'prepare' requires '${missing}'`); return; }
|
|
189
|
-
result =
|
|
215
|
+
result = await prepareCodexOrchestrationDelegation(taskRef!, {
|
|
190
216
|
client: values['--client'] as AgentClientId,
|
|
191
217
|
requestedModel: values['--requested-model'],
|
|
192
218
|
requestedReasoningEffort: values['--requested-reasoning-effort']
|
|
193
|
-
});
|
|
219
|
+
}, { orchestrationOptions: coreOptions });
|
|
194
220
|
} else if (intent === 'hook-start') {
|
|
195
221
|
const missing = requireValues([
|
|
196
222
|
...(taskRef === 'auto' ? ['--client'] : []),
|
|
@@ -206,15 +232,15 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
206
232
|
reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
|
|
207
233
|
};
|
|
208
234
|
result = taskRef === 'auto'
|
|
209
|
-
? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event)
|
|
210
|
-
: activateOrchestrationDelegation(taskRef!, event);
|
|
235
|
+
? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event, coreOptions)
|
|
236
|
+
: activateOrchestrationDelegation(taskRef!, event, coreOptions);
|
|
211
237
|
} else if (intent === 'hook-stop') {
|
|
212
238
|
if (taskRef === 'auto') {
|
|
213
239
|
const missing = requireValues(['--client', '--native-agent', '--child-id']);
|
|
214
240
|
if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
|
|
215
241
|
result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
|
|
216
242
|
nativeAgent: values['--native-agent']!, childId: values['--child-id']!
|
|
217
|
-
});
|
|
243
|
+
}, coreOptions);
|
|
218
244
|
} else {
|
|
219
245
|
const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
|
|
220
246
|
if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
|
|
@@ -224,15 +250,15 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
224
250
|
childId: values['--child-id']!, exitCode,
|
|
225
251
|
afterFingerprint: values['--after-fingerprint']!,
|
|
226
252
|
changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
|
|
227
|
-
});
|
|
253
|
+
}, coreOptions);
|
|
228
254
|
}
|
|
229
255
|
} else if (intent === 'advance') {
|
|
230
|
-
result = advanceOrchestration(taskRef
|
|
256
|
+
result = advanceOrchestration(taskRef!, coreOptions);
|
|
231
257
|
} else {
|
|
232
258
|
const missing = requireValues(['--code', '--message', '--recoverable']);
|
|
233
259
|
if (missing) { usageFailure(`intent 'pause' requires '${missing}'`); return; }
|
|
234
260
|
if (!['true', 'false'].includes(values['--recoverable']!)) { usageFailure('--recoverable must be true or false'); return; }
|
|
235
|
-
result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true');
|
|
261
|
+
result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true', coreOptions);
|
|
236
262
|
}
|
|
237
263
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
238
264
|
if (result.status === 'failed') process.exitCode = 1;
|
|
@@ -39,6 +39,7 @@ const ARTIFACT_TITLES: Record<string, string> = {
|
|
|
39
39
|
code: '实现报告',
|
|
40
40
|
'review-code': '代码审查',
|
|
41
41
|
'manual-validation': '人工验证报告',
|
|
42
|
+
'validation-run': '验证运行证据',
|
|
42
43
|
'pr-review': 'PR 审查报告'
|
|
43
44
|
};
|
|
44
45
|
|
|
@@ -75,7 +76,7 @@ function renderTaskComment(content: string, taskId: string, agent: string): stri
|
|
|
75
76
|
|
|
76
77
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
77
78
|
const stem = path.basename(artifact, '.md');
|
|
78
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|pr-review)(?:-r(\d+))?$/);
|
|
79
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
|
|
79
80
|
if (!match) throw new Error(`unsupported artifact '${artifact}'`);
|
|
80
81
|
const base = ARTIFACT_TITLES[match[1]!]!;
|
|
81
82
|
const round = match[2] ? Number(match[2]) : 1;
|
package/lib/release/workflow.ts
CHANGED
|
@@ -1,32 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
type ReleasePhase = 'unprepared' | 'prepared' | 'partially-published' | 'published' | 'post-pending' | 'post-prepared' | 'complete';
|
|
4
|
+
type PostReleaseFacts = {
|
|
5
|
+
commit: string | null;
|
|
6
|
+
isHead: boolean;
|
|
7
|
+
published: boolean;
|
|
8
|
+
branch: string;
|
|
9
|
+
upstream: string | null;
|
|
10
|
+
remoteHead: string | null;
|
|
11
|
+
newVersion: string | null;
|
|
12
|
+
changedPaths: string[];
|
|
13
|
+
demoInputSha256: string | null;
|
|
14
|
+
worktree: string[];
|
|
15
|
+
staged: string[];
|
|
16
|
+
};
|
|
17
|
+
type ReleaseFacts = {
|
|
18
|
+
localTag: boolean;
|
|
19
|
+
localTagAncestor?: boolean;
|
|
20
|
+
localTagConflict?: boolean;
|
|
21
|
+
remoteBranch: boolean;
|
|
22
|
+
remoteTag: boolean;
|
|
23
|
+
githubRelease: boolean | null;
|
|
24
|
+
npm: boolean | null;
|
|
25
|
+
homebrew: boolean | null;
|
|
26
|
+
smoke: 'success' | 'pending' | 'failed' | null;
|
|
27
|
+
post: PostReleaseFacts;
|
|
28
|
+
};
|
|
29
|
+
type PostConfirmation = {
|
|
30
|
+
version: string;
|
|
31
|
+
commit: string;
|
|
32
|
+
branch: string;
|
|
33
|
+
upstream: string | null;
|
|
34
|
+
remoteHead: string | null;
|
|
35
|
+
newVersion: string | null;
|
|
36
|
+
changedPaths: string[];
|
|
37
|
+
demoInputSha256: string | null;
|
|
38
|
+
worktree: string[];
|
|
39
|
+
staged: string[];
|
|
40
|
+
sha256: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function byteSort(values: readonly string[]): string[] {
|
|
44
|
+
return [...values].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
|
|
45
|
+
}
|
|
3
46
|
|
|
4
47
|
function deriveReleasePhase(facts: ReleaseFacts): ReleasePhase {
|
|
5
48
|
if (!facts.localTag && !facts.localTagAncestor) return 'unprepared';
|
|
49
|
+
if (facts.post.commit) return facts.post.published ? 'complete' : 'post-prepared';
|
|
6
50
|
if (!facts.remoteBranch && !facts.remoteTag) return 'prepared';
|
|
7
51
|
if (!facts.remoteBranch || !facts.remoteTag) return 'partially-published';
|
|
8
52
|
if (facts.githubRelease !== true || facts.npm !== true || facts.homebrew !== true) return 'published';
|
|
9
|
-
|
|
10
|
-
return 'complete';
|
|
53
|
+
return 'post-pending';
|
|
11
54
|
}
|
|
12
55
|
|
|
13
|
-
function
|
|
14
|
-
|
|
56
|
+
function createPostConfirmation(version: string, post: PostReleaseFacts): PostConfirmation | null {
|
|
57
|
+
if (!post.commit || !post.isHead || post.worktree.length || post.staged.length) return null;
|
|
58
|
+
const canonical = {
|
|
59
|
+
version,
|
|
60
|
+
commit: post.commit,
|
|
61
|
+
branch: post.branch,
|
|
62
|
+
upstream: post.upstream,
|
|
63
|
+
remoteHead: post.remoteHead,
|
|
64
|
+
newVersion: post.newVersion,
|
|
65
|
+
changedPaths: byteSort(post.changedPaths),
|
|
66
|
+
demoInputSha256: post.demoInputSha256,
|
|
67
|
+
worktree: byteSort(post.worktree),
|
|
68
|
+
staged: byteSort(post.staged)
|
|
69
|
+
};
|
|
70
|
+
const sha256 = `sha256:${crypto.createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
|
|
71
|
+
return { ...canonical, sha256 };
|
|
15
72
|
}
|
|
16
73
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const allowed = input.action === 'prepare' ? before.phase === 'unprepared'
|
|
22
|
-
: input.action === 'publish' ? before.facts.localTag && ['prepared', 'partially-published'].includes(before.phase)
|
|
23
|
-
: ['published', 'post-pending'].includes(before.phase);
|
|
24
|
-
if (!allowed) return { status: before.phase === 'complete' ? 'no-op' as const : 'failed' as const, changed: false, before, snapshot: before, error: before.phase === 'complete' ? null : { code: 'RELEASE_PHASE_INVALID', message: `Cannot ${input.action} from ${before.phase}` } };
|
|
25
|
-
await input[input.action]?.();
|
|
26
|
-
const snapshot = releaseSnapshot(input.version, await input.inspect());
|
|
27
|
-
const progressed = snapshot.phase !== before.phase;
|
|
28
|
-
return { status: progressed ? 'applied' as const : 'blocked' as const, changed: progressed, before, snapshot, error: progressed ? null : { code: 'RELEASE_PROGRESS_PENDING', message: `${input.action} has not reached the next observable phase` } };
|
|
74
|
+
function releaseSnapshot(version: string, facts: ReleaseFacts) {
|
|
75
|
+
const snapshot = { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
|
|
76
|
+
const postConfirmation = createPostConfirmation(version, facts.post);
|
|
77
|
+
return postConfirmation ? { ...snapshot, postConfirmation } : snapshot;
|
|
29
78
|
}
|
|
30
79
|
|
|
31
|
-
export { deriveReleasePhase, releaseSnapshot
|
|
32
|
-
export type { ReleaseFacts, ReleasePhase };
|
|
80
|
+
export { createPostConfirmation, deriveReleasePhase, releaseSnapshot };
|
|
81
|
+
export type { PostConfirmation, PostReleaseFacts, ReleaseFacts, ReleasePhase };
|
|
@@ -2,10 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { execFileSync
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
8
|
import * as p from '@clack/prompts';
|
|
10
9
|
import pc from 'picocolors';
|
|
11
10
|
import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
|
|
@@ -67,14 +66,16 @@ import type { SandboxTool, TmpfsSeedEntry } from '../tools.ts';
|
|
|
67
66
|
import {
|
|
68
67
|
assertFreshSandboxReady,
|
|
69
68
|
hydrateTmpfsSeedEntries,
|
|
70
|
-
prepareTmpfsMounts
|
|
69
|
+
prepareTmpfsMounts,
|
|
70
|
+
startSandboxControlBroker
|
|
71
71
|
} from '../recovery.ts';
|
|
72
72
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
73
73
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
74
74
|
import {
|
|
75
75
|
assertSandboxTaskSource,
|
|
76
76
|
materializeSandboxControl,
|
|
77
|
-
materializeSandboxWorkspaceView
|
|
77
|
+
materializeSandboxWorkspaceView,
|
|
78
|
+
prepareSandboxWorkspaceMountTargets
|
|
78
79
|
} from '../workspace-view.ts';
|
|
79
80
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
80
81
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
@@ -157,12 +158,6 @@ type HostShellConfig = {
|
|
|
157
158
|
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
158
159
|
};
|
|
159
160
|
|
|
160
|
-
function internalCliPath(): string {
|
|
161
|
-
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
162
|
-
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
163
|
-
return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
161
|
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
167
162
|
return tools.map((tool) => {
|
|
168
163
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -1254,9 +1249,13 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1254
1249
|
const requestedDescription = target.workspace.mode === 'task-bound'
|
|
1255
1250
|
? `task-bound:${target.workspace.taskId}`
|
|
1256
1251
|
: target.workspace.mode;
|
|
1252
|
+
const recoveryTarget = target.workspace.mode === 'task-bound'
|
|
1253
|
+
? target.workspace.taskId
|
|
1254
|
+
: branch;
|
|
1257
1255
|
throw new Error(
|
|
1258
1256
|
`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
|
|
1259
|
-
+ `but this request is ${requestedDescription}.
|
|
1257
|
+
+ `but this request is ${requestedDescription}. `
|
|
1258
|
+
+ `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`
|
|
1260
1259
|
);
|
|
1261
1260
|
}
|
|
1262
1261
|
}
|
|
@@ -1342,9 +1341,11 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1342
1341
|
container,
|
|
1343
1342
|
identity: target.workspace
|
|
1344
1343
|
});
|
|
1344
|
+
prepareSandboxWorkspaceMountTargets(worktree);
|
|
1345
1345
|
const control = materializeSandboxControl({
|
|
1346
1346
|
base: effectiveConfig.controlBase,
|
|
1347
1347
|
repoRoot: effectiveConfig.repoRoot,
|
|
1348
|
+
worktreeRoot: worktree,
|
|
1348
1349
|
project: effectiveConfig.project,
|
|
1349
1350
|
container,
|
|
1350
1351
|
branch,
|
|
@@ -1361,6 +1362,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1361
1362
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1362
1363
|
workspaceViewRoot: workspaceView.root,
|
|
1363
1364
|
controlDir: control.channelDir,
|
|
1365
|
+
controlStatusDir: control.statusDir,
|
|
1364
1366
|
...(target.workspace.mode === 'task-bound'
|
|
1365
1367
|
? {
|
|
1366
1368
|
taskSource: assertSandboxTaskSource(
|
|
@@ -1444,7 +1446,11 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1444
1446
|
'-e',
|
|
1445
1447
|
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1446
1448
|
'-e',
|
|
1449
|
+
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1450
|
+
'-e',
|
|
1447
1451
|
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1452
|
+
'-e',
|
|
1453
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1448
1454
|
'--label',
|
|
1449
1455
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1450
1456
|
...coreVolumes,
|
|
@@ -1466,16 +1472,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1466
1472
|
'/workspace',
|
|
1467
1473
|
effectiveConfig.imageName
|
|
1468
1474
|
]);
|
|
1469
|
-
|
|
1470
|
-
process.execPath,
|
|
1471
|
-
[internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath],
|
|
1472
|
-
{ cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' }
|
|
1473
|
-
);
|
|
1474
|
-
broker.once('error', () => {
|
|
1475
|
-
// Fresh readiness verification below remains the source of truth;
|
|
1476
|
-
// consume detached spawn errors so they cannot crash the command.
|
|
1477
|
-
});
|
|
1478
|
-
broker.unref();
|
|
1475
|
+
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1479
1476
|
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1480
1477
|
} finally {
|
|
1481
1478
|
envFile.cleanup();
|
|
@@ -6,16 +6,27 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { loadConfig } from '../config.ts';
|
|
7
7
|
import type { SandboxConfig } from '../config.ts';
|
|
8
8
|
import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
9
|
-
import { detectEngine } from '../engine.ts';
|
|
9
|
+
import { detectEngine, ENGINES } from '../engine.ts';
|
|
10
10
|
import { hostJoin } from '../engines/wsl2-paths.ts';
|
|
11
11
|
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
12
12
|
import { parseLabels } from './ls.ts';
|
|
13
13
|
import { runEngine, runSafe } from '../shell.ts';
|
|
14
14
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
15
15
|
import type { SandboxTool } from '../tools.ts';
|
|
16
|
+
import { createCleanPermit, formatWorktreeSnapshot, inspectWorktrees } from '../worktree-safety.ts';
|
|
17
|
+
import type { WorktreeRemovalPermit } from '../worktree-safety.ts';
|
|
16
18
|
|
|
17
19
|
const USAGE = `Usage: ai sandbox prune [--dry-run]`;
|
|
18
20
|
|
|
21
|
+
type PruneDependencies = {
|
|
22
|
+
config?: SandboxConfig;
|
|
23
|
+
tools?: SandboxTool[];
|
|
24
|
+
engine?: string;
|
|
25
|
+
labelsOutput?: string;
|
|
26
|
+
confirm?: typeof p.confirm;
|
|
27
|
+
isCancel?: typeof p.isCancel;
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
type OrphanKind = 'shell' | 'worktree' | 'share' | 'tool';
|
|
20
31
|
|
|
21
32
|
export type OrphanGroup = {
|
|
@@ -107,12 +118,19 @@ export function collectOrphanGroups(
|
|
|
107
118
|
return groups;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
export function removeOrphanGroups(
|
|
121
|
+
export function removeOrphanGroups(
|
|
122
|
+
config: SandboxConfig,
|
|
123
|
+
groups: OrphanGroup[],
|
|
124
|
+
permits: ReadonlyMap<string, WorktreeRemovalPermit>,
|
|
125
|
+
allowRegisteredPathFallback = false
|
|
126
|
+
): boolean {
|
|
111
127
|
let removedWorktrees = false;
|
|
112
128
|
for (const group of groups) {
|
|
113
129
|
for (const dir of group.dirs) {
|
|
114
130
|
if (group.kind === 'worktree') {
|
|
115
|
-
|
|
131
|
+
const permit = permits.get(path.resolve(dir));
|
|
132
|
+
if (!permit) throw new Error(`Missing worktree removal permit: ${dir}`);
|
|
133
|
+
removeWorktreeDir(config.repoRoot, group.base, dir, permit, { allowRegisteredPathFallback });
|
|
116
134
|
removedWorktrees = true;
|
|
117
135
|
} else {
|
|
118
136
|
removeManagedDir(group.base, dir);
|
|
@@ -142,7 +160,7 @@ function writeGroups(groups: OrphanGroup[]): void {
|
|
|
142
160
|
}
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
export async function prune(args: string[]): Promise<void> {
|
|
163
|
+
export async function prune(args: string[], dependencies: PruneDependencies = {}): Promise<void> {
|
|
146
164
|
const { values } = parseArgs({
|
|
147
165
|
args,
|
|
148
166
|
strict: true,
|
|
@@ -157,9 +175,9 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
157
175
|
return;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
const config = loadConfig();
|
|
161
|
-
const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
|
|
162
|
-
const engine = detectEngine(config);
|
|
178
|
+
const config = dependencies.config ?? loadConfig();
|
|
179
|
+
const tools = dependencies.tools ?? [...createSandboxCapabilityPlan(config).cleanupInventory];
|
|
180
|
+
const engine = dependencies.engine ?? detectEngine(config);
|
|
163
181
|
const psArgs = [
|
|
164
182
|
'ps',
|
|
165
183
|
'-a',
|
|
@@ -168,14 +186,25 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
168
186
|
'--format',
|
|
169
187
|
'{{.Labels}}'
|
|
170
188
|
];
|
|
171
|
-
let labelsOutput
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
let labelsOutput = dependencies.labelsOutput;
|
|
190
|
+
if (labelsOutput === undefined) {
|
|
191
|
+
try {
|
|
192
|
+
labelsOutput = runEngine(engine, 'docker', psArgs);
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error('Unable to determine active sandbox branches: docker ps failed');
|
|
195
|
+
}
|
|
176
196
|
}
|
|
197
|
+
const confirm = dependencies.confirm ?? p.confirm;
|
|
198
|
+
const isCancel = dependencies.isCancel ?? p.isCancel;
|
|
177
199
|
const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
|
|
178
200
|
const count = orphanCount(groups);
|
|
201
|
+
const worktrees = groups.filter((group) => group.kind === 'worktree').flatMap((group) => group.dirs);
|
|
202
|
+
const inspections = inspectWorktrees(worktrees);
|
|
203
|
+
const blockers = inspections.filter((inspection) => inspection.status !== 'clean');
|
|
204
|
+
const permits = new Map<string, WorktreeRemovalPermit>();
|
|
205
|
+
for (const inspection of inspections) {
|
|
206
|
+
if (inspection.status === 'clean') permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
|
|
207
|
+
}
|
|
179
208
|
|
|
180
209
|
p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
|
|
181
210
|
|
|
@@ -186,23 +215,32 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
writeGroups(groups);
|
|
218
|
+
for (const blocker of blockers) {
|
|
219
|
+
p.log.error(blocker.status === 'failed'
|
|
220
|
+
? `${JSON.stringify(blocker.worktree)}: ${blocker.message}`
|
|
221
|
+
: formatWorktreeSnapshot(blocker.snapshot));
|
|
222
|
+
}
|
|
189
223
|
|
|
190
224
|
if (values['dry-run']) {
|
|
191
225
|
p.outro(pc.green('Dry run complete'));
|
|
192
226
|
return;
|
|
193
227
|
}
|
|
194
228
|
|
|
195
|
-
|
|
229
|
+
if (blockers.length > 0) {
|
|
230
|
+
throw new Error('Refusing to prune because worktree preflight found blocker(s); no orphan state was deleted');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const shouldRemove = await confirm({
|
|
196
234
|
message: `Remove ${count} orphaned sandbox state dirs?`,
|
|
197
235
|
initialValue: true
|
|
198
236
|
});
|
|
199
237
|
|
|
200
|
-
if (
|
|
238
|
+
if (isCancel(shouldRemove) || !shouldRemove) {
|
|
201
239
|
p.outro('Cancelled');
|
|
202
240
|
return;
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
const removedWorktrees = removeOrphanGroups(config, groups);
|
|
243
|
+
const removedWorktrees = removeOrphanGroups(config, groups, permits, engine === ENGINES.WSL2);
|
|
206
244
|
if (removedWorktrees) {
|
|
207
245
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
208
246
|
}
|