@fitlab-ai/agent-infra 0.11.1 → 0.11.2

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 (88) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/platform-comment.js +59 -0
  3. package/dist/lib/internal/sandbox-control.js +4 -131
  4. package/dist/lib/internal/task-artifact.js +1 -1
  5. package/dist/lib/internal/task-operation-registry.js +1 -0
  6. package/dist/lib/internal/task-review.js +5 -5
  7. package/dist/lib/internal/task-validate.js +0 -6
  8. package/dist/lib/platform/comment-safety.js +7 -1
  9. package/dist/lib/platform/github-provider.js +13 -3
  10. package/dist/lib/platform/issue-comments.js +120 -13
  11. package/dist/lib/platform/provider-contract.d.ts +2 -0
  12. package/dist/lib/platform/provider-validation.js +7 -2
  13. package/dist/lib/platform/verification-sync.js +19 -2
  14. package/dist/lib/sandbox/control/executor.js +0 -38
  15. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  16. package/dist/lib/sandbox/control/task-view.js +3 -1
  17. package/dist/lib/sandbox/control/workflow-executor.js +28 -43
  18. package/dist/lib/task/artifact-command.js +6 -6
  19. package/dist/lib/task/codex-orchestration.js +10 -208
  20. package/dist/lib/task/commit-operation.js +3 -18
  21. package/dist/lib/task/control-authority.js +1 -8
  22. package/dist/lib/task/delegation-receipts.js +14 -76
  23. package/dist/lib/task/events.js +89 -237
  24. package/dist/lib/task/finalization-state.js +2 -1
  25. package/dist/lib/task/finalization.js +273 -49
  26. package/dist/lib/task/lifecycle-execution.js +2 -11
  27. package/dist/lib/task/local-artifact-finalization.js +25 -242
  28. package/dist/lib/task/orchestration.js +0 -18
  29. package/dist/lib/task/review-command.js +0 -1
  30. package/dist/lib/task/review-finalization.js +46 -211
  31. package/dist/lib/task/workflow-command.js +4 -1
  32. package/dist/lib/task/workflow-dispatch.js +55 -3
  33. package/dist/lifecycle-build-manifest.json +3 -3
  34. package/lib/internal/cli-route-inventory.ts +1 -1
  35. package/lib/internal/platform-comment.ts +38 -0
  36. package/lib/internal/sandbox-control.ts +3 -145
  37. package/lib/internal/task-artifact.ts +1 -1
  38. package/lib/internal/task-operation-registry.ts +1 -0
  39. package/lib/internal/task-review.ts +5 -5
  40. package/lib/internal/task-validate.ts +0 -5
  41. package/lib/platform/comment-safety.ts +8 -0
  42. package/lib/platform/github-provider.ts +11 -2
  43. package/lib/platform/issue-comments.ts +132 -13
  44. package/lib/platform/provider-contract.ts +2 -0
  45. package/lib/platform/provider-validation.ts +6 -2
  46. package/lib/platform/verification-sync.ts +19 -2
  47. package/lib/sandbox/control/executor.ts +0 -44
  48. package/lib/sandbox/control/finalization-response.ts +10 -2
  49. package/lib/sandbox/control/task-view.ts +3 -1
  50. package/lib/sandbox/control/workflow-executor.ts +23 -50
  51. package/lib/task/artifact-command.ts +6 -7
  52. package/lib/task/codex-orchestration.ts +8 -241
  53. package/lib/task/commit-operation.ts +3 -23
  54. package/lib/task/control-authority.ts +1 -8
  55. package/lib/task/delegation-receipts.ts +12 -79
  56. package/lib/task/events.ts +106 -255
  57. package/lib/task/finalization-state.ts +2 -1
  58. package/lib/task/finalization.ts +277 -57
  59. package/lib/task/lifecycle-execution.ts +10 -21
  60. package/lib/task/local-artifact-finalization.ts +23 -281
  61. package/lib/task/orchestration.ts +0 -34
  62. package/lib/task/review-command.ts +0 -1
  63. package/lib/task/review-finalization.ts +45 -193
  64. package/lib/task/workflow-command.ts +4 -1
  65. package/lib/task/workflow-dispatch.ts +46 -3
  66. package/package.json +2 -2
  67. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  68. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  69. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  70. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  71. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  72. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  73. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  74. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  75. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  76. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  79. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  80. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  81. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  82. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  83. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  85. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  86. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  87. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  88. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -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);
@@ -9,10 +9,13 @@ export const TASK_WORKFLOW_COMMANDS = Object.freeze({
9
9
  'ledger-finding-response': ['task-ledger', 'finding-respond'],
10
10
  'ledger-finding-review': ['task-ledger', 'finding-review'],
11
11
  'ledger-finding-upsert': ['task-ledger', 'finding-upsert'],
12
+ 'ledger-stage-status': ['task-ledger', 'stage-status'],
12
13
  'decision-next-id': ['task-ledger', 'decision-next-id'],
13
14
  'decision-upsert': ['task-ledger', 'decision-upsert'],
14
15
  'invalidation-reconcile': ['task-invalidation', 'reconcile'],
15
- 'warning-add': ['task-warning', 'add']
16
+ 'warning-add': ['task-warning', 'add'],
17
+ 'warning-list': ['task-warning', 'list'],
18
+ 'warning-set-status': ['task-warning', 'set-status']
16
19
  } as const);
