@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
@@ -4,6 +4,7 @@ import { declareImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplemen
4
4
  import { LEDGER_COLUMNS, LEDGER_HEADINGS, nextHdId, parseLedger, validateLedgerRows } from './ledger.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
6
  import { writeTask } from './write.js';
7
+ import { allowsManualOverride } from './guard-override.js';
7
8
  const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
8
9
  const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
9
10
  const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
@@ -84,7 +85,8 @@ function applyLedgerIntent(intent, options = {}) {
84
85
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
85
86
  if (!resolved.ok)
86
87
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
87
- if (resolved.state !== 'active')
88
+ const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
89
+ if (resolved.state !== 'active' && !stateOverride)
88
90
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
91
  let content;
90
92
  let rows;
@@ -176,7 +178,8 @@ function applyLedgerIntent(intent, options = {}) {
176
178
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
177
179
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
178
180
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
179
- if ((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot))) {
181
+ if (((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot)))
182
+ && !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
180
183
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
181
184
  }
182
185
  after = { ...before, status: intent.status, evidence };
@@ -209,7 +212,7 @@ function applyLedgerIntent(intent, options = {}) {
209
212
  }
210
213
  }
211
214
  const result = writeTask({
212
- taskRef: intent.taskRef, expectedState: 'active',
215
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
213
216
  mutations: [...ledgerSectionMutation(content), rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
214
217
  dryRun: 'dryRun' in intent ? intent.dryRun : false
215
218
  }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
@@ -10,6 +10,36 @@ import { captureTaskWriteMetadata, writeTask } from './write.js';
10
10
  const lifecycleIntentCatalog = [
11
11
  'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
12
12
  ];
13
+ const lifecycleFailureCatalog = [
14
+ 'LIFECYCLE_DIRECTORY_RENAME_FAILED',
15
+ 'LIFECYCLE_DOCUMENT_INVALID',
16
+ 'LIFECYCLE_FINAL_STATE_INVALID',
17
+ 'LIFECYCLE_IDENTITY_CONFLICT',
18
+ 'LIFECYCLE_IDENTITY_INVALID',
19
+ 'LIFECYCLE_INTENT_CONFLICT',
20
+ 'LIFECYCLE_JOURNAL_CLEANUP_FAILED',
21
+ 'LIFECYCLE_JOURNAL_INVALID',
22
+ 'LIFECYCLE_JOURNAL_WRITE_FAILED',
23
+ 'LIFECYCLE_LOG_MISSING',
24
+ 'LIFECYCLE_METADATA_FAILED',
25
+ 'LIFECYCLE_PAYLOAD_INVALID',
26
+ 'LIFECYCLE_SHORT_ID_FAILED',
27
+ 'LIFECYCLE_SHORT_ID_PRECONDITION',
28
+ 'LIFECYCLE_SOURCE_INVALID',
29
+ 'LIFECYCLE_STAGING_IDENTITY_INVALID',
30
+ 'LIFECYCLE_STAGING_INVALID',
31
+ 'LIFECYCLE_TARGET_CONFLICT',
32
+ 'LIFECYCLE_TASK_NOT_FOUND',
33
+ 'SHORT_ID_CAPACITY_EXCEEDED'
34
+ ];
35
+ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
36
+ producerId: 'lifecycle.apply',
37
+ guardId: 'G-03',
38
+ code
39
+ }));
40
+ function allowsManualOverride(options, code) {
41
+ return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
42
+ }
13
43
  const STEPS = ['task-written', 'directory-moved', 'registry-committed'];
