@fitlab-ai/agent-infra 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -12,6 +12,7 @@ import type { LedgerRow, ReviewStage } from './ledger.ts';
12
12
  import { resolveTaskRef } from './resolve-ref.ts';
13
13
  import { writeTask } from './write.ts';
14
14
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
15
+ import { allowsManualOverride } from './guard-override.ts';
15
16
 
16
17
  type ReviewSeverity = 'blocker' | 'major' | 'minor';
17
18
  type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
@@ -120,7 +121,8 @@ function mapWrite(intent: LedgerIntent, entityId: string, before: LedgerRow | nu
120
121
  function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {}): LedgerIntentResult {
121
122
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
122
123
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
123
- if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
124
+ const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
125
+ if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
124
126
  let content: string;
125
127
  let rows: LedgerRow[];
126
128
  try {
@@ -196,7 +198,8 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
196
198
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
197
199
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
198
200
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
199
- if ((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot))) {
201
+ if (((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot)))
202
+ && !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
200
203
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
201
204
  }
202
205
  after = { ...before, status: intent.status, evidence };
@@ -230,7 +233,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
230
233
  }
231
234
 
232
235
  const result = writeTask({
233
- taskRef: intent.taskRef, expectedState: 'active',
236
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
234
237
  mutations: [...ledgerSectionMutation(content), rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
235
238
  dryRun: 'dryRun' in intent ? intent.dryRun : false
236
239
  }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
@@ -61,7 +61,22 @@ function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError |
61
61
  for (const row of rows) {
62
62
  if (seen.has(row.id)) return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
63
63
  seen.add(row.id);
64
- if (/^PRC-[1-9]\d*$/.test(row.id)) continue;
64
+ if (/^PRC-[1-9]\d*$/.test(row.id)) {
65
+ if (
66
+ row.stage !== 'post-review-commit' ||
67
+ row.round !== '-' ||
68
+ row.severity !== '-' ||
69
+ row.status !== 'human-decided' ||
70
+ row.evidence === '' ||
71
+ /[\r\n]/.test(row.evidence)
72
+ ) {
73
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
74
+ }
75
+ continue;
76
+ }
77
+ if (row.stage === 'post-review-commit') {
78
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
79
+ }
65
80
  if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id)) return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
66
81
  if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage)) return { code: 'LEDGER_STAGE_INVALID', message: `ledger stage '${row.stage}' is invalid` };
67
82
  if (!FINDING_STATUSES.has(row.status)) return { code: 'LEDGER_STATUS_INVALID', message: `ledger status '${row.status}' is invalid` };
@@ -16,6 +16,33 @@ import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetad
16
16
  const lifecycleIntentCatalog = [
17
17
  'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
18
18
  ] as const;
19
+ const lifecycleFailureCatalog = [
20
+ 'LIFECYCLE_DIRECTORY_RENAME_FAILED',
21
+ 'LIFECYCLE_DOCUMENT_INVALID',
22
+ 'LIFECYCLE_FINAL_STATE_INVALID',
23
+ 'LIFECYCLE_IDENTITY_CONFLICT',
24
+ 'LIFECYCLE_IDENTITY_INVALID',
25
+ 'LIFECYCLE_INTENT_CONFLICT',
26
+ 'LIFECYCLE_JOURNAL_CLEANUP_FAILED',
27
+ 'LIFECYCLE_JOURNAL_INVALID',
28
+ 'LIFECYCLE_JOURNAL_WRITE_FAILED',
29
+ 'LIFECYCLE_LOG_MISSING',
30
+ 'LIFECYCLE_METADATA_FAILED',
31
+ 'LIFECYCLE_PAYLOAD_INVALID',
32
+ 'LIFECYCLE_SHORT_ID_FAILED',
33
+ 'LIFECYCLE_SHORT_ID_PRECONDITION',
34
+ 'LIFECYCLE_SOURCE_INVALID',
35
+ 'LIFECYCLE_STAGING_IDENTITY_INVALID',
36
+ 'LIFECYCLE_STAGING_INVALID',
37
+ 'LIFECYCLE_TARGET_CONFLICT',
38
+ 'LIFECYCLE_TASK_NOT_FOUND',
39
+ 'SHORT_ID_CAPACITY_EXCEEDED'
40
+ ] as const;
41
+ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
42
+ producerId: 'lifecycle.apply' as const,
43
+ guardId: 'G-03' as const,
44
+ code
45
+ }));
19
46
  type TaskLifecycleIntent = (typeof lifecycleIntentCatalog)[number];
