@fitlab-ai/agent-infra 0.11.1 → 0.11.3

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 (131) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/cli-route-inventory.js +1 -1
  8. package/dist/lib/internal/manual-validation.js +22 -27
  9. package/dist/lib/internal/platform-comment.js +59 -0
  10. package/dist/lib/internal/sandbox-control.js +2 -171
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +7 -2
  14. package/dist/lib/internal/task-review.js +5 -5
  15. package/dist/lib/internal/task-validate.js +0 -6
  16. package/dist/lib/platform/comment-safety.js +7 -1
  17. package/dist/lib/platform/github-provider.js +13 -3
  18. package/dist/lib/platform/issue-comments.js +93 -15
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/platform/provider-contract.d.ts +2 -0
  21. package/dist/lib/platform/provider-validation.js +7 -2
  22. package/dist/lib/platform/verification-sync.js +19 -2
  23. package/dist/lib/sandbox/control/client.js +3 -44
  24. package/dist/lib/sandbox/control/executor.js +14 -117
  25. package/dist/lib/sandbox/control/finalization-response.js +8 -2
  26. package/dist/lib/sandbox/control/protocol.js +5 -64
  27. package/dist/lib/sandbox/control/server.js +29 -206
  28. package/dist/lib/sandbox/control/state.js +1 -7
  29. package/dist/lib/sandbox/control/task-view.js +2 -1
  30. package/dist/lib/task/artifact-command.js +6 -6
  31. package/dist/lib/task/codex-orchestration.js +10 -208
  32. package/dist/lib/task/commit-operation.js +3 -18
  33. package/dist/lib/task/control-authority.js +35 -44
  34. package/dist/lib/task/control-recovery.js +3 -42
  35. package/dist/lib/task/delegation-receipts.js +16 -78
  36. package/dist/lib/task/events.js +89 -237
  37. package/dist/lib/task/finalization-artifacts.js +1 -3
  38. package/dist/lib/task/finalization-state.js +2 -1
  39. package/dist/lib/task/finalization.js +208 -57
  40. package/dist/lib/task/lifecycle-execution.js +2 -11
  41. package/dist/lib/task/lifecycle.js +1 -1
  42. package/dist/lib/task/local-artifact-finalization.js +25 -242
  43. package/dist/lib/task/manual-validation-receipt.js +8 -3
  44. package/dist/lib/task/orchestration.js +12 -24
  45. package/dist/lib/task/review-command.js +0 -1
  46. package/dist/lib/task/review-finalization.js +46 -211
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapter.ts +46 -0
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  51. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  52. package/lib/agent-clients/adapters/codex.ts +10 -0
  53. package/lib/internal/cli-route-inventory.ts +1 -1
  54. package/lib/internal/manual-validation.ts +22 -26
  55. package/lib/internal/platform-comment.ts +38 -0
  56. package/lib/internal/sandbox-control.ts +1 -185
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +2 -2
  59. package/lib/internal/task-operation-registry.ts +8 -4
  60. package/lib/internal/task-review.ts +5 -5
  61. package/lib/internal/task-validate.ts +0 -5
  62. package/lib/platform/comment-safety.ts +8 -0
  63. package/lib/platform/github-provider.ts +11 -2
  64. package/lib/platform/issue-comments.ts +102 -17
  65. package/lib/platform/pr-summary.ts +10 -2
  66. package/lib/platform/provider-contract.ts +2 -0
  67. package/lib/platform/provider-validation.ts +6 -2
  68. package/lib/platform/verification-sync.ts +19 -2
  69. package/lib/sandbox/control/client.ts +5 -68
  70. package/lib/sandbox/control/executor.ts +14 -125
  71. package/lib/sandbox/control/finalization-response.ts +8 -2
  72. package/lib/sandbox/control/protocol.ts +7 -83
  73. package/lib/sandbox/control/server.ts +30 -220
  74. package/lib/sandbox/control/state.ts +1 -9
  75. package/lib/sandbox/control/task-view.ts +2 -1
  76. package/lib/task/artifact-command.ts +6 -7
  77. package/lib/task/codex-orchestration.ts +8 -241
  78. package/lib/task/commit-operation.ts +3 -23
  79. package/lib/task/control-authority.ts +39 -55
  80. package/lib/task/control-recovery.ts +3 -43
  81. package/lib/task/delegation-receipts.ts +14 -81
  82. package/lib/task/events.ts +106 -255
  83. package/lib/task/finalization-artifacts.ts +1 -3
  84. package/lib/task/finalization-state.ts +2 -1
  85. package/lib/task/finalization.ts +221 -65
  86. package/lib/task/lifecycle-execution.ts +10 -21
  87. package/lib/task/lifecycle.ts +2 -2
  88. package/lib/task/local-artifact-finalization.ts +23 -281
  89. package/lib/task/manual-validation-receipt.ts +8 -2
  90. package/lib/task/orchestration.ts +30 -53
  91. package/lib/task/review-command.ts +0 -1
  92. package/lib/task/review-finalization.ts +45 -193
  93. package/package.json +2 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
  95. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
  96. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  97. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  101. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  102. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  105. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  106. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  109. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  110. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  111. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  112. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  113. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  114. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  115. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  116. package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
  117. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
  118. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  119. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  120. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  121. package/dist/lib/sandbox/control/workflow-executor.js +0 -157
  122. package/dist/lib/task/lifecycle-recovery.js +0 -549
  123. package/dist/lib/task/recovery-warning.js +0 -19
  124. package/dist/lib/task/workflow-command.js +0 -18
  125. package/dist/lib/task/workflow-dispatch.js +0 -161
  126. package/lib/sandbox/control/task-workflow.ts +0 -95
  127. package/lib/sandbox/control/workflow-executor.ts +0 -183
  128. package/lib/task/lifecycle-recovery.ts +0 -691
  129. package/lib/task/recovery-warning.ts +0 -21
  130. package/lib/task/workflow-command.ts +0 -19
  131. package/lib/task/workflow-dispatch.ts +0 -171
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
2
4
 
