@fitlab-ai/agent-infra 0.9.7 → 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 (96) hide show
  1. package/dist/bin/internal-cli.js +5 -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 +2 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  7. package/dist/lib/decide.js +68 -14
  8. package/dist/lib/init.js +1 -1
  9. package/dist/lib/internal/release-workflow.js +1 -3
  10. package/dist/lib/internal/task-activity.js +57 -7
  11. package/dist/lib/internal/task-event.js +57 -4
  12. package/dist/lib/internal/task-ledger.js +59 -5
  13. package/dist/lib/internal/task-lifecycle.js +59 -8
  14. package/dist/lib/internal/task-orchestration.js +11 -3
  15. package/dist/lib/internal/task-override.js +131 -0
  16. package/dist/lib/internal/task-review.js +64 -6
  17. package/dist/lib/internal/task-verify.js +65 -3
  18. package/dist/lib/internal/task-warning.js +57 -4
  19. package/dist/lib/platform/pr-summary.js +5 -3
  20. package/dist/lib/runtime/agent-runtime.js +56 -0
  21. package/dist/lib/sandbox/commands/create.js +389 -311
  22. package/dist/lib/sandbox/control/container-identity.js +1 -1
  23. package/dist/lib/sandbox/control/executor.js +10 -2
  24. package/dist/lib/sandbox/control/lifecycle.js +392 -72
  25. package/dist/lib/sandbox/control/state.js +27 -11
  26. package/dist/lib/sandbox/engine.js +14 -7
  27. package/dist/lib/sandbox/mounts.js +20 -5
  28. package/dist/lib/sandbox/recovery.js +78 -10
  29. package/dist/lib/sandbox/workspace-view.js +62 -40
  30. package/dist/lib/server/process-state.js +51 -1
  31. package/dist/lib/task/activity-intent.js +4 -1
  32. package/dist/lib/task/decision-intents.js +4 -2
  33. package/dist/lib/task/delegation-receipts.js +37 -16
  34. package/dist/lib/task/events.js +9 -5
  35. package/dist/lib/task/guard-override.js +31 -0
  36. package/dist/lib/task/human-override.js +1240 -0
  37. package/dist/lib/task/ledger-intents.js +6 -3
  38. package/dist/lib/task/lifecycle.js +69 -14
  39. package/dist/lib/task/orchestration.js +60 -6
  40. package/dist/lib/task/review-finalization.js +3 -2
  41. package/dist/lib/task/verification-engine.js +40 -14
  42. package/dist/lib/task/verification.js +13 -1
  43. package/dist/lib/task/workflow-warning-intents.js +4 -2
  44. package/dist/lib/task/write.js +2 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  47. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
  49. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  51. package/lib/decide.ts +59 -14
  52. package/lib/init.ts +1 -1
  53. package/lib/internal/release-workflow.ts +1 -3
  54. package/lib/internal/task-activity.ts +46 -6
  55. package/lib/internal/task-event.ts +53 -4
  56. package/lib/internal/task-ledger.ts +48 -4
  57. package/lib/internal/task-lifecycle.ts +56 -12
  58. package/lib/internal/task-orchestration.ts +12 -4
  59. package/lib/internal/task-override.ts +138 -0
  60. package/lib/internal/task-review.ts +51 -6
  61. package/lib/internal/task-verify.ts +52 -3
  62. package/lib/internal/task-warning.ts +44 -3
  63. package/lib/platform/pr-summary.ts +10 -3
  64. package/lib/runtime/agent-runtime.ts +76 -0
  65. package/lib/sandbox/commands/create.ts +88 -4
  66. package/lib/sandbox/config.ts +2 -1
  67. package/lib/sandbox/control/container-identity.ts +3 -3
  68. package/lib/sandbox/control/executor.ts +10 -2
  69. package/lib/sandbox/control/lifecycle.ts +463 -76
  70. package/lib/sandbox/control/protocol.ts +5 -2
  71. package/lib/sandbox/control/state.ts +35 -18
  72. package/lib/sandbox/engine.ts +20 -9
  73. package/lib/sandbox/mounts.ts +23 -5
  74. package/lib/sandbox/recovery.ts +90 -9
  75. package/lib/sandbox/workspace-view.ts +70 -40
  76. package/lib/server/process-state.ts +48 -1
  77. package/lib/task/activity-intent.ts +4 -2
  78. package/lib/task/decision-intents.ts +4 -2
  79. package/lib/task/delegation-receipts.ts +46 -18
  80. package/lib/task/events.ts +10 -5
  81. package/lib/task/guard-override.ts +50 -0
  82. package/lib/task/human-override.ts +1503 -0
  83. package/lib/task/ledger-intents.ts +6 -3
  84. package/lib/task/lifecycle.ts +71 -14
  85. package/lib/task/orchestration.ts +89 -7
  86. package/lib/task/review-finalization.ts +6 -2
  87. package/lib/task/verification-engine.ts +42 -16
  88. package/lib/task/verification.ts +15 -1
  89. package/lib/task/workflow-warning-intents.ts +4 -2
  90. package/lib/task/write.ts +4 -1
  91. package/package.json +1 -1
  92. package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
  95. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  96. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
