@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
|
@@ -13,12 +13,13 @@ import { validateCurrentTaskContract } from './current-contract.js';
|
|
|
13
13
|
import { isValidAgentInfraVersion } from '../version.js';
|
|
14
14
|
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
15
15
|
import { resourceIdentityEquals } from '../platform/resource-identity.js';
|
|
16
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
16
17
|
const lifecycleIntentCatalog = [
|
|
17
18
|
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
|
|
18
19
|
];
|
|
19
20
|
const lifecycleFailureCatalog = [
|
|
21
|
+
'LIFECYCLE_DIRECTORY_TRANSFER_FAILED',
|
|
20
22
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
21
|
-
'LIFECYCLE_DIRECTORY_COPY_FAILED',
|
|
22
23
|
'LIFECYCLE_DOCUMENT_INVALID',
|
|
23
24
|
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
24
25
|
'LIFECYCLE_IDENTITY_CONFLICT',
|
|
@@ -32,6 +33,7 @@ const lifecycleFailureCatalog = [
|
|
|
32
33
|
'LIFECYCLE_PAYLOAD_INVALID',
|
|
33
34
|
'LIFECYCLE_SHORT_ID_FAILED',
|
|
34
35
|
'LIFECYCLE_SHORT_ID_PRECONDITION',
|
|
36
|
+
'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
35
37
|
'LIFECYCLE_SOURCE_INVALID',
|
|
36
38
|
'LIFECYCLE_STAGING_IDENTITY_INVALID',
|
|
37
39
|
'LIFECYCLE_STAGING_INVALID',
|
|
@@ -45,6 +47,9 @@ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
|
|
|
45
47
|
code
|
|
46
48
|
}));
|
|
47
49
|
const STEPS = ['task-written', 'directory-moved', 'registry-committed'];
|
|
50
|
+
function stepsForIntent(intent) {
|
|
51
|
+
return intent === 'complete' ? ['task-written', 'directory-moved'] : STEPS;
|
|
52
|
+
}
|
|
48
53
|
const DEFAULT_IO = {
|
|
49
54
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
50
55
|
readFileSync: (file) => fs.readFileSync(file, 'utf8'),
|
|
@@ -57,7 +62,7 @@ function failed(request, error, extra = {}) {
|
|
|
57
62
|
intent: request.intent, sourceState: null, targetState: null, sourcePath: null,
|
|
58
63
|
targetPath: null, task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
59
64
|
shortId: { effect: 'unchanged', shortId: null, changed: false }, timestamp: null,
|
|
60
|
-
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps:
|
|
65
|
+
agentInfraVersion: null, journalPath: null, completedSteps: [], pendingSteps: stepsForIntent(request.intent),
|
|
61
66
|
error, ...extra
|
|
62
67
|
};
|
|
63
68
|
}
|
|
@@ -133,8 +138,105 @@ function transition(request) {
|
|
|
133
138
|
return { sources: ['active', 'blocked'], target: 'completed', registry: 'release' };
|
|
134
139
|
if (request.intent === 'restore')
|
|
135
140
|
return { sources: ['staging'], target: 'active', registry: 'alloc' };
|
|
141
|
+
if (request.intent === 'complete')
|
|
142
|
+
return { sources: ['active'], target: 'completed', registry: 'none' };
|
|
136
143
|
return { sources: ['active'], target: 'completed', registry: 'release' };
|
|
137
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
|
+
}
|
|
138
240
|
function actionAndNote(request, restoredFiles = 0) {
|
|
139
241
|
if (request.intent === 'block')
|
|
140
242
|
return { action: 'Block Task', note: request.reason };
|
|
@@ -366,7 +468,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
366
468
|
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
367
469
|
|| journal.sourceState !== 'active' || journal.targetState !== 'completed')
|
|
368
470
|
return 'unknown';
|
|
369
|
-
if (journal.completedSteps.some((step, index) =>
|
|
471
|
+
if (journal.completedSteps.some((step, index) => stepsForIntent(journal.intent)[index] !== step))
|
|
370
472
|
return 'unknown';
|
|
371
473
|
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
372
474
|
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
@@ -374,7 +476,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
374
476
|
if (state === 'active' && (directoryMoved || !hasShortId))
|
|
375
477
|
return 'unknown';
|
|
376
478
|
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
377
|
-
|| !matchingCompletion(content, request) ||
|
|
479
|
+
|| !matchingCompletion(content, request) || registryCommitted || !hasShortId))
|
|
378
480
|
return 'unknown';
|
|
379
481
|
if (frontmatter.status !== state
|
|
380
482
|
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
@@ -386,66 +488,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
386
488
|
return 'unknown';
|
|
387
489
|
}
|
|
388
490
|
}
|
|
389
|
-
function
|
|
390
|
-
const entries = [];
|
|
391
|
-
const visit = (current, relative = '') => {
|
|
392
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
393
|
-
const relativePath = relative ? path.join(relative, entry.name) : entry.name;
|
|
394
|
-
const absolute = path.join(current, entry.name);
|
|
395
|
-
const stat = fs.lstatSync(absolute);
|
|
396
|
-
if (stat.isSymbolicLink())
|
|
397
|
-
throw new Error(`symbolic link is not allowed in lifecycle directory: ${relativePath}`);
|
|
398
|
-
if (entry.isDirectory()) {
|
|
399
|
-
entries.push({ relativePath, kind: 'directory', size: 0, digest: '' });
|
|
400
|
-
visit(absolute, relativePath);
|
|
401
|
-
}
|
|
402
|
-
else if (entry.isFile()) {
|
|
403
|
-
const digest = createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
404
|
-
entries.push({ relativePath, kind: 'file', size: stat.size, digest });
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
throw new Error(`unsupported lifecycle directory entry: ${relativePath}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
visit(root);
|
|
412
|
-
return entries;
|
|
413
|
-
}
|
|
414
|
-
function copyDirectoryVerified(source, target) {
|
|
415
|
-
if (fs.existsSync(target))
|
|
416
|
-
throw new Error(`target directory already exists: ${target}`);
|
|
417
|
-
const copy = (currentSource, currentTarget) => {
|
|
418
|
-
fs.mkdirSync(currentTarget);
|
|
419
|
-
for (const entry of fs.readdirSync(currentSource, { withFileTypes: true })) {
|
|
420
|
-
const sourcePath = path.join(currentSource, entry.name);
|
|
421
|
-
const targetPath = path.join(currentTarget, entry.name);
|
|
422
|
-
const stat = fs.lstatSync(sourcePath);
|
|
423
|
-
if (stat.isSymbolicLink())
|
|
424
|
-
throw new Error(`symbolic link is not allowed in lifecycle directory: ${entry.name}`);
|
|
425
|
-
if (entry.isDirectory())
|
|
426
|
-
copy(sourcePath, targetPath);
|
|
427
|
-
else if (entry.isFile())
|
|
428
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
429
|
-
else
|
|
430
|
-
throw new Error(`unsupported lifecycle directory entry: ${entry.name}`);
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
copy(source, target);
|
|
434
|
-
const sourceManifest = directoryManifest(source);
|
|
435
|
-
const targetManifest = directoryManifest(target);
|
|
436
|
-
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
437
|
-
throw new Error('copied lifecycle directory failed manifest verification');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
function removeCopiedSource(source, target) {
|
|
441
|
-
const sourceManifest = directoryManifest(source);
|
|
442
|
-
const targetManifest = directoryManifest(target);
|
|
443
|
-
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
444
|
-
throw new Error('lifecycle source and target diverged before source cleanup');
|
|
445
|
-
}
|
|
446
|
-
fs.rmSync(source, { recursive: true, force: false });
|
|
447
|
-
}
|
|
448
|
-
function applyTaskLifecycle(requestInput, options = {}) {
|
|
491
|
+
function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
449
492
|
if (requestInput.intent === 'recover-started') {
|
|
450
493
|
return failed(requestInput, { code: 'LIFECYCLE_RECOVERY_REQUIRES_AUTHORITY', message: 'recover-started must be dispatched through task control authority' });
|
|
451
494
|
}
|
|
@@ -469,7 +512,12 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
469
512
|
}
|
|
470
513
|
const spec = transition(request);
|
|
471
514
|
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
472
|
-
|
|
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)
|
|
473
521
|
return failed(request, { code: 'LIFECYCLE_IDENTITY_CONFLICT', message: `task ${taskId} exists in multiple hot states` }, { taskId });
|
|
474
522
|
let sourceState;
|
|
475
523
|
let sourcePath;
|
|
@@ -482,6 +530,11 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
482
530
|
sourcePath = validation.stagingDir;
|
|
483
531
|
restoredFiles = validation.fileCount;
|
|
484
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
|
+
}
|
|
485
538
|
else if (hot[0]) {
|
|
486
539
|
sourceState = hot[0].state;
|
|
487
540
|
sourcePath = hot[0].taskDir;
|
|
@@ -491,6 +544,39 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
491
544
|
return failed(request, { code: 'LIFECYCLE_TASK_NOT_FOUND', message: `task ${taskId} has no lifecycle source` }, { taskId });
|
|
492
545
|
}
|
|
493
546
|
const targetPath = path.join(repoRoot, '.agents', 'workspace', spec.target, taskId);
|
|
547
|
+
const requiredSteps = stepsForIntent(request.intent);
|
|
548
|
+
const movingTaskDirectory = sourcePath !== targetPath;
|
|
549
|
+
const assertRuntimeMoveSafe = () => {
|
|
550
|
+
if (!movingTaskDirectory)
|
|
551
|
+
return null;
|
|
552
|
+
const controlManifest = path.join(sourcePath, '.runtime', 'sandbox-control', 'manifest.json');
|
|
553
|
+
const controlLease = path.join(sourcePath, '.runtime', 'sandbox-control', 'lease.json');
|
|
554
|
+
if (request.intent !== 'complete' && (fs.existsSync(controlManifest) || fs.existsSync(controlLease))) {
|
|
555
|
+
return {
|
|
556
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
557
|
+
message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory'
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
|
|
561
|
+
if (fs.existsSync(orchestrationPath)) {
|
|
562
|
+
try {
|
|
563
|
+
const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8'));
|
|
564
|
+
if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
|
|
565
|
+
return {
|
|
566
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
567
|
+
message: 'task orchestration has an active or pending delegation; settle it before moving the task directory'
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return {
|
|
573
|
+
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
574
|
+
message: 'task orchestration state is unreadable; refusing to move the task directory'
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
};
|
|
494
580
|
const currentTaskFile = path.join(sourcePath, 'task.md');
|
|
495
581
|
const existingJournal = fs.existsSync(path.join(sourcePath, '.task-lifecycle.json'));
|
|
496
582
|
if (sourceState === spec.target && !existingJournal) {
|
|
@@ -500,7 +586,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
500
586
|
return {
|
|
501
587
|
...failed(request, { code: '', message: '' }), status: 'no-op', error: null, taskId,
|
|
502
588
|
sourceState, targetState: spec.target, sourcePath, targetPath, pendingSteps: [],
|
|
503
|
-
completedSteps:
|
|
589
|
+
completedSteps: requiredSteps, timestamp: typeof frontmatter.updated_at === 'string' ? frontmatter.updated_at : null,
|
|
504
590
|
agentInfraVersion: typeof frontmatter.agent_infra_version === 'string' ? frontmatter.agent_infra_version : null
|
|
505
591
|
};
|
|
506
592
|
}
|
|
@@ -509,9 +595,13 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
509
595
|
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
510
596
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
511
597
|
}
|
|
512
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
598
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
599
|
+
&& !(request.intent === 'complete' && targetContainsTask(targetPath, taskId, request))) {
|
|
513
600
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
514
601
|
}
|
|
602
|
+
const moveSafety = assertRuntimeMoveSafe();
|
|
603
|
+
if (moveSafety)
|
|
604
|
+
return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
515
605
|
const content = io.readFileSync(currentTaskFile);
|
|
516
606
|
let frontmatter;
|
|
517
607
|
try {
|
|
@@ -538,7 +628,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
538
628
|
}
|
|
539
629
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
|
|
540
630
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
541
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
631
|
+
if ((spec.registry === 'release' || request.intent === 'complete') && sourceState === 'active' && !registeredKey) {
|
|
542
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 });
|
|
543
633
|
}
|
|
544
634
|
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
@@ -563,7 +653,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
563
653
|
if (request.dryRun) {
|
|
564
654
|
if (!journal.failure)
|
|
565
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 });
|
|
566
|
-
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)) });
|
|
567
657
|
}
|
|
568
658
|
if (journal.intentDigest !== digest)
|
|
569
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 });
|
|
@@ -614,7 +704,8 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
614
704
|
if (sourceState === spec.target && journal.sourceState !== spec.target)
|
|
615
705
|
completed.add('directory-moved');
|
|
616
706
|
const currentMappings = loadShortIdByTaskId(repoRoot);
|
|
617
|
-
if (
|
|
707
|
+
if (requiredSteps.includes('registry-committed')
|
|
708
|
+
&& ((spec.target === 'active' && currentMappings.has(taskId)) || (spec.target !== 'active' && !currentMappings.has(taskId)))) {
|
|
618
709
|
if (completed.has('directory-moved'))
|
|
619
710
|
completed.add('registry-committed');
|
|
620
711
|
}
|
|
@@ -626,7 +717,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
626
717
|
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
627
718
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
628
719
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
629
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
720
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
630
721
|
};
|
|
631
722
|
}
|
|
632
723
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles))
|
|
@@ -660,48 +751,42 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
660
751
|
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
661
752
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
662
753
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
663
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
754
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
664
755
|
};
|
|
665
756
|
}
|
|
666
|
-
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath))
|
|
757
|
+
if (fs.existsSync(targetPath) && !fs.existsSync(path.join(sourcePath, 'task.md')))
|
|
667
758
|
completed.add('directory-moved');
|
|
668
759
|
if (!completed.has('directory-moved')) {
|
|
760
|
+
const moveSafety = assertRuntimeMoveSafe();
|
|
761
|
+
if (moveSafety)
|
|
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 });
|
|
669
763
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (error.code === 'EXDEV') {
|
|
677
|
-
try {
|
|
678
|
-
copyDirectoryVerified(sourcePath, targetPath);
|
|
679
|
-
completed.add('directory-moved');
|
|
680
|
-
journal.completedSteps = [...completed];
|
|
681
|
-
const copiedJournalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
682
|
-
writeJournal(initialJournalPath, journal, io);
|
|
683
|
-
writeJournal(copiedJournalPath, journal, io);
|
|
684
|
-
removeCopiedSource(sourcePath, targetPath);
|
|
685
|
-
journalPath = copiedJournalPath;
|
|
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');
|
|
686
770
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(copyError) };
|
|
691
|
-
rememberJournalFailure(initialJournalPath, journal, io, failure);
|
|
692
|
-
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 });
|
|
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 });
|
|
693
774
|
}
|
|
775
|
+
fs.rmSync(path.join(sourcePath, 'task.md'), { force: false });
|
|
776
|
+
fs.rmSync(path.join(sourcePath, '.task-lifecycle.json'), { force: false });
|
|
694
777
|
}
|
|
695
|
-
|
|
696
|
-
const failure = { code: '
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
});
|
|
699
789
|
}
|
|
700
|
-
}
|
|
701
|
-
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
702
|
-
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
703
|
-
}
|
|
704
|
-
else if (!completed.has('directory-moved')) {
|
|
705
790
|
completed.add('directory-moved');
|
|
706
791
|
journal.completedSteps = [...completed];
|
|
707
792
|
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
@@ -709,23 +794,58 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
709
794
|
writeJournal(journalPath, journal, io);
|
|
710
795
|
}
|
|
711
796
|
catch (error) {
|
|
712
|
-
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
|
+
}
|
|
713
830
|
}
|
|
714
831
|
}
|
|
715
832
|
}
|
|
716
|
-
|
|
833
|
+
const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
834
|
+
if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
|
|
717
835
|
try {
|
|
718
|
-
|
|
719
|
-
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
836
|
+
fs.rmdirSync(activeTaskPath);
|
|
720
837
|
}
|
|
721
838
|
catch (error) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
+
});
|
|
725
845
|
}
|
|
726
846
|
}
|
|
727
|
-
let shortId = { effect: 'unchanged', shortId: null, changed: false };
|
|
728
|
-
if (!completed.has('registry-committed')) {
|
|
847
|
+
let shortId = { effect: 'unchanged', shortId: request.intent === 'complete' ? registeredKey ?? null : null, changed: false };
|
|
848
|
+
if (requiredSteps.includes('registry-committed') && !completed.has('registry-committed')) {
|
|
729
849
|
try {
|
|
730
850
|
shortId = mutateShortIdRegistry(repoRoot, taskId, spec.registry);
|
|
731
851
|
}
|
|
@@ -744,7 +864,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
744
864
|
const mappings = loadShortIdByTaskId(repoRoot);
|
|
745
865
|
if (spec.target === 'active' && !mappings.has(taskId))
|
|
746
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 });
|
|
747
|
-
if (spec.target !== 'active' && mappings.has(taskId))
|
|
867
|
+
if (request.intent !== 'complete' && spec.target !== 'active' && mappings.has(taskId))
|
|
748
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 });
|
|
749
869
|
try {
|
|
750
870
|
io.unlinkSync(journalPath);
|
|
@@ -760,5 +880,44 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
760
880
|
journalPath, completedSteps: [...completed], pendingSteps: []
|
|
761
881
|
};
|
|
762
882
|
}
|
|
763
|
-
|
|
883
|
+
function applyTaskLifecycle(request, options = {}) {
|
|
884
|
+
const normalized = normalizedRequest(request);
|
|
885
|
+
if ('code' in normalized)
|
|
886
|
+
return applyTaskLifecycleUnlocked(request, options);
|
|
887
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
888
|
+
const taskId = normalized.intent === 'restore'
|
|
889
|
+
? normalized.taskRef
|
|
890
|
+
: (() => {
|
|
891
|
+
const resolved = resolveTaskRef(normalized.taskRef, { repoRoot });
|
|
892
|
+
return resolved.ok ? resolved.taskId : null;
|
|
893
|
+
})();
|
|
894
|
+
if (!taskId)
|
|
895
|
+
return applyTaskLifecycleUnlocked(request, options);
|
|
896
|
+
try {
|
|
897
|
+
return withTaskExecutionLock(repoRoot, taskId, 'task-lifecycle.move', () => applyTaskLifecycleUnlocked(request, options));
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
if (error instanceof TaskExecutionLockError) {
|
|
901
|
+
return failed(request, { code: error.code, message: error.message }, { taskId });
|
|
902
|
+
}
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function applyTaskLifecycleWithinTaskLock(request, options, lockedTaskId) {
|
|
907
|
+
const normalized = normalizedRequest(request);
|
|
908
|
+
if ('code' in normalized)
|
|
909
|
+
return failed(request, normalized);
|
|
910
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
911
|
+
const resolved = normalized.intent === 'restore'
|
|
912
|
+
? { ok: true, taskId: normalized.taskRef }
|
|
913
|
+
: resolveTaskRef(normalized.taskRef, { repoRoot });
|
|
914
|
+
if (!resolved.ok || resolved.taskId !== lockedTaskId) {
|
|
915
|
+
return failed(request, {
|
|
916
|
+
code: 'LIFECYCLE_LOCK_SCOPE_MISMATCH',
|
|
917
|
+
message: 'task lifecycle request does not match the held task lock'
|
|
918
|
+
}, { taskId: resolved.ok ? resolved.taskId : resolved.taskId });
|
|
919
|
+
}
|
|
920
|
+
return applyTaskLifecycleUnlocked(request, options);
|
|
921
|
+
}
|
|
922
|
+
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle, applyTaskLifecycleWithinTaskLock, previewTaskCompletion };
|
|
764
923
|
//# sourceMappingURL=lifecycle.js.map
|