17
20
  export type TaskWorkflowOperation = keyof typeof TASK_WORKFLOW_COMMANDS;
18
21
  export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS) as TaskWorkflowOperation[]);
@@ -1,6 +1,10 @@
1
+ import fs from 'node:fs';
2
+
1
3
  import { reconcileTaskInvalidation } from './invalidation-command.ts';
2
4
  import { applyLedgerIntent } from './ledger-intents.ts';
3
5
  import type { LedgerIntent } from './ledger-intents.ts';
6
+ import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
7
+ import { resolveTaskRef } from './resolve-ref.ts';
4
8
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
5
9
  import type { TaskWorkflowOperation } from './workflow-command.ts';
6
10
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
@@ -116,7 +120,24 @@ function ledgerIntent(operation: TaskWorkflowOperation, args: readonly string[])
116
120
 
117
121
  function warningIntent(args: readonly string[]): WorkflowWarningIntent | DispatchFailure {
118
122
  const [taskRef, kind] = args;
119
- if (!taskRef || kind !== 'add') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
123
+ if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
124
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
125
+ }
126
+ if (kind === 'list') {
127
+ const values = options(args, { '--status': 'status' });
128
+ if (isFailure(values)) return values;
129
+ return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') as never } : {}) };
130
+ }
131
+ if (kind === 'set-status') {
132
+ const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
133
+ if (isFailure(values)) return values;
134
+ const missing = required(values, ['--id', '--status', '--resolution']);
135
+ if (missing) return missing;
136
+ return {
137
+ kind, taskRef, id: value(values, '--id')!, status: value(values, '--status') as never,
138
+ resolution: value(values, '--resolution')!, ...(values['--dry-run'] === true ? { dryRun: true } : {})
139
+ };
140
+ }
120
141
  const values = options(args, {
121
142
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
122
143
  '--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
@@ -131,6 +152,26 @@ function warningIntent(args: readonly string[]): WorkflowWarningIntent | Dispatc
131
152
  } as WorkflowWarningIntent;
132
153
  }
133
154
 
155
+ function stageStatus(repoRoot: string, args: readonly string[]): WorkflowDispatchResult {
156
+ const [taskRef, kind] = args;
157
+ if (!taskRef || kind !== 'stage-status') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
158
+ const values = options(args, { '--stage': 'stage' });
159
+ if (isFailure(values)) return values;
160
+ const stage = value(values, '--stage');
161
+ if (!stage || !isReviewStage(stage)) return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
162
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
163
+ if (!resolved.ok) return failed(resolved.code, resolved.message);
164
+ try {
165
+ const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
166
+ if (!ledger.present) return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
167
+ const invalid = validateLedgerRows(ledger.rows);
168
+ if (invalid) return failed(invalid.code, invalid.message);
169
+ return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
170
+ } catch (error) {
171
+ return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
172
+ }
173
+ }
174
+
134
175
  /**
135
176
  * The caller has already selected and authorized the execution boundary.
136
177
  * This function deliberately invokes task domains in that process: starting a
@@ -157,10 +198,12 @@ export function dispatchWorkflowCommand(
157
198
  ...(values['--dry-run'] === true ? { dryRun: true } : {})
158
199
  }));
159
200
  }
160
- if (operation === 'warning-add') {
201
+ if (operation === 'ledger-stage-status') return stageStatus(repoRoot, args);
202
+ if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
161
203
  const intent = warningIntent(args);
162
204
  if (isFailure(intent)) return intent;
163
- return withTaskExecutionLock(repoRoot, taskRef, 'task-warning.add', () => applyWorkflowWarningIntent(intent, { repoRoot }));
205
+ if (intent.kind === 'list') return applyWorkflowWarningIntent(intent, { repoRoot });
206
+ return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
164
207
  }
165
208
  const intent = ledgerIntent(operation, args);
166
209
  if (isFailure(intent)) return intent;
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.2",
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,18 +5,18 @@
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
+ ## Codex 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; forward task operations through the existing broker.
18
+ - 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.
19
+ - Local execution does not require capability, controller attestation, or one-use consumption authority. Do not add automatic child discovery, spawn under a new lock protocol, orphan recovery, or a dedicated recovery protocol.
20
20
 
21
21
  ## Model Policy
22
22
 
@@ -5,18 +5,18 @@
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
+ ## Codex 宿主
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 结果;任务操作通过现有 broker 转发。
18
+ - prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
19
+ - 不要求 capability、controller attestation 或一次性消费授权本地实现;不新增 child 自动发现、锁内启动交接、孤立 child 恢复或专用恢复协议。
20
20
 
21
21
  ## 模型策略
22
22