@@ -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 } });
@@ -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
@@ -14,6 +14,7 @@ import {
14
14
  completeDelegationStage,
15
15
  consumeDelegation,
16
16
  dispatchDelegation,
17
+ foldBlankToNull,
17
18
  managedDelegationRole,
18
19
  prepareDelegation,
19
20
  sealDelegation
@@ -25,6 +26,7 @@ import type {
25
26
  DelegationStage
26
27
  } from './delegation-receipts.ts';
27
28
  import { normalizeAgentClients } from '../agent-clients/config.ts';
29
+ import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.ts';
28
30
  import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
29
31
  import {
30
32
  getAgentClientCapability,
@@ -112,6 +114,25 @@ type ClientCapabilityRecovery = Readonly<{
112
114
  }>;
113
115
  resultingStatus: 'running';
114
116
  }>;
117
+ type ClaudeCodeCapabilityRecovery = Readonly<{
118
+ code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION';
119
+ recoveredAt: string;
120
+ previousSchemaVersion: 3;
121
+ previousStatus: 'paused';
122
+ previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
123
+ client: 'claude-code';
124
+ guards: Readonly<{
125
+ stepCount: 0;
126
+ nextStage: null;
127
+ baselineEmpty: true;
128
+ receiptCount: 0;
129
+ pendingDelegation: false;
130
+ commitAuthorizationUnused: true;
131
+ completionEvidenceAbsent: true;
132
+ commitIntentAbsent: true;
133
+ }>;
134
+ resultingStatus: 'running';
135
+ }>;
115
136
  type SchemaMigrationRecovery = Readonly<{
116
137
  code: 'SCHEMA_V3_MIGRATED';
117
138
  recoveredAt: string;
@@ -130,7 +151,7 @@ type SchemaMigrationRecovery = Readonly<{
130
151
  }>;
131
152
  resultingStatus: 'running';
132
153
  }>;
133
- type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | SchemaMigrationRecovery;
154
+ type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | ClaudeCodeCapabilityRecovery | SchemaMigrationRecovery;
134
155
  type CleanCompletionEvidence = Readonly<{
135
156
  kind: 'reviewed-head-clean';
136
157
  observedAt: string;
@@ -435,6 +456,23 @@ function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string)
435
456
  && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
436
457
  }
437
458
 
459
+ function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
460
+ return run.schemaVersion === 3
461
+ && run.status === 'paused'
462
+ && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
463
+ && run.modelPolicySource?.client === 'claude-code'
464
+ && run.stepCount === 0
465
+ && run.nextStage === null
466
+ && run.baseline === ''
467
+ && run.pendingDelegation === null
468
+ && run.receipts.length === 0
469
+ && run.commitAuthorization?.issuedAt === null
470
+ && run.commitAuthorization?.consumedAt === null
471
+ && run.completionEvidence == null
472
+ && !fs.existsSync(commitIntentPath(taskDir))
473
+ && (run.recoveryHistory ?? []).length === 0;
474
+ }
475
+
438
476
  function canMigrateRecoverableV2Pause(run: OrchestrationRun, taskDir: string): boolean {
439
477
  return run.schemaVersion === 2
440
478
  && run.status === 'paused'
@@ -504,6 +542,39 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
504
542
  }
505
543
  }
506
544
  if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
