@fitlab-ai/agent-infra 0.11.12 → 0.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +29 -20
- package/dist/lib/internal/task-lifecycle.js +4 -2
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +11 -6
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +15 -110
- package/dist/lib/sandbox/control/executor.js +9 -59
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +75 -9
- package/dist/lib/sandbox/control/protocol.js +11 -50
- package/dist/lib/sandbox/control/server.js +203 -253
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -52
- package/dist/lib/sandbox/recovery.js +30 -12
- package/dist/lib/sandbox/removal.js +37 -3
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +89 -6
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +41 -17
- package/dist/lib/task/control-recovery.js +2 -4
- package/dist/lib/task/finalization.js +27 -2
- package/dist/lib/task/lifecycle.js +294 -63
- 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/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +33 -19
- package/lib/internal/task-lifecycle.ts +4 -2
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +11 -5
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +17 -138
- package/lib/sandbox/control/executor.ts +8 -65
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +75 -10
- package/lib/sandbox/control/protocol.ts +12 -62
- package/lib/sandbox/control/server.ts +11 -61
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +93 -68
- package/lib/sandbox/recovery.ts +37 -19
- package/lib/sandbox/removal.ts +36 -5
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +81 -7
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +41 -26
- package/lib/task/control-recovery.ts +3 -5
- package/lib/task/finalization.ts +29 -2
- package/lib/task/lifecycle.ts +282 -57
- 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/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/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/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/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 +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
|
@@ -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,195 @@ 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 pathExists(candidate) {
|
|
219
|
+
try {
|
|
220
|
+
fs.lstatSync(candidate);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error.code === 'ENOENT')
|
|
225
|
+
return false;
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function assertTaskControlRootPath(controlRoot, taskId, expectedProject) {
|
|
230
|
+
if (path.basename(controlRoot) !== taskId || path.basename(path.dirname(controlRoot)) !== 'tasks'
|
|
231
|
+
|| (expectedProject !== undefined && path.basename(path.dirname(path.dirname(controlRoot))) !== expectedProject)) {
|
|
232
|
+
throw new Error('control root path does not match the task/project layout');
|
|
233
|
+
}
|
|
234
|
+
const managedBase = path.dirname(path.dirname(path.dirname(controlRoot)));
|
|
235
|
+
let current = managedBase;
|
|
236
|
+
for (const segment of path.relative(managedBase, controlRoot).split(path.sep).filter(Boolean)) {
|
|
237
|
+
const stat = fs.lstatSync(current);
|
|
238
|
+
if (stat.isSymbolicLink())
|
|
239
|
+
throw new Error('control root ancestor is a symbolic link');
|
|
240
|
+
current = path.join(current, segment);
|
|
241
|
+
}
|
|
242
|
+
const stat = fs.lstatSync(controlRoot);
|
|
243
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
244
|
+
throw new Error('control root is not a real directory');
|
|
245
|
+
}
|
|
246
|
+
function assertTaskControlManifestIdentity(controlRoot, taskId, repoRoot, expectedProject) {
|
|
247
|
+
const controlManifest = path.join(controlRoot, 'manifest.json');
|
|
248
|
+
if (!fs.existsSync(controlManifest))
|
|
249
|
+
throw new Error('control manifest is missing');
|
|
250
|
+
const manifestStat = fs.lstatSync(controlManifest);
|
|
251
|
+
if (!manifestStat.isFile() || manifestStat.isSymbolicLink())
|
|
252
|
+
throw new Error('control manifest is not a real file');
|
|
253
|
+
const manifest = JSON.parse(fs.readFileSync(controlManifest, 'utf8'));
|
|
254
|
+
const project = expectedProject ?? path.basename(path.dirname(path.dirname(controlRoot)));
|
|
255
|
+
if (manifest.mode !== 'task-bound' || manifest.taskId !== taskId || manifest.project !== project
|
|
256
|
+
|| typeof manifest.repoRoot !== 'string' || fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
|
|
257
|
+
throw new Error('control manifest identity does not match the task');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function taskBoundControlMoveSafety(request, controlRoot, options, taskId, repoRoot, validateManifestIdentity) {
|
|
261
|
+
if (!controlRoot)
|
|
262
|
+
return null;
|
|
263
|
+
const controlManifest = path.join(controlRoot, 'manifest.json');
|
|
264
|
+
const controlLease = path.join(controlRoot, 'lease.json');
|
|
265
|
+
const stateExists = pathExists(controlRoot) || pathExists(controlManifest) || pathExists(controlLease);
|
|
266
|
+
if (stateExists) {
|
|
267
|
+
try {
|
|
268
|
+
assertTaskControlRootPath(controlRoot, taskId, options.controlProject);
|
|
269
|
+
if (validateManifestIdentity) {
|
|
270
|
+
assertTaskControlManifestIdentity(controlRoot, taskId, repoRoot, options.controlProject);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const manifestStat = fs.lstatSync(controlManifest);
|
|
274
|
+
if (!manifestStat.isFile() || manifestStat.isSymbolicLink())
|
|
275
|
+
throw new Error('control manifest is not a real file');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task-bound sandbox control root is missing, malformed, or belongs to another task' };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (request.intent !== 'complete' && (pathExists(controlManifest) || pathExists(controlLease))) {
|
|
283
|
+
return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory' };
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function orchestrationMoveSafety(sourcePath) {
|
|
288
|
+
const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
|
|
289
|
+
if (!pathExists(orchestrationPath))
|
|
290
|
+
return null;
|
|
291
|
+
try {
|
|
292
|
+
const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8'));
|
|
293
|
+
if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
|
|
294
|
+
return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task orchestration has an active or pending delegation; settle it before moving the task directory' };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task orchestration state is unreadable; refusing to move the task directory' };
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function runtimeMoveSafety(request, movingTaskDirectory, options, taskId, repoRoot, sourcePath, validateControlManifestIdentity) {
|
|
303
|
+
if (!movingTaskDirectory)
|
|
304
|
+
return null;
|
|
305
|
+
const controlSafety = taskBoundControlMoveSafety(request, options.controlRoot, options, taskId, repoRoot, validateControlManifestIdentity);
|
|
306
|
+
return controlSafety ?? orchestrationMoveSafety(sourcePath);
|
|
307
|
+
}
|
|
308
|
+
function targetContainsTask(targetPath, taskId, request) {
|
|
309
|
+
try {
|
|
310
|
+
const stat = fs.lstatSync(targetPath);
|
|
311
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
312
|
+
return false;
|
|
313
|
+
const entries = fs.readdirSync(targetPath);
|
|
314
|
+
if (entries.length === 0)
|
|
315
|
+
return true;
|
|
316
|
+
const taskPath = path.join(targetPath, 'task.md');
|
|
317
|
+
const journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
318
|
+
if (!fs.existsSync(taskPath) || !fs.existsSync(journalPath))
|
|
319
|
+
return false;
|
|
320
|
+
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
|
|
321
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
322
|
+
return frontmatter.id === taskId && frontmatter.status === 'completed'
|
|
323
|
+
&& journal.taskId === taskId && journal.intent === request.intent
|
|
324
|
+
&& journal.intentDigest === intentIdentity(request, taskId);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
139
330
|
function actionAndNote(request, restoredFiles = 0) {
|
|
140
331
|
if (request.intent === 'block')
|
|
141
332
|
return { action: 'Block Task', note: request.reason };
|
|
@@ -367,7 +558,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
367
558
|
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
368
559
|
|| journal.sourceState !== 'active' || journal.targetState !== 'completed')
|
|
369
560
|
return 'unknown';
|
|
370
|
-
if (journal.completedSteps.some((step, index) =>
|
|
561
|
+
if (journal.completedSteps.some((step, index) => stepsForIntent(journal.intent)[index] !== step))
|
|
371
562
|
return 'unknown';
|
|
372
563
|
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
373
564
|
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
@@ -375,7 +566,7 @@ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
|
375
566
|
if (state === 'active' && (directoryMoved || !hasShortId))
|
|
376
567
|
return 'unknown';
|
|
377
568
|
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
378
|
-
|| !matchingCompletion(content, request) ||
|
|
569
|
+
|| !matchingCompletion(content, request) || registryCommitted || !hasShortId))
|
|
379
570
|
return 'unknown';
|
|
380
571
|
if (frontmatter.status !== state
|
|
381
572
|
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
@@ -411,7 +602,12 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
411
602
|
}
|
|
412
603
|
const spec = transition(request);
|
|
413
604
|
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
414
|
-
|
|
605
|
+
const activeSource = hot.find((candidate) => candidate.state === 'active');
|
|
606
|
+
const completedTarget = hot.find((candidate) => candidate.state === 'completed');
|
|
607
|
+
const transferInProgress = request.intent === 'complete' && Boolean(activeSource && completedTarget)
|
|
608
|
+
&& targetContainsTask(completedTarget.taskDir, taskId, request)
|
|
609
|
+
&& fs.existsSync(path.join(activeSource.taskDir, '.task-lifecycle.json'));
|
|
610
|
+
if (hot.length > 1 && !transferInProgress)
|
|
415
611
|
return failed(request, { code: 'LIFECYCLE_IDENTITY_CONFLICT', message: `task ${taskId} exists in multiple hot states` }, { taskId });
|
|
416
612
|
let sourceState;
|
|
417
613
|
let sourcePath;
|
|
@@ -424,6 +620,11 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
424
620
|
sourcePath = validation.stagingDir;
|
|
425
621
|
restoredFiles = validation.fileCount;
|
|
426
622
|
}
|
|
623
|
+
else if (activeSource && request.intent === 'complete') {
|
|
624
|
+
sourceState = activeSource.state;
|
|
625
|
+
sourcePath = activeSource.taskDir;
|
|
626
|
+
restoredFiles = fs.readdirSync(sourcePath).filter((name) => !name.startsWith('.')).length;
|
|
627
|
+
}
|
|
427
628
|
else if (hot[0]) {
|
|
428
629
|
sourceState = hot[0].state;
|
|
429
630
|
sourcePath = hot[0].taskDir;
|
|
@@ -433,38 +634,8 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
433
634
|
return failed(request, { code: 'LIFECYCLE_TASK_NOT_FOUND', message: `task ${taskId} has no lifecycle source` }, { taskId });
|
|
434
635
|
}
|
|
435
636
|
const targetPath = path.join(repoRoot, '.agents', 'workspace', spec.target, taskId);
|
|
637
|
+
const requiredSteps = stepsForIntent(request.intent);
|
|
436
638
|
const movingTaskDirectory = sourcePath !== targetPath;
|
|
437
|
-
const assertRuntimeMoveSafe = () => {
|
|
438
|
-
if (!movingTaskDirectory)
|
|
439
|
-
return null;
|
|
440
|
-
const controlManifest = path.join(sourcePath, '.runtime', 'sandbox-control', 'manifest.json');
|
|
441
|
-
const controlLease = path.join(sourcePath, '.runtime', 'sandbox-control', 'lease.json');
|
|
442
|
-
if (fs.existsSync(controlManifest) || fs.existsSync(controlLease)) {
|
|
443
|
-
return {
|
|
444
|
-
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
445
|
-
message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory'
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
|
|
449
|
-
if (fs.existsSync(orchestrationPath)) {
|
|
450
|
-
try {
|
|
451
|
-
const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8'));
|
|
452
|
-
if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
|
|
453
|
-
return {
|
|
454
|
-
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
455
|
-
message: 'task orchestration has an active or pending delegation; settle it before moving the task directory'
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
return {
|
|
461
|
-
code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
|
|
462
|
-
message: 'task orchestration state is unreadable; refusing to move the task directory'
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return null;
|
|
467
|
-
};
|
|
468
639
|
const currentTaskFile = path.join(sourcePath, 'task.md');
|
|
469
640
|
const existingJournal = fs.existsSync(path.join(sourcePath, '.task-lifecycle.json'));
|
|
470
641
|
if (sourceState === spec.target && !existingJournal) {
|
|
@@ -474,7 +645,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
474
645
|
return {
|
|
475
646
|
...failed(request, { code: '', message: '' }), status: 'no-op', error: null, taskId,
|
|
476
647
|
sourceState, targetState: spec.target, sourcePath, targetPath, pendingSteps: [],
|
|
477
|
-
completedSteps:
|
|
648
|
+
completedSteps: requiredSteps, timestamp: typeof frontmatter.updated_at === 'string' ? frontmatter.updated_at : null,
|
|
478
649
|
agentInfraVersion: typeof frontmatter.agent_infra_version === 'string' ? frontmatter.agent_infra_version : null
|
|
479
650
|
};
|
|
480
651
|
}
|
|
@@ -483,10 +654,11 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
483
654
|
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
484
655
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
485
656
|
}
|
|
486
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
657
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal
|
|
658
|
+
&& !(request.intent === 'complete' && targetContainsTask(targetPath, taskId, request))) {
|
|
487
659
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
488
660
|
}
|
|
489
|
-
const moveSafety =
|
|
661
|
+
const moveSafety = runtimeMoveSafety(request, movingTaskDirectory, options, taskId, repoRoot, sourcePath, true);
|
|
490
662
|
if (moveSafety)
|
|
491
663
|
return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
492
664
|
const content = io.readFileSync(currentTaskFile);
|
|
@@ -515,7 +687,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
515
687
|
}
|
|
516
688
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
|
|
517
689
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
518
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
690
|
+
if ((spec.registry === 'release' || request.intent === 'complete') && sourceState === 'active' && !registeredKey) {
|
|
519
691
|
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
692
|
}
|
|
521
693
|
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
@@ -540,7 +712,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
540
712
|
if (request.dryRun) {
|
|
541
713
|
if (!journal.failure)
|
|
542
714
|
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:
|
|
715
|
+
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
716
|
}
|
|
545
717
|
if (journal.intentDigest !== digest)
|
|
546
718
|
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 +763,8 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
591
763
|
if (sourceState === spec.target && journal.sourceState !== spec.target)
|
|
592
764
|
completed.add('directory-moved');
|
|
593
765
|
const currentMappings = loadShortIdByTaskId(repoRoot);
|
|
594
|
-
if (
|
|
766
|
+
if (requiredSteps.includes('registry-committed')
|
|
767
|
+
&& ((spec.target === 'active' && currentMappings.has(taskId)) || (spec.target !== 'active' && !currentMappings.has(taskId)))) {
|
|
595
768
|
if (completed.has('directory-moved'))
|
|
596
769
|
completed.add('registry-committed');
|
|
597
770
|
}
|
|
@@ -603,7 +776,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
603
776
|
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
604
777
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
605
778
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
606
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
779
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
607
780
|
};
|
|
608
781
|
}
|
|
609
782
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles))
|
|
@@ -637,30 +810,42 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
637
810
|
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
638
811
|
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
639
812
|
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
640
|
-
journalPath, completedSteps: [...completed], pendingSteps:
|
|
813
|
+
journalPath, completedSteps: [...completed], pendingSteps: requiredSteps.filter((step) => !completed.has(step))
|
|
641
814
|
};
|
|
642
815
|
}
|
|
643
|
-
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath))
|
|
816
|
+
if (fs.existsSync(targetPath) && !fs.existsSync(path.join(sourcePath, 'task.md')))
|
|
644
817
|
completed.add('directory-moved');
|
|
645
818
|
if (!completed.has('directory-moved')) {
|
|
646
|
-
const moveSafety =
|
|
819
|
+
const moveSafety = runtimeMoveSafety(request, movingTaskDirectory, options, taskId, repoRoot, sourcePath, false);
|
|
647
820
|
if (moveSafety)
|
|
648
821
|
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
822
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
823
|
+
if (request.intent === 'complete') {
|
|
824
|
+
try {
|
|
825
|
+
if (!fs.existsSync(targetPath))
|
|
826
|
+
fs.mkdirSync(targetPath, { recursive: false, mode: 0o700 });
|
|
827
|
+
if (!targetContainsTask(targetPath, taskId, request)) {
|
|
828
|
+
throw new Error('completed target has no matching task identity');
|
|
829
|
+
}
|
|
830
|
+
transferTaskContents(sourcePath, targetPath);
|
|
831
|
+
for (const name of fs.readdirSync(sourcePath).filter((entry) => entry !== '.task-lifecycle.json' && entry !== 'task.md').sort()) {
|
|
832
|
+
fs.rmSync(path.join(sourcePath, name), { recursive: true, force: false });
|
|
833
|
+
}
|
|
834
|
+
fs.rmSync(path.join(sourcePath, 'task.md'), { force: false });
|
|
835
|
+
fs.rmSync(path.join(sourcePath, '.task-lifecycle.json'), { force: false });
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_TRANSFER_FAILED', message: String(error) };
|
|
839
|
+
const sourceJournalPath = path.join(sourcePath, '.task-lifecycle.json');
|
|
840
|
+
rememberJournalFailure(sourceJournalPath, journal, io, failure);
|
|
841
|
+
return failed(request, failure, {
|
|
842
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
843
|
+
journalPath: sourceJournalPath, task: { operations: taskOperations },
|
|
844
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
845
|
+
pendingSteps: requiredSteps.filter((step) => !completed.has(step) || step === 'directory-moved'),
|
|
846
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
847
|
+
});
|
|
848
|
+
}
|
|
664
849
|
completed.add('directory-moved');
|
|
665
850
|
journal.completedSteps = [...completed];
|
|
666
851
|
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
@@ -668,12 +853,58 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
668
853
|
writeJournal(journalPath, journal, io);
|
|
669
854
|
}
|
|
670
855
|
catch (error) {
|
|
671
|
-
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, {
|
|
856
|
+
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, {
|
|
857
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath,
|
|
858
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
859
|
+
changed: true, completedSteps: [...completed].filter((step) => step !== 'directory-moved'),
|
|
860
|
+
pendingSteps: requiredSteps.filter((step) => step !== 'task-written'),
|
|
861
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
if (fs.existsSync(targetPath))
|
|
867
|
+
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 });
|
|
868
|
+
try {
|
|
869
|
+
(options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath);
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) };
|
|
873
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
874
|
+
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 });
|
|
672
875
|
}
|
|
876
|
+
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
877
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
878
|
+
}
|
|
879
|
+
else if (!completed.has('directory-moved')) {
|
|
880
|
+
completed.add('directory-moved');
|
|
881
|
+
journal.completedSteps = [...completed];
|
|
882
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
883
|
+
try {
|
|
884
|
+
writeJournal(journalPath, journal, io);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
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 });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
893
|
+
if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
|
|
894
|
+
try {
|
|
895
|
+
fs.rmdirSync(activeTaskPath);
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
return failed(request, { code: 'LIFECYCLE_SOURCE_DIRECTORY_CLEANUP_FAILED', message: String(error) }, {
|
|
899
|
+
taskId, sourceState, targetState: spec.target, sourcePath: activeTaskPath, targetPath, journalPath,
|
|
900
|
+
task: { operations: taskOperations }, directory: { effect: 'move', changed: true },
|
|
901
|
+
changed: true, completedSteps: [...completed], pendingSteps: [],
|
|
902
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion
|
|
903
|
+
});
|
|
673
904
|
}
|
|
674
905
|
}
|
|
675
|
-
let shortId = { effect: 'unchanged', shortId: null, changed: false };
|
|
676
|
-
if (!completed.has('registry-committed')) {
|
|
906
|
+
let shortId = { effect: 'unchanged', shortId: request.intent === 'complete' ? registeredKey ?? null : null, changed: false };
|
|
907
|
+
if (requiredSteps.includes('registry-committed') && !completed.has('registry-committed')) {
|
|
677
908
|
try {
|
|
678
909
|
shortId = mutateShortIdRegistry(repoRoot, taskId, spec.registry);
|
|
679
910
|
}
|
|
@@ -692,7 +923,7 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
|
|
|
692
923
|
const mappings = loadShortIdByTaskId(repoRoot);
|
|
693
924
|
if (spec.target === 'active' && !mappings.has(taskId))
|
|
694
925
|
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))
|
|
926
|
+
if (request.intent !== 'complete' && spec.target !== 'active' && mappings.has(taskId))
|
|
696
927
|
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
928
|
try {
|
|
698
929
|
io.unlinkSync(journalPath);
|