20
47
  type TaskLifecycleRequest =
21
48
  | { taskRef: string; intent: 'block'; agent: string; reason: string; unblockCondition: string; dryRun?: boolean }
@@ -39,6 +66,7 @@ type LifecycleJournal = {
39
66
  targetState: HotState;
40
67
  metadata: TaskWriteMetadata;
41
68
  completedSteps: LifecycleStep[];
69
+ failure?: LifecycleError;
42
70
  };
43
71
  type TaskLifecycleResult = {
44
72
  status: 'planned' | 'applied' | 'no-op' | 'failed';
@@ -72,8 +100,13 @@ type TaskLifecycleOptions = {
72
100
  fileSystem?: Partial<LifecycleFileSystem>;
73
101
  taskFileSystem?: Partial<TaskFileSystem>;
74
102
  directoryRenameSync?: (source: string, target: string) => void;
103
+ manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
75
104
  };
76
105
 
106
+ function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
107
+ return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
108
+ }
109
+
77
110
  const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
78
111
  const DEFAULT_IO: LifecycleFileSystem = {
79
112
  renameSync: (source, target) => fs.renameSync(source, target),
@@ -172,10 +205,16 @@ function matchingCompletion(content: string, request: TaskLifecycleRequest, rest
172
205
  }
173
206
 
174
207
  function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent: 'restore' }>, repoRoot: string, taskId: string): LifecycleError | { stagingDir: string; fileCount: number } {
175
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
176
- const stagingDir = path.resolve(request.stagingDir);
208
+ let workspace: string;
209
+ let stagingDir: string;
210
+ try {
211
+ workspace = fs.realpathSync.native(path.resolve(repoRoot, '.agents', 'workspace'));
212
+ stagingDir = fs.realpathSync.native(path.resolve(request.stagingDir));
213
+ } catch (error) {
214
+ return { code: 'LIFECYCLE_STAGING_INVALID', message: String(error) };
215
+ }
177
216
  const relative = path.relative(workspace, stagingDir);
178
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || path.basename(stagingDir) === taskId) {
217
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || path.basename(stagingDir) === taskId) {
179
218
  return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging must be a non-final directory inside .agents/workspace' };
180
219
  }
181
220
  let entries: fs.Dirent[];
@@ -213,8 +252,8 @@ function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent:
213
252
  return { stagingDir, fileCount: entries.filter((entry) => !entry.name.startsWith('.')).length };
214
253
  }
215
254
 