14
44
  const DEFAULT_IO = {
15
45
  renameSync: (source, target) => fs.renameSync(source, target),
@@ -121,10 +151,17 @@ function matchingCompletion(content, request, restoredFiles = 0) {
121
151
  return section.entries.some((entry) => entry.step === identity.action && entry.agent === request.agent && entry.note === identity.note);
122
152
  }
123
153
  function validateRestoreStaging(request, repoRoot, taskId) {
124
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
125
- const stagingDir = path.resolve(request.stagingDir);
154
+ let workspace;
155
+ let stagingDir;
156
+ try {
157
+ workspace = fs.realpathSync.native(path.resolve(repoRoot, '.agents', 'workspace'));
158
+ stagingDir = fs.realpathSync.native(path.resolve(request.stagingDir));
159
+ }
160
+ catch (error) {
161
+ return { code: 'LIFECYCLE_STAGING_INVALID', message: String(error) };
162
+ }
126
163
  const relative = path.relative(workspace, stagingDir);
127
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || path.basename(stagingDir) === taskId) {
164
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || path.basename(stagingDir) === taskId) {
128
165
  return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging must be a non-final directory inside .agents/workspace' };
129
166
  }
130
167
  let entries;
@@ -168,8 +205,8 @@ function validateRestoreStaging(request, repoRoot, taskId) {
168
205
  }
169
206
  return { stagingDir, fileCount: entries.filter((entry) => !entry.name.startsWith('.')).length };
170
207
  }
171
- function mutationsFor(request, content, metadata, restoredFiles) {
172
- const section = locateActivityLog(content);
208
+ function mutationsFor(request, content, metadata, restoredFiles, allowMissingActivityLog = false) {
209
+ const section = locateActivityLog(content) ?? (allowMissingActivityLog ? { heading: 'Activity Log', body: '', entries: [] } : null);
173
210
  if (!section)
174
211
  return { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' };
175
212
  const identity = actionAndNote(request, restoredFiles);
@@ -230,6 +267,14 @@ function writeJournal(file, journal, io, create = false) {
230
267
  throw error;
231
268
  }
232
269
  }
270
+ function rememberJournalFailure(file, journal, io, failure) {
271
+ try {
272
+ writeJournal(file, { ...journal, failure }, io);
273
+ }
274
+ catch {
275
+ // Preserve the original lifecycle failure when the diagnostic receipt cannot be updated.
276
+ }
277
+ }
233
278
  function parseJournal(content) {
234
279
  const value = JSON.parse(content);
235
280
  if (value.version !== 1 || typeof value.taskId !== 'string' ||
@@ -303,7 +348,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
303
348
  }
304
349
  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 });
305
350
  }
306
- if (!spec.sources.includes(sourceState) && !existingJournal) {
351
+ if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
307
352
  return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
308
353
  }
309
354
  if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
@@ -319,7 +364,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
319
364
  }
320
365
  if (frontmatter.id !== taskId)
321
366
  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 });
322
- if (!locateActivityLog(content))
367
+ if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'))
323
368
  return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
324
369
  const registryRequest = executeShortIdCommand({
325
370
  operation: 'list', activeDir: path.join(repoRoot, '.agents', 'workspace', 'active'),
@@ -330,10 +375,10 @@ function applyTaskLifecycle(requestInput, options = {}) {
330
375
  }
331
376
  const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
332
377
  const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
333
- if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
378
+ if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
334
379
  return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
335
380
  }
336
- if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
381
+ if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
337
382
  return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
338
383
  }
339
384
  const digest = intentIdentity(request, taskId);
@@ -347,7 +392,15 @@ function applyTaskLifecycle(requestInput, options = {}) {
347
392
  catch (error) {
348
393
  return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
349
394
  }
350
- if (journal.intentDigest !== digest || journal.taskId !== taskId)
395
+ if (journal.taskId !== taskId || journal.intent !== request.intent)
396
+ 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 });
397
+ if (request.dryRun) {
398
+ if (!journal.failure)
399
+ 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 });
400
+ 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)) });
401
+ }
402
+ const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
403
+ if (journal.intentDigest !== digest && !overrideResumesRecordedFailure)
351
404
  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 });
352
405
  }
353
406
  else {
@@ -360,7 +413,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
360
413
  }
361
414
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
362
415
  if (request.dryRun) {
363
- const plannedMutations = mutationsFor(request, content, metadata, restoredFiles);
416
+ const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
364
417
  if ('code' in plannedMutations)
365
418
  return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
366
419
  const taskPlan = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true }, {
@@ -402,7 +455,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
402
455
  completed.add('task-written');
403
456
  if (!completed.has('task-written')) {
404
457
  const current = io.readFileSync(path.join(sourcePath, 'task.md'));
405
- const mutations = mutationsFor(request, current, journal.metadata, restoredFiles);
458
+ const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
406
459
  if ('code' in mutations)
407
460
  return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
408
461
  const writeResult = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations }, {
@@ -432,7 +485,9 @@ function applyTaskLifecycle(requestInput, options = {}) {
432
485
  (options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath);
433
486
  }
434
487
  catch (error) {
435
- 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 });
488
+ const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) };
489
+ rememberJournalFailure(journalPath, journal, io, failure);
490
+ 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 });
436
491
  }
