@fitlab-ai/agent-infra 0.11.11 → 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/bin/internal-cli.js +2 -2
- package/dist/lib/agent-clients/adapter.js +7 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
- package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
- package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
- package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
- package/dist/lib/agent-clients/adapters/codex.js +40 -1
- package/dist/lib/agent-clients/adapters/index.js +2 -10
- package/dist/lib/agent-clients/tokens.js +3 -2
- package/dist/lib/internal/agent-client.js +132 -72
- package/dist/lib/internal/cli-route-inventory.js +2 -2
- 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/internal/task-operation-registry.js +1 -1
- package/dist/lib/internal/task-validate.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/issues.js +18 -6
- package/dist/lib/runtime/agent-runtime.js +6 -29
- package/dist/lib/sandbox/commands/control-status.js +1 -0
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/agent-operation.js +34 -0
- package/dist/lib/sandbox/control/client.js +14 -201
- package/dist/lib/sandbox/control/executor.js +12 -125
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +15 -54
- package/dist/lib/sandbox/control/server.js +216 -281
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +9 -1
- package/dist/lib/sandbox/recovery.js +31 -10
- package/dist/lib/sandbox/removal.js +28 -5
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +56 -19
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +56 -264
- package/dist/lib/task/control-recovery.js +2 -3
- package/dist/lib/task/create.js +2 -2
- package/dist/lib/task/delegation-receipts.js +74 -161
- package/dist/lib/task/events.js +88 -44
- package/dist/lib/task/finalization.js +35 -6
- package/dist/lib/task/lifecycle.js +274 -115
- package/dist/lib/task/orchestration.js +201 -194
- package/dist/lib/task/runtime-paths.js +10 -0
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +32 -26
- package/dist/lib/task/workspace-snapshot.js +1 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +39 -0
- package/lib/agent-clients/adapters/claude-code.ts +5 -1
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
- package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
- package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
- package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
- package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
- package/lib/agent-clients/adapters/codex.ts +44 -1
- package/lib/agent-clients/adapters/index.ts +7 -20
- package/lib/agent-clients/tokens.ts +4 -3
- package/lib/internal/agent-client.ts +142 -73
- package/lib/internal/cli-route-inventory.ts +2 -2
- 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/internal/task-operation-registry.ts +1 -1
- package/lib/internal/task-validate.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/issues.ts +18 -6
- package/lib/runtime/agent-runtime.ts +6 -41
- package/lib/sandbox/commands/control-status.ts +1 -0
- package/lib/sandbox/commands/create.ts +7 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/agent-operation.ts +49 -0
- package/lib/sandbox/control/client.ts +20 -254
- package/lib/sandbox/control/executor.ts +13 -136
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +22 -66
- package/lib/sandbox/control/server.ts +21 -95
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +10 -1
- package/lib/sandbox/recovery.ts +38 -17
- package/lib/sandbox/removal.ts +33 -7
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +50 -18
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +66 -378
- package/lib/task/control-recovery.ts +3 -4
- package/lib/task/create.ts +3 -3
- package/lib/task/delegation-receipts.ts +87 -231
- package/lib/task/events.ts +100 -42
- package/lib/task/finalization.ts +37 -7
- package/lib/task/lifecycle.ts +257 -109
- package/lib/task/orchestration.ts +260 -237
- package/lib/task/runtime-paths.ts +12 -0
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +30 -25
- package/lib/task/workspace-snapshot.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/next-step-output.en.md +6 -19
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
- 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/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- 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-manual-validation/SKILL.en.md +0 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
- 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/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
- package/dist/lib/internal/codex-lifecycle.js +0 -385
- package/dist/lib/task/codex-orchestration.js +0 -206
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
- package/lib/internal/codex-lifecycle.ts +0 -390
- package/lib/task/codex-orchestration.ts +0 -276
package/lib/task/lifecycle.ts
CHANGED
|
@@ -18,13 +18,14 @@ import { validateCurrentTaskContract } from './current-contract.ts';
|
|
|
18
18
|
import { isValidAgentInfraVersion } from '../version.ts';
|
|
19
19
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
20
20
|
import { resourceIdentityEquals } from '../platform/resource-identity.ts';
|
|
21
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
21
22
|
|
|
22
23
|
const lifecycleIntentCatalog = [
|
|
23
24
|
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
|
|
24
25
|
] as const;
|
|
25
26
|
const lifecycleFailureCatalog = [
|
|
27
|
+
'LIFECYCLE_DIRECTORY_TRANSFER_FAILED',
|
|
26
28
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
27
|
-
'LIFECYCLE_DIRECTORY_COPY_FAILED',
|
|
28
29
|
'LIFECYCLE_DOCUMENT_INVALID',
|
|
29
30
|
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
30
31
|
'LIFECYCLE_IDENTITY_CONFLICT',
|
|
@@ -38,6 +39,7 @@ const lifecycleFailureCatalog = [
|
|
|
38
39
|
'LIFECYCLE_PAYLOAD_INVALID',
|
|
39
40
|
'LIFECYCLE_SHORT_ID_FAILED',
|
|
40
41
|
'LIFECYCLE_SHORT_ID_PRECONDITION',
|
|
42
|
+
'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
41
43
|
'LIFECYCLE_SOURCE_INVALID',
|
|
42
44
|
'LIFECYCLE_STAGING_IDENTITY_INVALID',
|
|
43
45
|
'LIFECYCLE_STAGING_INVALID',
|
|
@@ -116,6 +118,9 @@ type TaskLifecycleOptions = {
|
|
|
116
118
|
type TaskCompletionProjection = Readonly<{ taskId: string; content: string; timestamp: string; agentInfraVersion: string }>;
|
|
117
119
|
|
|
118
120
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
121
|
+
function stepsForIntent(intent: TaskLifecycleIntent): readonly LifecycleStep[] {
|
|
122
|
+
return intent === 'complete' ? ['task-written', 'directory-moved'] : STEPS;
|
|
123
|
+
}
|
|
119
124
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
120
125
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
121
126
|
readFileSync: (file) => fs.readFileSync(file, 'utf8'),
|
|
@@ -129,7 +134,7 @@ function failed(request: Pick<TaskLifecycleRequest, 'taskRef' | 'intent'>, error
|
|
|
129
134
|
intent: request.intent, sourceState: null, targetState: null, sourcePath: null,
|
|
130
135
|
targetPath: null, task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
131
136
|
shortId: { effect: 'unchanged', shortId: null, changed: false }, timestamp: null,
|
|
132
|
-
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps:
|
|
137
|
+
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps: stepsForIntent(request.intent),
|
|
133
138
|
error, ...extra
|
|
134
139
|
};
|
|
135
140
|
}
|
|
@@ -200,9 +205,96 @@ function transition(request: TaskLifecycleRequest): { sources: readonly SourceSt
|
|
|
200
205
|
if (request.intent === 'activate') return { sources: ['blocked'], target: 'active', registry: 'alloc' };
|
|
201
206
|
if (request.intent === 'cancel') return { sources: ['active', 'blocked'], target: 'completed', registry: 'release' };
|
|
202
207
|
if (request.intent === 'restore') return { sources: ['staging'], target: 'active', registry: 'alloc' };
|
|
208
|
+
if (request.intent === 'complete') return { sources: ['active'], target: 'completed', registry: 'none' };
|
|
203
209
|
return { sources: ['active'], target: 'completed', registry: 'release' };
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
function sameTaskEntry(source: string, target: string): boolean {
|
|
213
|
+
const sourceStat = fs.lstatSync(source);
|
|
214
|
+
let targetStat: fs.Stats;
|
|
215
|
+
try { targetStat = fs.lstatSync(target); }
|
|
216
|
+
catch (error) {
|
|
217
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (sourceStat.isSymbolicLink() || targetStat.isSymbolicLink()) {
|
|
221
|
+
return sourceStat.isSymbolicLink() && targetStat.isSymbolicLink()
|
|
222
|
+
&& fs.readlinkSync(source) === fs.readlinkSync(target);
|
|
223
|
+
}
|
|
224
|
+
if (sourceStat.isDirectory() && targetStat.isDirectory()) {
|
|
225
|
+
const sourceNames = fs.readdirSync(source).sort();
|
|
226
|
+
const targetNames = fs.readdirSync(target).sort();
|
|
227
|
+
return sourceNames.join('\0') === targetNames.join('\0')
|
|
228
|
+
&& sourceNames.every((name) => sameTaskEntry(path.join(source, name), path.join(target, name)))
|
|
229
|
+
&& (sourceStat.mode & 0o777) === (targetStat.mode & 0o777);
|
|
230
|
+
}
|
|
231
|
+
return sourceStat.isFile() && targetStat.isFile()
|
|
232
|
+
&& (sourceStat.mode & 0o777) === (targetStat.mode & 0o777)
|
|
233
|
+
&& fs.readFileSync(source).equals(fs.readFileSync(target));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function copyTaskEntry(source: string, target: string): void {
|
|
237
|
+
const sourceStat = fs.lstatSync(source);
|
|
238
|
+
if (fs.existsSync(target)) {
|
|
239
|
+
const targetStat = fs.lstatSync(target);
|
|
240
|
+
if (sourceStat.isDirectory() && targetStat.isDirectory() && !targetStat.isSymbolicLink()) {
|
|
241
|
+
for (const name of fs.readdirSync(source).sort()) copyTaskEntry(path.join(source, name), path.join(target, name));
|
|
242
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (sameTaskEntry(source, target)) return;
|
|
246
|
+
throw new Error(`task content conflict at ${path.relative(path.dirname(source), source)}`);
|
|
247
|
+
}
|
|
248
|
+
if (sourceStat.isSymbolicLink()) {
|
|
249
|
+
fs.symlinkSync(fs.readlinkSync(source), target);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (sourceStat.isDirectory()) {
|
|
253
|
+
fs.mkdirSync(target, { mode: sourceStat.mode & 0o777 });
|
|
254
|
+
for (const name of fs.readdirSync(source).sort()) copyTaskEntry(path.join(source, name), path.join(target, name));
|
|
255
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!sourceStat.isFile()) throw new Error(`unsupported task entry: ${source}`);
|
|
259
|
+
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
260
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function transferTaskContents(sourcePath: string, targetPath: string): void {
|
|
264
|
+
const sourceRoot = fs.lstatSync(sourcePath);
|
|
265
|
+
if (!sourceRoot.isDirectory() || sourceRoot.isSymbolicLink()) throw new Error('task source is not a real directory');
|
|
266
|
+
const targetRoot = fs.lstatSync(targetPath);
|
|
267
|
+
if (!targetRoot.isDirectory() || targetRoot.isSymbolicLink()) throw new Error('completed target is not a real directory');
|
|
268
|
+
for (const name of fs.readdirSync(sourcePath).sort()) {
|
|
269
|
+
copyTaskEntry(path.join(sourcePath, name), path.join(targetPath, name));
|
|
270
|
+
}
|
|
271
|
+
const sourceNames = fs.readdirSync(sourcePath).sort();
|
|
272
|
+
const targetNames = fs.readdirSync(targetPath).sort();
|
|
273
|
+
if (sourceNames.join('\0') !== targetNames.join('\0')
|
|
274
|
+
|| !sourceNames.every((name) => sameTaskEntry(path.join(sourcePath, name), path.join(targetPath, name)))) {
|
|
275
|
+
throw new Error('completed target does not match task source after copy');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function targetContainsTask(targetPath: string, taskId: string, request: TaskLifecycleRequest): boolean {
|
|
280
|
+
try {
|
|
281
|
+
const stat = fs.lstatSync(targetPath);
|
|
282
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
283
|
+
const entries = fs.readdirSync(targetPath);
|
|
284
|
+
if (entries.length === 0) return true;
|
|
285
|
+
const taskPath = path.join(targetPath, 'task.md');
|
|
286
|
+
const journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
287
|
+
if (!fs.existsSync(taskPath) || !fs.existsSync(journalPath)) return false;
|
|
288
|
+
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
|
|
289
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
290
|
+
return frontmatter.id === taskId && frontmatter.status === 'completed'
|
|
291
|
+
&& journal.taskId === taskId && journal.intent === request.intent
|
|
292
|
+
&& journal.intentDigest === intentIdentity(request, taskId);
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
206
298
|
function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { action: string; note: string } {
|
|
207
299
|
if (request.intent === 'block') return { action: 'Block Task', note: request.reason };
|
|
208
300
|
if (request.intent === 'activate') return { action: 'Activate Task', note: request.note };
|
|
@@ -400,13 +492,13 @@ export function inspectTaskLifecycleProgress(
|
|
|
400
492
|
if (journal.taskId !== taskId || journal.intent !== 'complete'
|
|
401
493
|
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
402
494
|
|| journal.sourceState !== 'active' || journal.targetState !== 'completed') return 'unknown';
|
|
403
|
-
if (journal.completedSteps.some((step, index) =>
|
|
495
|
+
if (journal.completedSteps.some((step, index) => stepsForIntent(journal.intent)[index] !== step)) return 'unknown';
|
|
404
496
|
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
405
497
|
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
406
498
|
const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
|
|
407
499
|
if (state === 'active' && (directoryMoved || !hasShortId)) return 'unknown';
|
|
408
500
|
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
409
|
-
|| !matchingCompletion(content, request) ||
|
|
501
|
+
|| !matchingCompletion(content, request) || registryCommitted || !hasShortId)) return 'unknown';
|
|
410
502
|
if (frontmatter.status !== state
|
|
411
503
|
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
412
504
|
return 'unknown';
|
|
@@ -417,63 +509,7 @@ export function inspectTaskLifecycleProgress(
|
|
|
417
509
|
}
|
|
418
510
|
}
|
|
419
511
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
function directoryManifest(root: string): DirectoryEntry[] {
|
|
423
|
-
const entries: DirectoryEntry[] = [];
|
|
424
|
-
const visit = (current: string, relative = '') => {
|
|
425
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
426
|
-
const relativePath = relative ? path.join(relative, entry.name) : entry.name;
|
|
427
|
-
const absolute = path.join(current, entry.name);
|
|
428
|
-
const stat = fs.lstatSync(absolute);
|
|
429
|
-
if (stat.isSymbolicLink()) throw new Error(`symbolic link is not allowed in lifecycle directory: ${relativePath}`);
|
|
430
|
-
if (entry.isDirectory()) {
|
|
431
|
-
entries.push({ relativePath, kind: 'directory', size: 0, digest: '' });
|
|
432
|
-
visit(absolute, relativePath);
|
|
433
|
-
} else if (entry.isFile()) {
|
|
434
|
-
const digest = createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
435
|
-
entries.push({ relativePath, kind: 'file', size: stat.size, digest });
|
|
436
|
-
} else {
|
|
437
|
-
throw new Error(`unsupported lifecycle directory entry: ${relativePath}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
visit(root);
|
|
442
|
-
return entries;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function copyDirectoryVerified(source: string, target: string): void {
|
|
446
|
-
if (fs.existsSync(target)) throw new Error(`target directory already exists: ${target}`);
|
|
447
|
-
const copy = (currentSource: string, currentTarget: string) => {
|
|
448
|
-
fs.mkdirSync(currentTarget);
|
|
449
|
-
for (const entry of fs.readdirSync(currentSource, { withFileTypes: true })) {
|
|
450
|
-
const sourcePath = path.join(currentSource, entry.name);
|
|
451
|
-
const targetPath = path.join(currentTarget, entry.name);
|
|
452
|
-
const stat = fs.lstatSync(sourcePath);
|
|
453
|
-
if (stat.isSymbolicLink()) throw new Error(`symbolic link is not allowed in lifecycle directory: ${entry.name}`);
|
|
454
|
-
if (entry.isDirectory()) copy(sourcePath, targetPath);
|
|
455
|
-
else if (entry.isFile()) fs.copyFileSync(sourcePath, targetPath);
|
|
456
|
-
else throw new Error(`unsupported lifecycle directory entry: ${entry.name}`);
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
copy(source, target);
|
|
460
|
-
const sourceManifest = directoryManifest(source);
|
|
461
|
-
const targetManifest = directoryManifest(target);
|
|
462
|
-
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
463
|
-
throw new Error('copied lifecycle directory failed manifest verification');
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function removeCopiedSource(source: string, target: string): void {
|
|
468
|
-
const sourceManifest = directoryManifest(source);
|
|
469
|
-
const targetManifest = directoryManifest(target);
|
|
470
|
-
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
471
|
-
throw new Error('lifecycle source and target diverged before source cleanup');
|
|
472
|
-
}
|
|
473
|
-
fs.rmSync(source, { recursive: true, force: false });
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
512
|
+
function applyTaskLifecycleUnlocked(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
477
513
|
if (requestInput.intent === 'recover-started') {
|
|
478
514
|
return failed(requestInput, { code: 'LIFECYCLE_RECOVERY_REQUIRES_AUTHORITY', message: 'recover-started must be dispatched through task control authority' });
|
|
479
515
|
}
|
|
@@ -493,7 +529,12 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
493
529
|
}
|
|
494
530
|
const spec = transition(request);
|
|
495
531
|
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
496
|
-
|
|
532
|
+
const activeSource = hot.find((candidate) => candidate.state === 'active');
|
|
533
|
+
const completedTarget = hot.find((candidate) => candidate.state === 'completed');
|
|
534
|
+
const transferInProgress = request.intent === 'complete' && Boolean(activeSource && completedTarget)
|
|
535
|
+
&& targetContainsTask(completedTarget!.taskDir, taskId, request)
|
|
536
|
+
&& fs.existsSync(path.join(activeSource!.taskDir, '.task-lifecycle.json'));
|
|
537
|
+
if (hot.length > 1 && !transferInProgress) return failed(request, { code: 'LIFECYCLE_IDENTITY_CONFLICT', message: `task ${taskId} exists in multiple hot states` }, { taskId });
|
|
497
538
|
let sourceState: SourceState;
|
|
498
539
|
let sourcePath: string;
|
|
499
540
|
let restoredFiles = 0;
|
|
@@ -501,6 +542,9 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
501
542
|
const validation = validateRestoreStaging(request, repoRoot, taskId);
|
|
502
543
|
if ('code' in validation) return failed(request, validation, { taskId });
|
|
503
544
|
sourceState = 'staging'; sourcePath = validation.stagingDir; restoredFiles = validation.fileCount;
|
|
545
|
+
} else if (activeSource && request.intent === 'complete') {
|
|
546
|
+
sourceState = activeSource.state; sourcePath = activeSource.taskDir;
|
|
547
|
+
restoredFiles = fs.readdirSync(sourcePath).filter((name) => !name.startsWith('.')).length;
|
|
504
548
|
} else if (hot[0]) {
|
|
505
549
|
sourceState = hot[0].state; sourcePath = hot[0].taskDir;
|
|
506
550
|
restoredFiles = fs.readdirSync(sourcePath).filter((name) => !name.startsWith('.')).length;
|
|
@@ -508,6 +552,37 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
508
552
|
return failed(request, { code: 'LIFECYCLE_TASK_NOT_FOUND', message: `task ${taskId} has no lifecycle source` }, { taskId });
|
|
509
553
|
}
|
|
510
554
|
const targetPath = path.join(repoRoot, '.agents', 'workspace', spec.target, taskId);
|
|
555
|
+
const requiredSteps = stepsForIntent(request.intent);
|
|
556
|
+
const movingTaskDirectory = sourcePath !== targetPath;
|
|
557
|
+
const assertRuntimeMoveSafe = (): LifecycleError | null => {
|
|
558
|
+
if (!movingTaskDirectory) return null;
|
|
559
|
+
const controlManifest = path.join(sourcePath, '.runtime', 'sandbox-control', 'manifest.json');
|
|
560
|
+
const controlLease = path.join(sourcePath, '.runtime', 'sandbox-control', 'lease.json');
|
|
561
|
+
if (request.intent !== 'complete' && (fs.existsSync(controlManifest) || fs.existsSync(controlLease))) {
|
|
562
|
+
return {
|
|
563
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
564
|
+
message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory'
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
|
|
568
|
+
if (fs.existsSync(orchestrationPath)) {
|
|
569
|
+
try {
|
|
570
|
+
const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8')) as { status?: unknown; pendingDelegation?: unknown };
|
|
571
|
+
if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
|
|
572
|
+
return {
|
|
573
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
574
|
+
message: 'task orchestration has an active or pending delegation; settle it before moving the task directory'
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
return {
|
|
579
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
580
|
+
message: 'task orchestration state is unreadable; refusing to move the task directory'
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
};
|
|
511
586
|
const currentTaskFile = path.join(sourcePath, 'task.md');
|
|
512
587
|
const existingJournal = fs.existsSync(path.join(sourcePath, '.task-lifecycle.json'));
|
|
513
588
|
if (sourceState === spec.target && !existingJournal) {
|
|
@@ -517,7 +592,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
517
592
|
return {
|
|
518
593
|
...failed(request, { code: '', message: '' }), status: 'no-op', error: null, taskId,
|
|
519
594
|
sourceState, targetState: spec.target, sourcePath, targetPath, pendingSteps: [],
|
|
520
|
-
completedSteps:
|
|
595
|
+
completedSteps: requiredSteps, timestamp: typeof frontmatter.updated_at === 'string' ? frontmatter.updated_at : null,
|
|
521
596
|
agentInfraVersion: typeof frontmatter.agent_infra_version === 'string' ? frontmatter.agent_infra_version : null
|
|
522
597
|
};
|
|
523
598
|
}
|
|
@@ -526,9 +601,12 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
526
601
|
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
527
602
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
528
603
|
}
|
|
529
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
604
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
605
|
+
&& !(request.intent === 'complete' && targetContainsTask(targetPath, taskId, request))) {
|
|
530
606
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
531
607
|
}
|
|
608
|
+
const moveSafety = assertRuntimeMoveSafe();
|
|
609
|
+
if (moveSafety) return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
532
610
|
const content = io.readFileSync(currentTaskFile);
|
|
533
611
|
let frontmatter;
|
|
534
612
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
@@ -548,7 +626,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
548
626
|
}
|
|
549
627
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
|
|
550
628
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
551
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
629
|
+
if ((spec.registry === 'release' || request.intent === 'complete') && sourceState === 'active' && !registeredKey) {
|
|
552
630
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
553
631
|
}
|
|
554
632
|
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
@@ -567,7 +645,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
567
645
|
}
|
|
568
646
|
if (request.dryRun) {
|
|
569
647
|
if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
570
|
-
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps:
|
|
648
|
+
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: requiredSteps.filter((step) => !journal.completedSteps.includes(step)) });
|
|
571
649
|
}
|
|
572
650
|
if (journal.intentDigest !== digest) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
573
651
|
} else {
|
|
@@ -608,7 +686,8 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
608
686
|
const completed = new Set(journal.completedSteps);
|
|
609
687
|
if (sourceState === spec.target && journal.sourceState !== spec.target) completed.add('directory-moved');
|
|
610
688
|
const currentMappings = loadShortIdByTaskId(repoRoot);
|
|
611
|
-
if (
|
|
689
|
+
if (requiredSteps.includes('registry-committed')
|
|
690
|
+
&& ((spec.target === 'active' && currentMappings.has(taskId)) || (spec.target !== 'active' && !currentMappings.has(taskId)))) {
|
|
612
691
|
if (completed.has('directory-moved')) completed.add('registry-committed');
|
|
613
692
|
}
|
|
614
693
|
let taskOperations: readonly TaskOperationSummary[] = [];
|
|
@@ -619,7 +698,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
619
698
|
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
620
699
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
621
700
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
622
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
701
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
623
702
|
};
|
|
624
703
|
}
|
|
625
704
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
@@ -648,57 +727,82 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
648
727
|
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
649
728
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
650
729
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
651
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
730
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
652
731
|
};
|
|
653
732
|
}
|
|
654
|
-
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath)) completed.add('directory-moved');
|
|
733
|
+
if (fs.existsSync(targetPath) && !fs.existsSync(path.join(sourcePath, 'task.md'))) completed.add('directory-moved');
|
|
655
734
|
if (!completed.has('directory-moved')) {
|
|
735
|
+
const moveSafety = assertRuntimeMoveSafe();
|
|
736
|
+
if (moveSafety) return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0, timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
656
737
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
657
|
-
if (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
writeJournal(initialJournalPath, journal, io);
|
|
667
|
-
writeJournal(copiedJournalPath, journal, io);
|
|
668
|
-
removeCopiedSource(sourcePath, targetPath);
|
|
669
|
-
journalPath = copiedJournalPath;
|
|
670
|
-
} catch (copyError) {
|
|
671
|
-
if (fs.existsSync(targetPath)) fs.rmSync(targetPath, { recursive: true, force: false });
|
|
672
|
-
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(copyError) } as const;
|
|
673
|
-
rememberJournalFailure(initialJournalPath, journal, io, failure);
|
|
674
|
-
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
738
|
+
if (request.intent === 'complete') {
|
|
739
|
+
try {
|
|
740
|
+
if (!fs.existsSync(targetPath)) fs.mkdirSync(targetPath, { recursive: false, mode: 0o700 });
|
|
741
|
+
if (!targetContainsTask(targetPath, taskId, request)) {
|
|
742
|
+
throw new Error('completed target has no matching task identity');
|
|
743
|
+
}
|
|
744
|
+
transferTaskContents(sourcePath, targetPath);
|
|
745
|
+
for (const name of fs.readdirSync(sourcePath).filter((entry) => entry !== '.task-lifecycle.json' && entry !== 'task.md').sort()) {
|
|
746
|
+
fs.rmSync(path.join(sourcePath, name), { recursive: true, force: false });
|
|
675
747
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
748
|
+
fs.rmSync(path.join(sourcePath, 'task.md'), { force: false });
|
|
749
|
+
fs.rmSync(path.join(sourcePath, '.task-lifecycle.json'), { force: false });
|
|
750
|
+
} catch (error) {
|
|
751
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_TRANSFER_FAILED', message: String(error) } as const;
|
|
752
|
+
const sourceJournalPath = path.join(sourcePath, '.task-lifecycle.json');
|
|
753
|
+
rememberJournalFailure(sourceJournalPath, journal, io, failure);
|
|
754
|
+
return failed(request, failure, {
|
|
755
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
756
|
+
journalPath: sourceJournalPath, task: { operations: taskOperations },
|
|
757
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
758
|
+
pendingSteps: requiredSteps.filter((step) => !completed.has(step) || step === 'directory-moved'),
|
|
759
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
760
|
+
});
|
|
680
761
|
}
|
|
681
|
-
|
|
682
|
-
|
|
762
|
+
completed.add('directory-moved');
|
|
763
|
+
journal.completedSteps = [...completed];
|
|
683
764
|
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
684
|
-
} else if (!completed.has('directory-moved')) {
|
|
685
|
-
completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
686
765
|
try { writeJournal(journalPath, journal, io); }
|
|
687
|
-
catch (error) {
|
|
766
|
+
catch (error) {
|
|
767
|
+
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, {
|
|
768
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath,
|
|
769
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
770
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
771
|
+
pendingSteps: requiredSteps.filter((step) => step !== 'task-written'),
|
|
772
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
if (fs.existsSync(targetPath)) return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
777
|
+
try { (options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath); }
|
|
778
|
+
catch (error) {
|
|
779
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) } as const;
|
|
780
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
781
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
782
|
+
}
|
|
783
|
+
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
784
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
785
|
+
} else if (!completed.has('directory-moved')) {
|
|
786
|
+
completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
787
|
+
try { writeJournal(journalPath, journal, io); }
|
|
788
|
+
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
789
|
+
}
|
|
688
790
|
}
|
|
689
791
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
792
|
+
const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
793
|
+
if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
|
|
794
|
+
try { fs.rmdirSync(activeTaskPath); }
|
|
795
|
+
catch (error) {
|
|
796
|
+
return failed(request, { code: 'LIFECYCLE_SOURCE_DIRECTORY_CLEANUP_FAILED', message: String(error) }, {
|
|
797
|
+
taskId, sourceState, targetState: spec.target, sourcePath: activeTaskPath, targetPath, journalPath,
|
|
798
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
799
|
+
changed: true, completedSteps: [...completed], pendingSteps: [],
|
|
800
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
801
|
+
});
|
|
698
802
|
}
|
|
699
803
|
}
|
|
700
|
-
let shortId: TaskLifecycleResult['shortId'] = { effect: 'unchanged', shortId: null, changed: false };
|
|
701
|
-
if (!completed.has('registry-committed')) {
|
|
804
|
+
let shortId: TaskLifecycleResult['shortId'] = { effect: 'unchanged', shortId: request.intent === 'complete' ? registeredKey ?? null : null, changed: false };
|
|
805
|
+
if (requiredSteps.includes('registry-committed') && !completed.has('registry-committed')) {
|
|
702
806
|
try { shortId = mutateShortIdRegistry(repoRoot, taskId, spec.registry); }
|
|
703
807
|
catch (error) { return failed(request, { code: (error as { code?: string }).code ?? 'LIFECYCLE_SHORT_ID_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
704
808
|
completed.add('registry-committed'); journal.completedSteps = [...completed];
|
|
@@ -707,7 +811,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
707
811
|
}
|
|
708
812
|
const mappings = loadShortIdByTaskId(repoRoot);
|
|
709
813
|
if (spec.target === 'active' && !mappings.has(taskId)) return failed(request, { code: 'LIFECYCLE_FINAL_STATE_INVALID', message: 'active lifecycle target has no short id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, changed: true, completedSteps: [...completed], pendingSteps: [], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
710
|
-
if (spec.target !== 'active' && mappings.has(taskId)) return failed(request, { code: 'LIFECYCLE_FINAL_STATE_INVALID', message: 'non-active lifecycle target retained a short id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, changed: true, completedSteps: [...completed], pendingSteps: [], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
814
|
+
if (request.intent !== 'complete' && spec.target !== 'active' && mappings.has(taskId)) return failed(request, { code: 'LIFECYCLE_FINAL_STATE_INVALID', message: 'non-active lifecycle target retained a short id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, changed: true, completedSteps: [...completed], pendingSteps: [], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
711
815
|
try { io.unlinkSync(journalPath); }
|
|
712
816
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_CLEANUP_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, shortId, changed: true, completedSteps: [...completed], pendingSteps: [], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
713
817
|
return {
|
|
@@ -719,7 +823,51 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
719
823
|
};
|
|
720
824
|
}
|
|
721
825
|
|
|
722
|
-
|
|
826
|
+
function applyTaskLifecycle(request: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
827
|
+
const normalized = normalizedRequest(request);
|
|
828
|
+
if ('code' in normalized) return applyTaskLifecycleUnlocked(request, options);
|
|
829
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
830
|
+
const taskId = normalized.intent === 'restore'
|
|
831
|
+
? normalized.taskRef
|
|
832
|
+
: (() => {
|
|
833
|
+
const resolved = resolveTaskRef(normalized.taskRef, { repoRoot });
|
|
834
|
+
return resolved.ok ? resolved.taskId : null;
|
|
835
|
+
})();
|
|
836
|
+
if (!taskId) return applyTaskLifecycleUnlocked(request, options);
|
|
837
|
+
try {
|
|
838
|
+
return withTaskExecutionLock(repoRoot, taskId, 'task-lifecycle.move', () => applyTaskLifecycleUnlocked(request, options));
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (error instanceof TaskExecutionLockError) {
|
|
841
|
+
return failed(request, { code: error.code, message: error.message }, { taskId });
|
|
842
|
+
}
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function applyTaskLifecycleWithinTaskLock(
|
|
848
|
+
request: TaskLifecycleRequest,
|
|
849
|
+
options: TaskLifecycleOptions,
|
|
850
|
+
lockedTaskId: string
|
|
851
|
+
): TaskLifecycleResult {
|
|
852
|
+
const normalized = normalizedRequest(request);
|
|
853
|
+
if ('code' in normalized) return failed(request, normalized);
|
|
854
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
855
|
+
const resolved = normalized.intent === 'restore'
|
|
856
|
+
? { ok: true as const, taskId: normalized.taskRef }
|
|
857
|
+
: resolveTaskRef(normalized.taskRef, { repoRoot });
|
|
858
|
+
if (!resolved.ok || resolved.taskId !== lockedTaskId) {
|
|
859
|
+
return failed(request, {
|
|
860
|
+
code: 'LIFECYCLE_LOCK_SCOPE_MISMATCH',
|
|
861
|
+
message: 'task lifecycle request does not match the held task lock'
|
|
862
|
+
}, { taskId: resolved.ok ? resolved.taskId : resolved.taskId });
|
|
863
|
+
}
|
|
864
|
+
return applyTaskLifecycleUnlocked(request, options);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export {
|
|
868
|
+
lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog,
|
|
869
|
+
applyTaskLifecycle, applyTaskLifecycleWithinTaskLock, previewTaskCompletion
|
|
870
|
+
};
|
|
723
871
|
export type {
|
|
724
872
|
TaskLifecycleIntent, TaskLifecycleRequest, TaskLifecycleResult, TaskLifecycleOptions,
|
|
725
873
|
LifecycleError, LifecycleJournal, LifecycleStep, TaskCompletionProjection
|