3
5
  import { parseArtifactName } from './artifact-name.ts';
4
- import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.ts';
6
+ import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.ts';
5
7
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
6
8
  import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
7
9
  import { finalizeReviewSummaryContent } from './review-artifacts.ts';
@@ -15,17 +17,6 @@ import type { ManualOverrideCapability } from './guard-override.ts';
15
17
  import { getArtifactSchema } from './artifact-schema.ts';
16
18
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
17
19
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
18
- import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
19
- import {
20
- beginArtifactRecovery,
21
- commitArtifactRecovery,
22
- prepareArtifactRecoveryFinal,
23
- prepareArtifactRecoveryCommit,
24
- reconcileArtifactRecovery,
25
- recoveryContextFromIntent,
26
- stageArtifactCandidate
27
- } from './artifact-recovery.ts';
28
- import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
29
20
 
30
21
  type ReviewFinalizationErrorCode =
31
22
  | ResolveTaskRefErrorCode
@@ -51,7 +42,6 @@ type ReviewFinalizationRequest = {
51
42
  artifact: string;
52
43
  orchestrated?: boolean;
53
44
  dryRun?: boolean;
54
- recoveryId?: string;
55
45
  };
56
46
  type ReviewFinalizationResult = {
57
47
  status: 'planned' | 'applied' | 'no-op' | 'failed';
@@ -64,12 +54,6 @@ type ReviewFinalizationResult = {
64
54
  stageStatus: LedgerStageStatus | null;
65
55
  artifactSha256: string | null;
66
56
  semanticDigest: string | null;
67
- recovery?: Readonly<{
68
- recoveryId: string;
69
- candidatePath: string;
70
- baselineSha256: string;
71
- baselineSemanticDigest: string;
72
- }>;
73
57
  operations: readonly {
74
58
  kind: 'artifact';
75
59
  artifact: string;
@@ -90,7 +74,6 @@ type ReviewFinalizationOptions = {
90
74
  fileSystem?: Partial<ReviewFileSystem>;
91
75
  manualOverride?: ManualOverrideCapability;
92
76
  lockAlreadyHeld?: boolean;
93
- startRecovery?: boolean;
94
77
  };
95
78
 
96
79
  const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
@@ -143,7 +126,7 @@ function preflightFailed(
143
126
  };
144
127
  }
145
128
 
146
- /** Validate and seal a review draft before any finding-ledger write. */
129
+ /** Validate the current review draft before any finding-ledger write. */
147
130
  function preflightReviewSummaryUnlocked(
148
131
  request: ReviewFinalizationRequest,
149
132
  options: ReviewFinalizationOptions = {}
@@ -162,28 +145,13 @@ function preflightReviewSummaryUnlocked(
162
145
 
163
146
  let taskContent: string;
164
147
  let content: string;
165
- let recovery: ArtifactRecoveryContext | undefined;
166
148
  try {
167
149
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
168
- if (request.recoveryId) {
169
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
170
- if (!intent || intent.recoveryOperationId !== request.recoveryId) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
171
- if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state)) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
172
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
173
- content = fs.existsSync(recovery.stagingPath)
174
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
175
- : fs.readFileSync(recovery.formalPath, 'utf8');
176
- } else {
177
- const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
178
- if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
179
- content = fs.readFileSync(validated.artifact.path, 'utf8');
180
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
181
- if (intent?.state === 'preflight-ready') recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
182
- if (intent?.state === 'consumed') return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
183
- }
150
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
151
+ if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
152
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
184
153
  } catch (error) { return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
185
154
 
186
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
187
155
  const execution = validateLifecycleExecution(request.taskRef, {
188
156
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
189
157
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -204,14 +172,6 @@ function preflightReviewSummaryUnlocked(
204
172
  });
205
173
  const qualificationError = qualification.ok ? null : qualification;
206
174
  if (!detail.ok || !structure.ok || qualificationError) {
207
- if (!recovery && !request.dryRun) {
208
- try {
209
- recovery = beginArtifactRecovery({
210
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
211
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
212
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
213
- } catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId); }
214
- }
215
175
  const diagnostics = [
216
176
  ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
217
177
  ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
@@ -219,27 +179,18 @@ function preflightReviewSummaryUnlocked(
219
179
  ].join('; ');
220
180
  return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
221
181
  artifactSha256, semanticDigest,
222
- ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
223
182
  });
224
183
  }
225
- if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
184
+ if (request.dryRun) {
226
185
  return {
227
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
186
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
228
187
  status: 'passed', changed: false, error: null
229
188
  };
230
189
  }
231
- try {
232
- recovery ??= beginArtifactRecovery({
233
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
234
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
235
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
236
- const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
237
- prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
238
- return {
239
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
240
- status: 'passed', changed: false, error: null
241
- };
242
- } catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest }); }
190
+ return {
191
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
192
+ status: 'passed', changed: false, error: null
193
+ };
243
194
  }
244
195
 
245
196
  function preflightReviewSummary(
@@ -265,19 +216,9 @@ function preflightReviewSummary(
265
216
  type ReviewSummaryCandidatePreparation = Readonly<{
266
217
  result: ReviewFinalizationResult;
267
218
  content: string;
268
- recovery?: ArtifactRecoveryContext;
269
219
  lockAlreadyHeld?: boolean;
270
220
  }>;
271
221
 
272
- function recoveryInfo(recovery: ArtifactRecoveryContext): NonNullable<ReviewFinalizationResult['recovery']> {
273
- return {
274
- recoveryId: recovery.recoveryId,
275
- candidatePath: recovery.stagingPath,
276
- baselineSha256: recovery.baselineSha256,
277
- baselineSemanticDigest: recovery.baselineSemanticDigest
278
- };
279
- }
280
-
281
222
  /** Prepare domain validation, summary bytes and provenance before any publication. */
282
223
  function prepareReviewSummaryCandidate(
283
224
  request: ReviewFinalizationRequest,
@@ -306,36 +247,9 @@ function prepareReviewSummaryCandidate(
306
247
  let taskContent: string;
307
248
  try { taskContent = (options.fileSystem?.readFileSync ?? DEFAULT_FILE_SYSTEM.readFileSync)(resolved.taskMdPath); }
308
249
  catch (error) { return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error)); }
309
- let recovery: ArtifactRecoveryContext | undefined;
310
- if (request.recoveryId) {
311
- try {
312
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
313
- if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
314
- if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
315
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
316
- artifactContent = fs.existsSync(recovery.stagingPath)
317
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
318
- : fs.readFileSync(recovery.formalPath, 'utf8');
319
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error)); }
320
- }
321
250
  const artifactSha256 = sha256Content(artifactContent);
322
251
  const semanticDigest = canonicalSemanticDigest(artifactContent);
323
252
  const digests = { artifactSha256, semanticDigest };
324
- let repairIntent;
325
- try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
326
- catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
327
- if (repairIntent?.state === 'consumed') {
328
- if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
329
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
330
- }
331
- }
332
- if (repairIntent?.state === 'passed'
333
- && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
334
- repairIntent = undefined;
335
- }
336
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
337
- return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
338
- }
339
253
  const execution = validateLifecycleExecution(request.taskRef, {
340
254
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
341
255
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -356,59 +270,15 @@ function prepareReviewSummaryCandidate(
356
270
  const schema = getArtifactSchema(spec.family)!;
357
271
  const structure = inspectArtifactContract(artifactContent, schema);
358
272
  if (!structure.ok) {
359
- if (!recovery && !request.dryRun && options.startRecovery) {
360
- try {
361
- recovery = beginArtifactRecovery({
362
- taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
363
- requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
364
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
365
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
366
- }
367
- return {
368
- ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
369
- artifactSha256, semanticDigest: structure.semanticDigest,
370
- ...(recovery ? { recovery: {
371
- recoveryId: recovery.recoveryId,
372
- candidatePath: recovery.stagingPath,
373
- baselineSha256: recovery.baselineSha256,
374
- baselineSemanticDigest: recovery.baselineSemanticDigest
375
- } } : {})
376
- }),
377
- ...(recovery ? { recovery } : {})
378
- };
273
+ return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
274
+ artifactSha256, semanticDigest: structure.semanticDigest
275
+ });
379
276
  }
380
277
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
381
- refreshNumericCounts: Boolean(request.recoveryId && recovery)
278
+ refreshNumericCounts: true
382
279
  });
