@fitlab-ai/agent-infra 0.11.3 → 0.11.5
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 +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/sandbox/control/finalization-response.ts +0 -35
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
+
import { resolveBranchWorktree } from "../git/branch-worktree.ts";
|
|
9
|
+
|
|
8
10
|
import { resolvePlatformProviderContext } from "./context.ts";
|
|
9
11
|
import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.ts";
|
|
10
12
|
import { requirementSectionAnchors } from "./issues.ts";
|
|
@@ -1054,7 +1056,10 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1054
1056
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
1055
1057
|
}
|
|
1056
1058
|
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
1057
|
-
const gitCwd = branch ?
|
|
1059
|
+
const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
|
|
1060
|
+
if (!gitCwd) {
|
|
1061
|
+
return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
|
|
1062
|
+
}
|
|
1058
1063
|
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
1059
1064
|
if (!changedFilesResult.ok) {
|
|
1060
1065
|
return { ...changedFilesResult, type: "network_error" };
|
|
@@ -1082,25 +1087,6 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1082
1087
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
1083
1088
|
}
|
|
1084
1089
|
|
|
1085
|
-
function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
|
|
1086
|
-
try {
|
|
1087
|
-
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1088
|
-
cwd: repositoryRoot,
|
|
1089
|
-
encoding: "utf8",
|
|
1090
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1091
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
1092
|
-
const expectedRef = `refs/heads/${branch}`;
|
|
1093
|
-
for (const record of records) {
|
|
1094
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1095
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1096
|
-
if (worktree && branchRef === expectedRef) return worktree;
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
1100
|
-
}
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1090
|
function loadInLabelMapping(shared: VerificationShared): any {
|
|
1105
1091
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
1106
1092
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1141,42 +1127,16 @@ function resolvePrHeadSha(context: any): any {
|
|
|
1141
1127
|
return fallback();
|
|
1142
1128
|
}
|
|
1143
1129
|
|
|
1144
|
-
const
|
|
1145
|
-
if (!worktreeList.ok) {
|
|
1146
|
-
return fallback();
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
|
|
1130
|
+
const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
|
|
1150
1131
|
if (!matchedWorktree) {
|
|
1151
|
-
return
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
return fallback();
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return headInWorktree;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
|
|
1163
|
-
let currentWorktree = "";
|
|
1164
|
-
for (const rawLine of String(porcelainOutput || "").split("\n")) {
|
|
1165
|
-
const line = rawLine.trimEnd();
|
|
1166
|
-
if (line.startsWith("worktree ")) {
|
|
1167
|
-
currentWorktree = line.slice("worktree ".length).trim();
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
1172
|
-
const usedBranch = line.slice("branch refs/heads/".length).trim();
|
|
1173
|
-
if (usedBranch === branch && currentWorktree) {
|
|
1174
|
-
return currentWorktree;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
type: "check_failed",
|
|
1135
|
+
message: `Task branch '${branch}' is not checked out in a usable worktree`
|
|
1136
|
+
};
|
|
1177
1137
|
}
|
|
1178
1138
|
|
|
1179
|
-
return
|
|
1139
|
+
return gitText(["rev-parse", "HEAD"], matchedWorktree);
|
|
1180
1140
|
}
|
|
1181
1141
|
|
|
1182
1142
|
function interpolate(template: any, taskDir: any, artifactFile: any): any {
|
|
@@ -393,6 +393,7 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
393
393
|
|
|
394
394
|
export function requestSandboxTaskFinalization(params: Readonly<{
|
|
395
395
|
agent: string;
|
|
396
|
+
prepareHandoff?: (binding: Readonly<{ generation: string; requestId: string }>) => string;
|
|
396
397
|
channelDir?: string;
|
|
397
398
|
statusDir?: string;
|
|
398
399
|
token?: string;
|
|
@@ -403,15 +404,18 @@ export function requestSandboxTaskFinalization(params: Readonly<{
|
|
|
403
404
|
if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
404
405
|
const auth = authority(params);
|
|
405
406
|
const issuedAt = Date.now();
|
|
407
|
+
const id = randomUUID();
|
|
408
|
+
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
406
409
|
const request: SandboxTaskFinalizationRequest = {
|
|
407
410
|
version: 3,
|
|
408
|
-
id
|
|
411
|
+
id,
|
|
409
412
|
...auth,
|
|
410
413
|
issuedAt,
|
|
411
414
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
412
415
|
family: 'task-finalization',
|
|
413
416
|
operation: 'complete',
|
|
414
417
|
agent,
|
|
418
|
+
handoffSha256,
|
|
415
419
|
args: [],
|
|
416
420
|
controllerProcess: null,
|
|
417
421
|
controllerProof: null
|
|
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
6
7
|
import {
|
|
7
8
|
createTask,
|
|
@@ -41,6 +42,11 @@ import {
|
|
|
41
42
|
openCodexControllerRegistration,
|
|
42
43
|
resolveCodexControllerBinding
|
|
43
44
|
} from './controller-registration.ts';
|
|
45
|
+
import { shareBranchDir } from '../constants.ts';
|
|
46
|
+
|
|
47
|
+
function handoffDirectory(manifest: SandboxControlManifest): string {
|
|
48
|
+
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
export type SandboxControlExecutionResult = {
|
|
46
52
|
exitCode: number;
|
|
@@ -373,10 +379,11 @@ async function executeRequestInner(
|
|
|
373
379
|
}
|
|
374
380
|
}
|
|
375
381
|
if (request.family === 'task-finalization') {
|
|
376
|
-
const
|
|
382
|
+
const parsed = parseTaskControlOperation(
|
|
377
383
|
'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
|
|
378
384
|
);
|
|
379
|
-
if (
|
|
385
|
+
if (parsed.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
386
|
+
const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
|
|
380
387
|
const result = dispatchTaskControlOperation(
|
|
381
388
|
createSandboxExecutorExecutionContext({
|
|
382
389
|
repoRoot: manifest.repoRoot,
|
|
@@ -385,7 +392,8 @@ async function executeRequestInner(
|
|
|
385
392
|
taskId: manifest.taskId!,
|
|
386
393
|
generation: manifest.generation,
|
|
387
394
|
manifestPath,
|
|
388
|
-
requestId: request.id
|
|
395
|
+
requestId: request.id,
|
|
396
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
389
397
|
}),
|
|
390
398
|
operation
|
|
391
399
|
);
|
|
@@ -407,7 +415,8 @@ async function executeRequestInner(
|
|
|
407
415
|
taskId: manifest.taskId!,
|
|
408
416
|
generation: manifest.generation,
|
|
409
417
|
manifestPath,
|
|
410
|
-
requestId: request.id
|
|
418
|
+
requestId: request.id,
|
|
419
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
411
420
|
});
|
|
412
421
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
413
422
|
const result = value as { status?: string };
|
|
@@ -65,7 +65,7 @@ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
|
65
65
|
family: 'task-lifecycle'; args: string[];
|
|
66
66
|
}>;
|
|
67
67
|
export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
|
|
68
|
-
family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
|
|
68
|
+
family: 'task-finalization'; operation: 'complete'; agent: string; handoffSha256: string; args: [];
|
|
69
69
|
}>;
|
|
70
70
|
export type SandboxTaskCreateRequest = RequestBase & Readonly<{
|
|
71
71
|
family: 'task-create'; candidate: TaskCreateCandidateV1;
|
|
@@ -279,11 +279,12 @@ export function validateSandboxControlRequest(
|
|
|
279
279
|
return request as SandboxCodexControllerRequest;
|
|
280
280
|
}
|
|
281
281
|
if (request.family === 'task-finalization') {
|
|
282
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
282
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
283
283
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
284
284
|
|| request.operation !== 'complete'
|
|
285
285
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
286
286
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
287
|
+
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
287
288
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
288
289
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
289
290
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { finalizationTerminalResponse } from './finalization-response.ts';
|
|
2
1
|
import { completedReentryView } from './completed-reentry.ts';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
@@ -77,7 +76,6 @@ import { readCodexControllerRegistration } from './controller-registration.ts';
|
|
|
77
76
|
import { validateSandboxControlIdentity } from './identity-sentinel.ts';
|
|
78
77
|
import {
|
|
79
78
|
mergeSandboxTaskView,
|
|
80
|
-
taskViewAfterFinalization,
|
|
81
79
|
taskViewForManifest,
|
|
82
80
|
type SandboxTaskView
|
|
83
81
|
} from './task-view.ts';
|
|
@@ -207,61 +205,6 @@ export function writeSandboxControlResponse(manifest: SandboxControlManifest, re
|
|
|
207
205
|
return true;
|
|
208
206
|
}
|
|
209
207
|
|
|
210
|
-
type FinalizationRecovery = Readonly<{
|
|
211
|
-
status: 'matched' | 'deferred' | 'not-applicable';
|
|
212
|
-
response?: SandboxControlResponse;
|
|
213
|
-
}>;
|
|
214
|
-
|
|
215
|
-
function finalizationRecoveryResponse(
|
|
216
|
-
manifest: SandboxControlManifest,
|
|
217
|
-
requestId: string,
|
|
218
|
-
exitCode: number
|
|
219
|
-
): FinalizationRecovery {
|
|
220
|
-
if (!manifest.taskId || exitCode !== 0) return { status: 'not-applicable' };
|
|
221
|
-
let receipt;
|
|
222
|
-
try {
|
|
223
|
-
receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
|
|
224
|
-
if (!receipt || !receipt.controlBinding
|
|
225
|
-
|| receipt.controlBinding.generation !== manifest.generation
|
|
226
|
-
|| receipt.controlBinding.requestId !== requestId) return { status: 'deferred' };
|
|
227
|
-
const resolved = resolveTaskRef(manifest.taskId, { repoRoot: manifest.repoRoot });
|
|
228
|
-
if (!resolved.ok || resolved.state !== 'completed' || receipt.lifecycle !== 'done') return { status: 'deferred' };
|
|
229
|
-
} catch {
|
|
230
|
-
return { status: 'deferred' };
|
|
231
|
-
}
|
|
232
|
-
return { status: 'matched', response: finalizationTerminalResponse(manifest.taskId, requestId, receipt) };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function publishFinalizationTaskView(
|
|
236
|
-
manifest: SandboxControlManifest,
|
|
237
|
-
broker: BrokerOwner,
|
|
238
|
-
requestId: string,
|
|
239
|
-
state: 'starting' | 'healthy' | 'busy' | 'parked',
|
|
240
|
-
reasonCode: string | null,
|
|
241
|
-
activeRequestId: string | null
|
|
242
|
-
): SandboxTaskView {
|
|
243
|
-
let view: SandboxTaskView;
|
|
244
|
-
try {
|
|
245
|
-
const resolved = resolveTaskRef(manifest.taskId ?? '', { repoRoot: manifest.repoRoot });
|
|
246
|
-
const receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId ?? '');
|
|
247
|
-
view = resolved.ok && resolved.state === 'completed'
|
|
248
|
-
? taskViewAfterFinalization({
|
|
249
|
-
taskId: manifest.taskId ?? '', generation: manifest.generation, requestId, receipt
|
|
250
|
-
})
|
|
251
|
-
: {
|
|
252
|
-
state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null,
|
|
253
|
-
reasonCode: 'SANDBOX_TASK_VIEW_SOURCE_UNCONFIRMED'
|
|
254
|
-
};
|
|
255
|
-
} catch {
|
|
256
|
-
view = {
|
|
257
|
-
state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null,
|
|
258
|
-
reasonCode: 'SANDBOX_TASK_VIEW_RECEIPT_INVALID'
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
writeSandboxControlStatus(manifest, broker, state, reasonCode, activeRequestId, Date.now(), view);
|
|
262
|
-
return view;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
208
|
function payloadReference(payload: ReturnType<typeof createSandboxControlPayload>) {
|
|
266
209
|
return {
|
|
267
210
|
version: payload.version,
|
|
@@ -458,11 +401,7 @@ export async function recoveryResponse(
|
|
|
458
401
|
}, payload.stdout);
|
|
459
402
|
if (JSON.stringify(payloadTerminal) !== JSON.stringify(terminalResult)) return unknown(request.id);
|
|
460
403
|
}
|
|
461
|
-
const
|
|
462
|
-
if (evidence.exitCode === 0 && finalization?.status === 'deferred') return null;
|
|
463
|
-
const recovery: RecoveryDomainEvidence = finalization
|
|
464
|
-
? { domain: { consistent: finalization.status === 'matched' } }
|
|
465
|
-
: await readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
|
|
404
|
+
const recovery = await readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
|
|
466
405
|
const binding = operationRecoveryBinding(request.id, manifest.generation, manifest.taskId, request.family, operationName!);
|
|
467
406
|
const decision = classifySandboxControlRecovery({
|
|
468
407
|
operation,
|
|
@@ -476,16 +415,11 @@ export async function recoveryResponse(
|
|
|
476
415
|
if (decision.outcome === 'unknown' || decision.outcome === 'in-progress') return unknown(request.id);
|
|
477
416
|
if (decision.outcome === 'not-executed') return notExecuted(request.id);
|
|
478
417
|
if (decision.outcome !== 'success' && decision.outcome !== 'failure') return null;
|
|
479
|
-
if (request.family === 'task-finalization' && evidence.exitCode === 0) {
|
|
480
|
-
if (finalization?.status !== 'matched') return null;
|
|
481
|
-
return finalization.response ?? null;
|
|
482
|
-
}
|
|
483
418
|
return genericRecoveryResponse(request, evidence.exitCode, payload);
|
|
484
419
|
}
|
|
485
420
|
|
|
486
421
|
async function terminalMatchesEvidence(
|
|
487
422
|
manifest: SandboxControlManifest,
|
|
488
|
-
manifestPath: string,
|
|
489
423
|
request: SandboxControlRequest,
|
|
490
424
|
response: SandboxControlResponse,
|
|
491
425
|
evidence: ReturnType<typeof readSandboxControlResultEvidence>,
|
|
@@ -493,14 +427,6 @@ async function terminalMatchesEvidence(
|
|
|
493
427
|
payloadInvalid: boolean,
|
|
494
428
|
terminalResult: ReturnType<typeof readSandboxControlTerminalResult> | null
|
|
495
429
|
): Promise<{ valid: boolean; payloadReferenced: boolean }> {
|
|
496
|
-
if (request.family === 'task-finalization' && evidence.exitCode === 0) {
|
|
497
|
-
if (!terminalResult) return { valid: false, payloadReferenced: false };
|
|
498
|
-
const expected = await recoveryResponse(manifest, manifestPath, request, evidence, payload, terminalResult);
|
|
499
|
-
return {
|
|
500
|
-
valid: expected !== null && JSON.stringify(response) === JSON.stringify(expected),
|
|
501
|
-
payloadReferenced: false
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
430
|
if (response.version !== 2 || response.id !== request.id || response.phase !== 'completed'
|
|
505
431
|
|| response.exitCode !== evidence.exitCode || response.error !== null) {
|
|
506
432
|
return { valid: false, payloadReferenced: false };
|
|
@@ -539,7 +465,6 @@ function publishExecutionResult(
|
|
|
539
465
|
manifest: SandboxControlManifest,
|
|
540
466
|
request: SandboxControlRequest,
|
|
541
467
|
result: SandboxControlExecutionResult,
|
|
542
|
-
broker: BrokerOwner,
|
|
543
468
|
brokerOwns: () => boolean
|
|
544
469
|
): boolean {
|
|
545
470
|
const normalized = sanitizeSandboxControlResult(manifest, result);
|
|
@@ -553,41 +478,26 @@ function publishExecutionResult(
|
|
|
553
478
|
criticalRequestPhase(manifest, request, 'evidence-written', normalized.exitCode === 0 ? 'success' : 'failure');
|
|
554
479
|
criticalRequestPhase(manifest, request, 'publish-authorized', normalized.exitCode === 0 ? 'success' : 'failure');
|
|
555
480
|
}
|
|
556
|
-
let terminal: SandboxControlResponse
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
if (!terminal) {
|
|
563
|
-
const inline: SandboxControlResponse = {
|
|
564
|
-
version: 2, id: request.id, phase: 'completed', exitCode: normalized.exitCode,
|
|
565
|
-
stdout: normalized.stdout, stderr: normalized.stderr, error: null
|
|
566
|
-
};
|
|
567
|
-
if (sandboxControlEncodedJsonBytes(inline) <= SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES) {
|
|
568
|
-
terminal = inline;
|
|
569
|
-
}
|
|
481
|
+
let terminal: SandboxControlResponse = {
|
|
482
|
+
version: 2, id: request.id, phase: 'completed', exitCode: normalized.exitCode,
|
|
483
|
+
stdout: normalized.stdout, stderr: normalized.stderr, error: null
|
|
484
|
+
};
|
|
485
|
+
if (sandboxControlEncodedJsonBytes(terminal) > SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES) {
|
|
570
486
|
let payload: ReturnType<typeof createSandboxControlPayload> | null = null;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
} else {
|
|
578
|
-
payload = null;
|
|
579
|
-
}
|
|
580
|
-
} catch {
|
|
487
|
+
try {
|
|
488
|
+
payload = createSandboxControlPayload(manifest, request.id, normalized);
|
|
489
|
+
const usage = sandboxControlGenerationUsage(manifest);
|
|
490
|
+
if (usage.bytes + sandboxControlEncodedJsonBytes(payload) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES) {
|
|
491
|
+
writeSandboxControlPayload(manifest, request.id, normalized);
|
|
492
|
+
} else {
|
|
581
493
|
payload = null;
|
|
582
494
|
}
|
|
583
|
-
|
|
495
|
+
} catch {
|
|
496
|
+
payload = null;
|
|
584
497
|
}
|
|
498
|
+
terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish');
|
|
585
499
|
}
|
|
586
500
|
if (!brokerOwns()) return false;
|
|
587
|
-
if (request.family === 'task-finalization' && normalized.exitCode === 0) {
|
|
588
|
-
const view = publishFinalizationTaskView(manifest, broker, request.id, 'healthy', null, null);
|
|
589
|
-
if (view.state === 'unknown') return false;
|
|
590
|
-
}
|
|
591
501
|
const committed = writeSandboxControlResponse(manifest, terminal);
|
|
592
502
|
if (committed && fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
|
|
593
503
|
criticalRequestPhase(manifest, request, 'published-committed', normalized.exitCode === 0 ? 'success' : 'failure');
|
|
@@ -780,7 +690,6 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
780
690
|
if (!terminalResult && request.family !== 'task-create') continue;
|
|
781
691
|
const reconciliation = await terminalMatchesEvidence(
|
|
782
692
|
manifest,
|
|
783
|
-
manifestPath,
|
|
784
693
|
request,
|
|
785
694
|
existingTerminalResponse!,
|
|
786
695
|
resultEvidence,
|
|
@@ -791,10 +700,6 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
791
700
|
if (!reconciliation.valid) continue;
|
|
792
701
|
terminalReconciled = true;
|
|
793
702
|
payloadReferenced = reconciliation.payloadReferenced;
|
|
794
|
-
if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
|
|
795
|
-
const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
|
|
796
|
-
if (view.state === 'unknown') continue;
|
|
797
|
-
}
|
|
798
703
|
}
|
|
799
704
|
if (!terminalResult && !terminalReconciled) {
|
|
800
705
|
if (terminal) continue;
|
|
@@ -814,17 +719,13 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
814
719
|
if (!resultEvidence || !request || payloadInvalid) continue;
|
|
815
720
|
const recovered = await recoveryResponse(manifest, manifestPath, request, resultEvidence, payload, terminalResult!);
|
|
816
721
|
if (!recovered) continue;
|
|
817
|
-
if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
|
|
818
|
-
const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
|
|
819
|
-
if (view.state === 'unknown') continue;
|
|
820
|
-
}
|
|
821
722
|
writeSandboxControlResponse(manifest, recovered);
|
|
822
723
|
const preserveRecoveryEvidence = recovered.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN';
|
|
823
724
|
if (transitionProtocolActive && !preserveRecoveryEvidence) {
|
|
824
725
|
writeSandboxControlTransition(manifest, { requestId: entry.name, phase: 'recovered' });
|
|
825
726
|
}
|
|
826
727
|
if (preserveRecoveryEvidence) continue;
|
|
827
|
-
payloadReferenced = Boolean(payload)
|
|
728
|
+
payloadReferenced = Boolean(payload);
|
|
828
729
|
}
|
|
829
730
|
} else {
|
|
830
731
|
if (!brokerOwns()) return false;
|
|
@@ -922,14 +823,10 @@ export async function serveSandboxControl(
|
|
|
922
823
|
try {
|
|
923
824
|
startupReceipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
|
|
924
825
|
} catch {
|
|
925
|
-
// Preserve malformed receipt evidence as unknown rather than treating it as no receipt.
|
|
926
826
|
startupReceipt = {};
|
|
927
827
|
}
|
|
928
828
|
}
|
|
929
|
-
let taskView = taskViewForManifest({
|
|
930
|
-
...manifest,
|
|
931
|
-
receipt: startupReceipt
|
|
932
|
-
});
|
|
829
|
+
let taskView = taskViewForManifest({ ...manifest, receipt: startupReceipt });
|
|
933
830
|
try {
|
|
934
831
|
const previous = readSandboxControlStatus(manifest.publicStatusDir);
|
|
935
832
|
if (previous.generation === manifest.generation) taskView = mergeSandboxTaskView(taskView, previous.taskView);
|
|
@@ -1086,15 +983,7 @@ export async function serveSandboxControl(
|
|
|
1086
983
|
if (!brokerOwns()) break;
|
|
1087
984
|
let terminalCommitted = false;
|
|
1088
985
|
if (settledExecution.result && settledExecution.resultEvidenceWritten) {
|
|
1089
|
-
terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result,
|
|
1090
|
-
if (terminalCommitted && settledExecution.request.family === 'task-finalization') {
|
|
1091
|
-
try {
|
|
1092
|
-
const published = readSandboxControlStatus(manifest.publicStatusDir);
|
|
1093
|
-
if (published.generation === manifest.generation) taskView = published.taskView;
|
|
1094
|
-
} catch {
|
|
1095
|
-
taskView = { state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null, reasonCode: 'SANDBOX_TASK_VIEW_STATUS_INVALID' };
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
986
|
+
terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, brokerOwns);
|
|
1098
987
|
} else {
|
|
1099
988
|
terminalCommitted = writeSandboxControlResponse(manifest, unknown(settledExecution.request.id));
|
|
1100
989
|
}
|
|
@@ -1341,7 +1230,7 @@ export async function serveSandboxControl(
|
|
|
1341
1230
|
}
|
|
1342
1231
|
}
|
|
1343
1232
|
if (owned && active.result && active.resultEvidenceWritten) {
|
|
1344
|
-
if (publishExecutionResult(manifest, active.request, active.result,
|
|
1233
|
+
if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
|
|
1345
1234
|
if (brokerOwns()) {
|
|
1346
1235
|
removeAcceptedResponse(manifest, active.request.id);
|
|
1347
1236
|
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
@@ -1350,22 +1239,8 @@ export async function serveSandboxControl(
|
|
|
1350
1239
|
}
|
|
1351
1240
|
}
|
|
1352
1241
|
if (active) {
|
|
1353
|
-
|
|
1354
|
-
if (owned && brokerOwns() && active
|
|
1355
|
-
const recovered = finalizationRecoveryResponse(manifest, active.request.id, 0);
|
|
1356
|
-
const view = recovered.status === 'matched'
|
|
1357
|
-
? publishFinalizationTaskView(manifest, broker, active.request.id, 'healthy', null, null)
|
|
1358
|
-
: null;
|
|
1359
|
-
if (view?.state !== 'unknown' && recovered.status === 'matched' && recovered.response
|
|
1360
|
-
&& writeSandboxControlResponse(manifest, recovered.response)) {
|
|
1361
|
-
if (terminationConfirmed && brokerOwns()) {
|
|
1362
|
-
removeAcceptedResponse(manifest, active.request.id);
|
|
1363
|
-
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
1364
|
-
active = null;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
if (owned && brokerOwns() && active && active.request.family !== 'task-finalization' && !active.resultEvidenceWritten) {
|
|
1242
|
+
active.prepared.terminate(owned);
|
|
1243
|
+
if (owned && brokerOwns() && active && !active.resultEvidenceWritten) {
|
|
1369
1244
|
writeSandboxControlResponse(manifest, unknown(active.request.id));
|
|
1370
1245
|
}
|
|
1371
1246
|
}
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
|
|
|
47
47
|
import type { SandboxTool } from './tools.ts';
|
|
48
48
|
import { getProcessStartTime } from '../server/process-state.ts';
|
|
49
49
|
import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
|
|
50
|
+
import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
|
|
50
51
|
import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
|
|
51
52
|
import {
|
|
52
53
|
formatIntermediateCleanupReport,
|
|
@@ -959,7 +960,10 @@ async function rmOne(
|
|
|
959
960
|
branch: string,
|
|
960
961
|
options: RmOneOptions = {}
|
|
961
962
|
): Promise<void> {
|
|
962
|
-
await
|
|
963
|
+
await withRepositoryMutationLock(
|
|
964
|
+
config.repoRoot,
|
|
965
|
+
() => runRmOneUnderRepositoryLock(config, tools, branch, options)
|
|
966
|
+
);
|
|
963
967
|
}
|
|
964
968
|
|
|
965
969
|
async function removeUncheckedSandbox(
|
|
@@ -998,7 +1002,16 @@ async function removeUncheckedSandbox(
|
|
|
998
1002
|
return null;
|
|
999
1003
|
}
|
|
1000
1004
|
|
|
1005
|
+
const worktreePermits = options.permits ?? await authorizeWorktrees(
|
|
1006
|
+
existingWorktrees,
|
|
1007
|
+
{ allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) },
|
|
1008
|
+
{ ...options.prompt, interactive: options.interactive }
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1001
1011
|
for (const container of matchedContainers) runSafeEngine(engine, 'docker', ['rm', '-f', container]);
|
|
1012
|
+
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
1013
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
1014
|
+
}
|
|
1002
1015
|
for (const root of controlRoots) {
|
|
1003
1016
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1004
1017
|
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
@@ -1011,12 +1024,15 @@ async function removeUncheckedSandbox(
|
|
|
1011
1024
|
for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
|
|
1012
1025
|
if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
|
|
1013
1026
|
if (shouldRemoveWorktree) {
|
|
1014
|
-
for (const worktree of existingWorktrees)
|
|
1015
|
-
|
|
1027
|
+
for (const worktree of existingWorktrees) {
|
|
1028
|
+
const permit = worktreePermits.get(path.resolve(worktree));
|
|
1029
|
+
if (!permit) throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
|
|
1030
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
|
|
1031
|
+
}
|
|
1016
1032
|
}
|
|
1017
|
-
if (shouldDeleteBranch)
|
|
1018
|
-
|
|
1019
|
-
|
|
1033
|
+
if (shouldDeleteBranch) {
|
|
1034
|
+
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
1035
|
+
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
1020
1036
|
}
|
|
1021
1037
|
if (target.workspace.mode === 'task-bound') {
|
|
1022
1038
|
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
@@ -1045,7 +1061,7 @@ async function rmPurge(
|
|
|
1045
1061
|
tools: SandboxTool[],
|
|
1046
1062
|
prompt: PromptDependencies = {}
|
|
1047
1063
|
): Promise<void> {
|
|
1048
|
-
return rmPurgeCore(config, tools, prompt);
|
|
1064
|
+
return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
|
|
1049
1065
|
}
|
|
1050
1066
|
|
|
1051
1067
|
async function rmPurgeCore(
|
|
@@ -8,7 +8,6 @@ import { enumerateAllTaskDirs } from '../task/resolve-ref.ts';
|
|
|
8
8
|
import { resolveSandboxCleanupTarget } from './workspace-identity.ts';
|
|
9
9
|
import { readSandboxControlManifest } from './control/lifecycle.ts';
|
|
10
10
|
import { readJsonFile, readSandboxControlStatus } from './control/state.ts';
|
|
11
|
-
import { finalizationTerminalResponse } from './control/finalization-response.ts';
|
|
12
11
|
|
|
13
12
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
14
13
|
|
|
@@ -83,13 +82,10 @@ function canonicalPath(input: string): string {
|
|
|
83
82
|
|
|
84
83
|
function taskFinalizationReceiptComplete(
|
|
85
84
|
receipt: TaskFinalizationReceipt,
|
|
86
|
-
taskId: string
|
|
87
|
-
binding: SandboxControlBinding
|
|
85
|
+
taskId: string
|
|
88
86
|
): boolean {
|
|
89
87
|
return receipt.taskId === taskId
|
|
90
|
-
&& taskFinalizationReceiptState(receipt) === 'complete'
|
|
91
|
-
&& receipt.controlBinding?.generation === binding.generation
|
|
92
|
-
&& receipt.controlBinding.requestId === binding.requestId;
|
|
88
|
+
&& taskFinalizationReceiptState(receipt) === 'complete';
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
function terminalControlBindingEvidence(
|
|
@@ -114,24 +110,20 @@ function terminalControlBindingEvidence(
|
|
|
114
110
|
if (errorCode(error) !== 'ENOENT') return false;
|
|
115
111
|
}
|
|
116
112
|
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
117
|
-
if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId
|
|
113
|
+
if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId)) {
|
|
118
114
|
return false;
|
|
119
115
|
}
|
|
120
|
-
const status = readSandboxControlStatus(manifest.publicStatusDir);
|
|
121
|
-
const viewReceipt = status.taskView.receipt;
|
|
122
|
-
const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
|
|
123
|
-
|| (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
|
|
124
|
-
if (status.generation !== binding.generation || status.state !== 'healthy'
|
|
125
|
-
|| status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
|
|
126
|
-
|| !viewReceipt || viewReceipt.generation !== binding.generation
|
|
127
|
-
|| viewReceipt.requestId !== binding.requestId
|
|
128
|
-
|| viewReceipt.receiptId !== receipt.receiptId
|
|
129
|
-
|| viewReceipt.revision !== receipt.revision) return false;
|
|
130
116
|
const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
|
|
131
117
|
if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file')) return false;
|
|
132
118
|
const actual = readJsonFile(responsePath);
|
|
133
|
-
const
|
|
134
|
-
|
|
119
|
+
const response = actual as Record<string, unknown>;
|
|
120
|
+
if (response.version !== 2 || response.id !== binding.requestId || response.phase !== 'completed'
|
|
121
|
+
|| response.exitCode !== 0 || typeof response.stdout !== 'string') return false;
|
|
122
|
+
const envelope = JSON.parse(response.stdout) as Record<string, unknown>;
|
|
123
|
+
const result = envelope.result as Record<string, unknown> | null;
|
|
124
|
+
return envelope.status === 'completed' && envelope.accepted === true
|
|
125
|
+
&& result !== null && typeof result === 'object'
|
|
126
|
+
&& result.status === 'completed' && result.taskId === taskId;
|
|
135
127
|
} catch {
|
|
136
128
|
return false;
|
|
137
129
|
}
|
|
@@ -157,7 +149,7 @@ function removedControlBindingEvidence(
|
|
|
157
149
|
if (frontmatter.id !== taskId || frontmatter.branch !== journal.target.branch) return false;
|
|
158
150
|
try {
|
|
159
151
|
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
160
|
-
return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId
|
|
152
|
+
return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId);
|
|
161
153
|
} catch {
|
|
162
154
|
return false;
|
|
163
155
|
}
|
|
@@ -14,7 +14,6 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
14
14
|
import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
|
|
15
15
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
18
17
|
|
|
19
18
|
type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
|
|
20
19
|
type PrReviewOutcome = 'aborted' | 'superseded';
|
|
@@ -398,7 +397,7 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
|
|
|
398
397
|
if ('code' in validated) return failed(intent, validated.code, validated.message);
|
|
399
398
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
400
399
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
401
|
-
if (resolved.state !== 'active'
|
|
400
|
+
if (resolved.state !== 'active') {
|
|
402
401
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
403
402
|
}
|
|
404
403
|
if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
|