@fitlab-ai/agent-infra 0.11.12 → 0.11.13
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/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/sandbox/commands/create.js +6 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/client.js +4 -64
- package/dist/lib/sandbox/control/executor.js +4 -50
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +5 -16
- package/dist/lib/sandbox/control/server.js +203 -234
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +28 -9
- package/dist/lib/sandbox/removal.js +19 -0
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +48 -2
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +2 -0
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +14 -0
- package/dist/lib/task/lifecycle.js +203 -31
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +20 -23
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/sandbox/commands/create.ts +6 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/client.ts +5 -78
- package/lib/sandbox/control/executor.ts +2 -53
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +6 -20
- package/lib/sandbox/control/server.ts +9 -44
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +35 -16
- package/lib/sandbox/removal.ts +21 -2
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +41 -2
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +1 -0
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +14 -0
- package/lib/task/lifecycle.ts +180 -27
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
|
@@ -9,7 +9,7 @@ import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
|
|
|
9
9
|
import { toEnginePath } from './engines/wsl2-paths.js';
|
|
10
10
|
import { sandboxCoreBindMounts } from './mounts.js';
|
|
11
11
|
import { resolveSandboxGitMetadata } from './git-metadata.js';
|
|
12
|
-
import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
12
|
+
import { assertSandboxTaskSource, sandboxCompletedTaskMountSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
13
13
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
14
14
|
import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
15
15
|
import { inspectWorktree } from './worktree-safety.js';
|
|
@@ -460,25 +460,38 @@ export function startCompletedSandboxContainer(params) {
|
|
|
460
460
|
const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
|
|
461
461
|
const historicalSource = path.join(activeRoot, params.taskId);
|
|
462
462
|
fs.mkdirSync(activeRoot, { recursive: true });
|
|
463
|
+
let replacedEmptyRoot = false;
|
|
464
|
+
let emptyRootMode = 0o700;
|
|
463
465
|
try {
|
|
464
|
-
fs.lstatSync(historicalSource);
|
|
465
|
-
|
|
466
|
+
const existing = fs.lstatSync(historicalSource);
|
|
467
|
+
if (existing.isSymbolicLink() || !existing.isDirectory() || fs.readdirSync(historicalSource).length > 0) {
|
|
468
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
|
|
469
|
+
}
|
|
470
|
+
emptyRootMode = existing.mode & 0o777;
|
|
471
|
+
fs.rmdirSync(historicalSource);
|
|
472
|
+
replacedEmptyRoot = true;
|
|
466
473
|
}
|
|
467
474
|
catch (error) {
|
|
468
475
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
469
476
|
throw error;
|
|
470
477
|
}
|
|
471
|
-
|
|
478
|
+
let temporaryLink = false;
|
|
472
479
|
try {
|
|
480
|
+
fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
|
|
481
|
+
temporaryLink = true;
|
|
473
482
|
params.start(params.engine, params.container);
|
|
474
483
|
}
|
|
475
484
|
finally {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
485
|
+
if (temporaryLink) {
|
|
486
|
+
const temporarySource = fs.lstatSync(historicalSource);
|
|
487
|
+
const resolvedSource = fs.realpathSync.native(historicalSource);
|
|
488
|
+
if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
|
|
489
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
|
|
490
|
+
}
|
|
491
|
+
fs.unlinkSync(historicalSource);
|
|
480
492
|
}
|
|
481
|
-
|
|
493
|
+
if (replacedEmptyRoot)
|
|
494
|
+
fs.mkdirSync(historicalSource, { mode: emptyRootMode });
|
|
482
495
|
}
|
|
483
496
|
}
|
|
484
497
|
function sameHostSource(left, right) {
|
|
@@ -519,6 +532,7 @@ function expectedMounts(params) {
|
|
|
519
532
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
520
533
|
: null;
|
|
521
534
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
535
|
+
const completedTaskSource = taskId ? sandboxCompletedTaskMountSource(config.repoRoot, taskId) : null;
|
|
522
536
|
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) => fs.existsSync(path.join(candidate, '.git')));
|
|
523
537
|
const gitMetadata = worktrees.length === 1
|
|
524
538
|
? resolveSandboxGitMetadata(worktrees[0], control.channelDir)
|
|
@@ -538,6 +552,7 @@ function expectedMounts(params) {
|
|
|
538
552
|
: {
|
|
539
553
|
runtimeDir: control.runtimeDir,
|
|
540
554
|
taskSources: taskSources.accessiblePaths,
|
|
555
|
+
completedTaskSource: completedTaskSource,
|
|
541
556
|
taskId: taskId
|
|
542
557
|
})
|
|
543
558
|
}).map((mount) => ({
|
|
@@ -548,6 +563,10 @@ function expectedMounts(params) {
|
|
|
548
563
|
&& mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
|
|
549
564
|
? { sourceAccessiblePaths: taskSources.accessiblePaths }
|
|
550
565
|
: {}),
|
|
566
|
+
...(completedTaskSource !== null && taskId !== null
|
|
567
|
+
&& mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
|
|
568
|
+
? { sourceAccessiblePaths: [completedTaskSource] }
|
|
569
|
+
: {}),
|
|
551
570
|
expectedRW: !mount.readOnly
|
|
552
571
|
}));
|
|
553
572
|
const live = tools.flatMap((tool) => (tool.hostLiveMounts ?? []).flatMap(({ hostPath, containerSubpath }) => {
|
|
@@ -758,11 +758,20 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
758
758
|
const confirm = prompt.confirm ?? p.confirm;
|
|
759
759
|
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
760
760
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
761
|
+
const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
762
|
+
const rows = [...listed.running, ...listed.nonRunning];
|
|
761
763
|
const containers = runEngine(engine, 'docker', [
|
|
762
764
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
763
765
|
]).split('\n').filter(Boolean);
|
|
766
|
+
const completedTaskIds = [...new Set(rows
|
|
767
|
+
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
|
|
768
|
+
.map((row) => row.taskId))];
|
|
764
769
|
for (const name of containers)
|
|
765
770
|
runSafeEngine(engine, 'docker', ['rm', '-f', name]);
|
|
771
|
+
if (!sandboxContainersRemoved(engine, containers)) {
|
|
772
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${containers.join(', ')}`);
|
|
773
|
+
}
|
|
774
|
+
let cleanupCompleted = true;
|
|
766
775
|
const worktrees = fs.existsSync(config.worktreeBase)
|
|
767
776
|
? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
|
|
768
777
|
: [];
|
|
@@ -773,6 +782,8 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
773
782
|
fs.rmSync(worktree, { recursive: true, force: true });
|
|
774
783
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
775
784
|
}
|
|
785
|
+
else
|
|
786
|
+
cleanupCompleted = false;
|
|
776
787
|
}
|
|
777
788
|
for (const dir of projectToolDirs(config, tools))
|
|
778
789
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -780,15 +791,23 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
780
791
|
const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
|
|
781
792
|
if (!isCancel(selected) && selected)
|
|
782
793
|
fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
|
|
794
|
+
else
|
|
795
|
+
cleanupCompleted = false;
|
|
783
796
|
}
|
|
784
797
|
if (fs.existsSync(config.shareBase)) {
|
|
785
798
|
const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
|
|
786
799
|
if (!isCancel(selected) && selected)
|
|
787
800
|
fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
801
|
+
else
|
|
802
|
+
cleanupCompleted = false;
|
|
788
803
|
}
|
|
789
804
|
for (const base of [config.workspaceViewBase, config.controlBase]) {
|
|
790
805
|
fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
|
|
791
806
|
}
|
|
807
|
+
if (cleanupCompleted) {
|
|
808
|
+
for (const taskId of completedTaskIds)
|
|
809
|
+
releaseStaleShortIdRegistry(config.repoRoot, taskId);
|
|
810
|
+
}
|
|
792
811
|
const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
|
|
793
812
|
if (!isCancel(removeImage) && removeImage)
|
|
794
813
|
runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRemovedHashShortIdInput } from '../task/short-id.js';
|
|
4
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
5
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
5
6
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
7
|
const SHORT_ID_RE = /^\d+$/;
|
|
@@ -38,6 +39,16 @@ function taskIdentity(repoRoot, taskId, registry) {
|
|
|
38
39
|
}
|
|
39
40
|
return { mode: 'task-bound', taskId, shortId: shortIds[0] };
|
|
40
41
|
}
|
|
42
|
+
function cleanupTaskId(requestedRef, repoRoot) {
|
|
43
|
+
if (TASK_ID_RE.test(requestedRef))
|
|
44
|
+
return requestedRef;
|
|
45
|
+
if (!SHORT_ID_RE.test(requestedRef))
|
|
46
|
+
return null;
|
|
47
|
+
const task = resolveTaskRef(requestedRef, { repoRoot });
|
|
48
|
+
if (!task.ok)
|
|
49
|
+
throw new Error(task.message);
|
|
50
|
+
return task.taskId;
|
|
51
|
+
}
|
|
41
52
|
export function resolveSandboxTarget(requestedRef, repoRoot) {
|
|
42
53
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
43
54
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
@@ -83,26 +94,27 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
|
|
|
83
94
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
84
95
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
85
96
|
}
|
|
86
|
-
|
|
97
|
+
const taskId = cleanupTaskId(requestedRef, repoRoot);
|
|
98
|
+
if (taskId) {
|
|
87
99
|
let task;
|
|
88
100
|
try {
|
|
89
|
-
task = resolveTaskWorkspace(
|
|
101
|
+
task = resolveTaskWorkspace(taskId, repoRoot);
|
|
90
102
|
}
|
|
91
103
|
catch (error) {
|
|
92
|
-
if (!(error instanceof Error && error.message === `Task not found: ${
|
|
104
|
+
if (!(error instanceof Error && error.message === `Task not found: ${taskId}`))
|
|
93
105
|
throw error;
|
|
94
|
-
const branch = options.resolveMissingTask?.(
|
|
106
|
+
const branch = options.resolveMissingTask?.(taskId);
|
|
95
107
|
if (!branch)
|
|
96
108
|
throw error;
|
|
97
109
|
return {
|
|
98
110
|
requestedRef,
|
|
99
111
|
branch,
|
|
100
|
-
workspace: { mode: 'task-bound', taskId
|
|
112
|
+
workspace: { mode: 'task-bound', taskId },
|
|
101
113
|
taskState: 'unknown'
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
116
|
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
105
|
-
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${
|
|
117
|
+
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
|
|
106
118
|
}
|
|
107
119
|
return {
|
|
108
120
|
requestedRef,
|
|
@@ -111,13 +111,16 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
|
111
111
|
}
|
|
112
112
|
return canonical;
|
|
113
113
|
}
|
|
114
|
-
export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
114
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot, taskId) {
|
|
115
115
|
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
116
116
|
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
117
117
|
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
118
118
|
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
119
119
|
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
120
|
-
|
|
120
|
+
const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
|
|
121
|
+
if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
122
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
123
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
|
|
121
124
|
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
122
125
|
}
|
|
123
126
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
@@ -125,9 +128,52 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
|
125
128
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
126
129
|
fs.chmodSync(hostPath, 0o700);
|
|
127
130
|
}
|
|
131
|
+
if (completedTaskTarget)
|
|
132
|
+
fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
|
|
128
133
|
fs.chmodSync(workspaceRoot, 0o700);
|
|
129
134
|
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
130
135
|
}
|
|
136
|
+
export function prepareSandboxCompletedTaskMountSource(repoRoot, taskId) {
|
|
137
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
138
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
139
|
+
const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
|
|
140
|
+
fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
|
|
141
|
+
const completedRoot = path.join(workspaceRoot, 'completed');
|
|
142
|
+
const completedStat = fs.lstatSync(completedRoot);
|
|
143
|
+
if (!completedStat.isDirectory() || completedStat.isSymbolicLink())
|
|
144
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
145
|
+
const target = path.join(completedRoot, taskId);
|
|
146
|
+
try {
|
|
147
|
+
const stat = fs.lstatSync(target);
|
|
148
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(target).length > 0) {
|
|
149
|
+
throw new Error(`SANDBOX_COMPLETED_MOUNT_CONFLICT: ${target}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code !== 'ENOENT')
|
|
154
|
+
throw error;
|
|
155
|
+
fs.mkdirSync(target, { mode: 0o700 });
|
|
156
|
+
}
|
|
157
|
+
return sandboxCompletedTaskMountSource(repoRoot, taskId);
|
|
158
|
+
}
|
|
159
|
+
export function sandboxCompletedTaskMountSource(repoRoot, taskId) {
|
|
160
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
161
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
162
|
+
const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
|
|
163
|
+
const stat = fs.lstatSync(source);
|
|
164
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
165
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
166
|
+
const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
167
|
+
const canonicalSource = fs.realpathSync.native(source);
|
|
168
|
+
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
169
|
+
const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
|
|
170
|
+
const relative = path.relative(canonicalWorkspace, canonicalSource);
|
|
171
|
+
if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
|
|
172
|
+
|| path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
|
|
173
|
+
|| relative.startsWith('..') || path.isAbsolute(relative))
|
|
174
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
175
|
+
return canonicalSource;
|
|
176
|
+
}
|
|
131
177
|
export function materializeSandboxWorkspaceView(params) {
|
|
132
178
|
const projectRoot = path.resolve(params.base, params.project);
|
|
133
179
|
const { root } = sandboxWorkspaceViewPaths(params);
|
|
@@ -58,9 +58,9 @@ function collectTasks(repoRoot, state) {
|
|
|
58
58
|
const dir = path.join(repoRoot, '.agents', 'workspace', state);
|
|
59
59
|
if (!fs.existsSync(dir))
|
|
60
60
|
return [];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const shortIdByTaskId = state === 'active' || state === 'completed'
|
|
62
|
+
? loadShortIdByTaskId(repoRoot)
|
|
63
|
+
: new Map();
|
|
64
64
|
const rows = [];
|
|
65
65
|
for (const entry of fs.readdirSync(dir).sort()) {
|
|
66
66
|
if (!TASK_ID_RE.test(entry))
|
|
@@ -320,6 +320,8 @@ export function parseTaskControlOperation(family, args) {
|
|
|
320
320
|
const taskRef = args[0];
|
|
321
321
|
const intent = args[1];
|
|
322
322
|
if (family === 'task-lifecycle') {
|
|
323
|
+
if (intent === 'complete')
|
|
324
|
+
operationInvalid('complete must use task-finalization');
|
|
323
325
|
const values = parseValues(args, 2, LIFECYCLE_FLAGS);
|
|
324
326
|
const agent = normalizeAgentToken(value(values, '--agent') ?? '');
|
|
325
327
|
if (!agent)
|
|
@@ -16,10 +16,9 @@ export function parseControlOutput(output) {
|
|
|
16
16
|
export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
|
|
17
17
|
'completed', 'evidence-written', 'publish-authorized'
|
|
18
18
|
]);
|
|
19
|
-
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', '
|
|
19
|
+
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
|
|
20
20
|
export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
|
|
21
21
|
...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
|
|
22
|
-
{ family: 'task-finalization', intent: 'complete', class: 'finalization' },
|
|
23
22
|
{ family: 'task-create', intent: 'create', class: 'task-create' },
|
|
24
23
|
...['controller.close', 'controller.verify'].map((intent) => ({ family: 'agent-client', intent, class: 'agent-client-operation' }))
|
|
25
24
|
]);
|
|
@@ -1133,6 +1133,20 @@ async function applyTaskFinalization(request, options) {
|
|
|
1133
1133
|
const prepared = await prepareTaskFinalization(request, options);
|
|
1134
1134
|
if (prepared.status !== 'prepared')
|
|
1135
1135
|
return prepared;
|
|
1136
|
+
if (options.beforeLifecycle) {
|
|
1137
|
+
try {
|
|
1138
|
+
await options.beforeLifecycle(prepared.taskId);
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
const detail = errorOf(error, 'TASK_FINALIZATION_PRE_LIFECYCLE_FAILED', true);
|
|
1142
|
+
return failed(prepared.taskId, detail, {
|
|
1143
|
+
changed: prepared.changed,
|
|
1144
|
+
completedSteps: prepared.completedSteps,
|
|
1145
|
+
pendingSteps: prepared.pendingSteps,
|
|
1146
|
+
warnings: prepared.warnings
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1136
1150
|
return commitPreparedTaskFinalization(request, options);
|
|
1137
1151
|
}
|
|
1138
1152
|
export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
|