383
280
  if (!transformed.ok) return reject(transformed.code, transformed.message);
384
281
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
385
- if (!recovery && repairIntent?.state === 'commit-started') {
386
- if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
387
- return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
388
- }
389
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
390
- }
391
- if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
392
- if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
393
- return {
394
- content: transformed.content,
395
- result: {
396
- ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
397
- status: 'no-op',
398
- changed: false,
399
- error: null
400
- },
401
- lockAlreadyHeld: options.lockAlreadyHeld
402
- };
403
- }
404
- if (!recovery && !request.dryRun && options.startRecovery) {
405
- try {
406
- recovery = beginArtifactRecovery({
407
- taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
408
- requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
409
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
410
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests); }
411
- }
412
282
  return {
413
283
  content: transformed.content,
414
284
  result: {
@@ -418,7 +288,6 @@ function prepareReviewSummaryCandidate(
418
288
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
419
289
  error: null
420
290
  },
421
- ...(recovery ? { recovery } : {}),
422
291
  lockAlreadyHeld: options.lockAlreadyHeld
423
292
  };
424
293
  }
@@ -428,51 +297,18 @@ function commitReviewSummaryProvenance(
428
297
  repoRoot: string,
429
298
  options: Readonly<{ afterPublish?: () => void }> = {}
430
299
  ): ReviewFinalizationResult {
431
- if (prepared.result.status === 'failed' || !prepared.recovery) return prepared.result;
300
+ return prepared.result;
301
+ }
302
+
303
+ function replaceCurrentArtifactAtomically(file: string, content: string): void {
304
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
432
305
  try {
433
- const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
434
- if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
435
- let committed;
436
- if (intent.state === 'passed' || intent.state === 'consumed') {
437
- return {
438
- ...prepared.result,
439
- status: 'no-op',
440
- changed: false,
441
- artifactSha256: intent.finalArtifactSha256,
442
- semanticDigest: intent.finalSemanticDigest,
443
- error: null
444
- };
445
- } else if (intent.state === 'commit-started') {
446
- const reconciled = reconcileArtifactRecovery(prepared.recovery, {
447
- lockAlreadyHeld: prepared.lockAlreadyHeld,
448
- afterPublish: options.afterPublish
449
- });
450
- committed = reconciled.intent;
451
- } else if (intent.state === 'awaiting-preflight-recovery') {
452
- const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
453
- prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
454
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
455
- } else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
456
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
306
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
307
+ fs.renameSync(temporary, file);
308
+ } finally {
309
+ try { fs.unlinkSync(temporary); } catch (error) {
310
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
457
311
  }
458
- if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
459
- prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
460
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
461
- } else if (intent.state === 'full-finalizer-ready') {
462
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
463
- }
464
- if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
465
- return {
466
- ...prepared.result,
467
- artifactSha256: committed.finalArtifactSha256,
468
- semanticDigest: committed.finalSemanticDigest,
469
- error: null
470
- };
471
- } catch (error) {
472
- if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
473
- return { ...prepared.result, status: 'failed', error: {
474
- code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
475
- } };
476
312
  }
477
313
  }
