@fitlab-ai/agent-infra 0.9.8 → 0.9.10

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 (182) 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/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  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 +127 -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 +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -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)}`);
156
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
157
+ return failResult('orchestration-state', `Invalid orchestration.json: ${message}`);
173
158
  }
174
- if (isFailClosedLegacyPause(run)) {
175
- return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
176
- }
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,30 +249,8 @@ 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
281
- && recovery.previousStatus === 'paused'
282
- && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
283
- && recovery.client === 'codex'
284
- && recovery.resultingStatus === 'running'
285
- && exactText(recovery.recoveredAt)
286
- && guards?.stepCount === 0
287
- && guards?.nextStage === null
288
- && guards?.baselineEmpty === true
289
- && guards?.receiptCount === 0
290
- && guards?.pendingDelegation === false
291
- && guards?.commitAuthorizationUnused === true
292
- && guards?.completionEvidenceAbsent === true
293
- && guards?.commitIntentAbsent === true;
294
- const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
295
- && recovery.previousSchemaVersion === 3
253
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
296
254
  && recovery.previousStatus === 'paused'
297
255
  && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
298
256
  && recovery.client === 'claude-code'
@@ -304,9 +262,8 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
304
262
  && guards?.receiptCount === 0
305
263
  && guards?.pendingDelegation === false
306
264
  && guards?.commitAuthorizationUnused === true
307
- && guards?.completionEvidenceAbsent === true
308
- && guards?.commitIntentAbsent === true;
309
- if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
265
+ && guards?.completionEvidenceAbsent === true;
266
+ if (!validClaudeCodeRecovery) {
310
267
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
311
268
  }
312
269
  }
@@ -376,12 +333,34 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
376
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
377
334
  }
378
335
  if (receipt.client === 'codex') {
336
+ const activatedCodex = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
379
337
  const host = receipt.hostEvidence;
338
+ const provenance = receipt.lifecycleProvenance;
380
339
  if (
381
- host?.kind !== 'codex-lifecycle-v1'
382
- || !exactText(host.hookDefinitionHash)
383
- || !Number.isSafeInteger(host.startRevision)
384
- || host.startRevision < 1
340
+ !provenance
341
+ || (activatedCodex && (
342
+ host?.kind !== 'codex-lifecycle-v2'
343
+ || host.protocolVersion !== provenance.protocolVersion
344
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
345
+ || host.hookSource !== provenance.hookSource
346
+ || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
347
+ || host.hookSourceHash !== provenance.hookSourceHash
348
+ || host.capabilitySessionId !== provenance.capabilitySessionId
349
+ || receipt.parentId !== provenance.capabilitySessionId
350
+ || host.capabilityTurnId !== provenance.capabilityTurnId
351
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
352
+ || host.controlGeneration !== provenance.controlGeneration
353
+ || host.spawnToolUseId === provenance.capabilityToolUseId
354
+ || !exactText(host.spawnToolUseId)
355
+ || !Number.isFinite(Date.parse(host.spawnObservedAt ?? ''))
356
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt ?? ''))
357
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt ?? ''))
358
+ || Date.parse(host.spawnObservedAt ?? '') < Date.parse(receipt.spawnDispatchedAt ?? '')
359
+ || Date.parse(host.spawnObservedAt ?? '') > Date.parse(receipt.activationDeadlineAt ?? '')
360
+ || !exactText(host.hookDefinitionHash)
361
+ || !Number.isSafeInteger(host.startRevision)
362
+ || host.startRevision < 1
363
+ ))
385
364
  ) {
386
365
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
387
366
  }
@@ -510,6 +489,63 @@ function checkTaskMeta({ taskDir, config }: any): any {
510
489
  );
511
490
  }
512
491
 
492
+ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }: any): any {
493
+ const task = loadTask(taskDir);
494
+ if (!task.ok) return failResult('required-pr-delivery', task.message);
495
+ let projectConfig: any = {};
496
+ try {
497
+ const file = path.join(repositoryRoot, '.agents', '.airc.json');
498
+ if (safeStat(file)?.isFile()) projectConfig = JSON.parse(fs.readFileSync(file, 'utf8')) as any;
499
+ } catch {
500
+ return failResult('required-pr-delivery', 'Project configuration is invalid');
501
+ }
502
+ const flow = projectConfig.prFlow;
503
+ const status = String(task.metadata.pr_status || 'pending');
504
+ const prNumber = parsePrNumber(task.metadata.pr_number);
505
+ if (flow === 'disabled') return passResult('required-pr-delivery', 'Pull request delivery is disabled by project policy');
506
+ if (flow === 'required' && (status !== 'created' || prNumber === null)) {
507
+ return failResult('required-pr-delivery', 'Project policy requires a bound pull request before completion');
508
+ }
509
+ if (flow !== 'required' && status === 'pending') {
510
+ return failResult('required-pr-delivery', 'Pull request delivery is pending; create a PR or explicitly skip it');
511
+ }
512
+ if (status === 'created' && prNumber === null) {
513
+ return failResult('required-pr-delivery', 'Task claims a created pull request without a valid pr_number');
514
+ }
515
+ if (flow === 'required') {
516
+ if (mode === 'gate') {
517
+ return passResult('required-pr-delivery', 'Bound pull request shape is valid; merged-state inspection is reserved for hard preflight');
518
+ }
519
+ const inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: repositoryRoot });
520
+ if (inspected.status === 'blocked') {
521
+ return blockedResult(
522
+ 'required-pr-delivery',
523
+ inspected.error?.message || 'Unable to inspect the required pull request',
524
+ inspected.error?.code || 'PR_INSPECTION_BLOCKED'
525
+ );
526
+ }
527
+ const pullRequest = inspected.pullRequest;
528
+ const repository = inspected.platform.repository?.toLowerCase() || '';
529
+ const taskBranch = String(task.metadata.branch || '').trim();
530
+ if (inspected.status !== 'no-op' || !pullRequest || pullRequest.number !== prNumber) {
531
+ return failResult('required-pr-delivery', 'Bound pull request identity could not be verified');
532
+ }
533
+ if (
534
+ !repository
535
+ || pullRequest.repository.toLowerCase() !== repository
536
+ || pullRequest.base.repository.toLowerCase() !== repository
537
+ || (taskBranch && pullRequest.head.ref !== taskBranch)
538
+ ) {
539
+ return failResult('required-pr-delivery', 'Bound pull request identity does not match the task');
540
+ }
541
+ if (pullRequest.state !== 'closed' || !pullRequest.mergedAt || !pullRequest.mergeCommitSha) {
542
+ return failResult('required-pr-delivery', 'Project policy requires the bound pull request to be merged');
543
+ }
544
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (merged PR #${prNumber})`);
545
+ }
546
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (${status})`);
547
+ }
548
+
513
549
  function validateTaskBranch(metadata: any): any {
514
550
  if (isBlank(metadata.branch)) {
515
551
  return null;
@@ -1600,7 +1636,14 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1600
1636
  const checks = [];
1601
1637
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1602
1638
  if (checkConfig === null) continue;
1603
- const result = runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config: checkConfig }, shared);
1639
+ const result = runCheck(type, {
1640
+ mode,
1641
+ skillName,
1642
+ taskDir: path.resolve(taskDir),
1643
+ artifactFile,
1644
+ repositoryRoot: repoRoot,
1645
+ config: checkConfig
1646
+ }, shared);
1604
1647
  checks.push(result);
1605
1648
  if (result.status === "blocked") break;
1606
1649
  }
@@ -1611,7 +1654,16 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1611
1654
  const config = (verifyConfig.checks || {})[type];
1612
1655
  if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1613
1656
  if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1614
- return { skill: skillName, ...runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config }, shared) };
1657
+ return {
1658
+ skill: skillName,
1659
+ ...runCheck(type, {
1660
+ skillName,
1661
+ taskDir: path.resolve(taskDir),
1662
+ artifactFile,
1663
+ repositoryRoot: repoRoot,
1664
+ config
1665
+ }, shared)
1666
+ };
1615
1667
  }
1616
1668
 
1617
1669
  export { verifyInProcess };
@@ -15,7 +15,7 @@ type VerificationEvent =
15
15
  | 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
16
16
  | 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
17
17
  | 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
18
- | 'complete-task.preflight' | 'complete-task.completed'
18
+ | 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.completed'
19
19
  | 'create-pr.completed' | 'create-task.completed'
20
20
  | 'import-codescan.completed' | 'import-dependabot.completed'
21
21
  | 'import-issue.completed' | 'watch-pr.completed'
@@ -69,7 +69,8 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
69
69
  'block-task.completed': gate('block-task', 'blocked'),
70
70
  'cancel-task.completed': gate('cancel-task', 'completed'),
71
71
  'commit.completed': gate('commit', 'active'),
72
- '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'] },
73
74
  'complete-task.completed': gate('complete-task', 'completed'),
74
75
  'create-pr.completed': gate('create-pr', 'active'),
75
76
  'create-task.completed': gate('create-task', 'active'),
@@ -12,6 +12,7 @@ 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
14
  import { allowsManualOverride } from './guard-override.ts';
15
+ import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
15
16
 
16
17
  type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
17
18
  type WarningStatus = 'open' | 'resolved' | 'ignored';
@@ -28,6 +29,12 @@ type WorkflowWarningIntentResult = {
28
29
  error: { code: string; message: string } | null;
29
30
  };
30
31
 
32
+ type FinalizationWarningProjectionResult = Readonly<{
33
+ status: 'applied' | 'no-op' | 'failed';
34
+ changed: boolean;
35
+ error: { code: string; message: string } | null;
36
+ }>;
37
+
31
38
  function failed(intent: WorkflowWarningIntent, code: string, message: string, taskId: string | null = null, entityId: string | null = null): WorkflowWarningIntentResult {
32
39
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
33
40
  }
@@ -140,5 +147,53 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
140
147
  };
141
148
  }
142
149
 
143
- export { applyWorkflowWarningIntent };
144
- 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,7 +54,7 @@
54
54
  "yaml": "^2.9.0"
55
55
  },
56
56
  "scripts": {
57
- "build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
57
+ "build": "node scripts/run-build.js",
58
58
  "demo:regen": "sh scripts/demo-regen.sh",
59
59
  "prepare": "git config core.hooksPath .git-hooks || true",
60
60
  "typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
@@ -20,18 +20,17 @@ This makes Git invoke the hooks in the project repository's `.git-hooks/` direct
20
20
 
21
21
  ## Development Checkout CLI Alignment
22
22
 
23
- When developing agent-infra from a source checkout, install dependencies and rebuild the local CLI before running lifecycle orchestration:
23
+ When developing the agent-infra CLI from a source checkout, install dependencies and rebuild it as needed:
24
24
 
25
25
  ```bash