216
- function mutationsFor(request: TaskLifecycleRequest, content: string, metadata: TaskWriteMetadata, restoredFiles: number): TaskMutation[] | LifecycleError {
217
- const section = locateActivityLog(content);
255
+ function mutationsFor(request: TaskLifecycleRequest, content: string, metadata: TaskWriteMetadata, restoredFiles: number, allowMissingActivityLog = false): TaskMutation[] | LifecycleError {
256
+ const section = locateActivityLog(content) ?? (allowMissingActivityLog ? { heading: 'Activity Log', body: '', entries: [] } : null);
218
257
  if (!section) return { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' };
219
258
  const identity = actionAndNote(request, restoredFiles);
220
259
  let body = appendActivityEntry(section, { time: metadata.timestamp, step: `${identity.action} [started]`, agent: request.agent, note: 'started' });
@@ -261,6 +300,14 @@ function writeJournal(file: string, journal: LifecycleJournal, io: LifecycleFile
261
300
  }
262
301
  }
263
302
 
303
+ function rememberJournalFailure(file: string, journal: LifecycleJournal, io: LifecycleFileSystem, failure: LifecycleError): void {
304
+ try {
305
+ writeJournal(file, { ...journal, failure }, io);
306
+ } catch {
307
+ // Preserve the original lifecycle failure when the diagnostic receipt cannot be updated.
308
+ }
309
+ }
310
+
264
311
  function parseJournal(content: string): LifecycleJournal {
265
312
  const value = JSON.parse(content) as Partial<LifecycleJournal>;
266
313
  if (
@@ -326,7 +373,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
326
373
  }
327
374
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
328
375
  }
329
- if (!spec.sources.includes(sourceState) && !existingJournal) {
376
+ if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
330
377
  return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
331
378
  }
332
379
  if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
@@ -337,7 +384,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
337
384
  try { frontmatter = parseTypedTaskFrontmatter(content); }
338
385
  catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
339
386
  if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
340
- if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
387
+ if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING')) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
341
388
  const registryRequest = executeShortIdCommand({
342
389
  operation: 'list', activeDir: path.join(repoRoot, '.agents', 'workspace', 'active'),
343
390
  shortIdLength: configuredShortIdLength(repoRoot)
@@ -347,10 +394,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
347
394
  }
348
395
  const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
349
396
  const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
350
- if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
397
+ if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
351
398
  return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
352
399
  }
353
- if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
400
+ if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
354
401
  return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
355
402
  }
356
403
  const digest = intentIdentity(request, taskId);
@@ -360,14 +407,20 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
360
407
  if (fs.existsSync(initialJournalPath)) {
361
408
  try { journal = parseJournal(io.readFileSync(initialJournalPath)); }
362
409
  catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath }); }
363
- if (journal.intentDigest !== digest || journal.taskId !== taskId) 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 });
410
+ if (journal.taskId !== taskId || journal.intent !== request.intent) 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 });
411
+ if (request.dryRun) {
412
+ if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
413
+ return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
414
+ }
415
+ const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
416
+ if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) 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 });
364
417
  } else {
365
418
  let metadata: TaskWriteMetadata;
366
419
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
367
420
  catch (error) { return failed(request, { code: 'LIFECYCLE_METADATA_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
368
421
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
369
422
  if (request.dryRun) {
370
- const plannedMutations = mutationsFor(request, content, metadata, restoredFiles);
423
+ const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
371
424
  if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
372
425
  const taskPlan = writeTask(
373
426
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
@@ -403,7 +456,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
403
456
  if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
404
457
  if (!completed.has('task-written')) {
405
458
  const current = io.readFileSync(path.join(sourcePath, 'task.md'));
406
- const mutations = mutationsFor(request, current, journal.metadata, restoredFiles);
459
+ const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
407
460
  if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
408
461
  const writeResult = writeTask(
409
462
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
@@ -424,7 +477,11 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
424
477
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
425
478
  if (fs.existsSync(targetPath)) return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
426
479
  try { (options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath); }
427
- catch (error) { return failed(request, { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
480
+ catch (error) {
481
+ const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) } as const;
482
+ rememberJournalFailure(journalPath, journal, io, failure);
483
+ 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 });
484
+ }
428
485
  completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
429
486
  try { writeJournal(journalPath, journal, io); }
430
487
  catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
@@ -451,7 +508,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
451
508
  };
452
509
  }
453
510
 
454
- export { lifecycleIntentCatalog, applyTaskLifecycle };
511
+ export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
455
512
  export type {
456
513
  TaskLifecycleIntent, TaskLifecycleRequest, TaskLifecycleResult, TaskLifecycleOptions,
457
514
  LifecycleError, LifecycleJournal, LifecycleStep