437
492
  completed.add('directory-moved');
438
493
  journal.completedSteps = [...completed];
@@ -480,5 +535,5 @@ function applyTaskLifecycle(requestInput, options = {}) {
480
535
  journalPath, completedSteps: [...completed], pendingSteps: []
481
536
  };
482
537
  }
483
- export { lifecycleIntentCatalog, applyTaskLifecycle };
538
+ export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
484
539
  //# sourceMappingURL=lifecycle.js.map
@@ -7,8 +7,9 @@ import { parseReviewSummary } from './review-artifacts.js';
7
7
  import { inspectArtifactDirectory } from './artifact-lifecycle.js';
8
8
  import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
9
9
  import { resolveTaskRef } from './resolve-ref.js';
10
- import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
10
+ import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
11
11
  import { normalizeAgentClients } from '../agent-clients/config.js';
12
+ import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.js';
12
13
  import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
13
14
  import { getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
14
15
  import { isAgentClientId } from '../agent-clients/types.js';
@@ -176,6 +177,22 @@ function canRecoverCodexUnsupportedPause(run, taskDir) {
176
177
  && !fs.existsSync(commitIntentPath(taskDir))
177
178
  && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
178
179
  }
180
+ function canRecoverClaudeCodeUnsupportedPause(run, taskDir) {
181
+ return run.schemaVersion === 3
182
+ && run.status === 'paused'
183
+ && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
184
+ && run.modelPolicySource?.client === 'claude-code'
185
+ && run.stepCount === 0
186
+ && run.nextStage === null
187
+ && run.baseline === ''
188
+ && run.pendingDelegation === null
189
+ && run.receipts.length === 0
190
+ && run.commitAuthorization?.issuedAt === null
191
+ && run.commitAuthorization?.consumedAt === null
192
+ && run.completionEvidence == null
193
+ && !fs.existsSync(commitIntentPath(taskDir))
194
+ && (run.recoveryHistory ?? []).length === 0;
195
+ }
179
196
  function canMigrateRecoverableV2Pause(run, taskDir) {
180
197
  return run.schemaVersion === 2
181
198
  && run.status === 'paused'
@@ -239,6 +256,39 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
239
256
  }
240
257
  }
241
258
  if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
259
+ if (existing.modelPolicySource.client === 'claude-code') {
260
+ if (!canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
261
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
262
+ }
263
+ const now = (options.now ?? (() => new Date().toISOString()))();
264
+ const previousPause = existing.pause;
265
+ const recovery = {
266
+ code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION',
267
+ recoveredAt: now,
268
+ previousSchemaVersion: 3,
269
+ previousStatus: 'paused',
270
+ previousPause,
271
+ client: 'claude-code',
272
+ guards: {
273
+ stepCount: 0,
274
+ nextStage: null,
275
+ baselineEmpty: true,
276
+ receiptCount: 0,
277
+ pendingDelegation: false,
278
+ commitAuthorizationUnused: true,
279
+ completionEvidenceAbsent: true,
280
+ commitIntentAbsent: true
281
+ },
282
+ resultingStatus: 'running'
283
+ };
284
+ const resumed = withUpdatedRun(existing, {
285
+ status: 'running',
286
+ pause: null,
287
+ recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
288
+ }, () => now);
289
+ saveRun(resolved.taskDir, resumed);
290
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
291
+ }
242
292
  if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
243
293
  return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
244
294
  }