26
26
  npm install
27
27
  npm run build
28
28
  ```
29
29
 
30
- Lifecycle hooks prefer the checkout's `dist/bin/internal-cli.js` when it exists and fall back to `agent-infra-internal` on `PATH` otherwise. Rebuild after CLI source changes. If shell commands must also resolve to this checkout, link it and compare the active version with the package version:
30
+ Lifecycle hooks always invoke `agent-infra-internal` through `PATH`; they do not implicitly switch to checkout source or `dist`. In a Codex sandbox, the controller's `PATH`-first shim is authoritative and binds the controller's selected executable. Skills, rules, hooks, and task data continue to come from the current checkout. The commands above develop the CLI itself and do not change hook executable selection. To run the CLI from the current checkout, invoke the build output directly and compare its version with the package version. Do not run `npm link` on the host: it writes to npm's global prefix and can shadow a stable installation managed by Homebrew or npm.
31
31
 
32
32
  ```bash
33
- npm link
34
- ai version --raw
33
+ node ./dist/bin/cli.js version --raw
35
34
  node -p "'v' + require('./package.json').version"
36
35
  ```
37
36
 
@@ -20,18 +20,17 @@ git config core.hooksPath .git-hooks
20
20
 
21
21
  ## 开发检出中的 CLI 对齐
22
22
 
23
- 从源码检出开发 agent-infra 时,请先安装依赖并重新构建本地 CLI,再运行生命周期编排:
23
+ 从源码检出开发 agent-infra CLI 时,请按需安装依赖并重新构建 CLI:
24
24
 
25
25
  ```bash