478
314
 
@@ -494,14 +330,30 @@ function finalizeReviewSummaryUnlocked(
494
330
  catch (error) { return failed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
495
331
  const prepared = prepareReviewSummaryCandidate(request, content, options);
496
332
  if (request.dryRun) return prepared.result;
497
- return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
333
+ if (prepared.result.status === 'failed' || !prepared.result.changed) return prepared.result;
334
+ try {
335
+ replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
336
+ return {
337
+ ...prepared.result,
338
+ status: 'applied',
339
+ artifactSha256: sha256Content(prepared.content),
340
+ semanticDigest: canonicalSemanticDigest(prepared.content)
341
+ };
342
+ } catch (error) {
343
+ return failed(
344
+ request,
345
+ 'REVIEW_RECOVERY_COMMIT_FAILED',
346
+ `cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`,
347
+ resolved.taskId
348
+ );
349
+ }
498
350
  }
499
351
 
500
352
  function finalizeReviewSummary(
501
353
  request: ReviewFinalizationRequest,
502
354
  options: ReviewFinalizationOptions = {}
503
355
  ): ReviewFinalizationResult {
504
- const effectiveOptions = { ...options, startRecovery: !request.dryRun };
356
+ const effectiveOptions = options;
505
357
  if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
506
358
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
507
359
  if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
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",
@@ -52,7 +52,7 @@
52
52
  "installer"
53
53
  ],
54
54
  "dependencies": {
55
- "@clack/prompts": "1.7.0",
55
+ "@clack/prompts": "1.8.1",
56
56
  "@larksuiteoapi/node-sdk": "^1.68.0",
57
57
  "cross-spawn": "^7.0.6",
58
58
  "fs-native-extensions": "1.5.1",
@@ -5,26 +5,40 @@
5
5
  - The orchestrator only routes and delegates. Stage skills remain the single source of truth for behavior and artifacts.
6
6
  - Every stage and rework round uses a fresh executor. Every review uses a fresh reviewer; follow-up reuse is forbidden.
7
7
  - A reviewer may only write its review artifact and core-generated task metadata. Business-code, HEAD, or index changes invalidate the receipt.
8
- - An active run has one pending delegation. The child must pass the activation barrier before any stage side effect. Missing, late, mismatched, forked, replayed, hook-less, or drifted evidence pauses the run (on the claude-code path, missing or mismatched model/effort evidence, and fork/spawn-mode evidence this host structurally does not provide, follow the recording rules in `.agents/skills/run-task/reference/host-validation.md` instead and are exempt from this; missing or mismatched `parentId`/`childId` still pauses). Cross-root package/build/contract or hook/profile content drift is a deliverable warning; the current receipt's hook/evidence binding remains hard.
8
+ - An active run has one pending delegation. Keep the stage associated with the observed child and report actual start and terminal failures.
9
9
  - The first release ends after one existing safely gated `commit`; it does not create a PR, monitor checks, or complete the task.
10
10
 
11
- ## Recovery
11
+ ## Current Execution Records
12
12
 
13
- `orchestration.json` is the detailed state source. A current run persists complete policy, append-only recovery, build/contract/hook-source/controller provenance, and activation monotonic timestamps. Readers accept only the complete structure emitted by the current writer; unknown fields, missing fields, invalid provenance, and old runs fail closed without rewriting. If the state cannot be recognized, preserve the file, advise rebuilding the sandbox or manually repairing it, do not migrate it, and do not create a follow-up task. Finish or clear every active run before upgrading agent-infra. An expired prepared orphan may be explicitly recovered only with the exact task fingerprint, no consumed authorization, and no matching unconsumed active lifecycle evidence.
13
+ `orchestration.json` records stages, model policy, child identity, and actual outcomes. Records use the current structure; trusted local operators may correct them and rerun validation. Existing task write locks and atomic writes protect updates.
14
14
 
15
- ## Codex Host and Capability
15
+ ## Current Host
16
16
 
17
- - Direct-host accepts only trusted project or managed lifecycle hooks. A task-bound sandbox requires a controlled nested controller and isolated `CODEX_HOME`; only user hooks bound to that controller context are accepted. Ordinary user/plugin hooks are not evidence. The actual hook/profile used by the run is the source of record; content drift is reported as a warning with rebuild guidance.
18
- - The controller starts a nested loop only after control generation, task binding, protocol version, profiles, and hook discovery pass. Build/contract and content drift is reported as a structured warning with rebuild guidance instead of blocking natural evolution. Both bypass flags are restricted to this task-bound launch path.
19
- - Every prepare arms a one-use capability attested by the current loop's real PostToolUse. Atomic consumption binds task/session/build/controller and retains only a redacted tombstone.
17
+ - Use native spawn, wait, and App Server results from the current host. A task-bound sandbox executes ordinary task orchestration locally in the current worktree.
18
+ - Codex controller `open`, `verify`, and `close` remain restricted broker operations. After `open`, complete typed `verify` with the same lease proof before accepting the task, control-generation, and controller-instance binding.
19
+ - Prepare validates the current task, model policy, and host preflight. Start and terminal records associate observed parent/child identity; failure must not be recorded as success.
20
+ - Client-specific preflight, event sources, and stop-evidence validation belong to the client adapter. The common orchestrator calls only the shared capability and contains no client-ID branches.
21
+
22
+ ## Activated Delegation Recovery
23
+
24
+ - Before `begin-or-resume`, the orchestrator calls internal `task-lifecycle <task> recover-started --agent <client> --auto`. This entry is not user-facing and never infers child termination from missing liveness evidence.
25
+ - The client adapter declares recovery support and validates trusted stop evidence for the current pending delegation. Unsupported adapters and runs without an activated pending delegation return `no-op`; uncertain termination fails closed.
26
+ - Recovery consumes stop evidence, saves an aborted receipt while clearing pending, then appends the Activity Log terminal row. If the run save succeeds and the log write fails, the next invocation appends only the missing row.
27
+ - Routing continues only after `no-op` or `applied`. Lost control responses are not reconstructed; the next operator invocation relies on the idempotent persisted state.
28
+
29
+ ## Current-only Cutover and Rollback
30
+
31
+ - Enter a quiet window before cutover: let in-flight orchestration intents return, stop the wrapper/controller, and preserve the worktree, task/run/orchestration state, receipts, registration, and audit. For an unknown result, inspect local `status`/`route` before using existing recover/advance semantics; do not replay an old broker request.
32
+ - Deploy or roll back the CLI, broker, controller adapter, and protocol as one versioned unit. After stopping the old container/broker, rebuild and run `ai sandbox start --recreate <task-ref>` to resume with a new control generation; never reuse an old token, proof, or request.
33
+ - On either the new or rolled-back version, first confirm the same task through `status`/`route`, then require a successful typed controller `open`→`verify`. Keep the quiet window and evidence intact if any check fails; do not mix component versions or rewrite domain facts.
20
34
 
21
35
  ## Model Policy
22
36
 
23
37
  - A new run persists model and reasoning effort for both roles. Explicit policy is atomic across all four role fields; only a fully absent explicit policy may fall back to the current client's `agentClients[].orchestration`.
24
38
  - Route resolves requested model/effort by role, and prepare matches both before snapshotting. Native spawn must not inherit session defaults.
25
- - Native start records host-observed actual model/effort. Each mismatch needs its own fallback reason (the claude-code path may leave this blank and follow the recording rules instead), and requested values must never be fabricated as actual evidence.
39
+ - Native start records host-observed actual model/effort. Fields declared unobservable by the adapter follow the recording rules; each observable mismatch needs its own fallback reason, and requested values must never be fabricated as actual evidence.
26
40
  - Model selection is labeled as a complete catalog, partial catalog, or interactive-only guidance; a local override enum must not be presented as complete.
27
- - claude-code's requested reasoning effort does not yet support per-role dispatch (concurrent tasks would race on a shared file); `delegationEvidence.actualReasoningEffort` is declared `spawn-ack`, meaning it is only recorded when honestly observed in a native spawn lifecycle event (Start/Stop) — it is not a promise of per-role dispatch and does not gate activation.
41
+ - An adapter must declare any policy field it cannot dispatch per role. Values observed in host events are actual evidence only; they do not promise dispatch or gate activation.
28
42
 
29
43
  ## Stable Pause Conditions
30
44
 
@@ -5,26 +5,40 @@
5
5
  - 总控只路由和委派;阶段技能仍是业务规则与产物格式的单一事实源。
6
6
  - 每个阶段和每轮返工都创建 fresh executor;每轮审查创建 fresh reviewer,禁止 follow-up 复用。
7
7
  - reviewer 只能写当前审查产物和核心生成的任务元数据。业务代码、HEAD 或暂存区变化会使 receipt 失效。
8
- - active run 中只有一个 pending delegation。child 必须在任何阶段副作用前通过 activation barrier;缺失、超时、错配、fork、重放、hook 不可用或工作区漂移一律暂停(claude-code 路径下 model/effort 证据的缺失或错配、以及该宿主结构上不提供的 fork/spawn-mode 证据,按 `.agents/skills/run-task/reference/host-validation.md` 的记录规则处理,不在此列;`parentId`/`childId` 的缺失或错配仍然暂停)。跨根 package/build/contract 或 hook/profile 内容漂移只输出可交付 warning;本轮 receipt 的 hook/evidence 绑定仍硬校验。
8
+ - active run 中只有一个 pending delegation;阶段与实际 child 身份保持一致,启动和终态失败如实记录。
9
9
  - 首版成功终点是一次通过既有安全门禁的 `commit`;不创建 PR、不监控 checks、不执行 `complete-task`。
10
10
 
11
- ## 恢复语义
11
+ ## 当前执行记录
12
12
 
13
- `orchestration.json` 是详细状态源。current run 保存完整策略、append-only 恢复历史、build/contract/hook source/controller provenance 与 activation 单调时钟。读取时只接受当前 writer 的完整结构;未知字段、缺失字段、非法 provenance 或旧 run 一律失败关闭且不改写。遇到无法识别的 `orchestration.json`,保留原文件并提示重建 sandbox 或人工修复后重试,不做 schema 迁移,也不创建 follow-up task。升级 agent-infra 前必须完成或清空所有 active run。prepared orphan 只允许在 deadline 已过、task fingerprint 精确不变、无 authorization 消耗且无匹配的未消费 active lifecycle evidence 时显式恢复。
13
+ `orchestration.json` 记录阶段、模型策略、child 身份及实际执行结果。记录必须符合当前结构;本地操作者可直接修正后重新校验。已有任务写锁与原子写入继续保护写操作。
14
14
 
15
- ## Codex 宿主与 capability
15
+ ## 当前宿主
16
16
 
17
- - direct-host 只接受 trusted project 或 managed lifecycle hooks。task-bound sandbox 必须由受控 nested controller 建立隔离 `CODEX_HOME`,仅接受绑定 controller context 的 user hooks;普通 user/plugin hook 不得成为证据源。使用本轮实际加载的 hook/profile,内容漂移只记录 warning 并提示重建 sandbox。
18
- - controller 只有在 control generation、task binding、协议版本、profile 与 hook discovery 全部通过后才能启动 nested loop;build/contract 及内容漂移通过结构化 warning 告知并建议重建 sandbox,不阻塞自然演进。两个 bypass 参数只允许出现在该 task-bound 启动路径。
19
- - 每次 prepare 前必须 arm 一次性 capability,并由同一当前 loop 的真实 PostToolUse attestation;token 原子消费后保留去敏 tombstone,不能重放或跨 task/session/build/controller 使用。
17
+ - 使用当前宿主的原生 spawn、wait 与 App Server 结果;task-bound sandbox 在当前 worktree 本地执行普通任务编排。
18
+ - Codex controller 的 `open`、`verify`、`close` 继续通过受限 broker 执行。`open` 后必须用同一 lease proof 完成 typed `verify`,再接受 task、control generation 和 controller instance 绑定。
19
+ - prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
20
+ - 客户端特有的 preflight、事件来源与停止证据校验由客户端适配器实现;公共总控只调用统一能力。不得在公共流程中新增客户端 ID 分支。
21
+
22
+ ## Activated delegation 自动恢复
23
+
24
+ - 总控在 `begin-or-resume` 前调用内部 `task-lifecycle <task> recover-started --agent <client> --auto`。该入口不公开给用户,也不从缺少存活证据推断 child 已终止。
25
+ - 客户端适配器决定是否支持恢复,并验证当前 pending delegation 的可信停止证据;不支持或没有 activated pending 时返回 `no-op`,无法确认停止时失败关闭。
26
+ - 恢复按固定顺序消费停止证据、保存 aborted receipt 并清除 pending,再写 Activity Log 结束记录。任务状态已保存而日志未写时,下次调用只补日志。
27
+ - 只有 `no-op` 或 `applied` 才能继续 route。控制响应丢失时不重建恢复结果;下一次人为调用依靠幂等状态继续。
28
+
29
+ ## Current-only 切换与回滚
30
+
31
+ - 切换前进入静默窗口:等待在途编排 intent 返回,停止 wrapper/controller,并保留 worktree、task/run/orchestration 状态、receipts、registration 与 audit。结果未知时先用本地 `status`/`route` 判断 domain 是否已提交,再按现有 recover/advance 语义处理;旧 broker request 不重放。
32
+ - CLI、broker、controller adapter 和协议必须作为同一版本单元部署或回退。停止旧 container/broker 后执行 rebuild 与 `ai sandbox start --recreate <task-ref>`,以新 control generation 恢复执行;旧 token、proof 和 request 不得复用。
33
+ - 新版本或回退版本都必须先核对同一任务的 `status`/`route`,再要求 controller typed `open`→`verify` 成功。任一检查失败时继续保持静默并保留证据,不局部混用组件或改写 domain 事实。
20
34
 
21
35
  ## 模型策略
22
36
 
23
37
  - 新 run 必须固化 executor/reviewer 各自的 model 与 reasoning effort;显式策略必须四字段原子完整,完全没有显式字段时才读取当前 client 的 `agentClients[].orchestration`。重入不得静默改写策略。
24
38
  - route 按 role 返回 requested model/effort,prepare 必须在工作区快照前精确匹配两者;原生 spawn 不能继承会话默认值。
25
- - 原生 start 必须记录宿主观察到的 actual model/effort。任一字段与 requested 不同时必须记录独立 fallback reason(claude-code 路径下允许留空,按记录规则处理);requested 值不得补造 actual 证据。
39
+ - 原生 start 必须记录宿主观察到的 actual model/effort。适配器声明无法观察的字段按记录规则处理;任一可观察字段与 requested 不同时必须记录独立 fallback reason,requested 值不得补造 actual 证据。
26
40
  - 模型选择能力必须标记 complete catalog、partial catalog 或 interactive-only guidance;局部 override 枚举不得冒充完整目录。
27
- - claude-code 的 requested reasoning effort 暂不支持按角色下发(跨任务共享文件存在写入竞态);`delegationEvidence.actualReasoningEffort` 声明为 `spawn-ack`,语义为"仅能在宿主原生 spawn 生命周期事件(Start/Stop)里如实观察到时记录,不构成按角色下发的承诺,也不构成放行门禁"。
41
+ - 客户端无法按角色下发的策略字段必须由适配器明确声明;宿主事件中观察到的值只作为实际证据,不构成下发承诺或放行门禁。
28
42
 
29
43
  ## 稳定暂停条件
30
44
 
@@ -1,73 +1,27 @@
1
- # General Rule - Model-Driven Local Artifact Recovery
1
+ # Shared Rule - Current Local Artifact Revalidation
2
2
 
3
- This rule covers the pre-completion gate for `analyze-task`, `plan-task`, and `code-task`, plus the summary finalizers for the three review skills. It handles one canonical artifact in the current task directory only. It does not authorize changes to `task.md`, ledgers, receipts, source code, Git, platform resources, or lifecycle state.
3
+ This rule applies to lifecycle artifact validation. Finalizers read the canonical artifact directly and do not create recovery candidates, generations, baselines, journals, or recovery identities.
4
4
 
5
- ## Pre-completion gate
5
+ ## Current validation
6
6
 
7
- - Run the applicable finalizer before a completed event; pass only that invocation's `artifactSha256` and `semanticDigest` to the event.
8
- - When artifact content is invalid, the finalizer returns a controlled `recovery` with a `recoveryId`, `candidatePath`, and baseline digests. The formal artifact remains unchanged until recovery commit.
9
- - After the mechanical safety gates pass, the model may edit only the returned `candidatePath`. After each byte-changing edit, rerun the complete finalizer with the same task, stage/family, artifact, and `--recovery-id`. Never edit the formal artifact directly and call it a candidate.
10
- - Preflight stable-reads and validates candidate `S`, then seals immutable digest-addressed generation `R`; candidate SHA is an observation rather than an edit guard.
11
- - Preflight publishes active `R` through `preflight-ready → preflight-commit-started → preflight-passed`; the business finalizer publishes `F` through `full-finalizer-ready → commit-started → passed` and reuses `R` when `F=R`.
12
- - Candidate-only is a protocol authorization boundary, not OS isolation: a same-UID process with arbitrary host filesystem write access may tamper with recovery internals. Identity, fingerprint, and state checks detect such anomalies and fail closed, but the protocol does not claim an independent security principal or cross-platform isolation.
7
+ 1. Run the applicable finalizer before a completed event and use its returned `artifactSha256` and `semanticDigest`.
8
+ 2. On failure, read diagnostics, make one minimal repair to the canonical artifact, and rerun with the same task, family/stage, and artifact.
9
+ 3. Every rerun validates structure, qualification, upstream relations, ledger state, and current digests. Stop without publishing completed when there is no progress or repair is unsafe.
13
10
 
14
- ## Recovery continuation context
11
+ ## Editing boundary
15
12
 
16
- Whenever a preflight or finalizer returns `recovery`, bind the complete continuation context from that response for the current execution: `taskRef`, `stage/family`, `artifact`, `recoveryId`, `candidatePath`, baseline digests, and the original entry point. These fields belong to one controlled transaction; do not regenerate, guess, or mix any of them for a later call.
13
+ Agents may edit only the artifact declared by the current skill.
17
14
 
18
- - For a preflight failure, edit only that `candidatePath`, then rerun the same preflight entry point with the same `--recovery-id`. Only the successful response's recovery context may proceed to ledger writes or summary/finalization.
19
- - For a finalizer failure, edit only that `candidatePath`, then rerun that same finalizer entry point with the same `--recovery-id`.
20
- - Stop when the context is lost, the tuple differs, or the formal artifact was edited directly. A fresh invocation without `--recovery-id` cannot replace recovery, and a formal artifact is never a candidate.
15
+ ## Completed events
21
16
 
22
- ## Authorization boundary
17
+ Completed events still validate current digests, rounds, inputs, and external facts.
23
18
 
24
- - The finalizer and recovery core validate identity, state, permissions, stable reads, complete artifact semantics, and fingerprints. They do not infer repairability or expose a text-operation allow-list.
25
- - The model may edit only the one ordinary artifact declared by the current skill. It must not edit `task.md`, ledgers, receipts, source code, other reports, or remote resources.
26
- - `changed=false`, an error code, or a format shape is diagnostic evidence, not edit authorization. Stop on unproven human-decision semantics, concurrency, permission, I/O, identity, provenance, or unknown state.
27
-
28
- ## Non-bypassable mechanical gates
29
-
30
- Before every candidate edit, confirm that:
31
-
32
- 1. the finalizer failed with the same controlled recovery context and nothing formal was committed;
33
- 2. task, family/stage, round, artifact, request/authority, and baseline fingerprints still match;
34
- 3. `candidatePath` is the ordinary file generated by the recovery core and the formal target has not changed externally;
35
- 4. the edit does not change human-decision semantics, detail or ledger identity, and introduces no other side effect.
36
-
37
- If any condition fails, stop without editing or publishing a completed event.
38
-
39
- ## Durable recovery states
40
-
41
- | State | Formal artifact | Allowed action |
42
- | --- | --- | --- |
43
- | `awaiting-preflight-recovery` | baseline `B` | Edit `S`; stable-read, validate, and create `R` |
44
- | `preflight-ready` / `preflight-commit-started` | `B` or `R` | Reconcile/publish only active immutable `R` |
45
- | `preflight-passed` | `R` | Run the business finalizer for `F` |
46
- | `full-finalizer-ready` / `commit-started` | `R` or `F` | Reconcile/publish only pending immutable `F` |
47
- | `passed` | `F` | Completed event validates the final digest and consumes the intent |
48
- | `consumed` | `F` | Cleanup may reclaim the controlled staging and backup |
49
- | `aborted` | verified restored `B` | Preserve diagnostics; never fabricate success |
50
-
51
- The intent is current-only schema version 4. Legacy schemas fail closed; do not add migration, adapters, or dual writes. Candidate, baseline, generation, and intent files live in controlled repo-local directories and use canonical recovery IDs, regular-file checks, stable reads, the task lock, and expected-value CAS. No cross-file atomicity is claimed; unknown combinations remain indeterminate and retain their evidence.
52
-
53
- ## Dynamic convergence loop
54
-
55
- 1. Run the finalizer with the fixed invocation.
56
- 2. On every failure that passes the gates, read the structured diagnostic and `candidatePath`, then make only one minimal, explainable candidate edit.
57
- 3. Confirm byte-level progress and rerun every finalizer check with the same `--recovery-id`; if it still fails with a new diagnostic and fingerprint, return to step 2.
58
- 4. Stop on repeated diagnostics or fingerprints, no byte progress, unknown recovery state, an unsafe repair, or the eight-edit limit for the current invocation. Do not publish a completed event or generate a cross-stage next step.
59
-
60
- ## Shared entry points
61
-
62
- The current entry points are `task-artifact ... inspect|init|finalize-local` and `task-review ... finalize-summary`. Recovery retries use the `recoveryId` returned by the finalizer, for example:
19
+ ## Shared entry point
63
20
 
64
21
  ```text
65
- agent-infra-internal task-artifact {task-id} finalize-local --family code --artifact {code-artifact} --recovery-id {recovery-id}
66
- agent-infra-internal task-review {task-id} finalize-summary --stage code --artifact {review-artifact} --recovery-id {recovery-id}
22
+ agent-infra-internal task-artifact {task-id} finalize-local --family code --artifact {code-artifact}
67
23
  ```
68
24
 
69
- There is no `task-artifact repair`, `replace-line`, `insert-section`, or `repairable` authorization. The structural engine returns diagnostics and digests only; the current skill's gates and the model decide whether and how to edit the candidate.
70
-
71
- ## Lifecycle isolation
25
+ ## Stop conditions
72
26
 
73
- A completed event may legitimately modify `task.md`, but only after finalizer recovery is `passed` and digest, round, request, and authority all match. If authority consumption fails after the event write, retain the formal artifact and use the existing lifecycle recovery compensation; do not replay the completed write or roll back a passed artifact.
27
+ Stop when repair is unsafe or makes no progress.