@@ -1240,9 +1290,9 @@ function activateMatchingOrchestrationDelegation(client, event, options = {}) {
1240
1290
  return matched;
1241
1291
  const receipt = replay.run.pendingDelegation;
1242
1292
  const sameEvidence = receipt.parentId === event.parentId
1243
- && receipt.spawnMode === event.spawnMode
1244
- && receipt.actualModel === event.actualModel
1245
- && receipt.actualReasoningEffort === event.actualReasoningEffort
1293
+ && (receipt.spawnMode ?? null) === (event.spawnMode ?? null)
1294
+ && (receipt.actualModel ?? null) === foldBlankToNull(event.actualModel)
1295
+ && (receipt.actualReasoningEffort ?? null) === foldBlankToNull(event.actualReasoningEffort)
1246
1296
  && receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
1247
1297
  && receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
1248
1298
  && receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
@@ -1287,7 +1337,11 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
1287
1337
  childId: event.childId,
1288
1338
  exitCode: 0,
1289
1339
  afterFingerprint,
1290
- changedPaths
1340
+ changedPaths,
1341
+ actualModel: event.actualModel,
1342
+ actualReasoningEffort: event.actualReasoningEffort,
1343
+ modelFallbackReason: event.modelFallbackReason,
1344
+ reasoningEffortFallbackReason: event.reasoningEffortFallbackReason
1291
1345
  }, options);
1292
1346
  }