26
26
  npm install
27
27
  npm run build
28
28
  ```
29
29
 
30
- 本地 `dist/bin/internal-cli.js` 存在时,生命周期 hook 会优先使用它;不存在时则回退到 `PATH` 上的 `agent-infra-internal`。修改 CLI 源码后需要重新构建。如果 shell 命令也必须解析到当前检出,请建立链接并比较当前版本与包版本:
30
+ 生命周期 hook 始终通过 `PATH` 调用 `agent-infra-internal`,不会隐式切换到当前检出的 source 或 `dist`。在 Codex sandbox 中,controller 放在 `PATH` 首位的 shim 是权威入口,并绑定 controller 选定的 executable。skills、rules、hooks 和任务数据仍从当前检出读取。上面的命令只用于开发 CLI 本身,不会改变 hook 的 executable 选择。如需运行当前检出的 CLI,请直接执行构建产物并比较当前版本与包版本。不要在宿主机上执行 `npm link`:它会写入 npm 全局前缀,并可能遮蔽由 Homebrew 或 npm 安装的稳定版。
31
31
 
32
32
  ```bash
33
- npm link
34
- ai version --raw
33
+ node ./dist/bin/cli.js version --raw
35
34
  node -p "'v' + require('./package.json').version"
36
35
  ```
37
36
 
@@ -1,13 +1,9 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync } from 'node:fs';
4
3
  import { readFileSync } from 'node:fs';
5
4
  import { fileURLToPath } from 'node:url';
6
5
 
7
6
  const MAX_INPUT_BYTES = 64 * 1024;
8
- const repoPackageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
9
- const sourceInternalCli = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
10
- const localInternalCli = fileURLToPath(new URL('../../dist/bin/internal-cli.js', import.meta.url));
11
7
  const codexHooks = fileURLToPath(new URL('../../.codex/hooks.json', import.meta.url));
12
8
  const clientIndex = process.argv.indexOf('--client');
13
9
  const client = clientIndex >= 0 ? process.argv[clientIndex + 1] : '';
@@ -17,22 +13,6 @@ if (client !== 'claude-code' && client !== 'codex') {
17
13
  }
18
14
 
19
15
  function internalCliInvocation(args) {
20
- let isAgentInfraSourceCheckout = false;
21
- try {
22
- isAgentInfraSourceCheckout = JSON.parse(readFileSync(repoPackageJson, 'utf8')).name === '@fitlab-ai/agent-infra';
23
- } catch {
24
- // Installed project hooks do not require a repository package.json.
25
- }
26
- if (isAgentInfraSourceCheckout && existsSync(sourceInternalCli)) {
27
- return {
28
- command: process.execPath,
29
- commandArgs: ['--experimental-strip-types', sourceInternalCli, ...args],
30
- shell: false
31
- };
32
- }
33
- if (existsSync(localInternalCli)) {
34
- return { command: process.execPath, commandArgs: [localInternalCli, ...args], shell: false };
35
- }
36
16
  return { command: 'agent-infra-internal', commandArgs: args, shell: process.platform === 'win32' };
37
17
  }
38
18
 
@@ -125,7 +105,7 @@ process.stdin.on('end', () => {
125
105
  input: JSON.stringify(normalized),
126
106
  shell,
127
107
  stdio: ['pipe', 'pipe', 'pipe'],
128
- timeout: 15_000
108
+ timeout: 30_000
129
109
  });
130
110
  process.stdout.write(output);
131
111
  } catch (error) {
@@ -12,6 +12,7 @@ so you can quickly find "which ones to read" without opening each file.
12
12
  - [`next-step-output.md`](next-step-output.md) — "Next step" output rules: task short-id rendering and the `Completed at` trailer.
13
13
  - [`version-stamp.md`](version-stamp.md) — How and when to stamp `agent_infra_version`.
14
14
  - [`debugging-guide.md`](debugging-guide.md) — Structured debugging flow: gather evidence → form hypothesis → verify hypothesis → fix the root cause; no blind patch-and-retry.
15
+ - [`compatibility-policy.md`](compatibility-policy.md) — Compatibility by exception: admission evidence, implementation boundaries, exit conditions, and lifecycle review requirements.
15
16
 
16
17
  ## Issue / PR
17
18
 
@@ -11,6 +11,7 @@
11
11
  - [`next-step-output.md`](next-step-output.md) — 「下一步」输出规则:任务短号渲染与 `Completed at` 收尾行。
12
12
  - [`version-stamp.md`](version-stamp.md) — `agent_infra_version` 版本戳的取值命令与写入时机。
13
13
  - [`debugging-guide.md`](debugging-guide.md) — 结构化调试流程:收集证据→形成假设→验证假设→修复根因,禁止盲目改代码重试。
14
+ - [`compatibility-policy.md`](compatibility-policy.md) — 兼容性默认关闭:准入证据、实现边界、退出条件与生命周期审查要求。
14
15
 
15
16
  ## Issue / PR
16
17
 
@@ -0,0 +1,39 @@
1
+ # Compatibility Policy
2
+
3
+ This rule governs compatibility decisions during analysis, planning, implementation, and review. Support the current contract by default. Compatibility is an exception budget that requires evidence and an exit condition; it is not a synonym for safety.
4
+
5
+ ## What Counts as Compatibility
6
+
7
+ Compatibility logic includes accepting or writing multiple generations of schemas, fields, states, or commands; retaining legacy aliases, adapters, wrappers, or shims; migrating data in the normal runtime path; falling back to different semantics based on an old format; or maintaining parallel facts and tests for old callers.
8
+
9
+ Current-operation idempotency and crash recovery, explicit platform or permission capability branches, fail-closed handling of unknown state, and user-required public multi-version contracts are not inherently legacy compatibility. Do not use those labels to expand support for an old protocol.
10
+
11
+ ## Admission Gate
12
+
13
+ Add or extend compatibility only when the task or an existing public contract explicitly requires it and the artifact records all four items:
14
+
15
+ 1. **Subject**: concrete consumers, released versions, or stored data still using the old contract.
16
+ 2. **Necessity**: why an atomic cutover, one-time conversion, or actionable failure is insufficient.
17
+ 3. **Window**: a version, date, or verifiable endpoint for the compatibility period.
18
+ 4. **Exit**: removal conditions, ownership, and the code, tests, and documentation to delete.
19
+
20
+ If any item is missing, compatibility is unauthorized scope expansion. Implement the current contract, fail closed with an actionable repair message for old input, or perform an explicit one-time conversion outside the main path.
21
+
22
+ ## Implementation Boundaries
23
+
24
+ - Keep one canonical write format and one source of truth. Do not dual-write, maintain shadow state, or try the new path before silently falling back to the old path.
25
+ - Concentrate an approved conversion at one boundary and immediately enter the current model; business logic must not understand several historical versions.
26
+ - Use warnings only when the primary action remains correct and the user can retry an ancillary operation. Never hide uncertainty about identity, authorization, data interpretation, or side-effect correctness.
27
+ - When removing an old capability, remove its adapters, branches, fixtures, tests, and migration documentation together. Do not add negative tests that permanently remember deleted behavior.
28
+ - Never guess the meaning of unknown state. Preserve it, stop writes, and provide rebuild or manual-repair guidance.
29
+
30
+ ## Lifecycle Requirements
31
+
32
+ - **Analysis**: separate explicit compatibility requirements from model speculation and identify real consumers or stored data. State current-only when evidence is absent.
33
+ - **Plan**: for approved compatibility, identify the boundary, single conversion point, window, and deletion plan. Do not present a compatibility layer as the default low-risk option.
34
+ - **Implementation**: implement only the approved compatibility budget. Do not preserve an unapproved old branch “just in case.”
35
+ - **Review**: verify the four admission items, single source of truth, and exit condition. Flag unauthorized compatibility, indefinite shims, dual writes, and runtime-path migrations as scope and maintainability problems.
36
+
37
+ ## Existing Compatibility Logic
38
+
39
+ During an ordinary task, record out-of-scope compatibility logic and its risk without deleting it. A dedicated simplification task should first inventory consumers and stored data, then process items in this order: remove now, warn then remove, retain temporarily.