@fitlab-ai/agent-infra 0.11.12 → 0.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/sandbox/commands/create.js +6 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/client.js +4 -64
- package/dist/lib/sandbox/control/executor.js +4 -50
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +5 -16
- package/dist/lib/sandbox/control/server.js +203 -234
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +28 -9
- package/dist/lib/sandbox/removal.js +19 -0
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +48 -2
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +2 -0
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +14 -0
- package/dist/lib/task/lifecycle.js +203 -31
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +20 -23
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/sandbox/commands/create.ts +6 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/client.ts +5 -78
- package/lib/sandbox/control/executor.ts +2 -53
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +6 -20
- package/lib/sandbox/control/server.ts +9 -44
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +35 -16
- package/lib/sandbox/removal.ts +21 -2
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +41 -2
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +1 -0
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +14 -0
- package/lib/task/lifecycle.ts +180 -27
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
|
@@ -18,6 +18,7 @@ const lifecycleIntentCatalog = [
|
|
|
18
18
|
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
|
|
19
19
|
];
|
|
20
20
|
const lifecycleFailureCatalog = [
|
|
21
|
+
'LIFECYCLE_DIRECTORY_TRANSFER_FAILED',
|
|
21
22
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
22
23
|
'LIFECYCLE_DOCUMENT_INVALID',
|
|
23
24
|
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
@@ -46,6 +47,9 @@ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
|
|
|
46
47
|
code
|
|
47
48
|
}));
|
|
48
49
|
const STEPS = ['task-written', 'directory-moved', 'registry-committed'];
|
|
50
|
+
function stepsForIntent(intent) {
|
|
51
|
+
return intent === 'complete' ? ['task-written', 'directory-moved'] : STEPS;
|
|
52
|
+
}
|
|
49
53
|
const DEFAULT_IO = {
|
|
50
54
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
51
55
|
readFileSync: (file) => fs.readFileSync(file, 'utf8'),
|
|
@@ -58,7 +62,7 @@ function failed(request, error, extra = {}) {
|
|
|
58
62
|
intent: request.intent, sourceState: null, targetState: null, sourcePath: null,
|
|
59
63
|
targetPath: null, task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
60
64
|
shortId: { effect: 'unchanged', shortId: null, changed: false }, timestamp: null,
|
|
61
|
-
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps:
|
|
65
|
+
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps: stepsForIntent(request.intent),
|
|
62
66
|
error, ...extra
|
|
63
67
|
};
|
|
64
68
|
}
|
|
@@ -134,8 +138,105 @@ function transition(request) {
|
|
|
134
138
|
return { sources: ['active', 'blocked'], target: 'completed', registry: 'release' };
|
|
135
139
|
if (request.intent === 'restore')
|
|
136
140
|
return { sources: ['staging'], target: 'active', registry: 'alloc' };
|
|
141
|
+
if (request.intent === 'complete')
|
|
142
|
+
return { sources: ['active'], target: 'completed', registry: 'none' };
|
|
137
143
|
return { sources: ['active'], target: 'completed', registry: 'release' };
|
|
138
144
|
}
|
|
145
|
+
function sameTaskEntry(source, target) {
|
|
146
|
+
const sourceStat = fs.lstatSync(source);
|
|
147
|
+
let targetStat;
|
|
148
|
+
try {
|
|
149
|
+
targetStat = fs.lstatSync(target);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code === 'ENOENT')
|
|
153
|
+
return false;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (sourceStat.isSymbolicLink() || targetStat.isSymbolicLink()) {
|
|
157
|
+
return sourceStat.isSymbolicLink() && targetStat.isSymbolicLink()
|
|
158
|
+
&& fs.readlinkSync(source) === fs.readlinkSync(target);
|
|
159
|
+
}
|
|
160
|
+
if (sourceStat.isDirectory() && targetStat.isDirectory()) {
|
|
161
|
+
const sourceNames = fs.readdirSync(source).sort();
|
|
162
|
+
const targetNames = fs.readdirSync(target).sort();
|
|
163
|
+
return sourceNames.join('\0') === targetNames.join('\0')
|
|
164
|
+
&& sourceNames.every((name) => sameTaskEntry(path.join(source, name), path.join(target, name)))
|
|
165
|
+
&& (sourceStat.mode & 0o777) === (targetStat.mode & 0o777);
|
|
166
|
+
}
|
|
167
|
+
return sourceStat.isFile() && targetStat.isFile()
|
|
168
|
+
&& (sourceStat.mode & 0o777) === (targetStat.mode & 0o777)
|
|
169
|
+
&& fs.readFileSync(source).equals(fs.readFileSync(target));
|
|
170
|
+
}
|
|
171
|
+
function copyTaskEntry(source, target) {
|
|
172
|
+
const sourceStat = fs.lstatSync(source);
|
|
173
|
+
if (fs.existsSync(target)) {
|
|
174
|
+
const targetStat = fs.lstatSync(target);
|
|
175
|
+
if (sourceStat.isDirectory() && targetStat.isDirectory() && !targetStat.isSymbolicLink()) {
|
|
176
|
+
for (const name of fs.readdirSync(source).sort())
|
|
177
|
+
copyTaskEntry(path.join(source, name), path.join(target, name));
|
|
178
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (sameTaskEntry(source, target))
|
|
182
|
+
return;
|
|
183
|
+
throw new Error(`task content conflict at ${path.relative(path.dirname(source), source)}`);
|
|
184
|
+
}
|
|
185
|
+
if (sourceStat.isSymbolicLink()) {
|
|
186
|
+
fs.symlinkSync(fs.readlinkSync(source), target);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (sourceStat.isDirectory()) {
|
|
190
|
+
fs.mkdirSync(target, { mode: sourceStat.mode & 0o777 });
|
|
191
|
+
for (const name of fs.readdirSync(source).sort())
|
|
192
|
+
copyTaskEntry(path.join(source, name), path.join(target, name));
|
|
193
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!sourceStat.isFile())
|
|
197
|
+
throw new Error(`unsupported task entry: ${source}`);
|
|
198
|
+
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
199
|
+
fs.chmodSync(target, sourceStat.mode & 0o777);
|
|
200
|
+
}
|
|
201
|
+
function transferTaskContents(sourcePath, targetPath) {
|
|
202
|
+
const sourceRoot = fs.lstatSync(sourcePath);
|
|
203
|
+
if (!sourceRoot.isDirectory() || sourceRoot.isSymbolicLink())
|
|
204
|
+
throw new Error('task source is not a real directory');
|
|
205
|
+
const targetRoot = fs.lstatSync(targetPath);
|
|
206
|
+
if (!targetRoot.isDirectory() || targetRoot.isSymbolicLink())
|
|
207
|
+
throw new Error('completed target is not a real directory');
|
|
208
|
+
for (const name of fs.readdirSync(sourcePath).sort()) {
|
|
209
|
+
copyTaskEntry(path.join(sourcePath, name), path.join(targetPath, name));
|
|
210
|
+
}
|
|
211
|
+
const sourceNames = fs.readdirSync(sourcePath).sort();
|
|
212
|
+
const targetNames = fs.readdirSync(targetPath).sort();
|
|
213
|
+
if (sourceNames.join('\0') !== targetNames.join('\0')
|
|
214
|
+
|| !sourceNames.every((name) => sameTaskEntry(path.join(sourcePath, name), path.join(targetPath, name)))) {
|
|
215
|
+
throw new Error('completed target does not match task source after copy');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function targetContainsTask(targetPath, taskId, request) {
|
|
219
|
+
try {
|
|
220
|
+
const stat = fs.lstatSync(targetPath);
|
|
221
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
222
|
+
return false;
|
|
223
|
+
const entries = fs.readdirSync(targetPath);
|
|
224
|
+
if (entries.length === 0)
|
|
225
|
+
return true;
|
|
226
|
+
const taskPath = path.join(targetPath, 'task.md');
|
|
227
|
+
const journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
228
|
+
if (!fs.existsSync(taskPath) || !fs.existsSync(journalPath))
|
|
229
|
+
return false;
|
|
230
|
+
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
|
|
231
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
232
|
+
return frontmatter.id === taskId && frontmatter.status === 'completed'
|
|
233
|
+
&& journal.taskId === taskId && journal.intent === request.intent
|
|
234
|
+
&& journal.intentDigest === intentIdentity(request, taskId);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
139
240
|
function actionAndNote(request, restoredFiles = 0) {
|
|
140
241
|
if (request.intent === 'block')
|
|
141
242
|
return { action: 'Block Task', note: request.reason };
|
|
@@ -367,7 +468,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
367
468
|
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
368
469
|
|| journal.sourceState !== 'active' || journal.targetState !== 'completed')
|
|
369
470
|
return 'unknown';
|
|
370
|
-
if (journal.completedSteps.some((step, index) =>
|
|
471
|
+
if (journal.completedSteps.some((step, index) => stepsForIntent(journal.intent)[index] !== step))
|
|
371
472
|
return 'unknown';
|
|
372
473
|
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
373
474
|
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
@@ -375,7 +476,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
375
476
|
if (state === 'active' && (directoryMoved || !hasShortId))
|
|
376
477
|
return 'unknown';
|
|
377
478
|
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
378
|
-
|| !matchingCompletion(content, request) ||
|
|
479
|
+
|| !matchingCompletion(content, request) || registryCommitted || !hasShortId))
|
|
379
480
|
return 'unknown';
|
|
380
481
|
if (frontmatter.status !== state
|
|
381
482
|
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
@@ -411,7 +512,12 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
411
512
|
}
|
|
412
513
|
const spec = transition(request);
|
|
413
514
|
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
414
|
-
|
|
515
|
+
const activeSource = hot.find((candidate) => candidate.state === 'active');
|
|
516
|
+
const completedTarget = hot.find((candidate) => candidate.state === 'completed');
|
|
517
|
+
const transferInProgress = request.intent === 'complete' && Boolean(activeSource && completedTarget)
|
|
518
|
+
&& targetContainsTask(completedTarget.taskDir, taskId, request)
|
|
519
|
+
&& fs.existsSync(path.join(activeSource.taskDir, '.task-lifecycle.json'));
|
|
520
|
+
if (hot.length > 1 && !transferInProgress)
|
|
415
521
|
return failed(request, { code: 'LIFECYCLE_IDENTITY_CONFLICT', message: `task ${taskId} exists in multiple hot states` }, { taskId });
|
|
416
522
|
let sourceState;
|
|
417
523
|
let sourcePath;
|
|
@@ -424,6 +530,11 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
424
530
|
sourcePath = validation.stagingDir;
|
|
425
531
|
restoredFiles = validation.fileCount;
|
|
426
532
|
}
|
|
533
|
+
else if (activeSource && request.intent === 'complete') {
|
|
534
|
+
sourceState = activeSource.state;
|
|
535
|
+
sourcePath = activeSource.taskDir;
|
|
536
|
+
restoredFiles = fs.readdirSync(sourcePath).filter((name) => !name.startsWith('.')).length;
|
|
537
|
+
}
|
|
427
538
|
else if (hot[0]) {
|
|
428
539
|
sourceState = hot[0].state;
|
|
429
540
|
sourcePath = hot[0].taskDir;
|
|
@@ -433,13 +544,14 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
433
544
|
return failed(request, { code: 'LIFECYCLE_TASK_NOT_FOUND', message: `task ${taskId} has no lifecycle source` }, { taskId });
|
|
434
545
|
}
|
|
435
546
|
const targetPath = path.join(repoRoot, '.agents', 'workspace', spec.target, taskId);
|
|
547
|
+
const requiredSteps = stepsForIntent(request.intent);
|
|
436
548
|
const movingTaskDirectory = sourcePath !== targetPath;
|
|
437
549
|
const assertRuntimeMoveSafe = () => {
|
|
438
550
|
if (!movingTaskDirectory)
|
|
439
551
|
return null;
|
|
440
552
|
const controlManifest = path.join(sourcePath, '.runtime', 'sandbox-control', 'manifest.json');
|
|
441
553
|
const controlLease = path.join(sourcePath, '.runtime', 'sandbox-control', 'lease.json');
|
|
442
|
-
if (fs.existsSync(controlManifest) || fs.existsSync(controlLease)) {
|
|
554
|
+
if (request.intent !== 'complete' && (fs.existsSync(controlManifest) || fs.existsSync(controlLease))) {
|
|
443
555
|
return {
|
|
444
556
|
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
445
557
|
message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory'
|
|
@@ -474,7 +586,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
474
586
|
return {
|
|
475
587
|
...failed(request, { code: '', message: '' }), status: 'no-op', error: null, taskId,
|
|
476
588
|
sourceState, targetState: spec.target, sourcePath, targetPath, pendingSteps: [],
|
|
477
|
-
completedSteps:
|
|
589
|
+
completedSteps: requiredSteps, timestamp: typeof frontmatter.updated_at === 'string' ? frontmatter.updated_at : null,
|
|
478
590
|
agentInfraVersion: typeof frontmatter.agent_infra_version === 'string' ? frontmatter.agent_infra_version : null
|
|
479
591
|
};
|
|
480
592
|
}
|
|
@@ -483,7 +595,8 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
483
595
|
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
484
596
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
485
597
|
}
|
|
486
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
598
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
599
|
+
&& !(request.intent === 'complete' && targetContainsTask(targetPath, taskId, request))) {
|
|
487
600
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
488
601
|
}
|
|
489
602
|
const moveSafety = assertRuntimeMoveSafe();
|
|
@@ -515,7 +628,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
515
628
|
}
|
|
516
629
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
|
|
517
630
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
518
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
631
|
+
if ((spec.registry === 'release' || request.intent === 'complete') && sourceState === 'active' && !registeredKey) {
|
|
519
632
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
520
633
|
}
|
|
521
634
|
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
@@ -540,7 +653,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
540
653
|
if (request.dryRun) {
|
|
541
654
|
if (!journal.failure)
|
|
542
655
|
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 });
|
|
543
|
-
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps:
|
|
656
|
+
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)) });
|
|
544
657
|
}
|
|
545
658
|
if (journal.intentDigest !== digest)
|
|
546
659
|
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 });
|
|
@@ -591,7 +704,8 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
591
704
|
if (sourceState === spec.target && journal.sourceState !== spec.target)
|
|
592
705
|
completed.add('directory-moved');
|
|
593
706
|
const currentMappings = loadShortIdByTaskId(repoRoot);
|
|
594
|
-
if (
|
|
707
|
+
if (requiredSteps.includes('registry-committed')
|
|
708
|
+
&& ((spec.target === 'active' && currentMappings.has(taskId)) || (spec.target !== 'active' && !currentMappings.has(taskId)))) {
|
|
595
709
|
if (completed.has('directory-moved'))
|
|
596
710
|
completed.add('registry-committed');
|
|
597
711
|
}
|
|
@@ -603,7 +717,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
603
717
|
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
604
718
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
605
719
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
606
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
720
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
607
721
|
};
|
|
608
722
|
}
|
|
609
723
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles))
|
|
@@ -637,30 +751,42 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
637
751
|
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
638
752
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
639
753
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
640
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
754
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
641
755
|
};
|
|
642
756
|
}
|
|
643
|
-
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath))
|
|
757
|
+
if (fs.existsSync(targetPath) && !fs.existsSync(path.join(sourcePath, 'task.md')))
|
|
644
758
|
completed.add('directory-moved');
|
|
645
759
|
if (!completed.has('directory-moved')) {
|
|
646
760
|
const moveSafety = assertRuntimeMoveSafe();
|
|
647
761
|
if (moveSafety)
|
|
648
762
|
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 });
|
|
649
763
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
764
|
+
if (request.intent === 'complete') {
|
|
765
|
+
try {
|
|
766
|
+
if (!fs.existsSync(targetPath))
|
|
767
|
+
fs.mkdirSync(targetPath, { recursive: false, mode: 0o700 });
|
|
768
|
+
if (!targetContainsTask(targetPath, taskId, request)) {
|
|
769
|
+
throw new Error('completed target has no matching task identity');
|
|
770
|
+
}
|
|
771
|
+
transferTaskContents(sourcePath, targetPath);
|
|
772
|
+
for (const name of fs.readdirSync(sourcePath).filter((entry) => entry !== '.task-lifecycle.json' && entry !== 'task.md').sort()) {
|
|
773
|
+
fs.rmSync(path.join(sourcePath, name), { recursive: true, force: false });
|
|
774
|
+
}
|
|
775
|
+
fs.rmSync(path.join(sourcePath, 'task.md'), { force: false });
|
|
776
|
+
fs.rmSync(path.join(sourcePath, '.task-lifecycle.json'), { force: false });
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_TRANSFER_FAILED', message: String(error) };
|
|
780
|
+
const sourceJournalPath = path.join(sourcePath, '.task-lifecycle.json');
|
|
781
|
+
rememberJournalFailure(sourceJournalPath, journal, io, failure);
|
|
782
|
+
return failed(request, failure, {
|
|
783
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
784
|
+
journalPath: sourceJournalPath, task: { operations: taskOperations },
|
|
785
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
786
|
+
pendingSteps: requiredSteps.filter((step) => !completed.has(step) || step === 'directory-moved'),
|
|
787
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
788
|
+
});
|
|
789
|
+
}
|
|
664
790
|
completed.add('directory-moved');
|
|
665
791
|
journal.completedSteps = [...completed];
|
|
666
792
|
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
@@ -668,12 +794,58 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
668
794
|
writeJournal(journalPath, journal, io);
|
|
669
795
|
}
|
|
670
796
|
catch (error) {
|
|
671
|
-
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, {
|
|
797
|
+
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, {
|
|
798
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath,
|
|
799
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
800
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
801
|
+
pendingSteps: requiredSteps.filter((step) => step !== 'task-written'),
|
|
802
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
if (fs.existsSync(targetPath))
|
|
808
|
+
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 });
|
|
809
|
+
try {
|
|
810
|
+
(options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath);
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) };
|
|
814
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
815
|
+
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 });
|
|
816
|
+
}
|
|
817
|
+
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
818
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
819
|
+
}
|
|
820
|
+
else if (!completed.has('directory-moved')) {
|
|
821
|
+
completed.add('directory-moved');
|
|
822
|
+
journal.completedSteps = [...completed];
|
|
823
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
824
|
+
try {
|
|
825
|
+
writeJournal(journalPath, journal, io);
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
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 });
|
|
829
|
+
}
|
|
672
830
|
}
|
|
673
831
|
}
|
|
674
832
|
}
|
|
675
|
-
|
|
676
|
-
if (
|
|
833
|
+
const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
834
|
+
if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
|
|
835
|
+
try {
|
|
836
|
+
fs.rmdirSync(activeTaskPath);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
return failed(request, { code: 'LIFECYCLE_SOURCE_DIRECTORY_CLEANUP_FAILED', message: String(error) }, {
|
|
840
|
+
taskId, sourceState, targetState: spec.target, sourcePath: activeTaskPath, targetPath, journalPath,
|
|
841
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
842
|
+
changed: true, completedSteps: [...completed], pendingSteps: [],
|
|
843
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
let shortId = { effect: 'unchanged', shortId: request.intent === 'complete' ? registeredKey ?? null : null, changed: false };
|
|
848
|
+
if (requiredSteps.includes('registry-committed') && !completed.has('registry-committed')) {
|
|
677
849
|
try {
|
|
678
850
|
shortId = mutateShortIdRegistry(repoRoot, taskId, spec.registry);
|
|
679
851
|
}
|
|
@@ -692,7 +864,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
692
864
|
const mappings = loadShortIdByTaskId(repoRoot);
|
|
693
865
|
if (spec.target === 'active' && !mappings.has(taskId))
|
|
694
866
|
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 });
|
|
695
|
-
if (spec.target !== 'active' && mappings.has(taskId))
|
|
867
|
+
if (request.intent !== 'complete' && spec.target !== 'active' && mappings.has(taskId))
|
|
696
868
|
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 });
|
|
697
869
|
try {
|
|
698
870
|
io.unlinkSync(journalPath);
|
|
@@ -6,7 +6,7 @@ import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
|
6
6
|
import { validateCurrentTaskContract } from './current-contract.js';
|
|
7
7
|
import { parseReviewSummary } from './review-artifacts.js';
|
|
8
8
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
9
|
-
import { resolveTaskRef } from './resolve-ref.js';
|
|
9
|
+
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
|
|
10
10
|
import { activateDelegation, abortActivatedDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
|
|
11
11
|
import { normalizeAgentClients } from '../agent-clients/config.js';
|
|
12
12
|
import { getAgentClientAdapter, getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
|
|
@@ -77,7 +77,7 @@ function isModelPolicy(value) {
|
|
|
77
77
|
}
|
|
78
78
|
const RECOVERY_GUARD_KEYS = [
|
|
79
79
|
'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
|
|
80
|
-
'
|
|
80
|
+
'completionEvidenceAbsent'
|
|
81
81
|
];
|
|
82
82
|
function isRecovery(value) {
|
|
83
83
|
if (!hasExactKeys(value, [
|
|
@@ -97,7 +97,6 @@ function isRecovery(value) {
|
|
|
97
97
|
&& guards.baselineEmpty === true
|
|
98
98
|
&& guards.receiptCount === 0
|
|
99
99
|
&& guards.pendingDelegation === false
|
|
100
|
-
&& guards.commitAuthorizationUnused === true
|
|
101
100
|
&& guards.completionEvidenceAbsent === true
|
|
102
101
|
&& value.resultingStatus === 'running';
|
|
103
102
|
}
|
|
@@ -117,7 +116,7 @@ export function isCompletionEvidence(value) {
|
|
|
117
116
|
const ORCHESTRATION_RUN_KEYS = [
|
|
118
117
|
'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
|
|
119
118
|
'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
|
|
120
|
-
'pause', '
|
|
119
|
+
'pause', 'completionEvidence', 'createdAt', 'updatedAt'
|
|
121
120
|
];
|
|
122
121
|
function orchestrationStateReasonCodes(value) {
|
|
123
122
|
if (!hasExactKeys(value, ORCHESTRATION_RUN_KEYS))
|
|
@@ -151,15 +150,6 @@ function orchestrationStateReasonCodes(value) {
|
|
|
151
150
|
reasons.push('receipts');
|
|
152
151
|
if (!(record.pause === null || isPause(record.pause)))
|
|
153
152
|
reasons.push('pause');
|
|
154
|
-
if (!hasExactKeys(record.commitAuthorization, ['issuedAt', 'consumedAt'])) {
|
|
155
|
-
reasons.push('commitAuthorization');
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
if (!nullableText(record.commitAuthorization.issuedAt))
|
|
159
|
-
reasons.push('commitAuthorization.issuedAt');
|
|
160
|
-
if (!nullableText(record.commitAuthorization.consumedAt))
|
|
161
|
-
reasons.push('commitAuthorization.consumedAt');
|
|
162
|
-
}
|
|
163
153
|
if (!(record.completionEvidence === null || isCompletionEvidence(record.completionEvidence)))
|
|
164
154
|
reasons.push('completionEvidence');
|
|
165
155
|
if (!exactText(record.createdAt))
|
|
@@ -392,8 +382,6 @@ function canRecoverUnsupportedClientPause(run) {
|
|
|
392
382
|
&& run.baseline === ''
|
|
393
383
|
&& run.pendingDelegation === null
|
|
394
384
|
&& run.receipts.length === 0
|
|
395
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
396
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
397
385
|
&& run.completionEvidence == null
|
|
398
386
|
&& run.recoveryHistory.length === 0;
|
|
399
387
|
}
|
|
@@ -443,7 +431,6 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
443
431
|
baselineEmpty: true,
|
|
444
432
|
receiptCount: 0,
|
|
445
433
|
pendingDelegation: false,
|
|
446
|
-
commitAuthorizationUnused: true,
|
|
447
434
|
completionEvidenceAbsent: true
|
|
448
435
|
},
|
|
449
436
|
resultingStatus: 'running'
|
|
@@ -497,7 +484,6 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
497
484
|
pendingDelegation: null,
|
|
498
485
|
receipts: [],
|
|
499
486
|
pause: null,
|
|
500
|
-
commitAuthorization: { issuedAt: null, consumedAt: null },
|
|
501
487
|
completionEvidence: null,
|
|
502
488
|
createdAt: now,
|
|
503
489
|
updatedAt: now
|
|
@@ -858,72 +844,40 @@ function dispatchOrchestrationDelegation(taskRef, options = {}) {
|
|
|
858
844
|
}
|
|
859
845
|
}
|
|
860
846
|
function matchingDelegations(predicate, options) {
|
|
847
|
+
const taskId = options.taskId;
|
|
848
|
+
if (taskId === undefined || taskId === '') {
|
|
849
|
+
return { ok: false, error: failed('ORCHESTRATION_TASK_ID_REQUIRED', 'taskId is required for lifecycle matching') };
|
|
850
|
+
}
|
|
851
|
+
if (typeof taskId !== 'string' || !TASK_ID_RE.test(taskId)) {
|
|
852
|
+
return { ok: false, error: failed('ORCHESTRATION_TASK_ID_INVALID', 'taskId must be a canonical TASK-id') };
|
|
853
|
+
}
|
|
861
854
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
const activeRoot = path.join(repoRoot, '.agents', 'workspace', 'active');
|
|
870
|
-
if (!fs.existsSync(activeRoot))
|
|
871
|
-
return [];
|
|
872
|
-
emitDiagnostic(options, 'orchestration-active-scan-start', {
|
|
873
|
-
activeRoot
|
|
874
|
-
});
|
|
875
|
-
const matches = fs.readdirSync(activeRoot, { withFileTypes: true })
|
|
876
|
-
.filter((entry) => entry.isDirectory())
|
|
877
|
-
.flatMap((entry) => {
|
|
878
|
-
const candidateTaskId = entry.name;
|
|
879
|
-
const candidateTaskDir = path.join(activeRoot, candidateTaskId);
|
|
880
|
-
emitDiagnostic(options, 'orchestration-active-scan-candidate', {
|
|
881
|
-
candidateTaskId,
|
|
882
|
-
candidateTaskDir,
|
|
883
|
-
orchestrationPath: orchestrationPath(candidateTaskDir)
|
|
884
|
-
});
|
|
885
|
-
let run;
|
|
886
|
-
try {
|
|
887
|
-
run = readRun(candidateTaskDir, options);
|
|
888
|
-
}
|
|
889
|
-
catch (error) {
|
|
890
|
-
emitDiagnostic(options, 'orchestration-active-scan-candidate-failed', {
|
|
891
|
-
candidateTaskId,
|
|
892
|
-
candidateTaskDir,
|
|
893
|
-
errorType: error instanceof Error ? error.name : typeof error,
|
|
894
|
-
reasonCodes: error instanceof OrchestrationStateError ? error.reasonCodes.join(',') : null
|
|
895
|
-
});
|
|
896
|
-
throw error;
|
|
897
|
-
}
|
|
898
|
-
if (!run || run.status !== 'running' || !run.pendingDelegation)
|
|
899
|
-
return [];
|
|
900
|
-
const receipt = run.pendingDelegation;
|
|
901
|
-
return predicate(receipt)
|
|
902
|
-
? [{ taskId: candidateTaskId, run }]
|
|
903
|
-
: [];
|
|
904
|
-
});
|
|
905
|
-
emitDiagnostic(options, 'orchestration-active-scan-finished', {
|
|
906
|
-
activeRoot,
|
|
907
|
-
matchingDelegations: matches.length
|
|
908
|
-
});
|
|
909
|
-
return matches;
|
|
855
|
+
const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
856
|
+
const run = readRun(taskDir, options);
|
|
857
|
+
const matches = run && run.status === 'running' && run.pendingDelegation && predicate(run.pendingDelegation)
|
|
858
|
+
? [{ taskId, run }]
|
|
859
|
+
: [];
|
|
860
|
+
return { ok: true, matches };
|
|
910
861
|
}
|
|
911
862
|
function uniqueMatchingDelegation(client, predicate, options) {
|
|
912
|
-
const
|
|
863
|
+
const lookup = matchingDelegations((receipt) => receipt.client === client && predicate(receipt), options);
|
|
864
|
+
if (!lookup.ok)
|
|
865
|
+
return lookup.error;
|
|
866
|
+
const matches = lookup.matches;
|
|
913
867
|
if (matches.length === 0)
|
|
914
868
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
|
|
915
|
-
if (matches.length > 1)
|
|
916
|
-
return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
|
|
917
869
|
return matches[0];
|
|
918
870
|
}
|
|
919
871
|
function hasActivatableOrchestrationDelegation(client, childId, options = {}) {
|
|
920
|
-
|
|
921
|
-
|| (receipt.childId === childId && ['activated', 'stage-completed'].includes(receipt.status))), options)
|
|
872
|
+
const lookup = matchingDelegations((receipt) => receipt.client === client && (receipt.status === 'prepared'
|
|
873
|
+
|| (receipt.childId === childId && ['activated', 'stage-completed'].includes(receipt.status))), options);
|
|
874
|
+
return lookup.ok && lookup.matches.length > 0;
|
|
922
875
|
}
|
|
923
876
|
function hasSealableOrchestrationDelegation(client, childId, options = {}) {
|
|
924
|
-
|
|
877
|
+
const lookup = matchingDelegations((receipt) => receipt.client === client
|
|
925
878
|
&& receipt.childId === childId
|
|
926
|
-
&& ['stage-completed', 'sealed'].includes(receipt.status), options)
|
|
879
|
+
&& ['stage-completed', 'sealed'].includes(receipt.status), options);
|
|
880
|
+
return lookup.ok && lookup.matches.length > 0;
|
|
927
881
|
}
|
|
928
882
|
function activateMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
929
883
|
const role = managedDelegationRole(event.nativeAgent);
|