@fitlab-ai/agent-infra 0.9.8 → 0.9.9

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 (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. package/lib/task/commit-intent.ts +0 -230
@@ -343,6 +343,64 @@ function loadShortIdByTaskId(repoRoot) {
343
343
  }
344
344
  return map;
345
345
  }
346
+ function inspectShortIdRegistry(repoRoot) {
347
+ const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
348
+ if (!fs.existsSync(registryPath)) {
349
+ return {
350
+ status: 'missing',
351
+ error: {
352
+ code: 'SHORT_ID_REGISTRY_NOT_FOUND',
353
+ message: 'canonical short-id registry is missing'
354
+ }
355
+ };
356
+ }
357
+ let raw;
358
+ try {
359
+ raw = fs.readFileSync(registryPath, 'utf8');
360
+ }
361
+ catch {
362
+ return {
363
+ status: 'invalid',
364
+ error: {
365
+ code: 'SHORT_ID_REGISTRY_READ_FAILED',
366
+ message: 'canonical short-id registry is unreadable'
367
+ }
368
+ };
369
+ }
370
+ let parsed;
371
+ try {
372
+ parsed = JSON.parse(raw);
373
+ }
374
+ catch {
375
+ return {
376
+ status: 'invalid',
377
+ error: {
378
+ code: 'SHORT_ID_REGISTRY_INVALID_JSON',
379
+ message: 'canonical short-id registry contains invalid JSON'
380
+ }
381
+ };
382
+ }
383
+ try {
384
+ const registry = validateRegistry(parsed, registryPath, configuredShortIdLength(repoRoot));
385
+ const shortIds = new Map();
386
+ for (const [key, taskId] of Object.entries(registry.ids))
387
+ shortIds.set(taskId, key);
388
+ return { status: 'valid', shortIds };
389
+ }
390
+ catch (error) {
391
+ return {
392
+ status: 'invalid',
393
+ error: {
394
+ code: typeof error.code === 'string'
395
+ ? error.code
396
+ : 'SHORT_ID_REGISTRY_INVALID_SCHEMA',
397
+ message: error.code === 'SHORT_ID_REGISTRY_DUPLICATE_TASK'
398
+ ? 'canonical short-id registry contains duplicate task entries'
399
+ : 'canonical short-id registry has invalid schema'
400
+ }
401
+ };
402
+ }
403
+ }
346
404
  /**
347
405
  * Resolve a branch to its active-task short id (`NN`), or `null` when no
348
406
  * active task is bound to that branch.
@@ -371,5 +429,5 @@ function lookupShortIdByBranch(branch, repoRoot, _opts) {
371
429
  }
372
430
  return matches[0];
373
431
  }
374
- export { normalizeShortIdInput, isRemovedHashShortIdInput, resolveShortIdReadOnly, lookupShortIdByBranch, loadShortIdByTaskId, configuredShortIdLength, mutateShortIdRegistry, executeShortIdCommand };
432
+ export { normalizeShortIdInput, isRemovedHashShortIdInput, resolveShortIdReadOnly, lookupShortIdByBranch, loadShortIdByTaskId, inspectShortIdRegistry, configuredShortIdLength, mutateShortIdRegistry, executeShortIdCommand };
375
433
  //# sourceMappingURL=short-id.js.map
@@ -207,6 +207,8 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
207
207
  catch (error) {
208
208
  if (error instanceof TaskExecutionLockError)
209
209
  throw error;
210
+ if (error instanceof Error && error.name === 'OrchestrationStateError')
211
+ throw error;
210
212
  throw new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', 'lifecycle task lock operation failed', { operation: ownsFixed ? 'callback' : 'acquire', errno: errno(error), key });
211
213
  }
212
214
  finally {
@@ -228,5 +230,8 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
228
230
  }
229
231
  }
230
232
  }
231
- export { TaskExecutionLockError, lockKey, mapLinkError, withTaskExecutionLock };
233
+ function withRepositoryMutationLock(repoRoot, callback, options = {}) {
234
+ return withTaskExecutionLock(repoRoot, '__repository_mutation__', 'repository-mutation', callback, options);
235
+ }
236
+ export { TaskExecutionLockError, lockKey, mapLinkError, withRepositoryMutationLock, withTaskExecutionLock };
232
237
  //# sourceMappingURL=task-execution-lock.js.map
@@ -16,6 +16,7 @@ import { parseLedger, summarizeLedgerStage, validateLedgerRows } from "./ledger.
16
16
  import { equalCounts, parseReviewSummary } from "./review-artifacts.js";
17
17
  import { loadVerificationConfig } from "./verification-config.js";
18
18
  import { snapshotReview } from "../git/review-snapshot.js";
19
+ import { OrchestrationStateError, readRun } from "./orchestration.js";
19
20
  const TASK_ENUMS = {
20
21
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
21
22
  workflow: ["feature-development", "bug-fix", "refactoring"],
@@ -104,6 +105,8 @@ function runCheck(type, context, shared) {
104
105
  return checkReviewFact(context);
105
106
  case "post-review-commit":
106
107
  return checkPostReviewCommit(context);
108
+ case "required-pr-delivery":
109
+ return checkRequiredPrDelivery(context);
107
110
  case "orchestration-state":
108
111
  return checkOrchestrationState(context);
109
112
  case "orchestration-evidence":
@@ -120,22 +123,6 @@ function runCheck(type, context, shared) {
120
123
  }
121
124
  }
122
125
  }
123
- function isFailClosedLegacyPause(run) {
124
- if (run.schemaVersion !== 1
125
- || run.status !== 'paused'
126
- || run.pause?.recoverable !== false
127
- || !exactText(run.pause?.message)
128
- || !Array.isArray(run.receipts)) {
129
- return false;
130
- }
131
- if (run.pause.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED') {
132
- return run.receipts.length > 0 && run.pendingDelegation === null;
133
- }
134
- if (run.pause.code === 'ORCHESTRATION_DELEGATION_BUSY') {
135
- return run.pendingDelegation != null && run.pendingDelegation.status !== 'sealed';
136
- }
137
- return false;
138
- }
139
126
  function checkOrchestrationState({ taskDir }) {
140
127
  const file = path.join(taskDir, 'orchestration.json');
141
128
  const stat = safeStat(file);
@@ -143,16 +130,14 @@ function checkOrchestrationState({ taskDir }) {
143
130
  return failResult('orchestration-state', 'orchestration.json is missing');
144
131
  let run;
145
132
  try {
146
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
133
+ run = readRun(taskDir);
147
134
  }
148
135
  catch (error) {
149
- return failResult('orchestration-state', `Invalid orchestration.json: ${String(error)}`);
136
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
137
+ return failResult('orchestration-state', `Invalid orchestration.json: ${message}`);
150
138
  }
151
- if (isFailClosedLegacyPause(run)) {
152
- return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
153
- }
154
- if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
155
- return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
139
+ if (!run || !['paused', 'completed'].includes(run.status)) {
140
+ return failResult('orchestration-state', `Expected paused or completed run, received '${run?.status ?? 'missing'}'`);
156
141
  }
157
142
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
158
143
  return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
@@ -215,13 +200,11 @@ function checkOrchestrationEvidence({ taskDir }) {
215
200
  return failResult('orchestration-evidence', 'orchestration.json is missing');
216
201
  let run;
217
202
  try {
218
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
203
+ run = readRun(taskDir);
219
204
  }
220
205
  catch (error) {
221
- return failResult('orchestration-evidence', `Invalid orchestration.json: ${String(error)}`);
222
- }
223
- if (isFailClosedLegacyPause(run)) {
224
- return passResult('orchestration-evidence', 'Legacy orchestration evidence is preserved in a fail-closed pause');
206
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
207
+ return failResult('orchestration-evidence', `Invalid orchestration.json: ${message}`);
225
208
  }
226
209
  const policy = run.modelPolicy;
227
210
  if (!policy
@@ -239,30 +222,8 @@ function checkOrchestrationEvidence({ taskDir }) {
239
222
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
240
223
  }
241
224
  for (const recovery of run.recoveryHistory) {
242
- const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
243
- && recovery.previousSchemaVersion === 1
244
- && recovery.receiptCount === 0
245
- && recovery.pendingDelegation === false
246
- && exactText(recovery.recoveredAt)
247
- && recovery.policySource;
248
225
  const guards = recovery.guards;
249
- const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
250
- && recovery.previousSchemaVersion === 2
251
- && recovery.previousStatus === 'paused'
252
- && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
253
- && recovery.client === 'codex'
254
- && recovery.resultingStatus === 'running'
255
- && exactText(recovery.recoveredAt)
256
- && guards?.stepCount === 0
257
- && guards?.nextStage === null
258
- && guards?.baselineEmpty === true
259
- && guards?.receiptCount === 0
260
- && guards?.pendingDelegation === false
261
- && guards?.commitAuthorizationUnused === true
262
- && guards?.completionEvidenceAbsent === true
263
- && guards?.commitIntentAbsent === true;
264
- const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
265
- && recovery.previousSchemaVersion === 3
226
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
266
227
  && recovery.previousStatus === 'paused'
267
228
  && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
268
229
  && recovery.client === 'claude-code'
@@ -274,9 +235,8 @@ function checkOrchestrationEvidence({ taskDir }) {
274
235
  && guards?.receiptCount === 0
275
236
  && guards?.pendingDelegation === false
276
237
  && guards?.commitAuthorizationUnused === true
277
- && guards?.completionEvidenceAbsent === true
278
- && guards?.commitIntentAbsent === true;
279
- if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
238
+ && guards?.completionEvidenceAbsent === true;
239
+ if (!validClaudeCodeRecovery) {
280
240
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
281
241
  }
282
242
  }
@@ -342,11 +302,34 @@ function checkOrchestrationEvidence({ taskDir }) {
342
302
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
343
303
  }
344
304
  if (receipt.client === 'codex') {
305
+ const activatedCodex = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
345
306
  const host = receipt.hostEvidence;
346
- if (host?.kind !== 'codex-lifecycle-v1'
347
- || !exactText(host.hookDefinitionHash)
348
- || !Number.isSafeInteger(host.startRevision)
349
- || host.startRevision < 1) {
307
+ const provenance = receipt.lifecycleProvenance;
308
+ if (!provenance
309
+ || (activatedCodex && (host?.kind !== 'codex-lifecycle-v2'
310
+ || host.protocolVersion !== provenance.protocolVersion
311
+ || host.packageVersion !== provenance.packageVersion
312
+ || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
313
+ || host.lifecycleContractHash !== provenance.lifecycleContractHash
314
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
315
+ || host.hookSource !== provenance.hookSource
316
+ || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
317
+ || host.hookSourceHash !== provenance.hookSourceHash
318
+ || host.capabilitySessionId !== provenance.capabilitySessionId
319
+ || receipt.parentId !== provenance.capabilitySessionId
320
+ || host.capabilityTurnId !== provenance.capabilityTurnId
321
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
322
+ || host.controlGeneration !== provenance.controlGeneration
323
+ || host.spawnToolUseId === provenance.capabilityToolUseId
324
+ || !exactText(host.spawnToolUseId)
325
+ || !Number.isFinite(Date.parse(host.spawnObservedAt ?? ''))
326
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt ?? ''))
327
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt ?? ''))
328
+ || Date.parse(host.spawnObservedAt ?? '') < Date.parse(receipt.spawnDispatchedAt ?? '')
329
+ || Date.parse(host.spawnObservedAt ?? '') > Date.parse(receipt.activationDeadlineAt ?? '')
330
+ || !exactText(host.hookDefinitionHash)
331
+ || !Number.isSafeInteger(host.startRevision)
332
+ || host.startRevision < 1))) {
350
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
351
334
  }
352
335
  if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
@@ -438,6 +421,60 @@ function checkTaskMeta({ taskDir, config }) {
438
421
  const warningSuffix = warnings.length > 0 ? `; warnings: ${warnings.join("; ")}` : "";
439
422
  return passResult("task-meta", `Task metadata valid (${requiredFields.length} required fields checked${warningSuffix})`, warnings);
440
423
  }
424
+ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }) {
425
+ const task = loadTask(taskDir);
426
+ if (!task.ok)
427
+ return failResult('required-pr-delivery', task.message);
428
+ let projectConfig = {};
429
+ try {
430
+ const file = path.join(repositoryRoot, '.agents', '.airc.json');
431
+ if (safeStat(file)?.isFile())
432
+ projectConfig = JSON.parse(fs.readFileSync(file, 'utf8'));
433
+ }
434
+ catch {
435
+ return failResult('required-pr-delivery', 'Project configuration is invalid');
436
+ }
437
+ const flow = projectConfig.prFlow;
438
+ const status = String(task.metadata.pr_status || 'pending');
439
+ const prNumber = parsePrNumber(task.metadata.pr_number);
440
+ if (flow === 'disabled')
441
+ return passResult('required-pr-delivery', 'Pull request delivery is disabled by project policy');
442
+ if (flow === 'required' && (status !== 'created' || prNumber === null)) {
443
+ return failResult('required-pr-delivery', 'Project policy requires a bound pull request before completion');
444
+ }
445
+ if (flow !== 'required' && status === 'pending') {
446
+ return failResult('required-pr-delivery', 'Pull request delivery is pending; create a PR or explicitly skip it');
447
+ }
448
+ if (status === 'created' && prNumber === null) {
449
+ return failResult('required-pr-delivery', 'Task claims a created pull request without a valid pr_number');
450
+ }
451
+ if (flow === 'required') {
452
+ if (mode === 'gate') {
453
+ return passResult('required-pr-delivery', 'Bound pull request shape is valid; merged-state inspection is reserved for hard preflight');
454
+ }
455
+ const inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: repositoryRoot });
456
+ if (inspected.status === 'blocked') {
457
+ return blockedResult('required-pr-delivery', inspected.error?.message || 'Unable to inspect the required pull request', inspected.error?.code || 'PR_INSPECTION_BLOCKED');
458
+ }
459
+ const pullRequest = inspected.pullRequest;
460
+ const repository = inspected.platform.repository?.toLowerCase() || '';
461
+ const taskBranch = String(task.metadata.branch || '').trim();
462
+ if (inspected.status !== 'no-op' || !pullRequest || pullRequest.number !== prNumber) {
463
+ return failResult('required-pr-delivery', 'Bound pull request identity could not be verified');
464
+ }
465
+ if (!repository
466
+ || pullRequest.repository.toLowerCase() !== repository
467
+ || pullRequest.base.repository.toLowerCase() !== repository
468
+ || (taskBranch && pullRequest.head.ref !== taskBranch)) {
469
+ return failResult('required-pr-delivery', 'Bound pull request identity does not match the task');
470
+ }
471
+ if (pullRequest.state !== 'closed' || !pullRequest.mergedAt || !pullRequest.mergeCommitSha) {
472
+ return failResult('required-pr-delivery', 'Project policy requires the bound pull request to be merged');
473
+ }
474
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (merged PR #${prNumber})`);
475
+ }
476
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (${status})`);
477
+ }
441
478
  function validateTaskBranch(metadata) {
442
479
  if (isBlank(metadata.branch)) {
443
480
  return null;
@@ -1337,7 +1374,14 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1337
1374
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1338
1375
  if (checkConfig === null)
1339
1376
  continue;
1340
- const result = runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config: checkConfig }, shared);
1377
+ const result = runCheck(type, {
1378
+ mode,
1379
+ skillName,
1380
+ taskDir: path.resolve(taskDir),
1381
+ artifactFile,
1382
+ repositoryRoot: repoRoot,
1383
+ config: checkConfig
1384
+ }, shared);
1341
1385
  checks.push(result);
1342
1386
  if (result.status === "blocked")
1343
1387
  break;
@@ -1351,7 +1395,16 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1351
1395
  return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1352
1396
  if (config === null)
1353
1397
  return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1354
- return { skill: skillName, ...runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config }, shared) };
1398
+ return {
1399
+ skill: skillName,
1400
+ ...runCheck(type, {
1401
+ skillName,
1402
+ taskDir: path.resolve(taskDir),
1403
+ artifactFile,
1404
+ repositoryRoot: repoRoot,
1405
+ config
1406
+ }, shared)
1407
+ };
1355
1408
  }
1356
1409
  export { verifyInProcess };
1357
1410
  //# sourceMappingURL=verification-engine.js.map
@@ -21,7 +21,8 @@ const VERIFICATION_CATALOG = {
21
21
  'block-task.completed': gate('block-task', 'blocked'),
22
22
  'cancel-task.completed': gate('cancel-task', 'completed'),
23
23
  'commit.completed': gate('commit', 'active'),
24
- 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['review-ledger', 'manual-validation', 'post-review-commit', 'platform-sync-preflight'] },
24
+ 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
25
+ 'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
25
26
  'complete-task.completed': gate('complete-task', 'completed'),
26
27
  'create-pr.completed': gate('create-pr', 'active'),
27
28
  'create-task.completed': gate('create-task', 'active'),
@@ -3,6 +3,7 @@ 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
5
  import { allowsManualOverride } from './guard-override.js';
6
+ import { retryHintForWarning } from './operation-outcome.js';
6
7
  function failed(intent, code, message, taskId = null, entityId = null) {
7
8
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
8
9
  }
@@ -129,5 +130,53 @@ function applyWorkflowWarningIntent(intent, options = {}) {
129
130
  entityId: after.id, before, after, warnings: [], operations: writeResult.operations, error: null
130
131
  };
131
132
  }
132
- export { applyWorkflowWarningIntent };
133
+ function projectFinalizationWarning(taskRef, warning, options = {}) {
134
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
135
+ if (!resolved.ok)
136
+ return { status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } };
137
+ if (resolved.state !== 'completed')
138
+ return { status: 'failed', changed: false, error: { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected completed` } };
139
+ let content;
140
+ let rows;
141
+ try {
142
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
143
+ rows = parseWorkflowWarnings(content);
144
+ }
145
+ catch (error) {
146
+ return { status: 'failed', changed: false, error: { code: 'WARNING_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) } };
147
+ }
148
+ const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
149
+ const existing = rows.find((row) => row.step === warning.step && row.code === warning.code && row.target === warning.target);
150
+ const after = existing
151
+ ? {
152
+ ...existing,
153
+ severity: warning.severity,
154
+ status: warning.status,
155
+ message: warning.message,
156
+ action: retryHintForWarning(warning),
157
+ resolvedAt: warning.resolvedAt || (warning.status === 'resolved' ? metadata.timestamp : ''),
158
+ resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
159
+ }
160
+ : {
161
+ id: nextWarningId(rows), time: metadata.timestamp, step: warning.step,
162
+ severity: warning.severity, code: warning.code, status: warning.status,
163
+ target: warning.target, message: warning.message, action: retryHintForWarning(warning),
164
+ resolvedAt: warning.resolvedAt || '', resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
165
+ };
166
+ if (existing && JSON.stringify(existing) === JSON.stringify(after))
167
+ return { status: 'no-op', changed: false, error: null };
168
+ const written = writeTask({
169
+ taskRef: resolved.taskId,
170
+ expectedState: 'completed',
171
+ mutations: [...sectionMutation(content), rowMutation(after)]
172
+ }, {
173
+ ...options,
174
+ taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: 'completed' },
175
+ metadataProvider: () => metadata
176
+ });
177
+ if (written.status === 'failed')
178
+ return { status: 'failed', changed: false, error: { code: written.error.code, message: written.error.message } };
179
+ return { status: written.status === 'applied' ? 'applied' : 'no-op', changed: written.changed, error: null };
180
+ }
181
+ export { applyWorkflowWarningIntent, projectFinalizationWarning };
133
182
  //# sourceMappingURL=workflow-warning-intents.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.9.8",
4
- "sourceInputHash": "fb481e6579519b3fd10c9ca9bf284d65498f935c034a14c047d023345abeb87c",
5
- "internalExecutableBuildHash": "d8dcdf602b61001ba9829aa2027cccadcc9c371ccf9e29c3d681fd02d5a3a5af"
3
+ "packageVersion": "0.9.9",
4
+ "sourceInputHash": "2f2db695db557531b3139eccd1bd530fbeb7b99409e2d49338f3b4646aac9ef6",
5
+ "internalExecutableBuildHash": "1391ebe2614f3071e7a60920325046a47f1473e7fb23c821327072f947f639d3"
6
6
  }
@@ -36,6 +36,10 @@ type AppServerTransportOptions = Readonly<{
36
36
  timeoutMs?: number;
37
37
  rolloutReadAttempts?: number;
38
38
  rolloutRetryMs?: number;
39
+ threadReadAttempts?: number;
40
+ threadRetryMs?: number;
41
+ terminalReadAttempts?: number;
42
+ terminalRetryMs?: number;
39
43
  }>;
40
44
 
41
45
  type CodexRuntimeIdentity = Readonly<{
@@ -52,6 +56,10 @@ const LIFECYCLE_HOOKS = Object.freeze([
52
56
  ]);
53
57
  const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
54
58
  const DEFAULT_ROLLOUT_RETRY_MS = 100;
59
+ const DEFAULT_THREAD_READ_ATTEMPTS = 21;
60
+ const DEFAULT_THREAD_RETRY_MS = 100;
61
+ const DEFAULT_TERMINAL_READ_ATTEMPTS = 21;
62
+ const DEFAULT_TERMINAL_RETRY_MS = 100;
55
63
 
56
64
  function object(value: unknown): JsonObject | null {
57
65
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -67,6 +75,7 @@ function resolveCodexSpawnedChild(
67
75
  transcriptPath: string,
68
76
  expected: Readonly<{
69
77
  sessionId: string;
78
+ turnId: string;
70
79
  toolUseId: string;
71
80
  nativeAgent: string;
72
81
  taskName: string;
@@ -111,15 +120,19 @@ function resolveCodexSpawnedChild(
111
120
  ) throw new Error('Codex parent rollout spawn identity does not match the hook event');
112
121
  const activities = records.filter((record) => {
113
122
  const payload = object(record?.payload);
123
+ const item = object(payload?.item);
114
124
  return record?.type === 'event_msg'
115
- && payload?.type === 'sub_agent_activity'
116
- && payload.event_id === expected.toolUseId
117
- && payload.kind === 'started'
118
- && payload.agent_path === `/root/${expected.taskName}`
119
- && nonEmpty(payload.agent_thread_id);
125
+ && payload?.type === 'item_completed'
126
+ && payload.thread_id === expected.sessionId
127
+ && payload.turn_id === expected.turnId
128
+ && item?.type === 'SubAgentActivity'
129
+ && item.id === expected.toolUseId
130
+ && item.kind === 'started'
131
+ && item.agent_path === `/root/${expected.taskName}`
132
+ && nonEmpty(item.agent_thread_id);
120
133
  });
121
134
  if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
122
- return nonEmpty(object(activities[0]?.payload)?.agent_thread_id)!;
135
+ return nonEmpty(object(object(activities[0]?.payload)?.item)?.agent_thread_id)!;
123
136
  }
124
137
 
125
138
  function validateCodexLifecycleHookConfig(value: unknown): void {
@@ -133,7 +146,7 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
133
146
  return entry.hooks.some((hookValue) => {
134
147
  const hook = object(hookValue);
135
148
  return hook?.type === 'command'
136
- && hook.timeout === 15
149
+ && hook.timeout === 30
137
150
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
138
151
  });
139
152
  });
@@ -478,18 +491,30 @@ async function resolveCodexThread(
478
491
  const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
479
492
  const readThread = object(readResult?.thread);
480
493
  if (nonEmpty(readThread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
481
- let resolution: ThreadResolution;
494
+ let rolloutRecords: readonly unknown[];
482
495
  try {
483
- const rolloutRecords = await resolveCodexRolloutRecords(
496
+ rolloutRecords = await resolveCodexRolloutRecords(
484
497
  readResult,
485
498
  childThreadId,
486
499
  options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS,
487
500
  options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS
488
501
  );
489
- resolution = parseCodexThreadResolution(readResult, rolloutRecords);
490
502
  } catch {
491
503
  throw new Error('Codex App Server rollout metadata is unavailable');
492
504
  }
505
+ let resolution = parseCodexThreadResolution(readResult, rolloutRecords);
506
+ const threadReadAttempts = options.threadReadAttempts ?? DEFAULT_THREAD_READ_ATTEMPTS;
507
+ for (let attempt = 1; resolution.thread.forkedFromId !== null && attempt < threadReadAttempts; attempt += 1) {
508
+ await new Promise((resolve) => setTimeout(resolve, options.threadRetryMs ?? DEFAULT_THREAD_RETRY_MS));
509
+ const refreshed = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
510
+ if (nonEmpty(object(refreshed?.thread)?.id) !== childThreadId) {
511
+ throw new Error('Codex App Server returned the wrong child thread');
512
+ }
513
+ resolution = parseCodexThreadResolution(refreshed, rolloutRecords);
514
+ }
515
+ if (resolution.thread.forkedFromId !== null) {
516
+ throw new Error('Codex App Server fresh thread metadata is unavailable');
517
+ }
493
518
  const reroutes = transport.notifications
494
519
  .filter((entry) => entry.method === 'model/rerouted')
495
520
  .map((entry) => parseCodexModelReroute(entry.params))
@@ -507,20 +532,37 @@ async function resolveCodexThread(
507
532
 
508
533
  async function resolveCodexTerminal(
509
534
  childThreadId: string,
510
- options: AppServerTransportOptions = {}
535
+ expectedTurnIdOrOptions: string | AppServerTransportOptions = {},
536
+ terminalOptions: AppServerTransportOptions = {}
511
537
  ): Promise<Extract<CodexLifecycleEvent, { type: 'app-terminal' }>> {
538
+ const expectedTurnId = typeof expectedTurnIdOrOptions === 'string' ? expectedTurnIdOrOptions : null;
539
+ const options = typeof expectedTurnIdOrOptions === 'string' ? terminalOptions : expectedTurnIdOrOptions;
512
540
  const transport = new CodexAppServerTransport(options);
513
541
  transport.start();
514
542
  try {
515
543
  await transport.initialize();
516
- const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
517
- const thread = object(result?.thread);
518
- if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
519
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
520
- const turn = turns.at(-1);
521
- const turnValue = object(turn);
522
- if (!turnValue || turnValue.status === 'inProgress') throw new Error('CODEX_TURN_NOT_TERMINAL');
523
- return parseCodexTurnCompleted({ threadId: childThreadId, turn });
544
+ const attempts = expectedTurnId ? options.terminalReadAttempts ?? DEFAULT_TERMINAL_READ_ATTEMPTS : 1;
545
+ let abnormal: Extract<CodexLifecycleEvent, { type: 'app-terminal' }> | null = null;
546
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
547
+ const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
548
+ const thread = object(result?.thread);
549
+ if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
550
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
551
+ const turn = expectedTurnId
552
+ ? turns.find((candidate) => object(candidate)?.id === expectedTurnId)
553
+ : turns.at(-1);
554
+ const turnValue = object(turn);
555
+ if (turnValue && turnValue.status !== 'inProgress') {
556
+ const terminal = parseCodexTurnCompleted({ threadId: childThreadId, turn });
557
+ if (terminal.status === 'completed') return terminal;
558
+ abnormal = terminal;
559
+ }
560
+ if (attempt + 1 < attempts) {
561
+ await new Promise((resolve) => setTimeout(resolve, options.terminalRetryMs ?? DEFAULT_TERMINAL_RETRY_MS));
562
+ }
563
+ }
564
+ if (abnormal) return abnormal;
565
+ throw new Error('CODEX_TURN_NOT_TERMINAL');
524
566
  } finally {
525
567
  transport.close();
526
568
  }
@@ -3,6 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
+ import semver from 'semver';
7
+
6
8
  const LIFECYCLE_PROTOCOL_VERSION = 3;
7
9
 
8
10
  type LifecycleBuildIdentity = Readonly<{
@@ -32,6 +34,14 @@ function exactText(value: unknown): value is string {
32
34
  return typeof value === 'string' && value.trim() === value && value.length > 0;
33
35
  }
34
36
 
37
+ function isCanonicalLifecyclePackageVersion(value: unknown): value is string {
38
+ return typeof value === 'string' && semver.valid(value) === value;
39
+ }
40
+
41
+ function isLifecycleProtocolVersion(value: unknown): value is typeof LIFECYCLE_PROTOCOL_VERSION {
42
+ return value === LIFECYCLE_PROTOCOL_VERSION;
43
+ }
44
+
35
45
  function normalizeRelativePath(value: string): string {
36
46
  return value.replaceAll('\\', '/');
37
47
  }
@@ -215,6 +225,8 @@ function verifyLifecycleBuildIdentity(
215
225
  export {
216
226
  LIFECYCLE_PROTOCOL_VERSION,
217
227
  computeLifecycleBuildIdentity,
228
+ isCanonicalLifecyclePackageVersion,
229
+ isLifecycleProtocolVersion,
218
230
  readLifecycleManifestFiles,
219
231
  resolveLifecycleExecutableFiles,
220
232
  verifyLifecycleBuildIdentity