@fitlab-ai/agent-infra 0.11.13 → 0.11.15
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/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/task-finalization.js +7 -5
- package/dist/lib/internal/task-lifecycle.js +3 -1
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/pr-change-report.js +10 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/process-data/index.js +15 -4
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +5 -4
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +12 -47
- package/dist/lib/sandbox/control/executor.js +6 -10
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +22 -37
- package/dist/lib/sandbox/control/protocol.js +8 -36
- package/dist/lib/sandbox/control/server.js +2 -21
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -59
- package/dist/lib/sandbox/recovery.js +2 -3
- package/dist/lib/sandbox/removal.js +19 -4
- package/dist/lib/sandbox/workspace-identity.js +30 -15
- package/dist/lib/sandbox/workspace-view.js +41 -4
- package/dist/lib/task/control-authority.js +39 -17
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +13 -2
- package/dist/lib/task/lifecycle.js +122 -35
- package/dist/lib/task/short-id.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/task-finalization.ts +8 -6
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/pr-change-report.ts +9 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/process-data/index.ts +15 -4
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +5 -4
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +13 -61
- package/lib/sandbox/control/executor.ts +7 -13
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +22 -37
- package/lib/sandbox/control/protocol.ts +9 -45
- package/lib/sandbox/control/server.ts +2 -17
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +92 -75
- package/lib/sandbox/recovery.ts +2 -3
- package/lib/sandbox/removal.ts +16 -4
- package/lib/sandbox/workspace-identity.ts +36 -13
- package/lib/sandbox/workspace-view.ts +40 -5
- package/lib/task/control-authority.ts +40 -26
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +15 -2
- package/lib/task/lifecycle.ts +135 -33
- package/lib/task/short-id.ts +5 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +19 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/runtime/agent-runtime.js +0 -33
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/dist/lib/task/runtime-paths.js +0 -10
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/runtime/agent-runtime.ts +0 -41
- package/lib/sandbox/control/agent-operation.ts +0 -49
- package/lib/task/runtime-paths.ts +0 -12
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -15,6 +15,89 @@ export type SandboxBindMountDeclaration = {
|
|
|
15
15
|
readOnly: boolean;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
function workspaceStateMounts(workspaceViewRoot: string, taskBound: boolean): SandboxBindMountDeclaration[] {
|
|
19
|
+
return sandboxWorkspaceViewStatePaths(workspaceViewRoot)
|
|
20
|
+
// The writable completed-task mount below is nested under this path. Keep
|
|
21
|
+
// the worktree target visible so Docker can mount it without creating a
|
|
22
|
+
// mountpoint inside a read-only parent bind mount.
|
|
23
|
+
.filter(({ state }) => !(taskBound && state === 'completed'))
|
|
24
|
+
.map(({ state, hostPath }) => taskBound && state === 'active'
|
|
25
|
+
? {
|
|
26
|
+
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
27
|
+
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
28
|
+
readOnly: true
|
|
29
|
+
}
|
|
30
|
+
: {
|
|
31
|
+
hostPaths: [hostPath],
|
|
32
|
+
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
33
|
+
readOnly: true
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertGitMetadataPair(overrides: {
|
|
39
|
+
gitCommonDir?: string;
|
|
40
|
+
worktreeGitFile?: string;
|
|
41
|
+
}): void {
|
|
42
|
+
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
43
|
+
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function gitCommonMount(gitCommonDir?: string): SandboxBindMountDeclaration[] {
|
|
48
|
+
return gitCommonDir ? [{ hostPaths: [gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }] : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function worktreeGitFileMount(worktreeGitFile?: string): SandboxBindMountDeclaration[] {
|
|
52
|
+
return worktreeGitFile ? [{ hostPaths: [worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function taskWorkspaceMounts(taskSources: string[], taskBound: boolean, overrides: {
|
|
56
|
+
taskId?: string;
|
|
57
|
+
completedTaskSource?: string;
|
|
58
|
+
}): SandboxBindMountDeclaration[] {
|
|
59
|
+
if (!taskBound) return [];
|
|
60
|
+
const taskId = overrides.taskId!;
|
|
61
|
+
const mounts: SandboxBindMountDeclaration[] = [{
|
|
62
|
+
hostPaths: taskSources,
|
|
63
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
|
|
64
|
+
readOnly: false
|
|
65
|
+
}];
|
|
66
|
+
if (overrides.completedTaskSource) mounts.push({
|
|
67
|
+
hostPaths: [overrides.completedTaskSource],
|
|
68
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
|
|
69
|
+
readOnly: false
|
|
70
|
+
});
|
|
71
|
+
return mounts;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sharedMounts(config: Pick<SandboxConfig, 'repoRoot' | 'worktreeBase' | 'shareBase' | 'shellConfigBase'>, branch: string,
|
|
75
|
+
shellConfigHostDir?: string): SandboxBindMountDeclaration[] {
|
|
76
|
+
return [{
|
|
77
|
+
hostPaths: [shareCommonDir(config)], containerPath: '/share/common', readOnly: false
|
|
78
|
+
}, {
|
|
79
|
+
hostPaths: [shareBranchDir(config, branch)], containerPath: '/share/branch', readOnly: false
|
|
80
|
+
}, {
|
|
81
|
+
hostPaths: shellConfigHostDir ? [shellConfigHostDir] : shellConfigDirCandidates(config, branch),
|
|
82
|
+
containerPath: '/home/devuser/.host-shell-config', readOnly: true
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function controlMounts(overrides: { controlDir: string; controlRootDir?: string; controlStatusDir: string; runtimeDir?: string }, taskBound: boolean): SandboxBindMountDeclaration[] {
|
|
87
|
+
const mounts: SandboxBindMountDeclaration[] = [{
|
|
88
|
+
hostPaths: [overrides.controlStatusDir], containerPath: '/run/agent-infra/control-status', readOnly: true
|
|
89
|
+
}, {
|
|
90
|
+
hostPaths: [overrides.controlDir], containerPath: '/run/agent-infra/control', readOnly: false
|
|
91
|
+
}];
|
|
92
|
+
if (taskBound && overrides.runtimeDir) mounts.push({
|
|
93
|
+
hostPaths: [overrides.runtimeDir], containerPath: '/run/agent-infra/runtime', readOnly: false
|
|
94
|
+
});
|
|
95
|
+
if (taskBound && overrides.controlRootDir) mounts.push({
|
|
96
|
+
hostPaths: [overrides.controlRootDir], containerPath: '/run/agent-infra/control-root', readOnly: false
|
|
97
|
+
});
|
|
98
|
+
return mounts;
|
|
99
|
+
}
|
|
100
|
+
|
|
18
101
|
export function sandboxCoreBindMounts(
|
|
19
102
|
config: Pick<
|
|
20
103
|
SandboxConfig,
|
|
@@ -26,6 +109,7 @@ export function sandboxCoreBindMounts(
|
|
|
26
109
|
shellConfigHostDir?: string;
|
|
27
110
|
workspaceViewRoot: string;
|
|
28
111
|
controlDir: string;
|
|
112
|
+
controlRootDir?: string;
|
|
29
113
|
controlStatusDir: string;
|
|
30
114
|
gitCommonDir?: string;
|
|
31
115
|
worktreeGitFile?: string;
|
|
@@ -37,30 +121,10 @@ export function sandboxCoreBindMounts(
|
|
|
37
121
|
): SandboxBindMountDeclaration[] {
|
|
38
122
|
const taskSources = overrides.taskSources ?? [];
|
|
39
123
|
const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? {
|
|
43
|
-
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
44
|
-
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
45
|
-
readOnly: true
|
|
46
|
-
}
|
|
47
|
-
: {
|
|
48
|
-
hostPaths: [hostPath],
|
|
49
|
-
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
50
|
-
readOnly: true
|
|
51
|
-
}
|
|
52
|
-
);
|
|
53
|
-
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
54
|
-
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
55
|
-
}
|
|
56
|
-
const gitCommonMount = overrides.gitCommonDir
|
|
57
|
-
? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
|
|
58
|
-
: null;
|
|
59
|
-
const worktreeGitFileMount = overrides.worktreeGitFile
|
|
60
|
-
? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
|
|
61
|
-
: null;
|
|
124
|
+
assertGitMetadataPair(overrides);
|
|
125
|
+
if (taskBound && !overrides.controlRootDir) throw new Error('SANDBOX_CONTROL_ROOT_MISSING');
|
|
62
126
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
63
|
-
...(
|
|
127
|
+
...gitCommonMount(overrides.gitCommonDir),
|
|
64
128
|
{
|
|
65
129
|
hostPaths: overrides.worktree
|
|
66
130
|
? [overrides.worktree]
|
|
@@ -68,58 +132,11 @@ export function sandboxCoreBindMounts(
|
|
|
68
132
|
containerPath: '/workspace',
|
|
69
133
|
readOnly: false
|
|
70
134
|
},
|
|
71
|
-
...(
|
|
72
|
-
...
|
|
135
|
+
...worktreeGitFileMount(overrides.worktreeGitFile),
|
|
136
|
+
...workspaceStateMounts(overrides.workspaceViewRoot, taskBound),
|
|
137
|
+
...taskWorkspaceMounts(taskSources, taskBound, overrides),
|
|
138
|
+
...sharedMounts(config, branch, overrides.shellConfigHostDir),
|
|
139
|
+
...controlMounts(overrides, taskBound)
|
|
73
140
|
];
|
|
74
|
-
if (taskBound) {
|
|
75
|
-
const taskMountPath = path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!);
|
|
76
|
-
mounts.push({
|
|
77
|
-
hostPaths: taskSources,
|
|
78
|
-
containerPath: taskMountPath,
|
|
79
|
-
readOnly: false
|
|
80
|
-
});
|
|
81
|
-
if (overrides.completedTaskSource) {
|
|
82
|
-
mounts.push({
|
|
83
|
-
hostPaths: [overrides.completedTaskSource],
|
|
84
|
-
containerPath: path.posix.join('/workspace/.agents/workspace/completed', overrides.taskId!),
|
|
85
|
-
readOnly: false
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
mounts.push(
|
|
90
|
-
{
|
|
91
|
-
hostPaths: [shareCommonDir(config)],
|
|
92
|
-
containerPath: '/share/common',
|
|
93
|
-
readOnly: false
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
hostPaths: [shareBranchDir(config, branch)],
|
|
97
|
-
containerPath: '/share/branch',
|
|
98
|
-
readOnly: false
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
hostPaths: overrides.shellConfigHostDir
|
|
102
|
-
? [overrides.shellConfigHostDir]
|
|
103
|
-
: shellConfigDirCandidates(config, branch),
|
|
104
|
-
containerPath: '/home/devuser/.host-shell-config',
|
|
105
|
-
readOnly: true
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
mounts.push({
|
|
109
|
-
hostPaths: [overrides.controlStatusDir],
|
|
110
|
-
containerPath: '/run/agent-infra/control-status',
|
|
111
|
-
readOnly: true
|
|
112
|
-
}, {
|
|
113
|
-
hostPaths: [overrides.controlDir],
|
|
114
|
-
containerPath: '/run/agent-infra/control',
|
|
115
|
-
readOnly: false
|
|
116
|
-
});
|
|
117
|
-
if (taskBound && overrides.runtimeDir) {
|
|
118
|
-
mounts.push({
|
|
119
|
-
hostPaths: [overrides.runtimeDir],
|
|
120
|
-
containerPath: '/run/agent-infra/runtime',
|
|
121
|
-
readOnly: false
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
141
|
return mounts;
|
|
125
142
|
}
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -342,7 +342,6 @@ async function ensureSandboxControlBroker(params: {
|
|
|
342
342
|
}): Promise<void> {
|
|
343
343
|
const control = sandboxControlPaths({
|
|
344
344
|
base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
|
|
345
|
-
repoRoot: params.config.repoRoot,
|
|
346
345
|
project: params.config.project,
|
|
347
346
|
container: params.container,
|
|
348
347
|
identity: params.workspace
|
|
@@ -753,7 +752,6 @@ function expectedMounts(params: {
|
|
|
753
752
|
});
|
|
754
753
|
const control = sandboxControlPaths({
|
|
755
754
|
base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
|
|
756
|
-
repoRoot: config.repoRoot,
|
|
757
755
|
project: config.project,
|
|
758
756
|
container: params.container,
|
|
759
757
|
identity: params.workspace
|
|
@@ -778,6 +776,7 @@ function expectedMounts(params: {
|
|
|
778
776
|
}),
|
|
779
777
|
workspaceViewRoot: view.root,
|
|
780
778
|
controlDir: control.channelDir,
|
|
779
|
+
...(params.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
|
|
781
780
|
controlStatusDir: control.statusDir,
|
|
782
781
|
...(taskSources === null
|
|
783
782
|
? {}
|
|
@@ -1296,7 +1295,7 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1296
1295
|
let completedReentry: { manifest: SandboxControlManifest; evidence: Awaited<ReturnType<typeof prepareCompletedReentry>> } | null = null;
|
|
1297
1296
|
if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
|
|
1298
1297
|
const control = sandboxControlPaths({
|
|
1299
|
-
base: params.config.controlBase,
|
|
1298
|
+
base: params.config.controlBase, project: params.config.project,
|
|
1300
1299
|
container: params.row.name, identity: params.workspace
|
|
1301
1300
|
});
|
|
1302
1301
|
const manifest = readSandboxControlManifest(control.manifestPath);
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -829,7 +829,7 @@ function resolveRmTarget(
|
|
|
829
829
|
? matchedContainers
|
|
830
830
|
: [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
831
831
|
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
832
|
-
base: config.controlBase,
|
|
832
|
+
base: config.controlBase, project: config.project, container, identity
|
|
833
833
|
}).root));
|
|
834
834
|
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
835
835
|
base: config.workspaceViewBase, project: config.project, container, identity
|
|
@@ -1083,6 +1083,19 @@ async function rmPurgeCore(
|
|
|
1083
1083
|
const containers = runEngine(engine, 'docker', [
|
|
1084
1084
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
1085
1085
|
]).split('\n').filter(Boolean);
|
|
1086
|
+
const controlRoots = rows.flatMap((row) => {
|
|
1087
|
+
if (!row.branch) return [];
|
|
1088
|
+
const identity = row.workspaceMode === 'task-bound' && row.taskId
|
|
1089
|
+
? { mode: 'task-bound' as const, taskId: row.taskId }
|
|
1090
|
+
: row.workspaceMode === 'branch-only'
|
|
1091
|
+
? { mode: 'branch-only' as const }
|
|
1092
|
+
: null;
|
|
1093
|
+
if (!identity) return [];
|
|
1094
|
+
return [sandboxControlPaths({
|
|
1095
|
+
base: config.controlBase, project: config.project,
|
|
1096
|
+
container: row.name, identity
|
|
1097
|
+
}).root];
|
|
1098
|
+
});
|
|
1086
1099
|
const completedTaskIds = [...new Set(rows
|
|
1087
1100
|
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
|
|
1088
1101
|
.map((row) => row.taskId!))];
|
|
@@ -1115,9 +1128,8 @@ async function rmPurgeCore(
|
|
|
1115
1128
|
if (!isCancel(selected) && selected) fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
1116
1129
|
else cleanupCompleted = false;
|
|
1117
1130
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1131
|
+
fs.rmSync(path.join(config.workspaceViewBase, config.project), { recursive: true, force: true });
|
|
1132
|
+
for (const root of new Set(controlRoots)) fs.rmSync(root, { recursive: true, force: true });
|
|
1121
1133
|
if (cleanupCompleted) {
|
|
1122
1134
|
for (const taskId of completedTaskIds) releaseStaleShortIdRegistry(config.repoRoot, taskId);
|
|
1123
1135
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { isRemovedHashShortIdInput } from '../task/short-id.ts';
|
|
4
|
-
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
3
|
import {
|
|
4
|
+
configuredShortIdLength,
|
|
5
|
+
inspectShortIdRegistry,
|
|
6
|
+
isRemovedHashShortIdInput,
|
|
7
|
+
normalizeShortIdInput
|
|
8
|
+
} from '../task/short-id.ts';
|
|
9
|
+
import {
|
|
10
|
+
TASK_WORKSPACE_STATES,
|
|
11
|
+
listTaskWorkspaces,
|
|
6
12
|
resolveTaskWorkspace,
|
|
7
13
|
type TaskWorkspaceState
|
|
8
14
|
} from './task-resolver.ts';
|
|
@@ -79,10 +85,29 @@ function taskIdentity(repoRoot: string, taskId: string, registry: Record<string,
|
|
|
79
85
|
|
|
80
86
|
function cleanupTaskId(requestedRef: string, repoRoot: string): string | null {
|
|
81
87
|
if (TASK_ID_RE.test(requestedRef)) return requestedRef;
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
88
|
+
const shortIdLength = configuredShortIdLength(repoRoot);
|
|
89
|
+
const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
|
|
90
|
+
if (normalized.kind === 'error') throw new Error(normalized.message);
|
|
91
|
+
if (normalized.kind !== 'shortId') return null;
|
|
92
|
+
|
|
93
|
+
const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
|
|
94
|
+
if (registry.status !== 'valid') throw new Error(registry.error.message);
|
|
95
|
+
const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
|
|
96
|
+
if (!match) {
|
|
97
|
+
throw new Error(`Short id '${normalized.value}' not found in active task registry`);
|
|
98
|
+
}
|
|
99
|
+
return match[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanupTaskIdByBranch(requestedRef: string, repoRoot: string): string | null {
|
|
103
|
+
const taskIds = new Set(TASK_WORKSPACE_STATES
|
|
104
|
+
.flatMap((state) => listTaskWorkspaces(repoRoot, state))
|
|
105
|
+
.filter((task) => task.branch === requestedRef)
|
|
106
|
+
.map((task) => task.taskId));
|
|
107
|
+
if (taskIds.size > 1) {
|
|
108
|
+
throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
|
|
109
|
+
}
|
|
110
|
+
return taskIds.values().next().value ?? null;
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
|
|
@@ -139,7 +164,8 @@ export function resolveSandboxCleanupTarget(
|
|
|
139
164
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
140
165
|
}
|
|
141
166
|
|
|
142
|
-
const taskId = cleanupTaskId(requestedRef, repoRoot)
|
|
167
|
+
const taskId = cleanupTaskId(requestedRef, repoRoot)
|
|
168
|
+
?? cleanupTaskIdByBranch(requestedRef, repoRoot);
|
|
143
169
|
if (taskId) {
|
|
144
170
|
let task: ReturnType<typeof resolveTaskWorkspace>;
|
|
145
171
|
try {
|
|
@@ -166,14 +192,11 @@ export function resolveSandboxCleanupTarget(
|
|
|
166
192
|
};
|
|
167
193
|
}
|
|
168
194
|
|
|
169
|
-
const target = resolveSandboxTarget(requestedRef, repoRoot);
|
|
170
195
|
return {
|
|
171
196
|
requestedRef,
|
|
172
|
-
branch:
|
|
173
|
-
workspace:
|
|
174
|
-
|
|
175
|
-
: { mode: 'branch-only' },
|
|
176
|
-
taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
|
|
197
|
+
branch: requestedRef,
|
|
198
|
+
workspace: { mode: 'branch-only' },
|
|
199
|
+
taskState: 'branch-only'
|
|
177
200
|
};
|
|
178
201
|
}
|
|
179
202
|
|
|
@@ -4,7 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
|
|
5
5
|
import { resolveTaskWorkspace } from './task-resolver.ts';
|
|
6
6
|
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
7
|
-
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
|
|
8
7
|
import {
|
|
9
8
|
createSandboxControlIdentitySentinel,
|
|
10
9
|
writeSandboxControlIdentitySentinel
|
|
@@ -64,13 +63,21 @@ export type SandboxControlManifestDraft = Readonly<Omit<SandboxControlManifest,
|
|
|
64
63
|
|
|
65
64
|
export function sandboxControlPaths(params: Readonly<{
|
|
66
65
|
base: string;
|
|
67
|
-
repoRoot?: string;
|
|
68
66
|
project: string;
|
|
69
67
|
container: string;
|
|
70
68
|
identity: SandboxWorkspaceIdentity | SandboxWorkspaceKey;
|
|
71
69
|
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; runtimeDir: string; manifestPath: string }> {
|
|
72
70
|
const root = params.identity.mode === 'task-bound'
|
|
73
|
-
?
|
|
71
|
+
? (() => {
|
|
72
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(params.identity.taskId)
|
|
73
|
+
|| !params.project || path.basename(params.project) !== params.project || params.project === '.' || params.project === '..') {
|
|
74
|
+
throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
75
|
+
}
|
|
76
|
+
const candidate = path.resolve(params.base, params.project, 'tasks', params.identity.taskId);
|
|
77
|
+
const relative = path.relative(path.resolve(params.base), candidate);
|
|
78
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
79
|
+
return candidate;
|
|
80
|
+
})()
|
|
74
81
|
: (() => {
|
|
75
82
|
const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
|
|
76
83
|
return path.resolve(params.base, params.project, params.container, digest);
|
|
@@ -95,6 +102,23 @@ function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
function assertSafeTaskBoundControlRoot(basePath: string, root: string): void {
|
|
106
|
+
const relative = path.relative(basePath, root);
|
|
107
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
108
|
+
throw new Error(`Sandbox control root escapes its configured base: ${root}`);
|
|
109
|
+
}
|
|
110
|
+
if (fs.existsSync(basePath) && fs.lstatSync(basePath).isSymbolicLink()) {
|
|
111
|
+
throw new Error(`Sandbox control base must not be a symbolic link: ${basePath}`);
|
|
112
|
+
}
|
|
113
|
+
let current = basePath;
|
|
114
|
+
for (const segment of relative.split(path.sep)) {
|
|
115
|
+
current = path.join(current, segment);
|
|
116
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
117
|
+
throw new Error(`Sandbox control root must not contain a symbolic link: ${current}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
function assertSafeMountTargetPath(
|
|
99
123
|
target: string,
|
|
100
124
|
worktreeRoot: string,
|
|
@@ -263,7 +287,11 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
263
287
|
}>;
|
|
264
288
|
}>): SandboxControlSetup {
|
|
265
289
|
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
266
|
-
|
|
290
|
+
if (params.identity.mode === 'task-bound') {
|
|
291
|
+
assertSafeTaskBoundControlRoot(path.resolve(params.base), root);
|
|
292
|
+
} else {
|
|
293
|
+
assertSafeDirectory(root, path.resolve(params.base));
|
|
294
|
+
}
|
|
267
295
|
if (fs.existsSync(root)) {
|
|
268
296
|
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
269
297
|
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
@@ -329,9 +357,16 @@ export function finalizeSandboxControlManifest(
|
|
|
329
357
|
containerIdentity: { id: identity.id, labels: { ...identity.labels } },
|
|
330
358
|
authorityEvidence: identity.authorityEvidence
|
|
331
359
|
};
|
|
360
|
+
const persistedManifest = {
|
|
361
|
+
...manifest,
|
|
362
|
+
channelDir: 'channel',
|
|
363
|
+
publicStatusDir: 'public',
|
|
364
|
+
processingDir: 'processing',
|
|
365
|
+
runtimeDir: 'runtime'
|
|
366
|
+
};
|
|
332
367
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
333
368
|
try {
|
|
334
|
-
fs.writeFileSync(temporary, `${JSON.stringify(
|
|
369
|
+
fs.writeFileSync(temporary, `${JSON.stringify(persistedManifest)}\n`, { mode: 0o600, flag: 'wx' });
|
|
335
370
|
fs.renameSync(temporary, setup.manifestPath);
|
|
336
371
|
} finally {
|
|
337
372
|
fs.rmSync(temporary, { force: true });
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
4
4
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
5
5
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
6
|
+
import { sandboxControlPaths } from '../sandbox/workspace-view.ts';
|
|
6
7
|
import { getAgentClientAdapter } from '../agent-clients/registry.ts';
|
|
7
8
|
import {
|
|
8
9
|
activateMatchingOrchestrationDelegation,
|
|
@@ -41,18 +42,14 @@ import { resolveTaskContext, resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts
|
|
|
41
42
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
42
43
|
import { verifyTaskEvent } from './verification.ts';
|
|
43
44
|
|
|
44
|
-
export type TaskControlControllerBinding = Readonly<{
|
|
45
|
-
instanceDigest: string;
|
|
46
|
-
controlGeneration: string;
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
45
|
export type TaskControlExecutionContext =
|
|
50
46
|
| Readonly<{
|
|
51
47
|
source: 'direct-host';
|
|
52
48
|
mode: 'direct-host';
|
|
53
49
|
repoRoot: string;
|
|
54
50
|
runtimeDir?: string;
|
|
55
|
-
|
|
51
|
+
controlBase?: string;
|
|
52
|
+
project?: string;
|
|
56
53
|
}>
|
|
57
54
|
| Readonly<{
|
|
58
55
|
source: 'sandbox-executor';
|
|
@@ -64,7 +61,6 @@ export type TaskControlExecutionContext =
|
|
|
64
61
|
generation: string;
|
|
65
62
|
manifestPath: string;
|
|
66
63
|
requestId: string;
|
|
67
|
-
controllerBinding?: TaskControlControllerBinding | null;
|
|
68
64
|
}>;
|
|
69
65
|
|
|
70
66
|
export type TaskControlOrchestrationIntent =
|
|
@@ -112,25 +108,19 @@ function absolute(name: string, value: string): string {
|
|
|
112
108
|
return path.resolve(value);
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
function binding(value: TaskControlControllerBinding | null | undefined): TaskControlControllerBinding | null | undefined {
|
|
116
|
-
if (value === null || value === undefined) return value;
|
|
117
|
-
if (!/^[a-f0-9]{64}$/u.test(value.instanceDigest) || !value.controlGeneration) {
|
|
118
|
-
invalidContext('controller binding is invalid');
|
|
119
|
-
}
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
111
|
export function createDirectHostExecutionContext(params: Readonly<{
|
|
124
112
|
repoRoot: string;
|
|
125
113
|
runtimeDir?: string;
|
|
126
|
-
|
|
114
|
+
controlBase?: string;
|
|
115
|
+
project?: string;
|
|
127
116
|
}>): TaskControlExecutionContext {
|
|
128
117
|
return {
|
|
129
118
|
source: 'direct-host',
|
|
130
119
|
mode: 'direct-host',
|
|
131
120
|
repoRoot: absolute('repoRoot', params.repoRoot),
|
|
132
|
-
...(params.
|
|
133
|
-
...(params.
|
|
121
|
+
...(params.controlBase === undefined ? {} : { controlBase: absolute('controlBase', params.controlBase) }),
|
|
122
|
+
...(params.project === undefined ? {} : { project: params.project }),
|
|
123
|
+
...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) })
|
|
134
124
|
};
|
|
135
125
|
}
|
|
136
126
|
|
|
@@ -142,7 +132,6 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
142
132
|
generation: string;
|
|
143
133
|
manifestPath: string;
|
|
144
134
|
requestId: string;
|
|
145
|
-
controllerBinding?: TaskControlControllerBinding | null;
|
|
146
135
|
}>): TaskControlExecutionContext {
|
|
147
136
|
if (!/^TASK-\d{8}-\d{6}$/u.test(params.taskId) || !params.generation || !params.requestId) {
|
|
148
137
|
invalidContext('sandbox binding is incomplete');
|
|
@@ -160,8 +149,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
160
149
|
taskId: params.taskId,
|
|
161
150
|
generation: params.generation,
|
|
162
151
|
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
163
|
-
requestId: params.requestId
|
|
164
|
-
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
152
|
+
requestId: params.requestId
|
|
165
153
|
};
|
|
166
154
|
}
|
|
167
155
|
|
|
@@ -170,7 +158,8 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
|
|
|
170
158
|
if (context.mode !== 'direct-host') invalidContext('direct-host mode is inconsistent');
|
|
171
159
|
absolute('repoRoot', context.repoRoot);
|
|
172
160
|
if (context.runtimeDir !== undefined) absolute('runtimeDir', context.runtimeDir);
|
|
173
|
-
|
|
161
|
+
if (Boolean(context.controlBase) !== Boolean(context.project)) invalidContext('control configuration is incomplete');
|
|
162
|
+
if (context.controlBase !== undefined) absolute('controlBase', context.controlBase);
|
|
174
163
|
return;
|
|
175
164
|
}
|
|
176
165
|
if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
|
|
@@ -184,7 +173,6 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
|
|
|
184
173
|
if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
|
|
185
174
|
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
186
175
|
}
|
|
187
|
-
binding(context.controllerBinding);
|
|
188
176
|
}
|
|
189
177
|
|
|
190
178
|
function boundTaskRef(context: TaskControlExecutionContext, taskRef: string): string {
|
|
@@ -225,6 +213,8 @@ async function applyLifecycleWithAuthority(
|
|
|
225
213
|
context: TaskControlExecutionContext,
|
|
226
214
|
request: TaskLifecycleControlRequest
|
|
227
215
|
): Promise<TaskLifecycleResult | LifecycleRecoveryResult> {
|
|
216
|
+
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
217
|
+
const controlRoot = taskId ? controlRootForTask(context, taskId) : undefined;
|
|
228
218
|
const execute = async (underTaskLock = false): Promise<TaskLifecycleResult | LifecycleRecoveryResult> => {
|
|
229
219
|
if (request.intent === 'recover-started') {
|
|
230
220
|
const adapter = isAgentClientId(request.agent)
|
|
@@ -240,10 +230,17 @@ async function applyLifecycleWithAuthority(
|
|
|
240
230
|
return adapter.recoverStarted(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
|
|
241
231
|
}
|
|
242
232
|
return underTaskLock
|
|
243
|
-
? applyTaskLifecycleWithinTaskLock(request, {
|
|
244
|
-
|
|
233
|
+
? applyTaskLifecycleWithinTaskLock(request, {
|
|
234
|
+
repoRoot: context.repoRoot,
|
|
235
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
236
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
|
|
237
|
+
}, taskId!)
|
|
238
|
+
: applyTaskLifecycle(request, {
|
|
239
|
+
repoRoot: context.repoRoot,
|
|
240
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
241
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
|
|
242
|
+
});
|
|
245
243
|
};
|
|
246
|
-
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
247
244
|
if (!taskId) return await execute();
|
|
248
245
|
try {
|
|
249
246
|
return await withTaskExecutionLock(
|
|
@@ -266,6 +263,17 @@ async function applyLifecycleWithAuthority(
|
|
|
266
263
|
}
|
|
267
264
|
}
|
|
268
265
|
|
|
266
|
+
function controlRootForTask(context: TaskControlExecutionContext, taskId: string): string | undefined {
|
|
267
|
+
if (context.source === 'sandbox-executor') return path.dirname(context.manifestPath);
|
|
268
|
+
if (!context.controlBase || !context.project) return undefined;
|
|
269
|
+
return sandboxControlPaths({
|
|
270
|
+
base: context.controlBase,
|
|
271
|
+
project: context.project,
|
|
272
|
+
container: '',
|
|
273
|
+
identity: { mode: 'task-bound', taskId }
|
|
274
|
+
}).root;
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
function orchestration(
|
|
270
278
|
context: TaskControlExecutionContext,
|
|
271
279
|
operation: Extract<TaskControlOperation, { family: 'task-orchestration' }>
|
|
@@ -367,6 +375,10 @@ export function dispatchTaskControlOperation(
|
|
|
367
375
|
});
|
|
368
376
|
}
|
|
369
377
|
if (operation.family === 'task-finalization') {
|
|
378
|
+
const resolved = resolveTaskRef(boundTaskRef(context, operation.request.taskRef), { repoRoot: context.repoRoot });
|
|
379
|
+
const controlRoot = context.source === 'sandbox-executor'
|
|
380
|
+
? path.dirname(context.manifestPath)
|
|
381
|
+
: resolved.ok ? controlRootForTask(context, resolved.taskId) : undefined;
|
|
370
382
|
return applyTaskFinalization({
|
|
371
383
|
...operation.request,
|
|
372
384
|
taskRef: boundTaskRef(context, operation.request.taskRef)
|
|
@@ -376,6 +388,8 @@ export function dispatchTaskControlOperation(
|
|
|
376
388
|
controlBinding: { generation: context.generation, requestId: context.requestId },
|
|
377
389
|
manifestPath: context.manifestPath
|
|
378
390
|
} : {}),
|
|
391
|
+
...(controlRoot ? { controlRoot } : {}),
|
|
392
|
+
...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {}),
|
|
379
393
|
preflight: (request, options) => verifyTaskEvent(
|
|
380
394
|
{ ...request, event: 'complete-task.hard-preflight' }, options
|
|
381
395
|
)
|
|
@@ -13,7 +13,7 @@ export function parseControlOutput(output: string | null): Record<string, unknow
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type ControlRecoveryOperation = Readonly<{
|
|
16
|
-
family: 'task-lifecycle' | 'task-create'
|
|
16
|
+
family: 'task-lifecycle' | 'task-create';
|
|
17
17
|
intent: string;
|
|
18
18
|
class: string;
|
|
19
19
|
}>;
|
|
@@ -54,8 +54,7 @@ export type ControlRecoveryDecision = Readonly<{
|
|
|
54
54
|
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'] as const;
|
|
55
55
|
export const SANDBOX_CONTROL_RECOVERY_OPERATIONS: readonly ControlRecoveryOperation[] = Object.freeze([
|
|
56
56
|
...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle' as const, intent, class: 'lifecycle-mutation' })),
|
|
57
|
-
{ family: 'task-create', intent: 'create', class: 'task-create' }
|
|
58
|
-
...(['controller.close', 'controller.verify'] as const).map((intent) => ({ family: 'agent-client' as const, intent, class: 'agent-client-operation' }))
|
|
57
|
+
{ family: 'task-create', intent: 'create', class: 'task-create' }
|
|
59
58
|
]);
|
|
60
59
|
|
|
61
60
|
export function digestControlRecoveryIntent(family: string, intent: string): string {
|
package/lib/task/finalization.ts
CHANGED
|
@@ -70,6 +70,8 @@ type TaskFinalizationRequest = Readonly<{
|
|
|
70
70
|
type TaskFinalizationOptions = Readonly<{
|
|
71
71
|
repoRoot: string;
|
|
72
72
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
73
|
+
controlRoot?: string;
|
|
74
|
+
controlProject?: string;
|
|
73
75
|
manifestPath?: string;
|
|
74
76
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
75
77
|
lifecycle?: typeof applyTaskLifecycle;
|
|
@@ -1068,7 +1070,13 @@ async function prepareUnderLock(
|
|
|
1068
1070
|
} else {
|
|
1069
1071
|
const result = lifecycle(
|
|
1070
1072
|
{ taskRef: taskId, intent: 'complete', agent: request.agent },
|
|
1071
|
-
{
|
|
1073
|
+
{
|
|
1074
|
+
repoRoot,
|
|
1075
|
+
prepareOnly: true,
|
|
1076
|
+
...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
|
|
1077
|
+
...(options.controlProject ? { controlProject: options.controlProject } : {}),
|
|
1078
|
+
...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
|
|
1079
|
+
}
|
|
1072
1080
|
);
|
|
1073
1081
|
lifecycleResult = lifecycleStep(result);
|
|
1074
1082
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
@@ -1207,7 +1215,12 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1207
1215
|
});
|
|
1208
1216
|
receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
|
|
1209
1217
|
const lifecycleRequest = { taskRef: resolved.taskId, intent: 'complete' as const, agent: request.agent };
|
|
1210
|
-
const lifecycleOptions = {
|
|
1218
|
+
const lifecycleOptions = {
|
|
1219
|
+
repoRoot,
|
|
1220
|
+
...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
|
|
1221
|
+
...(options.controlProject ? { controlProject: options.controlProject } : {}),
|
|
1222
|
+
...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
|
|
1223
|
+
};
|
|
1211
1224
|
const result = options.lifecycle
|
|
1212
1225
|
? options.lifecycle(lifecycleRequest, lifecycleOptions)
|
|
1213
1226
|
: applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions, resolved.taskId);
|