@fitlab-ai/agent-infra 0.9.7 → 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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -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 {
@@ -201,6 +201,10 @@ type RegistrySchema = {
201
201
  ids: Record<string, string>;
202
202
  };
203
203
 
204
+ type ShortIdRegistryInspection =
205
+ | { status: 'valid'; shortIds: Map<string, string> }
206
+ | { status: 'missing' | 'invalid'; error: { code: string; message: string } };
207
+
204
208
  type ShortIdMutationEffect = 'alloc' | 'release' | 'none';
205
209
  type ShortIdMutationResult = {
206
210
  effect: 'allocated' | 'released' | 'unchanged';
@@ -446,6 +450,64 @@ function loadShortIdByTaskId(repoRoot: string): Map<string, string> {
446
450
  return map;
447
451
  }
448
452
 
453
+ function inspectShortIdRegistry(repoRoot: string): ShortIdRegistryInspection {
454
+ const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
455
+ if (!fs.existsSync(registryPath)) {
456
+ return {
457
+ status: 'missing',
458
+ error: {
459
+ code: 'SHORT_ID_REGISTRY_NOT_FOUND',
460
+ message: 'canonical short-id registry is missing'
461
+ }
462
+ };
463
+ }
464
+
465
+ let raw: string;
466
+ try {
467
+ raw = fs.readFileSync(registryPath, 'utf8');
468
+ } catch {
469
+ return {
470
+ status: 'invalid',
471
+ error: {
472
+ code: 'SHORT_ID_REGISTRY_READ_FAILED',
473
+ message: 'canonical short-id registry is unreadable'
474
+ }
475
+ };
476
+ }
477
+
478
+ let parsed: unknown;
479
+ try {
480
+ parsed = JSON.parse(raw);
481
+ } catch {
482
+ return {
483
+ status: 'invalid',
484
+ error: {
485
+ code: 'SHORT_ID_REGISTRY_INVALID_JSON',
486
+ message: 'canonical short-id registry contains invalid JSON'
487
+ }
488
+ };
489
+ }
490
+
491
+ try {
492
+ const registry = validateRegistry(parsed, registryPath, configuredShortIdLength(repoRoot));
493
+ const shortIds = new Map<string, string>();
494
+ for (const [key, taskId] of Object.entries(registry.ids)) shortIds.set(taskId, key);
495
+ return { status: 'valid', shortIds };
496
+ } catch (error) {
497
+ return {
498
+ status: 'invalid',
499
+ error: {
500
+ code: typeof (error as { code?: unknown }).code === 'string'
501
+ ? (error as { code: string }).code
502
+ : 'SHORT_ID_REGISTRY_INVALID_SCHEMA',
503
+ message: (error as { code?: unknown }).code === 'SHORT_ID_REGISTRY_DUPLICATE_TASK'
504
+ ? 'canonical short-id registry contains duplicate task entries'
505
+ : 'canonical short-id registry has invalid schema'
506
+ }
507
+ };
508
+ }
509
+ }
510
+
449
511
  /**
450
512
  * Resolve a branch to its active-task short id (`NN`), or `null` when no
451
513
  * active task is bound to that branch.
@@ -485,6 +547,7 @@ export {
485
547
  resolveShortIdReadOnly,
486
548
  lookupShortIdByBranch,
487
549
  loadShortIdByTaskId,
550
+ inspectShortIdRegistry,
488
551
  configuredShortIdLength,
489
552
  mutateShortIdRegistry,
490
553
  executeShortIdCommand
@@ -497,5 +560,6 @@ export type {
497
560
  ShortIdMutationEffect,
498
561
  ShortIdMutationResult,
499
562
  ShortIdCommandRequest,
500
- ShortIdCommandResult
563
+ ShortIdCommandResult,
564
+ ShortIdRegistryInspection
501
565
  };
@@ -272,6 +272,7 @@ function withTaskExecutionLock<T>(
272
272
  return callback();
273
273
  } catch (error) {
274
274
  if (error instanceof TaskExecutionLockError) throw error;
275
+ if (error instanceof Error && error.name === 'OrchestrationStateError') throw error;
275
276
  throw new TaskExecutionLockError(
276
277
  'ORCHESTRATION_LOCK_FAILED',
277
278
  'lifecycle task lock operation failed',
@@ -294,5 +295,13 @@ function withTaskExecutionLock<T>(
294
295
  }
295
296
  }
296
297
 
297
- export { TaskExecutionLockError, lockKey, mapLinkError, withTaskExecutionLock };
298
+ function withRepositoryMutationLock<T>(
299
+ repoRoot: string,
300
+ callback: () => T,
301
+ options: TaskExecutionLockOptions = {}
302
+ ): T {
303
+ return withTaskExecutionLock(repoRoot, '__repository_mutation__', 'repository-mutation', callback, options);
304
+ }
305
+
306
+ export { TaskExecutionLockError, lockKey, mapLinkError, withRepositoryMutationLock, withTaskExecutionLock };
298
307
  export type { LinkDisposition, LinkOperation, TaskExecutionLockErrorCode, TaskExecutionLockOptions, TaskExecutionLockOwner };
@@ -27,6 +27,7 @@ import type { LedgerRow } from "./ledger.ts";
27
27
  import { equalCounts, parseReviewSummary } from "./review-artifacts.ts";
28
28
  import { loadVerificationConfig } from "./verification-config.ts";
29
29
  import { snapshotReview } from "../git/review-snapshot.ts";
30
+ import { OrchestrationStateError, readRun } from "./orchestration.ts";
30
31
 
31
32
  const TASK_ENUMS = {
32
33
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
@@ -123,6 +124,8 @@ function runCheck(type: any, context: any, shared: any): any {
123
124
  return checkReviewFact(context);
124
125
  case "post-review-commit":
125
126
  return checkPostReviewCommit(context);
127
+ case "required-pr-delivery":
128
+ return checkRequiredPrDelivery(context);
126
129
  case "orchestration-state":
127
130
  return checkOrchestrationState(context);
128
131
  case "orchestration-evidence":
@@ -142,40 +145,19 @@ function runCheck(type: any, context: any, shared: any): any {
142
145
  }
143
146
  }
144
147
 
145
- function isFailClosedLegacyPause(run: any): boolean {
146
- if (
147
- run.schemaVersion !== 1
148
- || run.status !== 'paused'
149
- || run.pause?.recoverable !== false
150
- || !exactText(run.pause?.message)
151
- || !Array.isArray(run.receipts)
152
- ) {
153
- return false;
154
- }
155
- if (run.pause.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED') {
156
- return run.receipts.length > 0 && run.pendingDelegation === null;
157
- }
158
- if (run.pause.code === 'ORCHESTRATION_DELEGATION_BUSY') {
159
- return run.pendingDelegation != null && run.pendingDelegation.status !== 'sealed';
160
- }
161
- return false;
162
- }
163
-
164
148
  function checkOrchestrationState({ taskDir }: any): any {
165
149
  const file = path.join(taskDir, 'orchestration.json');
166
150
  const stat = safeStat(file);
167
151
  if (!stat?.isFile()) return failResult('orchestration-state', 'orchestration.json is missing');
168
152
  let run: any;
169
153
  try {
170
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
154
+ run = readRun(taskDir);
171
155
  } catch (error) {
172
- return failResult('orchestration-state', `Invalid orchestration.json: ${String(error)}`);
173
- }
174
- if (isFailClosedLegacyPause(run)) {
175
- return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
156
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
157
+ return failResult('orchestration-state', `Invalid orchestration.json: ${message}`);
176
158
  }
177
- if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
178
- return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
159
+ if (!run || !['paused', 'completed'].includes(run.status)) {
160
+ return failResult('orchestration-state', `Expected paused or completed run, received '${run?.status ?? 'missing'}'`);
179
161
  }
180
162
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
181
163
  return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
@@ -242,12 +224,10 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
242
224
  if (!stat?.isFile()) return failResult('orchestration-evidence', 'orchestration.json is missing');
243
225
  let run: any;
244
226
  try {
245
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
227
+ run = readRun(taskDir);
246
228
  } catch (error) {
247
- return failResult('orchestration-evidence', `Invalid orchestration.json: ${String(error)}`);
248
- }
249
- if (isFailClosedLegacyPause(run)) {
250
- return passResult('orchestration-evidence', 'Legacy orchestration evidence is preserved in a fail-closed pause');
229
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
230
+ return failResult('orchestration-evidence', `Invalid orchestration.json: ${message}`);
251
231
  }
252
232
  const policy = run.modelPolicy;
253
233
  if (
@@ -269,18 +249,11 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
269
249
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
270
250
  }
271
251
  for (const recovery of run.recoveryHistory) {
272
- const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
273
- && recovery.previousSchemaVersion === 1
274
- && recovery.receiptCount === 0
275
- && recovery.pendingDelegation === false
276
- && exactText(recovery.recoveredAt)
277
- && recovery.policySource;
278
252
  const guards = recovery.guards;
279
- const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
280
- && recovery.previousSchemaVersion === 2
253
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
281
254
  && recovery.previousStatus === 'paused'
282
255
  && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
283
- && recovery.client === 'codex'
256
+ && recovery.client === 'claude-code'
284
257
  && recovery.resultingStatus === 'running'
285
258
  && exactText(recovery.recoveredAt)
286
259
  && guards?.stepCount === 0
@@ -289,9 +262,8 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
289
262
  && guards?.receiptCount === 0
290
263
  && guards?.pendingDelegation === false
291
264
  && guards?.commitAuthorizationUnused === true
292
- && guards?.completionEvidenceAbsent === true
293
- && guards?.commitIntentAbsent === true;
294
- if (!validModelRecovery && !validCapabilityRecovery) {
265
+ && guards?.completionEvidenceAbsent === true;
266
+ if (!validClaudeCodeRecovery) {
295
267
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
296
268
  }
297
269
  }
@@ -322,40 +294,76 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
322
294
  }
323
295
  const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
324
296
  if (activated) {
325
- if (!exactText(receipt.actualModel)) {
326
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
297
+ const isClaudeCode = receipt.client === 'claude-code';
298
+ if (!isClaudeCode) {
299
+ if (!exactText(receipt.actualModel)) {
300
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
301
+ }
302
+ if (!exactText(receipt.actualReasoningEffort)) {
303
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
304
+ }
327
305
  }
328
- if (!exactText(receipt.actualReasoningEffort)) {
329
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
306
+ // parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
307
+ if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
308
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
330
309
  }
331
- if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId || receipt.spawnMode !== 'fresh') {
310
+ if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
332
311
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
333
312
  }
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`);
313
+ if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
314
+ if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
315
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
316
+ }
339
317
  }
340
318
  if (
341
- receipt.actualReasoningEffort !== receipt.requestedReasoningEffort
342
- && !exactText(receipt.reasoningEffortFallbackReason)
319
+ (receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
320
+ && receipt.modelFallbackReason !== null
343
321
  ) {
344
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
322
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
323
+ }
324
+ if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
325
+ if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
326
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
327
+ }
345
328
  }
346
329
  if (
347
- receipt.actualReasoningEffort === receipt.requestedReasoningEffort
330
+ (receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
348
331
  && receipt.reasoningEffortFallbackReason !== null
349
332
  ) {
350
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
351
334
  }
352
335
  if (receipt.client === 'codex') {
336
+ const activatedCodex = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
353
337
  const host = receipt.hostEvidence;
338
+ const provenance = receipt.lifecycleProvenance;
354
339
  if (
355
- host?.kind !== 'codex-lifecycle-v1'
356
- || !exactText(host.hookDefinitionHash)
357
- || !Number.isSafeInteger(host.startRevision)
358
- || host.startRevision < 1
340
+ !provenance
341
+ || (activatedCodex && (
342
+ host?.kind !== 'codex-lifecycle-v2'
343
+ || host.protocolVersion !== provenance.protocolVersion
344
+ || host.packageVersion !== provenance.packageVersion
345
+ || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
346
+ || host.lifecycleContractHash !== provenance.lifecycleContractHash
347
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
348
+ || host.hookSource !== provenance.hookSource
349
+ || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
350
+ || host.hookSourceHash !== provenance.hookSourceHash
351
+ || host.capabilitySessionId !== provenance.capabilitySessionId
352
+ || receipt.parentId !== provenance.capabilitySessionId
353
+ || host.capabilityTurnId !== provenance.capabilityTurnId
354
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
355
+ || host.controlGeneration !== provenance.controlGeneration
356
+ || host.spawnToolUseId === provenance.capabilityToolUseId
357
+ || !exactText(host.spawnToolUseId)
358
+ || !Number.isFinite(Date.parse(host.spawnObservedAt ?? ''))
359
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt ?? ''))
360
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt ?? ''))
361
+ || Date.parse(host.spawnObservedAt ?? '') < Date.parse(receipt.spawnDispatchedAt ?? '')
362
+ || Date.parse(host.spawnObservedAt ?? '') > Date.parse(receipt.activationDeadlineAt ?? '')
363
+ || !exactText(host.hookDefinitionHash)
364
+ || !Number.isSafeInteger(host.startRevision)
365
+ || host.startRevision < 1
366
+ ))
359
367
  ) {
360
368
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
361
369
  }
@@ -484,6 +492,63 @@ function checkTaskMeta({ taskDir, config }: any): any {
484
492
  );
485
493
  }
486
494
 
495
+ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }: any): any {
496
+ const task = loadTask(taskDir);
497
+ if (!task.ok) return failResult('required-pr-delivery', task.message);
498
+ let projectConfig: any = {};
499
+ try {
500
+ const file = path.join(repositoryRoot, '.agents', '.airc.json');
501
+ if (safeStat(file)?.isFile()) projectConfig = JSON.parse(fs.readFileSync(file, 'utf8')) as any;
502
+ } catch {
503
+ return failResult('required-pr-delivery', 'Project configuration is invalid');
504
+ }
505
+ const flow = projectConfig.prFlow;
506
+ const status = String(task.metadata.pr_status || 'pending');
507
+ const prNumber = parsePrNumber(task.metadata.pr_number);
508
+ if (flow === 'disabled') return passResult('required-pr-delivery', 'Pull request delivery is disabled by project policy');
509
+ if (flow === 'required' && (status !== 'created' || prNumber === null)) {
510
+ return failResult('required-pr-delivery', 'Project policy requires a bound pull request before completion');
511
+ }
512
+ if (flow !== 'required' && status === 'pending') {
513
+ return failResult('required-pr-delivery', 'Pull request delivery is pending; create a PR or explicitly skip it');
514
+ }
515
+ if (status === 'created' && prNumber === null) {
516
+ return failResult('required-pr-delivery', 'Task claims a created pull request without a valid pr_number');
517
+ }
518
+ if (flow === 'required') {
519
+ if (mode === 'gate') {
520
+ return passResult('required-pr-delivery', 'Bound pull request shape is valid; merged-state inspection is reserved for hard preflight');
521
+ }
522
+ const inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: repositoryRoot });
523
+ if (inspected.status === 'blocked') {
524
+ return blockedResult(
525
+ 'required-pr-delivery',
526
+ inspected.error?.message || 'Unable to inspect the required pull request',
527
+ inspected.error?.code || 'PR_INSPECTION_BLOCKED'
528
+ );
529
+ }
530
+ const pullRequest = inspected.pullRequest;
531
+ const repository = inspected.platform.repository?.toLowerCase() || '';
532
+ const taskBranch = String(task.metadata.branch || '').trim();
533
+ if (inspected.status !== 'no-op' || !pullRequest || pullRequest.number !== prNumber) {
534
+ return failResult('required-pr-delivery', 'Bound pull request identity could not be verified');
535
+ }
536
+ if (
537
+ !repository
538
+ || pullRequest.repository.toLowerCase() !== repository
539
+ || pullRequest.base.repository.toLowerCase() !== repository
540
+ || (taskBranch && pullRequest.head.ref !== taskBranch)
541
+ ) {
542
+ return failResult('required-pr-delivery', 'Bound pull request identity does not match the task');
543
+ }
544
+ if (pullRequest.state !== 'closed' || !pullRequest.mergedAt || !pullRequest.mergeCommitSha) {
545
+ return failResult('required-pr-delivery', 'Project policy requires the bound pull request to be merged');
546
+ }
547
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (merged PR #${prNumber})`);
548
+ }
549
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (${status})`);
550
+ }
551
+
487
552
  function validateTaskBranch(metadata: any): any {
488
553
  if (isBlank(metadata.branch)) {
489
554
  return null;
@@ -902,7 +967,7 @@ function checkReviewLedger({ taskDir, config }: any): any {
902
967
  }
903
968
 
904
969
  const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
905
- const maxRounds = Number(resolveReviewSetting(config, "max_handshake_rounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
970
+ const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
906
971
  const problems = [];
907
972
  let inScopeCount = 0;
908
973
 
@@ -1574,7 +1639,14 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1574
1639
  const checks = [];
1575
1640
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1576
1641
  if (checkConfig === null) continue;
1577
- const result = runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config: checkConfig }, shared);
1642
+ const result = runCheck(type, {
1643
+ mode,
1644
+ skillName,
1645
+ taskDir: path.resolve(taskDir),
1646
+ artifactFile,
1647
+ repositoryRoot: repoRoot,
1648
+ config: checkConfig
1649
+ }, shared);
1578
1650
  checks.push(result);
1579
1651
  if (result.status === "blocked") break;
1580
1652
  }
@@ -1585,7 +1657,16 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1585
1657
  const config = (verifyConfig.checks || {})[type];
1586
1658
  if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1587
1659
  if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1588
- return { skill: skillName, ...runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config }, shared) };
1660
+ return {
1661
+ skill: skillName,
1662
+ ...runCheck(type, {
1663
+ skillName,
1664
+ taskDir: path.resolve(taskDir),
1665
+ artifactFile,
1666
+ repositoryRoot: repoRoot,
1667
+ config
1668
+ }, shared)
1669
+ };
1589
1670
  }
1590
1671
 
1591
1672
  export { verifyInProcess };
@@ -7,13 +7,15 @@ 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'
13
15
  | 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
14
16
  | 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
15
17
  | 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
16
- | 'complete-task.preflight' | 'complete-task.completed'
18
+ | 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.completed'
17
19
  | 'create-pr.completed' | 'create-task.completed'
18
20
  | 'import-codescan.completed' | 'import-dependabot.completed'
19
21
  | 'import-issue.completed' | 'watch-pr.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 => ({
@@ -66,7 +69,8 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
66
69
  'block-task.completed': gate('block-task', 'blocked'),
67
70
  'cancel-task.completed': gate('cancel-task', 'completed'),
68
71
  'commit.completed': gate('commit', 'active'),
69
- 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['review-ledger', 'manual-validation', 'post-review-commit', 'platform-sync-preflight'] },
72
+ 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
73
+ 'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
70
74
  'complete-task.completed': gate('complete-task', 'completed'),
71
75
  'create-pr.completed': gate('create-pr', 'active'),
72
76
  'create-task.completed': gate('create-task', 'active'),
@@ -93,7 +97,7 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
93
97
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
94
98
  if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
95
99
  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) {
100
+ if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
97
101
  return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
98
102
  }
99
103
  if (spec.artifactFamily) {
@@ -111,11 +115,22 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
111
115
  if (spec.mode === 'gate') {
112
116
  const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
113
117
  const status = payload.gate as 'pass' | 'fail' | 'blocked';
118
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
119
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
120
+ return {
121
+ status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
122
+ ...identity, artifact: request.artifact ?? null, invocations, error: null
123
+ };
124
+ }
114
125
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
115
126
  } else {
116
127
  for (const check of spec.checks ?? []) {
117
128
  const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
118
129
  const status = payload.status as 'pass' | 'fail' | 'blocked';
130
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
131
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
132
+ break;
133
+ }
119
134
  invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
120
135
  if (status !== 'pass') break;
121
136
  }
@@ -11,6 +11,8 @@ 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';
15
+ import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
14
16
 
15
17
  type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
16
18
  type WarningStatus = 'open' | 'resolved' | 'ignored';
@@ -27,6 +29,12 @@ type WorkflowWarningIntentResult = {
27
29
  error: { code: string; message: string } | null;
28
30
  };
29
31
 
32
+ type FinalizationWarningProjectionResult = Readonly<{
33
+ status: 'applied' | 'no-op' | 'failed';
34
+ changed: boolean;
35
+ error: { code: string; message: string } | null;
36
+ }>;
37
+
30
38
  function failed(intent: WorkflowWarningIntent, code: string, message: string, taskId: string | null = null, entityId: string | null = null): WorkflowWarningIntentResult {
31
39
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
32
40
  }
@@ -79,7 +87,8 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
79
87
  function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
80
88
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
81
89
  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);
90
+ const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
91
+ if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
83
92
  let content: string;
84
93
  let rows: WorkflowWarning[];
85
94
  try {
@@ -125,7 +134,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
125
134
  }
126
135
 
127
136
  const writeResult = writeTask({
128
- taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
137
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
129
138
  }, {
130
139
  ...options,
131
140
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
@@ -138,5 +147,53 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
138
147
  };
139
148
  }
140
149
 
141
- export { applyWorkflowWarningIntent };
142
- export type { WarningSeverity, WarningStatus, WorkflowWarningIntent, WorkflowWarningIntentResult };
150
+ function projectFinalizationWarning(
151
+ taskRef: string,
152
+ warning: OperationWarning & { status: 'open' | 'resolved'; resolvedAt: string | null },
153
+ options: TaskWriteOptions = {}
154
+ ): FinalizationWarningProjectionResult {
155
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
156
+ if (!resolved.ok) return { status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } };
157
+ if (resolved.state !== 'completed') return { status: 'failed', changed: false, error: { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected completed` } };
158
+ let content: string;
159
+ let rows: WorkflowWarning[];
160
+ try {
161
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
162
+ rows = parseWorkflowWarnings(content);
163
+ } catch (error) {
164
+ return { status: 'failed', changed: false, error: { code: 'WARNING_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) } };
165
+ }
166
+ const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
167
+ const existing = rows.find((row) => row.step === warning.step && row.code === warning.code && row.target === warning.target);
168
+ const after: WorkflowWarning = existing
169
+ ? {
170
+ ...existing,
171
+ severity: warning.severity,
172
+ status: warning.status,
173
+ message: warning.message,
174
+ action: retryHintForWarning(warning),
175
+ resolvedAt: warning.resolvedAt || (warning.status === 'resolved' ? metadata.timestamp : ''),
176
+ resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
177
+ }
178
+ : {
179
+ id: nextWarningId(rows), time: metadata.timestamp, step: warning.step,
180
+ severity: warning.severity, code: warning.code, status: warning.status,
181
+ target: warning.target, message: warning.message, action: retryHintForWarning(warning),
182
+ resolvedAt: warning.resolvedAt || '', resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
183
+ };
184
+ if (existing && JSON.stringify(existing) === JSON.stringify(after)) return { status: 'no-op', changed: false, error: null };
185
+ const written = writeTask({
186
+ taskRef: resolved.taskId,
187
+ expectedState: 'completed',
188
+ mutations: [...sectionMutation(content), rowMutation(after)]
189
+ }, {
190
+ ...options,
191
+ taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: 'completed' },
192
+ metadataProvider: () => metadata
193
+ });
194
+ if (written.status === 'failed') return { status: 'failed', changed: false, error: { code: written.error.code, message: written.error.message } };
195
+ return { status: written.status === 'applied' ? 'applied' : 'no-op', changed: written.changed, error: null };
196
+ }
197
+
198
+ export { applyWorkflowWarningIntent, projectFinalizationWarning };
199
+ export type { FinalizationWarningProjectionResult, WarningSeverity, WarningStatus, WorkflowWarningIntent, WorkflowWarningIntentResult };
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,