545
+ if (existing.modelPolicySource.client === 'claude-code') {
546
+ if (!canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
547
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
548
+ }
549
+ const now = (options.now ?? (() => new Date().toISOString()))();
550
+ const previousPause = existing.pause as ClaudeCodeCapabilityRecovery['previousPause'];
551
+ const recovery: ClaudeCodeCapabilityRecovery = {
552
+ code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION',
553
+ recoveredAt: now,
554
+ previousSchemaVersion: 3,
555
+ previousStatus: 'paused',
556
+ previousPause,
557
+ client: 'claude-code',
558
+ guards: {
559
+ stepCount: 0,
560
+ nextStage: null,
561
+ baselineEmpty: true,
562
+ receiptCount: 0,
563
+ pendingDelegation: false,
564
+ commitAuthorizationUnused: true,
565
+ completionEvidenceAbsent: true,
566
+ commitIntentAbsent: true
567
+ },
568
+ resultingStatus: 'running'
569
+ };
570
+ const resumed = withUpdatedRun(existing, {
571
+ status: 'running',
572
+ pause: null,
573
+ recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
574
+ }, () => now);
575
+ saveRun(resolved.taskDir, resumed);
576
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
577
+ }
507
578
  if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
508
579
  return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
509
580
  }
@@ -1667,9 +1738,9 @@ function activateMatchingOrchestrationDelegation(
1667
1738
  if ('status' in replay) return matched;
1668
1739
  const receipt = replay.run.pendingDelegation!;
1669
1740
  const sameEvidence = receipt.parentId === event.parentId
1670
- && receipt.spawnMode === event.spawnMode
1671
- && receipt.actualModel === event.actualModel
1672
- && receipt.actualReasoningEffort === event.actualReasoningEffort
1741
+ && (receipt.spawnMode ?? null) === (event.spawnMode ?? null)
1742
+ && (receipt.actualModel ?? null) === foldBlankToNull(event.actualModel)
1743
+ && (receipt.actualReasoningEffort ?? null) === foldBlankToNull(event.actualReasoningEffort)
1673
1744
  && receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
1674
1745
  && receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
1675
1746
  && receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
@@ -1697,7 +1768,14 @@ function pauseMatchingOrchestrationDelegation(
1697
1768
 
1698
1769
  function sealMatchingOrchestrationDelegation(
1699
1770
  client: AgentClientId,
1700
- event: Readonly<{ nativeAgent: string; childId: string }>,
1771
+ event: Readonly<{
1772
+ nativeAgent: string;
1773
+ childId: string;
1774
+ actualModel?: string;
1775
+ actualReasoningEffort?: string;
1776
+ modelFallbackReason?: string;
1777
+ reasoningEffortFallbackReason?: string;
1778
+ }>,
1701
1779
  options: OrchestrationOptions = {}
1702
1780
  ): OrchestrationResult {
1703
1781
  const role = managedDelegationRole(event.nativeAgent);
@@ -1726,7 +1804,11 @@ function sealMatchingOrchestrationDelegation(
1726
1804
  childId: event.childId,
1727
1805
  exitCode: 0,
1728
1806
  afterFingerprint,
1729
- changedPaths
1807
+ changedPaths,
1808
+ actualModel: event.actualModel,
1809
+ actualReasoningEffort: event.actualReasoningEffort,
1810
+ modelFallbackReason: event.modelFallbackReason,
1811
+ reasoningEffortFallbackReason: event.reasoningEffortFallbackReason
1730
1812
  }, options);
1731
1813
  } catch (error) {
1732
1814
  return pauseOrchestration(
@@ -1955,7 +2037,7 @@ function recoverPreparedOrchestrationDelegation(
1955
2037
  receipt.client === 'codex'
1956
2038
  && receipt.lifecycleProvenance
1957
2039
  && hasActiveCodexLifecycleEvidence(
1958
- path.join(resolved.repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle'),
2040
+ resolveAgentRuntimeStoreRoot({ repoRoot: resolved.repoRoot, store: 'lifecycle' }),
1959
2041
  {
1960
2042
  nativeAgent: `agent-infra-lifecycle-${receipt.role}`,
1961
2043
  hookDefinitionHash: receipt.lifecycleProvenance.hookDefinitionHash
@@ -11,6 +11,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
11
11
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
12
12
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
13
13
  import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
14
+ import { allowsManualOverride } from './guard-override.ts';
15
+ import type { ManualOverrideCapability } from './guard-override.ts';
14
16
 
15
17
  type ReviewFinalizationErrorCode =
16
18
  | ResolveTaskRefErrorCode
@@ -62,6 +64,8 @@ type ReviewFinalizationOptions = {
62
64
  repoRoot?: string;
63
65
  randomSuffix?: () => string;
64
66
  fileSystem?: Partial<ReviewFileSystem>;
67
+ manualOverride?: ManualOverrideCapability;
68
+ lockAlreadyHeld?: boolean;
65
69
  };
66
70
 
67
71
  const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code'; action: string }> = {
@@ -127,7 +131,7 @@ function finalizeReviewSummaryUnlocked(
127
131
  }
128
132
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
129
133
  if (!resolved.ok) return failed(request, resolved.code, resolved.message, resolved.taskId);
130
- if (resolved.state !== 'active') {
134
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
131
135
  return failed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
132
136
  }
133
137
  const parsedArtifact = parseArtifactName(request.artifact);
@@ -275,7 +279,7 @@ function finalizeReviewSummary(
275
279
  request: ReviewFinalizationRequest,
276
280
  options: ReviewFinalizationOptions = {}
277
281
  ): ReviewFinalizationResult {
278
- if (request.dryRun) return finalizeReviewSummaryUnlocked(request, options);
282
+ if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, options);
279
283
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
280
284
  if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, options);
281
285
  try {
@@ -291,7 +291,22 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
291
291
  && guards?.commitAuthorizationUnused === true
292
292
  && guards?.completionEvidenceAbsent === true
293
293
  && guards?.commitIntentAbsent === true;
294
- if (!validModelRecovery && !validCapabilityRecovery) {
294
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
295
+ && recovery.previousSchemaVersion === 3
296
+ && recovery.previousStatus === 'paused'
297
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
298
+ && recovery.client === 'claude-code'
299
+ && recovery.resultingStatus === 'running'
300
+ && exactText(recovery.recoveredAt)
301
+ && guards?.stepCount === 0
302
+ && guards?.nextStage === null
303
+ && guards?.baselineEmpty === true
304
+ && guards?.receiptCount === 0
305
+ && guards?.pendingDelegation === false
306
+ && guards?.commitAuthorizationUnused === true
307
+ && guards?.completionEvidenceAbsent === true
308
+ && guards?.commitIntentAbsent === true;
309
+ if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
295
310
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
296
311
  }
297
312
  }
@@ -322,29 +337,40 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
322
337
  }
323
338
  const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
324
339
  if (activated) {
325
- if (!exactText(receipt.actualModel)) {
326
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
340
+ const isClaudeCode = receipt.client === 'claude-code';
341
+ if (!isClaudeCode) {
342
+ if (!exactText(receipt.actualModel)) {
343
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
344
+ }
345
+ if (!exactText(receipt.actualReasoningEffort)) {
346
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
347
+ }
327
348
  }
328
- if (!exactText(receipt.actualReasoningEffort)) {
329
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
349
+ // parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
350
+ if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
351
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
330
352
  }
331
- if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId || receipt.spawnMode !== 'fresh') {
353
+ if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
332
354
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
333
355
  }
334
- if (receipt.actualModel !== receipt.requestedModel && !exactText(receipt.modelFallbackReason)) {
335
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
336
- }
337
- if (receipt.actualModel === receipt.requestedModel && receipt.modelFallbackReason !== null) {
338
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
356
+ if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
357
+ if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
358
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
359
+ }
339
360
  }
340
361
  if (
341
- receipt.actualReasoningEffort !== receipt.requestedReasoningEffort
342
- && !exactText(receipt.reasoningEffortFallbackReason)
362
+ (receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
363
+ && receipt.modelFallbackReason !== null
343
364
  ) {
344
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
365
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
366
+ }
367
+ if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
368
+ if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
369
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
370
+ }
345
371
  }
346
372
  if (
347
- receipt.actualReasoningEffort === receipt.requestedReasoningEffort
373
+ (receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
348
374
  && receipt.reasoningEffortFallbackReason !== null
349
375
  ) {
350
376
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
@@ -902,7 +928,7 @@ function checkReviewLedger({ taskDir, config }: any): any {
902
928
  }
903
929
 
904
930
  const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
905
- const maxRounds = Number(resolveReviewSetting(config, "max_handshake_rounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
931
+ const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
906
932
  const problems = [];
907
933
  let inScopeCount = 0;
908
934
 
@@ -7,6 +7,8 @@ import { parseArtifactName } from './artifact-lifecycle.ts';
7
7
  import type { ArtifactFamily } from './artifact-lifecycle.ts';
8
8
  import { resolveTaskRef } from './resolve-ref.ts';
9
9
  import type { TaskWorkspaceState } from './resolve-ref.ts';
10
+ import { allowsManualOverride } from './guard-override.ts';
11
+ import type { ManualOverrideCapability } from './guard-override.ts';
10
12
 
11
13
  type VerificationEvent =
12
14
  | 'analyze.awaiting-input' | 'analyze.completed'
@@ -48,6 +50,7 @@ type TaskVerificationResult = {
48
50
  type VerificationOptions = {
49
51
  repoRoot?: string;
50
52
  engine?: typeof verifyInProcess;
53
+ manualOverride?: ManualOverrideCapability;
51
54
  };
52
55
 
53
56
  const gate = (skill: string, expectedState: VerificationSpec['expectedState'], artifactFamily?: ArtifactFamily): VerificationSpec => ({
@@ -93,7 +96,7 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
93
96
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
94
97
  if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
95
98
  const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode } as const;
96
- if (resolved.state !== spec.expectedState) {
99
+ if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
97
100
  return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
98
101
  }
99
102
  if (spec.artifactFamily) {
@@ -111,11 +114,22 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
111
114
  if (spec.mode === 'gate') {
112
115
  const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
113
116
  const status = payload.gate as 'pass' | 'fail' | 'blocked';
117
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
118
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
119
+ return {
120
+ status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
121
+ ...identity, artifact: request.artifact ?? null, invocations, error: null
122
+ };
123
+ }
114
124
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
115
125
  } else {
116
126
  for (const check of spec.checks ?? []) {
117
127
  const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
118
128
  const status = payload.status as 'pass' | 'fail' | 'blocked';
129
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
130
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
131
+ break;
132
+ }
119
133
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
120
134
  if (status !== 'pass') break;
121
135
  }
@@ -11,6 +11,7 @@ import {
11
11
  import type { WorkflowWarning } from './workflow-warnings.ts';
12
12
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
13
13
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
14
+ import { allowsManualOverride } from './guard-override.ts';
14
15
 
15
16
  type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
16
17
  type WarningStatus = 'open' | 'resolved' | 'ignored';
@@ -79,7 +80,8 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
79
80
  function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
80
81
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
81
82
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
82
- if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
83
+ const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
84
+ if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
83
85
  let content: string;
84
86
  let rows: WorkflowWarning[];
85
87
  try {
@@ -125,7 +127,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
125
127
  }
126
128
 
127
129
  const writeResult = writeTask({
128
- taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
130
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
129
131
  }, {
130
132
  ...options,
131
133
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
package/lib/task/write.ts CHANGED
@@ -12,6 +12,8 @@ import type {
12
12
  ResolveTaskRefErrorCode,
13
13
  TaskWorkspaceState
14
14
  } from './resolve-ref.ts';
15
+ import { allowsManualOverride } from './guard-override.ts';
16
+ import type { ManualOverrideCapability } from './guard-override.ts';
15
17
  import { mutateTableRow, upsertSection } from './sections.ts';
16
18
  import type {
17
19
  TableRowDeleteMutation,
@@ -156,6 +158,7 @@ type TaskWriteOptions = {
156
158
  metadataProvider?: () => TaskWriteMetadata;
157
159
  randomSuffix?: () => string;
158
160
  fileSystem?: Partial<TaskFileSystem>;
161
+ manualOverride?: ManualOverrideCapability;
159
162
  };
160
163
 
161
164
  const DEFAULT_FILE_SYSTEM: TaskFileSystem = {
@@ -241,7 +244,7 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
241
244
  taskMdPath: resolved.taskMdPath,
242
245
  actualState: resolved.state
243
246
  };
244
- if (resolved.state !== request.expectedState) {
247
+ if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
245
248
  return failure(
246
249
  request,
247
250
  identity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",