1293
1347
  catch (error) {
@@ -1440,7 +1494,7 @@ function recoverPreparedOrchestrationDelegation(taskRef, options = {}) {
1440
1494
  const activeLifecycleEvidence = options.hasActiveLifecycleEvidence?.(receipt)
1441
1495
  ?? (receipt.client === 'codex'
1442
1496
  && receipt.lifecycleProvenance
1443
- && hasActiveCodexLifecycleEvidence(path.join(resolved.repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle'), {
1497
+ && hasActiveCodexLifecycleEvidence(resolveAgentRuntimeStoreRoot({ repoRoot: resolved.repoRoot, store: 'lifecycle' }), {
1444
1498
  nativeAgent: `agent-infra-lifecycle-${receipt.role}`,
1445
1499
  hookDefinitionHash: receipt.lifecycleProvenance.hookDefinitionHash
1446
1500
  }));
@@ -8,6 +8,7 @@ import { finalizeReviewSummaryContent } from './review-artifacts.js';
8
8
  import { resolveTaskRef } from './resolve-ref.js';
9
9
  import { validateLifecycleExecution } from './lifecycle-execution.js';
10
10
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
11
+ import { allowsManualOverride } from './guard-override.js';
11
12
  const STAGES = {
12
13
  analysis: { family: 'review-analysis', action: 'Review Analysis' },
13
14
  plan: { family: 'review-plan', action: 'Review Plan' },
@@ -60,7 +61,7 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
60
61
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
61
62
  if (!resolved.ok)
62
63
  return failed(request, resolved.code, resolved.message, resolved.taskId);
63
- if (resolved.state !== 'active') {
64
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
64
65
  return failed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
65
66
  }
66
67
  const parsedArtifact = parseArtifactName(request.artifact);
@@ -168,7 +169,7 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
168
169
  };
169
170
  }
170
171
  function finalizeReviewSummary(request, options = {}) {
171
- if (request.dryRun)
172
+ if (request.dryRun || options.lockAlreadyHeld)
172
173
  return finalizeReviewSummaryUnlocked(request, options);
173
174
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
174
175
  if (!resolved.ok)
@@ -261,7 +261,22 @@ function checkOrchestrationEvidence({ taskDir }) {
261
261
  && guards?.commitAuthorizationUnused === true
262
262
  && guards?.completionEvidenceAbsent === true
263
263
  && guards?.commitIntentAbsent === true;
264
- if (!validModelRecovery && !validCapabilityRecovery) {
264
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
265
+ && recovery.previousSchemaVersion === 3
266
+ && recovery.previousStatus === 'paused'
267
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
268
+ && recovery.client === 'claude-code'
269
+ && recovery.resultingStatus === 'running'
270
+ && exactText(recovery.recoveredAt)
271
+ && guards?.stepCount === 0
272
+ && guards?.nextStage === null
273
+ && guards?.baselineEmpty === true
274
+ && guards?.receiptCount === 0
275
+ && guards?.pendingDelegation === false
276
+ && guards?.commitAuthorizationUnused === true
277
+ && guards?.completionEvidenceAbsent === true
278
+ && guards?.commitIntentAbsent === true;
279
+ if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
265
280
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
266
281
  }
267
282
  }
@@ -292,26 +307,37 @@ function checkOrchestrationEvidence({ taskDir }) {
292
307
  }
293
308
  const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
294
309
  if (activated) {
295
- if (!exactText(receipt.actualModel)) {
296
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
310
+ const isClaudeCode = receipt.client === 'claude-code';
311
+ if (!isClaudeCode) {
312
+ if (!exactText(receipt.actualModel)) {
313
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
314
+ }
315
+ if (!exactText(receipt.actualReasoningEffort)) {
316
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
317
+ }
297
318
  }
298
- if (!exactText(receipt.actualReasoningEffort)) {
299
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
319
+ // parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
320
+ if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
321
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
300
322
  }
301
- if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId || receipt.spawnMode !== 'fresh') {
323
+ if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
302
324
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
303
325
  }
304
- if (receipt.actualModel !== receipt.requestedModel && !exactText(receipt.modelFallbackReason)) {
305
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
326
+ if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
327
+ if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
328
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
329
+ }
306
330
  }
307
- if (receipt.actualModel === receipt.requestedModel && receipt.modelFallbackReason !== null) {
331
+ if ((receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
332
+ && receipt.modelFallbackReason !== null) {
308
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
309
334
  }
310
- if (receipt.actualReasoningEffort !== receipt.requestedReasoningEffort
311
- && !exactText(receipt.reasoningEffortFallbackReason)) {
312
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
335
+ if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
336
+ if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
337
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
338
+ }
313
339
  }
314
- if (receipt.actualReasoningEffort === receipt.requestedReasoningEffort
340
+ if ((receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
315
341
  && receipt.reasoningEffortFallbackReason !== null) {
316
342
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
317
343
  }
@@ -774,7 +800,7 @@ function checkReviewLedger({ taskDir, config }) {
774
800
  return passResult("review-ledger", "Disagreement ledger has no entries");
775
801
  }
776
802
  const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
777
- const maxRounds = Number(resolveReviewSetting(config, "max_handshake_rounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
803
+ const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
778
804
  const problems = [];
779
805
  let inScopeCount = 0;
780
806
  for (const cells of rows) {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { verifyInProcess } from './verification-engine.js';
5
5
  import { parseArtifactName } from './artifact-lifecycle.js';
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
+ import { allowsManualOverride } from './guard-override.js';
7
8
  const gate = (skill, expectedState, artifactFamily) => ({
8
9
  skill, expectedState, mode: 'gate', ...(artifactFamily ? { artifactFamily } : {})
9
10
  });
@@ -47,7 +48,7 @@ function verifyTaskEvent(request, options = {}) {
47
48
  if (!resolved.ok)
48
49
  return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
49
50
  const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode };
50
- if (resolved.state !== spec.expectedState) {
51
+ if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
51
52
  return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
52
53
  }
53
54
  if (spec.artifactFamily) {
@@ -66,12 +67,23 @@ function verifyTaskEvent(request, options = {}) {
66
67
  if (spec.mode === 'gate') {
67
68
  const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot });
68
69
  const status = payload.gate;
70
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
71
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
72
+ return {
73
+ status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
74
+ ...identity, artifact: request.artifact ?? null, invocations, error: null
75
+ };
76
+ }
69
77
  invocations.push({ status, exitCode: { pass: 0, fail: 1, blocked: 2 }[status], payload });
70
78
  }
71
79
  else {
72
80
  for (const check of spec.checks ?? []) {
73
81
  const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
74
82
  const status = payload.status;
83
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
84
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
85
+ break;
86
+ }
75
87
  invocations.push({ status, exitCode: { pass: 0, fail: 1, blocked: 2 }[status], payload });
76
88
  if (status !== 'pass')
77
89
  break;
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { resolveTaskRef } from './resolve-ref.js';
3
3
  import { WORKFLOW_WARNING_COLUMNS, WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_SEVERITIES, WORKFLOW_WARNING_STATUSES, parseWorkflowWarnings } from './workflow-warnings.js';
4
4
  import { captureTaskWriteMetadata, writeTask } from './write.js';
5
+ import { allowsManualOverride } from './guard-override.js';
5
6
  function failed(intent, code, message, taskId = null, entityId = null) {
6
7
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
7
8
  }
@@ -54,7 +55,8 @@ function applyWorkflowWarningIntent(intent, options = {}) {
54
55
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
55
56
  if (!resolved.ok)
56
57
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
57
- if (resolved.state !== 'active')
58
+ const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
59
+ if (resolved.state !== 'active' && !stateOverride)
58
60
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
59
61
  let content;
60
62
  let rows;
@@ -114,7 +116,7 @@ function applyWorkflowWarningIntent(intent, options = {}) {
114
116
  after = { ...before, status: intent.status, resolvedAt: metadata.timestamp, resolution };
115
117
  }
116
118
  const writeResult = writeTask({
117
- taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
119
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
118
120
  }, {
119
121
  ...options,
120
122
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { VERSION } from '../version.js';
4
4
  import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
+ import { allowsManualOverride } from './guard-override.js';
6
7
  import { mutateTableRow, upsertSection } from './sections.js';
7
8
  const DEFAULT_FILE_SYSTEM = {
8
9
  readFileSync: (file) => fs.readFileSync(file, 'utf8'),
@@ -70,7 +71,7 @@ function writeTask(request, options = {}) {
70
71
  taskMdPath: resolved.taskMdPath,
71
72
  actualState: resolved.state
72
73
  };
73
- if (resolved.state !== request.expectedState) {
74
+ if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
74
75
  return failure(request, identity, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected ${request.expectedState}`);
75
76
  }
76
77
  const io = { ...DEFAULT_FILE_SYSTEM, ...options.fileSystem };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.9.7",
4
- "sourceInputHash": "ee12d1ec7a586eeaba04deb4253ccd7f5d16552915ca141e493248c0728459a9",
5
- "internalExecutableBuildHash": "0eceb914e6c1cb9937b86d34bfbf933d1010f499b9976813556146d2e6fa0132"
3
+ "packageVersion": "0.9.8",
4
+ "sourceInputHash": "fb481e6579519b3fd10c9ca9bf284d65498f935c034a14c047d023345abeb87c",
5
+ "internalExecutableBuildHash": "d8dcdf602b61001ba9829aa2027cccadcc9c371ccf9e29c3d681fd02d5a3a5af"
6
6
  }
@@ -16,7 +16,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
16
16
  commands: { level: 'integrated' },
17
17
  hooks: { level: 'integrated' },
18
18
  subagents: { level: 'experimental' },
19
- orchestration: { level: 'unsupported' },
19
+ orchestration: { level: 'experimental' },
20
20
  sandbox: { level: 'integrated' },
21
21
  verification: { level: 'compatible' }
22
22
  },
@@ -26,8 +26,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
26
26
  guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
27
27
  },
28
28
  delegationEvidence: {
29
- actualModel: 'unavailable',
30
- actualReasoningEffort: 'unavailable'
29
+ actualModel: 'host-event',
30
+ actualReasoningEffort: 'spawn-ack'
31
31
  },
32
32
  project: {
33
33
  ownedPathPrefixes: ['.claude/'],
@@ -7,6 +7,7 @@ import semver from 'semver';
7
7
 
8
8
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
9
9
  import type { CodexLifecycleEvent } from './evidence.ts';
10
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
10
11
 
11
12
  type JsonObject = Record<string, unknown>;
12
13
  type ThreadResolution = Readonly<{
@@ -558,7 +559,7 @@ async function preflightCodexLifecycleEvidence(
558
559
  }
559
560
 
560
561
  const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
561
- const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
562
+ const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
562
563
  const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
563
564
 
564
565
  const transport = new CodexAppServerTransport();
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  import { verifyLifecycleBuildIdentity } from './build-identity.ts';
6
6
  import type { LifecycleBuildIdentity } from './build-identity.ts';
7
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
7
8
 
8
9
  type CapabilityStatus = 'armed' | 'attested' | 'consumed' | 'expired';
9
10
  type CodexControllerBinding = Readonly<{
@@ -65,7 +66,7 @@ function sameController(
65
66
 
66
67
  function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
67
68
  const root = options.root
68
- ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
69
+ ?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
69
70
  const now = options.now ?? Date.now;
70
71
  const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
71
72
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;