@fitlab-ai/agent-infra 0.9.7 → 0.9.8
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 +5 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +11 -3
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/executor.js +10 -2
- package/dist/lib/sandbox/control/lifecycle.js +392 -72
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +37 -16
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +60 -6
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/verification-engine.js +40 -14
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/init.ts +1 -1
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +12 -4
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/executor.ts +10 -2
- package/lib/sandbox/control/lifecycle.ts +463 -76
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/workspace-view.ts +70 -40
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +46 -18
- package/lib/task/events.ts +10 -5
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +89 -7
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/verification-engine.ts +42 -16
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProcessIdentityState } from '../../server/process-state.js';
|
|
7
7
|
export const SANDBOX_CONTROL_AUDIT_MAX_BYTES = 1024 * 1024;
|
|
8
8
|
export const SANDBOX_CONTROL_EXECUTION_STOP_MS = 2_000;
|
|
9
9
|
function assertRegularFile(filePath) {
|
|
@@ -72,7 +72,7 @@ export function readActiveLease(manifest, now = Date.now()) {
|
|
|
72
72
|
return null;
|
|
73
73
|
return lease;
|
|
74
74
|
}
|
|
75
|
-
export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
75
|
+
export function cleanupStaleSandboxControlLease(manifest, now = Date.now(), options = {}) {
|
|
76
76
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
77
77
|
if (!fs.existsSync(filePath))
|
|
78
78
|
return false;
|
|
@@ -87,10 +87,13 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
|
87
87
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
88
88
|
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
|| lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
|
|
90
|
+
const ownerState = (options.identityProbe ?? getProcessIdentityState)({
|
|
92
91
|
pid: lease.owner.pid, startTime: lease.owner.startTime
|
|
93
92
|
});
|
|
93
|
+
if (ownerState === 'unknown')
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_LEASE_OWNER_UNAVAILABLE');
|
|
95
|
+
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
96
|
+
|| lease.branch !== manifest.branch || lease.expiresAt <= now || ownerState === 'dead';
|
|
94
97
|
if (!stale)
|
|
95
98
|
return false;
|
|
96
99
|
if (fs.readFileSync(filePath, 'utf8') !== raw)
|
|
@@ -125,29 +128,42 @@ export function readExecution(filePath) {
|
|
|
125
128
|
export function terminateSandboxControlExecution(execution, options = {}) {
|
|
126
129
|
const platform = options.platform ?? process.platform;
|
|
127
130
|
const processGroupId = execution.child.processGroupId;
|
|
128
|
-
const
|
|
131
|
+
const processGroupState = (groupId) => {
|
|
129
132
|
try {
|
|
130
133
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
131
134
|
return rows.split('\n').some((row) => {
|
|
132
135
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
133
136
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
134
|
-
});
|
|
137
|
+
}) ? 'alive' : 'dead';
|
|
135
138
|
}
|
|
136
139
|
catch {
|
|
137
140
|
try {
|
|
138
141
|
process.kill(-groupId, 0);
|
|
139
|
-
return
|
|
142
|
+
return 'alive';
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
142
|
-
|
|
145
|
+
const code = error.code;
|
|
146
|
+
if (code === 'ESRCH')
|
|
147
|
+
return 'dead';
|
|
148
|
+
if (code === 'EPERM')
|
|
149
|
+
return 'alive';
|
|
150
|
+
return 'unknown';
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
};
|
|
146
|
-
const
|
|
154
|
+
const treeState = () => {
|
|
147
155
|
if (platform === 'win32' || !processGroupId) {
|
|
148
|
-
return
|
|
156
|
+
return (options.identityProbe ?? getProcessIdentityState)({
|
|
157
|
+
pid: execution.child.pid, startTime: execution.child.startTime
|
|
158
|
+
});
|
|
149
159
|
}
|
|
150
|
-
return
|
|
160
|
+
return processGroupState(processGroupId);
|
|
161
|
+
};
|
|
162
|
+
const treeAlive = () => {
|
|
163
|
+
const state = treeState();
|
|
164
|
+
if (state === 'unknown')
|
|
165
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
166
|
+
return state === 'alive';
|
|
151
167
|
};
|
|
152
168
|
if (!treeAlive())
|
|
153
169
|
return true;
|
|
@@ -14,6 +14,12 @@ const PLATFORM_DEFAULTS = Object.freeze({
|
|
|
14
14
|
darwin: ENGINES.COLIMA,
|
|
15
15
|
win32: ENGINES.WSL2
|
|
16
16
|
});
|
|
17
|
+
function isPerPlatformEngine(engine) {
|
|
18
|
+
return typeof engine === 'object' && engine !== null;
|
|
19
|
+
}
|
|
20
|
+
function resolveEngineForPlatform(engine, os) {
|
|
21
|
+
return isPerPlatformEngine(engine) ? engine[os] : engine;
|
|
22
|
+
}
|
|
17
23
|
function runFns({ runFn = run, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose } = {}) {
|
|
18
24
|
return {
|
|
19
25
|
run: runFn,
|
|
@@ -56,23 +62,24 @@ function ensureBuildKit(engine, runOkEngineFn) {
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
export function validateSandboxEngine(engine, { platformFn = platform } = {}) {
|
|
59
|
-
|
|
65
|
+
const os = platformFn();
|
|
66
|
+
const resolved = resolveEngineForPlatform(engine, os);
|
|
67
|
+
if (resolved === null || resolved === undefined) {
|
|
60
68
|
return null;
|
|
61
69
|
}
|
|
62
|
-
|
|
63
|
-
if (!(engine in ADAPTERS)) {
|
|
70
|
+
if (!(resolved in ADAPTERS)) {
|
|
64
71
|
const known = Object.keys(ADAPTERS).join(', ');
|
|
65
|
-
throw new Error(`sandbox: invalid "sandbox.engine" value "${
|
|
72
|
+
throw new Error(`sandbox: invalid "sandbox.engine" value "${resolved}" (unknown sandbox engine). `
|
|
66
73
|
+ `Valid engines: ${known}.`);
|
|
67
74
|
}
|
|
68
|
-
const adapter = ADAPTERS[
|
|
75
|
+
const adapter = ADAPTERS[resolved];
|
|
69
76
|
if (!adapter.supportedPlatforms.includes(os)) {
|
|
70
77
|
const supported = enginesForPlatform(os);
|
|
71
78
|
const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
|
|
72
|
-
throw new Error(`sandbox: "sandbox.engine" value "${
|
|
79
|
+
throw new Error(`sandbox: "sandbox.engine" value "${resolved}" is not supported on ${os}. `
|
|
73
80
|
+ `Supported engines on ${os}: ${supportedList}.`);
|
|
74
81
|
}
|
|
75
|
-
return
|
|
82
|
+
return resolved;
|
|
76
83
|
}
|
|
77
84
|
export function detectEngine(config = {}, { platformFn = platform } = {}) {
|
|
78
85
|
const configured = validateSandboxEngine(config.engine, { platformFn });
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
|
+
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
3
4
|
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
4
5
|
const taskBound = Boolean(overrides.taskSource && overrides.taskId);
|
|
6
|
+
const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
|
|
7
|
+
? {
|
|
8
|
+
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
9
|
+
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
10
|
+
readOnly: true
|
|
11
|
+
}
|
|
12
|
+
: {
|
|
13
|
+
hostPaths: [hostPath],
|
|
14
|
+
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
15
|
+
readOnly: true
|
|
16
|
+
});
|
|
5
17
|
const mounts = [
|
|
6
18
|
{
|
|
7
19
|
hostPaths: overrides.worktree
|
|
@@ -10,11 +22,7 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
10
22
|
containerPath: '/workspace',
|
|
11
23
|
readOnly: false
|
|
12
24
|
},
|
|
13
|
-
|
|
14
|
-
hostPaths: [overrides.workspaceViewRoot],
|
|
15
|
-
containerPath: '/workspace/.agents/workspace',
|
|
16
|
-
readOnly: true
|
|
17
|
-
},
|
|
25
|
+
...workspaceMounts,
|
|
18
26
|
];
|
|
19
27
|
if (taskBound) {
|
|
20
28
|
mounts.push({
|
|
@@ -47,6 +55,13 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
47
55
|
containerPath: '/run/agent-infra/control',
|
|
48
56
|
readOnly: false
|
|
49
57
|
});
|
|
58
|
+
if (taskBound && overrides.runtimeDir) {
|
|
59
|
+
mounts.push({
|
|
60
|
+
hostPaths: [overrides.runtimeDir],
|
|
61
|
+
containerPath: '/run/agent-infra/runtime',
|
|
62
|
+
readOnly: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
50
65
|
return mounts;
|
|
51
66
|
}
|
|
52
67
|
//# sourceMappingURL=mounts.js.map
|
|
@@ -14,7 +14,7 @@ import { inspectWorktree } from './worktree-safety.js';
|
|
|
14
14
|
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
|
|
15
15
|
import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
|
|
16
16
|
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.js';
|
|
18
18
|
import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './control/lifecycle.js';
|
|
19
19
|
import { appendSandboxControlAudit, readSandboxControlStatus } from './control/state.js';
|
|
20
20
|
import { inspectSandboxControlContainer } from './control/container-identity.js';
|
|
@@ -89,7 +89,10 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
89
89
|
if (broker === null || broker.version !== 3 || owner === null) {
|
|
90
90
|
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
91
91
|
}
|
|
92
|
-
const
|
|
92
|
+
const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
|
|
93
|
+
if (ownerState === 'unknown')
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
95
|
+
const live = ownerState === 'alive';
|
|
93
96
|
if (live
|
|
94
97
|
&& owner !== null
|
|
95
98
|
&& broker !== null
|
|
@@ -176,6 +179,15 @@ async function ensureSandboxControlBroker(params) {
|
|
|
176
179
|
const brokerPath = path.join(control.root, 'broker.json');
|
|
177
180
|
try {
|
|
178
181
|
const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
|
|
182
|
+
const validIdentity = typeof broker.pid === 'number'
|
|
183
|
+
&& Number.isSafeInteger(broker.pid)
|
|
184
|
+
&& typeof broker.startTime === 'number'
|
|
185
|
+
&& Number.isSafeInteger(broker.startTime);
|
|
186
|
+
const ownerState = validIdentity
|
|
187
|
+
? getProcessIdentityState({ pid: broker.pid, startTime: broker.startTime })
|
|
188
|
+
: 'dead';
|
|
189
|
+
if (ownerState === 'unknown')
|
|
190
|
+
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
179
191
|
if (broker.version === 3
|
|
180
192
|
&& typeof broker.pid === 'number'
|
|
181
193
|
&& Number.isSafeInteger(broker.pid)
|
|
@@ -185,7 +197,7 @@ async function ensureSandboxControlBroker(params) {
|
|
|
185
197
|
&& broker.brokerId.length > 0
|
|
186
198
|
&& broker.token === validatedManifest.token
|
|
187
199
|
&& broker.generation === validatedManifest.generation
|
|
188
|
-
&&
|
|
200
|
+
&& ownerState === 'alive') {
|
|
189
201
|
try {
|
|
190
202
|
const status = readSandboxControlStatus(control.statusDir);
|
|
191
203
|
if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
|
|
@@ -207,7 +219,9 @@ async function ensureSandboxControlBroker(params) {
|
|
|
207
219
|
return;
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
|
-
catch {
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE')
|
|
224
|
+
throw error;
|
|
211
225
|
// A missing, stale, or malformed broker record is replaced below.
|
|
212
226
|
}
|
|
213
227
|
await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
|
|
@@ -242,6 +256,30 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
242
256
|
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
243
257
|
});
|
|
244
258
|
}
|
|
259
|
+
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
260
|
+
findings.push({
|
|
261
|
+
repairKind: 'hard-failure',
|
|
262
|
+
code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
|
|
263
|
+
message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
|
|
264
|
+
path: '/workspace/.agents/workspace'
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (snapshot.taskView && !snapshot.taskView.readable) {
|
|
268
|
+
findings.push({
|
|
269
|
+
repairKind: 'hard-failure',
|
|
270
|
+
code: 'SANDBOX_TASK_VIEW_UNREADABLE',
|
|
271
|
+
message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
|
|
272
|
+
path: snapshot.taskView.path
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (snapshot.runtimeStoreOk === false) {
|
|
276
|
+
findings.push({
|
|
277
|
+
repairKind: 'hard-failure',
|
|
278
|
+
code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
|
|
279
|
+
message: 'Task-bound runtime store is not writable and readable by devuser.',
|
|
280
|
+
path: '/run/agent-infra/runtime'
|
|
281
|
+
});
|
|
282
|
+
}
|
|
245
283
|
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
246
284
|
findings.push({
|
|
247
285
|
repairKind: 'hard-failure',
|
|
@@ -250,6 +288,9 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
250
288
|
});
|
|
251
289
|
}
|
|
252
290
|
for (const mount of snapshot.mounts) {
|
|
291
|
+
if (snapshot.workspaceTopology === 'legacy-parent'
|
|
292
|
+
&& mount.path.startsWith('/workspace/.agents/workspace'))
|
|
293
|
+
continue;
|
|
253
294
|
if (mount.actualType !== mount.expectedType
|
|
254
295
|
|| !mount.sourceMatches
|
|
255
296
|
|| mount.actualRW !== mount.expectedRW
|
|
@@ -331,6 +372,20 @@ function inspectContainer(engine, container, runFn) {
|
|
|
331
372
|
}
|
|
332
373
|
return parsed[0];
|
|
333
374
|
}
|
|
375
|
+
function workspaceTopology(mountsByDestination) {
|
|
376
|
+
if (mountsByDestination.has('/workspace/.agents/workspace'))
|
|
377
|
+
return 'legacy-parent';
|
|
378
|
+
const perState = [
|
|
379
|
+
'/workspace/.agents/workspace/active',
|
|
380
|
+
'/workspace/.agents/workspace/active/.short-ids.json',
|
|
381
|
+
'/workspace/.agents/workspace/completed',
|
|
382
|
+
'/workspace/.agents/workspace/blocked',
|
|
383
|
+
'/workspace/.agents/workspace/archive'
|
|
384
|
+
];
|
|
385
|
+
return perState.some((destination) => mountsByDestination.has(destination))
|
|
386
|
+
? 'per-state'
|
|
387
|
+
: 'unknown';
|
|
388
|
+
}
|
|
334
389
|
function probe(engine, container, script, args, runOkFn, user = 'devuser') {
|
|
335
390
|
return runOkFn(engine, 'docker', [
|
|
336
391
|
'exec', '--user', user, container, 'sh', '-c', script, 'agent-infra-recovery', ...args
|
|
@@ -391,6 +446,7 @@ function expectedMounts(params) {
|
|
|
391
446
|
controlStatusDir: control.statusDir,
|
|
392
447
|
...(params.workspace.mode === 'task-bound'
|
|
393
448
|
? {
|
|
449
|
+
runtimeDir: control.runtimeDir,
|
|
394
450
|
taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
|
|
395
451
|
taskId: params.workspace.taskId
|
|
396
452
|
}
|
|
@@ -489,6 +545,10 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
489
545
|
const capabilityPlan = createSandboxCapabilityPlan(params.config);
|
|
490
546
|
const workspace = params.workspace ?? { mode: 'branch-only' };
|
|
491
547
|
const tools = [...capabilityPlan.tools];
|
|
548
|
+
const topology = workspaceTopology(mountsByDestination);
|
|
549
|
+
const taskViewPath = workspace.mode === 'task-bound'
|
|
550
|
+
? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
|
|
551
|
+
: null;
|
|
492
552
|
const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
|
|
493
553
|
const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
|
|
494
554
|
return {
|
|
@@ -507,6 +567,9 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
507
567
|
permissionsOk: probe(params.engine, params.container, 'test "$(stat -c %u:%g:%a -- "$1")" = "$(id -u devuser):$(id -g devuser):700"', [tool.containerMount], runOkFn),
|
|
508
568
|
writable: probe(params.engine, params.container, 'probe="$1/.agent-infra-ready-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"', [tool.containerMount], runOkFn)
|
|
509
569
|
}));
|
|
570
|
+
const runtimeStoreOk = workspace.mode === 'task-bound'
|
|
571
|
+
? probe(params.engine, params.container, 'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe', ['/run/agent-infra/runtime'], runOkFn)
|
|
572
|
+
: undefined;
|
|
510
573
|
const labels = inspection.Config?.Labels ?? {};
|
|
511
574
|
const branchLabel = labels[sandboxBranchLabel(params.config)];
|
|
512
575
|
const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
|
|
@@ -562,6 +625,14 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
562
625
|
actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
|
|
563
626
|
expectedWorkspace: workspace,
|
|
564
627
|
actualWorkspace: containerWorkspace,
|
|
628
|
+
workspaceTopology: topology,
|
|
629
|
+
...(taskViewPath === null ? {} : {
|
|
630
|
+
taskView: {
|
|
631
|
+
path: taskViewPath,
|
|
632
|
+
readable: probe(params.engine, params.container, 'test -r "$1"', [taskViewPath], runOkFn)
|
|
633
|
+
}
|
|
634
|
+
}),
|
|
635
|
+
...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
|
|
565
636
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
566
637
|
&& branchLabel === params.branch
|
|
567
638
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
@@ -677,10 +748,12 @@ function assess(params) {
|
|
|
677
748
|
}
|
|
678
749
|
function canRetryFreshReadiness(findings) {
|
|
679
750
|
return findings.length > 0 && findings.every((finding) => finding.repairKind === 'hard-failure'
|
|
751
|
+
&& finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
|
|
680
752
|
&& finding.path !== undefined
|
|
681
753
|
&& (finding.path.startsWith('/workspace/.agents/workspace')
|
|
682
754
|
|| finding.path === '/home/devuser/.host-shell-config'
|
|
683
|
-
|| finding.path === '/home/devuser/.bash_aliases'
|
|
755
|
+
|| finding.path === '/home/devuser/.bash_aliases'
|
|
756
|
+
|| finding.path === '/run/agent-infra/runtime'));
|
|
684
757
|
}
|
|
685
758
|
export async function assertFreshSandboxReady(params) {
|
|
686
759
|
validateTmpfsSeedEntries({
|
|
@@ -891,14 +964,9 @@ export async function ensureSandboxReady(params) {
|
|
|
891
964
|
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
892
965
|
warnings.push(warning);
|
|
893
966
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
894
|
-
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
895
967
|
let replacementResult = null;
|
|
896
968
|
let replacementFailure = null;
|
|
897
969
|
try {
|
|
898
|
-
if (params.row.running) {
|
|
899
|
-
runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
|
|
900
|
-
}
|
|
901
|
-
runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
|
|
902
970
|
await params.recreate(params.branch);
|
|
903
971
|
const rows = (deps?.fetchRows ?? fetchSandboxRows)(params.engine, sandboxLabel(params.config), sandboxBranchLabel(params.config));
|
|
904
972
|
const replacement = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(params.config, params.branch));
|
|
@@ -38,6 +38,7 @@ export function sandboxControlPaths(params) {
|
|
|
38
38
|
channelDir: path.join(root, 'channel'),
|
|
39
39
|
statusDir: path.join(root, 'public'),
|
|
40
40
|
processingDir: path.join(root, 'processing'),
|
|
41
|
+
runtimeDir: path.join(root, 'runtime'),
|
|
41
42
|
manifestPath: path.join(root, 'manifest.json')
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -50,6 +51,29 @@ function assertSafeDirectory(directory, expectedBase) {
|
|
|
50
51
|
throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
|
|
55
|
+
const relative = path.relative(worktreeRoot, target);
|
|
56
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
57
|
+
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
58
|
+
}
|
|
59
|
+
let current = worktreeRoot;
|
|
60
|
+
for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
61
|
+
current = segment ? path.join(current, segment) : current;
|
|
62
|
+
try {
|
|
63
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
64
|
+
throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
|
|
65
|
+
}
|
|
66
|
+
const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
|
|
67
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
68
|
+
throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT')
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
53
77
|
export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
54
78
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
55
79
|
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
@@ -65,42 +89,22 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
|
65
89
|
}
|
|
66
90
|
return canonical;
|
|
67
91
|
}
|
|
68
|
-
export function
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
fs.mkdirSync(agentsRoot, { mode: 0o700 });
|
|
80
|
-
}
|
|
81
|
-
const canonicalAgents = fs.realpathSync.native(agentsRoot);
|
|
82
|
-
if (path.dirname(canonicalAgents) !== canonicalWorktree) {
|
|
83
|
-
throw new Error(`Sandbox workspace mount ancestor escapes the worktree: ${agentsRoot}`);
|
|
92
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
93
|
+
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
94
|
+
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
95
|
+
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
96
|
+
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
97
|
+
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
98
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
99
|
+
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
84
100
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
throw new Error(`Sandbox workspace mount target must not be a symbolic link: ${target}`);
|
|
90
|
-
}
|
|
91
|
-
if (!stat.isDirectory()) {
|
|
92
|
-
throw new Error(`Sandbox workspace mount target must be a directory: ${target}`);
|
|
93
|
-
}
|
|
94
|
-
if (path.dirname(fs.realpathSync.native(target)) !== canonicalAgents) {
|
|
95
|
-
throw new Error(`Sandbox workspace mount target escapes the worktree: ${target}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
fs.mkdirSync(target, { mode: 0o700 });
|
|
101
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
102
|
+
for (const { hostPath } of statePaths) {
|
|
103
|
+
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
104
|
+
fs.chmodSync(hostPath, 0o700);
|
|
100
105
|
}
|
|
101
|
-
fs.chmodSync(
|
|
102
|
-
fs.
|
|
103
|
-
return target;
|
|
106
|
+
fs.chmodSync(workspaceRoot, 0o700);
|
|
107
|
+
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
104
108
|
}
|
|
105
109
|
export function materializeSandboxWorkspaceView(params) {
|
|
106
110
|
const projectRoot = path.resolve(params.base, params.project);
|
|
@@ -131,15 +135,32 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
131
135
|
return { root, taskMountPath };
|
|
132
136
|
}
|
|
133
137
|
export function materializeSandboxControl(params) {
|
|
134
|
-
const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
|
|
138
|
+
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
135
139
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
140
|
+
if (fs.existsSync(root)) {
|
|
141
|
+
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
142
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
143
|
+
}
|
|
144
|
+
params.replacementLease.assertOwned();
|
|
145
|
+
}
|
|
136
146
|
const consumedDir = path.join(root, 'consumed');
|
|
137
147
|
assertSafeDirectory(consumedDir, root);
|
|
138
148
|
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
139
149
|
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
140
|
-
for (const directory of [statusDir, processingDir]) {
|
|
150
|
+
for (const directory of [statusDir, processingDir, runtimeDir]) {
|
|
141
151
|
assertSafeDirectory(directory, root);
|
|
152
|
+
if (directory === runtimeDir)
|
|
153
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
154
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
155
|
+
}
|
|
156
|
+
for (const directory of [
|
|
157
|
+
path.join(runtimeDir, 'clients'),
|
|
158
|
+
path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
|
|
159
|
+
path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
|
|
160
|
+
]) {
|
|
161
|
+
assertSafeDirectory(directory, runtimeDir);
|
|
142
162
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
163
|
+
fs.chmodSync(directory, 0o700);
|
|
143
164
|
}
|
|
144
165
|
for (const queue of ['requests', 'responses']) {
|
|
145
166
|
const directory = path.join(channelDir, queue);
|
|
@@ -151,7 +172,7 @@ export function materializeSandboxControl(params) {
|
|
|
151
172
|
const generation = randomBytes(16).toString('hex');
|
|
152
173
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
153
174
|
const manifest = {
|
|
154
|
-
version:
|
|
175
|
+
version: 5,
|
|
155
176
|
engine: params.engine ?? 'docker',
|
|
156
177
|
repoRoot,
|
|
157
178
|
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
@@ -165,16 +186,17 @@ export function materializeSandboxControl(params) {
|
|
|
165
186
|
generation,
|
|
166
187
|
channelDir,
|
|
167
188
|
publicStatusDir: statusDir,
|
|
168
|
-
processingDir
|
|
189
|
+
processingDir,
|
|
190
|
+
runtimeDir
|
|
169
191
|
};
|
|
170
192
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
171
|
-
return { root, channelDir, statusDir, manifestPath, token, generation };
|
|
193
|
+
return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
|
|
172
194
|
}
|
|
173
195
|
export function finalizeSandboxControlManifest(setup, identity) {
|
|
174
196
|
if (!identity.engine || !identity.id)
|
|
175
197
|
throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
176
198
|
const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8'));
|
|
177
|
-
manifest.version =
|
|
199
|
+
manifest.version = 5;
|
|
178
200
|
manifest.engine = identity.engine;
|
|
179
201
|
manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
|
|
180
202
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
@@ -97,6 +97,56 @@ export function getProcessStartTime(pid, platform = process.platform) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
export function getProcessIdentityState(identity, platform = process.platform) {
|
|
101
|
+
if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
|
|
102
|
+
return 'dead';
|
|
103
|
+
}
|
|
104
|
+
if (platform === 'linux') {
|
|
105
|
+
try {
|
|
106
|
+
const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
|
|
107
|
+
if (stat === null)
|
|
108
|
+
return 'unknown';
|
|
109
|
+
if (stat.state === 'Z')
|
|
110
|
+
return 'dead';
|
|
111
|
+
return stat.startTime === identity.startTime ? 'alive' : 'dead';
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return error.code === 'ENOENT' ? 'dead' : 'unknown';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
process.kill(identity.pid, 0);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const code = error.code;
|
|
122
|
+
if (code === 'ESRCH')
|
|
123
|
+
return 'dead';
|
|
124
|
+
if (code !== 'EPERM')
|
|
125
|
+
return 'unknown';
|
|
126
|
+
}
|
|
127
|
+
const query = buildProcessStartTimeQuery(identity.pid, platform);
|
|
128
|
+
if (query === null)
|
|
129
|
+
return 'unknown';
|
|
130
|
+
try {
|
|
131
|
+
const output = execFileSync(query.command, query.args, {
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
|
|
134
|
+
}).trim();
|
|
135
|
+
if (output.length === 0)
|
|
136
|
+
return 'unknown';
|
|
137
|
+
const startTime = platform === 'win32'
|
|
138
|
+
? new Date(output).getTime()
|
|
139
|
+
: platform === 'darwin'
|
|
140
|
+
? parseDarwinStartTime(output)
|
|
141
|
+
: null;
|
|
142
|
+
if (startTime === null || Number.isNaN(startTime))
|
|
143
|
+
return 'unknown';
|
|
144
|
+
return startTime === identity.startTime ? 'alive' : 'dead';
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
100
150
|
function parsePidRecord(raw) {
|
|
101
151
|
const trimmed = raw.trim();
|
|
102
152
|
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
@@ -200,6 +250,6 @@ export function removePidRecordIfMatches(pidFile, expected) {
|
|
|
200
250
|
return removePidFileIfMatches(pidFile, state.snapshot);
|
|
201
251
|
}
|
|
202
252
|
export function processIdentityMatches(identity) {
|
|
203
|
-
return
|
|
253
|
+
return getProcessIdentityState(identity) === 'alive';
|
|
204
254
|
}
|
|
205
255
|
//# sourceMappingURL=process-state.js.map
|
|
@@ -5,6 +5,7 @@ import { locateActivityLog, appendActivityEntry } from './activity-log.js';
|
|
|
5
5
|
import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts, parseArtifactName } from './artifact-lifecycle.js';
|
|
6
6
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
7
7
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
8
|
+
import { allowsManualOverride } from './guard-override.js';
|
|
8
9
|
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
9
10
|
const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
|
|
10
11
|
const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
|
|
@@ -292,9 +293,11 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
|
|
|
292
293
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
293
294
|
if (!resolved.ok)
|
|
294
295
|
return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
295
|
-
if (resolved.state !== 'active') {
|
|
296
|
+
if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
|
|
296
297
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
297
298
|
}
|
|
299
|
+
if (options.lockAlreadyHeld)
|
|
300
|
+
return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
|
|
298
301
|
try {
|
|
299
302
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity:${intent.kind}`, () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }), options.lockOptions);
|
|
300
303
|
}
|
|
@@ -6,6 +6,7 @@ import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.js';
|
|
|
6
6
|
import { resolveTaskContext } from './resolve-ref.js';
|
|
7
7
|
import { extractSection, extractSubSection, findSectionHeading } from './sections.js';
|
|
8
8
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
9
|
+
import { allowsManualOverride } from './guard-override.js';
|
|
9
10
|
const DECISION_ALIASES = ['人工裁决', 'Human Rulings', 'Human Decisions', 'Human Decision'];
|
|
10
11
|
const ACTIVITY_ALIASES = ['活动日志', 'Activity Log'];
|
|
11
12
|
function failed(code, message, taskId = null, ledgerId = null) {
|
|
@@ -56,7 +57,8 @@ function applyHumanDecision(request, options = {}) {
|
|
|
56
57
|
const resolved = resolveTaskContext(request.taskRef, { repoRoot: options.repoRoot });
|
|
57
58
|
if (!resolved.ok)
|
|
58
59
|
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
59
|
-
|
|
60
|
+
const stateOverride = allowsManualOverride(options.manualOverride, 'decision-intent', 'TASK_STATE_MISMATCH');
|
|
61
|
+
if (resolved.state !== 'active' && !stateOverride)
|
|
60
62
|
return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
61
63
|
let content;
|
|
62
64
|
let rows;
|
|
@@ -155,7 +157,7 @@ function applyHumanDecision(request, options = {}) {
|
|
|
155
157
|
kind: 'section', aliases: ACTIVITY_ALIASES, heading: activity.heading,
|
|
156
158
|
body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Human Decision', agent: 'human', note: `${row.id} decided → ${recordId}` })
|
|
157
159
|
});
|
|
158
|
-
const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: 'active', mutations, dryRun: request.dryRun }, {
|
|
160
|
+
const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
|
|
159
161
|
...options,
|
|
160
162
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
|
161
163
|
metadataProvider: () => metadata
|