@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,289 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { loadConfig } from '../../sandbox/config.js';
|
|
7
|
+
import { detectEngine } from '../../sandbox/engine.js';
|
|
8
|
+
import { runEngine } from '../../sandbox/shell.js';
|
|
9
|
+
import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.js';
|
|
10
|
+
import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.js';
|
|
11
|
+
import { containerNameCandidates } from '../../sandbox/constants.js';
|
|
12
|
+
import { resolveSandboxTarget } from '../../sandbox/workspace-identity.js';
|
|
13
|
+
import { sandboxControlPaths } from '../../sandbox/workspace-view.js';
|
|
14
|
+
import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../../sandbox/control/state.js';
|
|
15
|
+
import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../../sandbox/control/protocol.js';
|
|
16
|
+
import { getProcessStartTime } from '../../server/process-state.js';
|
|
17
|
+
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
18
|
+
const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
|
|
19
|
+
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
20
|
+
const RECOVERY_GRACE_MS = 30_000;
|
|
21
|
+
export function parseValidateArgs(args) {
|
|
22
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
23
|
+
return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', command: [], help: true };
|
|
24
|
+
}
|
|
25
|
+
const separator = args.indexOf('--');
|
|
26
|
+
if (separator < 0)
|
|
27
|
+
throw new Error(`${USAGE}\nA literal -- must separate validation options from the command.`);
|
|
28
|
+
const optionArgs = args.slice(0, separator);
|
|
29
|
+
const command = args.slice(separator + 1);
|
|
30
|
+
let scope = 'snapshot';
|
|
31
|
+
let timeoutMs = 300_000;
|
|
32
|
+
let format = 'text';
|
|
33
|
+
const positionals = [];
|
|
34
|
+
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
35
|
+
const arg = optionArgs[index];
|
|
36
|
+
if (arg === '--scope') {
|
|
37
|
+
const value = optionArgs[++index];
|
|
38
|
+
if (value !== 'snapshot' && value !== 'inplace')
|
|
39
|
+
throw new Error('validate --scope must be snapshot or inplace');
|
|
40
|
+
scope = value;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === '--timeout') {
|
|
43
|
+
const value = Number(optionArgs[++index]);
|
|
44
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS)
|
|
45
|
+
throw new Error(`validate --timeout must be 1..${MAX_TIMEOUT_MS}`);
|
|
46
|
+
timeoutMs = value;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--format') {
|
|
49
|
+
const value = optionArgs[++index];
|
|
50
|
+
if (value !== 'text' && value !== 'json')
|
|
51
|
+
throw new Error('validate --format must be text or json');
|
|
52
|
+
format = value;
|
|
53
|
+
}
|
|
54
|
+
else if (arg.startsWith('-')) {
|
|
55
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
positionals.push(arg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (positionals.length !== 1 || command.length === 0)
|
|
62
|
+
throw new Error(USAGE);
|
|
63
|
+
return { target: positionals[0], scope, timeoutMs, format, command, help: false };
|
|
64
|
+
}
|
|
65
|
+
function runValidationCommand(command, cwd, scope, timeoutMs, json) {
|
|
66
|
+
const [file, ...args] = command;
|
|
67
|
+
const result = spawnSync(file, args, {
|
|
68
|
+
cwd,
|
|
69
|
+
env: { ...process.env, AGENT_INFRA_VALIDATION_SCOPE: scope },
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
73
|
+
shell: process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file)
|
|
74
|
+
});
|
|
75
|
+
if (json) {
|
|
76
|
+
if (result.stdout)
|
|
77
|
+
process.stderr.write(result.stdout);
|
|
78
|
+
if (result.stderr)
|
|
79
|
+
process.stderr.write(result.stderr);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (result.stdout)
|
|
83
|
+
process.stdout.write(result.stdout);
|
|
84
|
+
if (result.stderr)
|
|
85
|
+
process.stderr.write(result.stderr);
|
|
86
|
+
}
|
|
87
|
+
return { exitCode: result.status ?? 1, signal: result.signal ?? null };
|
|
88
|
+
}
|
|
89
|
+
function git(root, args) {
|
|
90
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
91
|
+
}
|
|
92
|
+
function waitForState(statusDir, predicate, timeoutMs = 5_000) {
|
|
93
|
+
const deadline = Date.now() + timeoutMs;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
try {
|
|
96
|
+
const status = readSandboxControlStatus(statusDir);
|
|
97
|
+
if (predicate(status))
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Atomic status publication may be between renames.
|
|
102
|
+
}
|
|
103
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
104
|
+
}
|
|
105
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
|
|
106
|
+
}
|
|
107
|
+
function snapshotValidation(repoRoot, commit, options) {
|
|
108
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
|
|
109
|
+
const worktree = path.join(tempRoot, 'worktree');
|
|
110
|
+
let cleanup = 'not-created';
|
|
111
|
+
try {
|
|
112
|
+
git(repoRoot, ['worktree', 'add', '--detach', worktree, commit]);
|
|
113
|
+
cleanup = 'pending';
|
|
114
|
+
return { ...runValidationCommand(options.command, worktree, 'snapshot', options.timeoutMs, options.format === 'json'), cleanup: () => cleanup };
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
let cleanupError = null;
|
|
118
|
+
if (fs.existsSync(worktree)) {
|
|
119
|
+
try {
|
|
120
|
+
git(repoRoot, ['worktree', 'remove', '--force', worktree]);
|
|
121
|
+
cleanup = 'completed';
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
cleanupError = error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
git(repoRoot, ['worktree', 'prune']);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
cleanupError ??= error;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
cleanupError ??= error;
|
|
138
|
+
}
|
|
139
|
+
if (cleanupError)
|
|
140
|
+
throw cleanupError;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function inplaceValidation(config, target, options) {
|
|
144
|
+
const engine = detectEngine(config);
|
|
145
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
146
|
+
const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
|
|
147
|
+
if (!row)
|
|
148
|
+
throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
|
|
149
|
+
const control = sandboxControlPaths({
|
|
150
|
+
base: config.controlBase, project: config.project,
|
|
151
|
+
container: row.name, identity: target.workspace
|
|
152
|
+
});
|
|
153
|
+
const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
|
|
154
|
+
const initial = readSandboxControlStatus(control.statusDir);
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (initial.generation !== manifest.generation
|
|
157
|
+
|| now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
|
|
158
|
+
|| initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
159
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
|
|
160
|
+
}
|
|
161
|
+
if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
|
|
162
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
|
|
163
|
+
}
|
|
164
|
+
const startTime = getProcessStartTime(process.pid);
|
|
165
|
+
if (!startTime)
|
|
166
|
+
throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
|
|
167
|
+
const leasePath = path.join(control.root, 'lease.json');
|
|
168
|
+
const nonce = randomUUID();
|
|
169
|
+
const issuedAt = Date.now();
|
|
170
|
+
const lease = {
|
|
171
|
+
version: 1, generation: manifest.generation, nonce,
|
|
172
|
+
owner: { pid: process.pid, startTime }, issuedAt,
|
|
173
|
+
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
174
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
175
|
+
branch: target.branch, reason: 'manual-validation'
|
|
176
|
+
};
|
|
177
|
+
const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
178
|
+
const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
179
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
180
|
+
let stopped = false;
|
|
181
|
+
let restored = false;
|
|
182
|
+
try {
|
|
183
|
+
appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
|
|
184
|
+
waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
185
|
+
if (row.running) {
|
|
186
|
+
runEngine(engine, 'docker', ['stop', row.name]);
|
|
187
|
+
stopped = true;
|
|
188
|
+
}
|
|
189
|
+
const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
190
|
+
const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
|
|
191
|
+
if (!refreshed || refreshed.running)
|
|
192
|
+
throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
|
|
193
|
+
const activeLease = readActiveLease(manifest);
|
|
194
|
+
if (!activeLease || activeLease.nonce !== nonce)
|
|
195
|
+
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
196
|
+
assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
|
|
197
|
+
const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
198
|
+
return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
const recoveryErrors = [];
|
|
202
|
+
try {
|
|
203
|
+
const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
204
|
+
if (currentBranch !== originalBranch)
|
|
205
|
+
git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
try {
|
|
209
|
+
git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
|
|
210
|
+
git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
211
|
+
}
|
|
212
|
+
catch (fallbackError) {
|
|
213
|
+
recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (stopped) {
|
|
217
|
+
try {
|
|
218
|
+
runEngine(engine, 'docker', ['start', row.name]);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
226
|
+
if (currentLease.nonce !== nonce)
|
|
227
|
+
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
231
|
+
}
|
|
232
|
+
if (recoveryErrors.length === 0) {
|
|
233
|
+
try {
|
|
234
|
+
fs.unlinkSync(leasePath);
|
|
235
|
+
appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
|
|
236
|
+
waitForState(control.statusDir, (status) => status.state === 'healthy');
|
|
237
|
+
restored = true;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
try {
|
|
241
|
+
if (!fs.existsSync(leasePath)) {
|
|
242
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
243
|
+
}
|
|
244
|
+
appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
|
|
245
|
+
}
|
|
246
|
+
catch (leaseError) {
|
|
247
|
+
recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
|
|
248
|
+
}
|
|
249
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (recoveryErrors.length > 0) {
|
|
253
|
+
throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export async function validate(args) {
|
|
258
|
+
const options = parseValidateArgs(args);
|
|
259
|
+
if (options.help) {
|
|
260
|
+
process.stdout.write(`${USAGE}\n`);
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
const config = loadConfig();
|
|
264
|
+
const target = resolveSandboxTarget(options.target, config.repoRoot);
|
|
265
|
+
const commit = git(config.repoRoot, ['rev-parse', target.branch]);
|
|
266
|
+
const startedAt = new Date().toISOString();
|
|
267
|
+
const result = options.scope === 'snapshot'
|
|
268
|
+
? snapshotValidation(config.repoRoot, commit, options)
|
|
269
|
+
: inplaceValidation(config, target, options);
|
|
270
|
+
const evidence = {
|
|
271
|
+
version: 1,
|
|
272
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
273
|
+
branch: target.branch,
|
|
274
|
+
scope: options.scope,
|
|
275
|
+
commit,
|
|
276
|
+
command: path.basename(options.command[0]),
|
|
277
|
+
startedAt,
|
|
278
|
+
completedAt: new Date().toISOString(),
|
|
279
|
+
exitCode: result.exitCode,
|
|
280
|
+
signal: result.signal,
|
|
281
|
+
cleanup: result.cleanup()
|
|
282
|
+
};
|
|
283
|
+
if (options.format === 'json')
|
|
284
|
+
process.stdout.write(`${JSON.stringify(evidence)}\n`);
|
|
285
|
+
else
|
|
286
|
+
process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
|
|
287
|
+
return result.exitCode;
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.js';
|
|
4
|
+
import { locateActivityLog, appendActivityEntry, pairEntries, parseCommitAttemptStarted } from './activity-log.js';
|
|
5
5
|
import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
|
|
6
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
|
-
import { parseReviewSummary } from './review-artifacts.js';
|
|
8
|
-
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
|
|
7
|
+
import { artifactName, maxRound, parseReviewSummary } from './review-artifacts.js';
|
|
8
|
+
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewedGitTree } from './review-fingerprint.js';
|
|
9
9
|
const SHA_RE = /^[a-f0-9]{40,64}$/;
|
|
10
10
|
function git(repoRoot, args) {
|
|
11
11
|
return execFileSync('git', args, {
|
|
@@ -26,6 +26,22 @@ function isAncestor(repoRoot, ancestor, descendant) {
|
|
|
26
26
|
stdio: 'ignore'
|
|
27
27
|
}).status === 0;
|
|
28
28
|
}
|
|
29
|
+
function hasMatchingApprovedReview(taskDir, beforeRound, baseline, tree) {
|
|
30
|
+
const entries = fs.readdirSync(taskDir);
|
|
31
|
+
for (let round = Math.min(beforeRound - 1, maxRound(entries, 'review-code')); round >= 1; round -= 1) {
|
|
32
|
+
const file = path.join(taskDir, artifactName('review-code', round));
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
continue;
|
|
35
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
36
|
+
const summary = parseReviewSummary(content);
|
|
37
|
+
if (summary.ok
|
|
38
|
+
&& summary.summary.verdict === 'Approved'
|
|
39
|
+
&& extractReviewBaseline(content) === baseline
|
|
40
|
+
&& extractReviewedSnapshotTree(content) === tree)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
29
45
|
function outcome(disposition, overrides = {}) {
|
|
30
46
|
return {
|
|
31
47
|
disposition,
|
|
@@ -34,10 +50,12 @@ function outcome(disposition, overrides = {}) {
|
|
|
34
50
|
currentHead: '',
|
|
35
51
|
committedHead: null,
|
|
36
52
|
commitNote: null,
|
|
53
|
+
activityNote: null,
|
|
37
54
|
needsAnchor: false,
|
|
38
55
|
needsLog: false,
|
|
39
56
|
intent: null,
|
|
40
57
|
intentDigest: null,
|
|
58
|
+
attempt: null,
|
|
41
59
|
...overrides
|
|
42
60
|
};
|
|
43
61
|
}
|
|
@@ -63,6 +81,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
63
81
|
const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
|
|
64
82
|
const file = commitIntentPath(taskDir);
|
|
65
83
|
if (!fs.existsSync(file)) {
|
|
84
|
+
if (openRows.length === 1) {
|
|
85
|
+
const attempt = parseCommitAttemptStarted(openRows[0].note);
|
|
86
|
+
if (attempt && attempt.baseline === currentHead) {
|
|
87
|
+
return outcome('retryable-start', {
|
|
88
|
+
message: 'a structured Commit attempt can be safely reused',
|
|
89
|
+
currentHead,
|
|
90
|
+
attempt
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
66
94
|
if (openRows.length > 0) {
|
|
67
95
|
return outcome('orphaned-start', {
|
|
68
96
|
code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
@@ -119,6 +147,12 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
119
147
|
message: 'repository HEAD or ancestry conflicts with the commit intent'
|
|
120
148
|
});
|
|
121
149
|
}
|
|
150
|
+
const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
|
|
151
|
+
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
152
|
+
const pushOnlyRetry = intent.phase === 'pushed'
|
|
153
|
+
&& intent.baselineHead === intent.committedHead
|
|
154
|
+
&& matchingDone.length === 1
|
|
155
|
+
&& openRows.length === 1;
|
|
122
156
|
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
123
157
|
let needsAnchor = false;
|
|
124
158
|
if (review.ok && review.path) {
|
|
@@ -132,11 +166,13 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
132
166
|
const summary = parseReviewSummary(reviewContent);
|
|
133
167
|
const reviewBaseline = extractReviewBaseline(reviewContent);
|
|
134
168
|
const reviewTree = extractReviewedSnapshotTree(reviewContent);
|
|
135
|
-
if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !
|
|
169
|
+
if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !parseReviewedGitTree(reviewTree)) {
|
|
136
170
|
return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
137
171
|
}
|
|
138
172
|
const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
|
|
139
|
-
|
|
173
|
+
const matchingCurrentReview = reviewBaseline === intent.baselineHead && reviewTree === committedTree;
|
|
174
|
+
if (!matchingCurrentReview
|
|
175
|
+
&& !hasMatchingApprovedReview(taskDir, review.round, intent.baselineHead, committedTree)) {
|
|
140
176
|
return outcome('conflict', {
|
|
141
177
|
...shared,
|
|
142
178
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
@@ -144,18 +180,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
144
180
|
});
|
|
145
181
|
}
|
|
146
182
|
const anchor = String(metadata.last_reviewed_commit ?? '').trim();
|
|
147
|
-
if (anchor && anchor !== intent.committedHead) {
|
|
183
|
+
if (anchor && anchor !== intent.committedHead && anchor !== intent.baselineHead) {
|
|
148
184
|
return outcome('conflict', {
|
|
149
185
|
...shared,
|
|
150
186
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
151
187
|
message: 'task review anchor conflicts with the committed intent'
|
|
152
188
|
});
|
|
153
189
|
}
|
|
154
|
-
needsAnchor = anchor
|
|
190
|
+
needsAnchor = anchor !== intent.committedHead;
|
|
155
191
|
}
|
|
156
|
-
const
|
|
157
|
-
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
158
|
-
const needsLog = matchingDone.length === 0 && openRows.length === 1;
|
|
192
|
+
const needsLog = (matchingDone.length === 0 && openRows.length === 1) || pushOnlyRetry;
|
|
159
193
|
if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
|
|
160
194
|
return outcome('conflict', {
|
|
161
195
|
...shared,
|
|
@@ -168,12 +202,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
168
202
|
...shared,
|
|
169
203
|
message: 'commit finalization can be recovered',
|
|
170
204
|
commitNote,
|
|
205
|
+
activityNote: pushOnlyRetry ? `Pushed ${commitNote}` : commitNote,
|
|
171
206
|
needsAnchor,
|
|
172
207
|
needsLog
|
|
173
208
|
});
|
|
174
209
|
}
|
|
175
210
|
function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
|
|
176
|
-
if (inspection.disposition !== 'recoverable'
|
|
211
|
+
if (inspection.disposition !== 'recoverable'
|
|
212
|
+
|| !inspection.committedHead
|
|
213
|
+
|| !inspection.commitNote
|
|
214
|
+
|| !inspection.activityNote) {
|
|
177
215
|
throw new Error('commit finalization task mutations require a recoverable inspection');
|
|
178
216
|
}
|
|
179
217
|
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
@@ -196,7 +234,7 @@ function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
|
|
|
196
234
|
time: timestamp,
|
|
197
235
|
step: 'Commit',
|
|
198
236
|
agent,
|
|
199
|
-
note: inspection.
|
|
237
|
+
note: inspection.activityNote
|
|
200
238
|
})
|
|
201
239
|
});
|
|
202
240
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { syncPlatformComment } from '../platform/issue-comments.js';
|
|
4
|
+
import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
5
|
+
import { VERSION } from '../version.js';
|
|
6
|
+
import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
7
|
+
import { createLocalTask } from './create.js';
|
|
8
|
+
import { verifyTaskEvent } from './verification.js';
|
|
9
|
+
const DEFAULT_DEPENDENCIES = {
|
|
10
|
+
createIssue: createPlatformIssue,
|
|
11
|
+
syncIssue: syncPlatformIssue,
|
|
12
|
+
syncComment: syncPlatformComment,
|
|
13
|
+
addWarning: applyWorkflowWarningIntent
|
|
14
|
+
};
|
|
15
|
+
function verifyCreatedTask(repoRoot, taskId) {
|
|
16
|
+
const verification = verifyTaskEvent({ taskRef: taskId, event: 'create-task.completed' }, { repoRoot });
|
|
17
|
+
if (verification.status === 'pass') {
|
|
18
|
+
return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
|
|
19
|
+
}
|
|
20
|
+
const blocked = verification.status === 'blocked';
|
|
21
|
+
return {
|
|
22
|
+
operation: {
|
|
23
|
+
name: 'task:verify', status: verification.status,
|
|
24
|
+
reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
|
|
25
|
+
},
|
|
26
|
+
error: {
|
|
27
|
+
code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
|
|
28
|
+
message: `Created task did not pass the ${verification.event} gate`,
|
|
29
|
+
retryable: blocked
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function platformOperations(prefix, operations) {
|
|
34
|
+
return operations.map((operation) => ({
|
|
35
|
+
name: `${prefix}:${operation.name}`,
|
|
36
|
+
status: operation.status,
|
|
37
|
+
reasonCode: operation.reasonCode ?? null
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function issueFromTask(repoRoot, taskId) {
|
|
41
|
+
const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
|
|
42
|
+
const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
|
|
43
|
+
return match ? { number: Number(match[1]), url: '' } : null;
|
|
44
|
+
}
|
|
45
|
+
function createTask(value, options) {
|
|
46
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
|
|
47
|
+
let candidate;
|
|
48
|
+
let local;
|
|
49
|
+
try {
|
|
50
|
+
candidate = value;
|
|
51
|
+
local = createLocalTask(candidate, {
|
|
52
|
+
repoRoot: options.repoRoot,
|
|
53
|
+
agentInfraVersion: options.agentInfraVersion ?? VERSION
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
|
|
59
|
+
return {
|
|
60
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
61
|
+
operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const operations = [{
|
|
65
|
+
name: 'task:local', status: local.status, reasonCode: null
|
|
66
|
+
}];
|
|
67
|
+
let issue = issueFromTask(options.repoRoot, local.task.id);
|
|
68
|
+
const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
69
|
+
operations.push(...platformOperations('platform-create', created.operations));
|
|
70
|
+
if (created.issue)
|
|
71
|
+
issue = { number: created.issue.number, url: created.issue.url };
|
|
72
|
+
let platformFailure = created.status === 'failed' || created.status === 'blocked' ? created : null;
|
|
73
|
+
if (!platformFailure && (created.task.issueNumber || issue)) {
|
|
74
|
+
const synced = dependencies.syncIssue(local.task.id, {
|
|
75
|
+
cwd: options.repoRoot,
|
|
76
|
+
agent: candidate.agent,
|
|
77
|
+
status: 'waiting-for-triage',
|
|
78
|
+
assignees: 'current',
|
|
79
|
+
milestone: 'initial',
|
|
80
|
+
issueType: true,
|
|
81
|
+
fields: true
|
|
82
|
+
});
|
|
83
|
+
operations.push(...platformOperations('platform-sync', synced.operations));
|
|
84
|
+
if (synced.issue)
|
|
85
|
+
issue = { number: synced.issue.number, url: synced.issue.url };
|
|
86
|
+
if (synced.status === 'failed' || synced.status === 'blocked')
|
|
87
|
+
platformFailure = synced;
|
|
88
|
+
if (!platformFailure) {
|
|
89
|
+
const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
|
|
90
|
+
operations.push(...platformOperations('platform-comment', commented.operations));
|
|
91
|
+
if (commented.status === 'failed' || commented.status === 'blocked')
|
|
92
|
+
platformFailure = commented;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (platformFailure) {
|
|
96
|
+
const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
|
|
97
|
+
dependencies.addWarning({
|
|
98
|
+
kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
|
|
99
|
+
code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
|
|
100
|
+
action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
|
|
101
|
+
}, { repoRoot: options.repoRoot });
|
|
102
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
103
|
+
operations.push(verified.operation);
|
|
104
|
+
if (verified.error)
|
|
105
|
+
return {
|
|
106
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
107
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
|
|
111
|
+
changed: local.changed,
|
|
112
|
+
task: local.task,
|
|
113
|
+
issue,
|
|
114
|
+
operations,
|
|
115
|
+
warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
|
|
116
|
+
error
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
120
|
+
operations.push(verified.operation);
|
|
121
|
+
if (verified.error)
|
|
122
|
+
return {
|
|
123
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
124
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
status: local.status,
|
|
128
|
+
changed: local.changed,
|
|
129
|
+
task: local.task,
|
|
130
|
+
issue,
|
|
131
|
+
operations,
|
|
132
|
+
warnings: [],
|
|
133
|
+
error: null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export { createTask };
|
|
137
|
+
//# sourceMappingURL=create-service.js.map
|