@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,6 +1,8 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
2
4
  import { parseArtifactName } from './artifact-name.js';
3
- import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.js';
5
+ import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.js';
4
6
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
5
7
  import { finalizeReviewSummaryContent } from './review-artifacts.js';
6
8
  import { inspectDecisionDetailDuplicates } from './decision-details.js';
@@ -11,8 +13,6 @@ import { allowsManualOverride } from './guard-override.js';
11
13
  import { getArtifactSchema } from './artifact-schema.js';
12
14
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
13
15
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
14
- import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
15
- import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
16
16
  const STAGES = {
17
17
  analysis: { family: 'review-analysis' },
18
18
  plan: { family: 'review-plan' },
@@ -47,7 +47,7 @@ function preflightFailed(request, code, message, taskId = null, extra = {}) {
47
47
  stageStatus: null
48
48
  };
49
49
  }
50
- /** Validate and seal a review draft before any finding-ledger write. */
50
+ /** Validate the current review draft before any finding-ledger write. */
51
51
  function preflightReviewSummaryUnlocked(request, options = {}) {
52
52
  const stage = request.stage;
53
53
  const spec = STAGES[stage];
@@ -68,37 +68,16 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
68
68
  return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
69
69
  let taskContent;
70
70
  let content;
71
- let recovery;
72
71
  try {
73
72
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
74
- if (request.recoveryId) {
75
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
76
- if (!intent || intent.recoveryOperationId !== request.recoveryId)
77
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
78
- if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
79
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
80
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
81
- content = fs.existsSync(recovery.stagingPath)
82
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
83
- : fs.readFileSync(recovery.formalPath, 'utf8');
84
- }
85
- else {
86
- const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
87
- if (!validated.ok)
88
- return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
89
- content = fs.readFileSync(validated.artifact.path, 'utf8');
90
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
91
- if (intent?.state === 'preflight-ready')
92
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
93
- if (intent?.state === 'consumed')
94
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
95
- }
73
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
74
+ if (!validated.ok)
75
+ return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
76
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
96
77
  }
97
78
  catch (error) {
98
79
  return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
99
80
  }
100
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
101
- return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
102
81
  const execution = validateLifecycleExecution(request.taskRef, {
103
82
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
104
83
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -120,17 +99,6 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
120
99
  });
121
100
  const qualificationError = qualification.ok ? null : qualification;
122
101
  if (!detail.ok || !structure.ok || qualificationError) {
123
- if (!recovery && !request.dryRun) {
124
- try {
125
- recovery = beginArtifactRecovery({
126
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
127
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
128
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
129
- }
130
- catch (error) {
131
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
132
- }
133
- }
134
102
  const diagnostics = [
135
103
  ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
136
104
  ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
@@ -138,30 +106,18 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
138
106
  ].join('; ');
139
107
  return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
140
108
  artifactSha256, semanticDigest,
141
- ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
142
109
  });
143
110
  }
144
- if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
111
+ if (request.dryRun) {
145
112
  return {
146
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
113
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
147
114
  status: 'passed', changed: false, error: null
148
115
  };
149
116
  }
150
- try {
151
- recovery ??= beginArtifactRecovery({
152
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
153
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
154
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
155
- const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
156
- prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
157
- return {
158
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
159
- status: 'passed', changed: false, error: null
160
- };
161
- }
162
- catch (error) {
163
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
164
- }
117
+ return {
118
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
119
+ status: 'passed', changed: false, error: null
120
+ };
165
121
  }
166
122
  function preflightReviewSummary(request, options = {}) {
167
123
  if (request.dryRun || options.lockAlreadyHeld)
@@ -178,14 +134,6 @@ function preflightReviewSummary(request, options = {}) {
178
134
  return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
179
135
  }
180
136
  }
181
- function recoveryInfo(recovery) {
182
- return {
183
- recoveryId: recovery.recoveryId,
184
- candidatePath: recovery.stagingPath,
185
- baselineSha256: recovery.baselineSha256,
186
- baselineSemanticDigest: recovery.baselineSemanticDigest
187
- };
188
- }
189
137
  /** Prepare domain validation, summary bytes and provenance before any publication. */
190
138
  function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
191
139
  let taskId = null;
@@ -219,45 +167,9 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
219
167
  catch (error) {
220
168
  return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error));
221
169
  }
222
- let recovery;
223
- if (request.recoveryId) {
224
- try {
225
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
226
- if (!intent || intent.recoveryOperationId !== request.recoveryId)
227
- return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
228
- if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
229
- return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
230
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
231
- artifactContent = fs.existsSync(recovery.stagingPath)
232
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
233
- : fs.readFileSync(recovery.formalPath, 'utf8');
234
- }
235
- catch (error) {
236
- return reject('REVIEW_PROVENANCE_INVALID', String(error));
237
- }
238
- }
239
170
  const artifactSha256 = sha256Content(artifactContent);
240
171
  const semanticDigest = canonicalSemanticDigest(artifactContent);
241
172
  const digests = { artifactSha256, semanticDigest };
242
- let repairIntent;
243
- try {
244
- repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
245
- }
246
- catch (error) {
247
- return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
248
- }
249
- if (repairIntent?.state === 'consumed') {
250
- if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
251
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
252
- }
253
- }
254
- if (repairIntent?.state === 'passed'
255
- && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
256
- repairIntent = undefined;
257
- }
258
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
259
- return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
260
- }
261
173
  const execution = validateLifecycleExecution(request.taskRef, {
262
174
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
263
175
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -284,67 +196,16 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
284
196
  const schema = getArtifactSchema(spec.family);
285
197
  const structure = inspectArtifactContract(artifactContent, schema);
286
198
  if (!structure.ok) {
287
- if (!recovery && !request.dryRun && options.startRecovery) {
288
- try {
289
- recovery = beginArtifactRecovery({
290
- taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
291
- requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
292
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
293
- }
294
- catch (error) {
295
- return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
296
- }
297
- }
298
- return {
299
- ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
300
- artifactSha256, semanticDigest: structure.semanticDigest,
301
- ...(recovery ? { recovery: {
302
- recoveryId: recovery.recoveryId,
303
- candidatePath: recovery.stagingPath,
304
- baselineSha256: recovery.baselineSha256,
305
- baselineSemanticDigest: recovery.baselineSemanticDigest
306
- } } : {})
307
- }),
308
- ...(recovery ? { recovery } : {})
309
- };
199
+ return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
200
+ artifactSha256, semanticDigest: structure.semanticDigest
201
+ });
310
202
  }
311
203
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
312
- refreshNumericCounts: Boolean(request.recoveryId && recovery)
204
+ refreshNumericCounts: true
313
205
  });
314
206
  if (!transformed.ok)
315
207
  return reject(transformed.code, transformed.message);
316
208
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
317
- if (!recovery && repairIntent?.state === 'commit-started') {
318
- if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
319
- return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
320
- }
321
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
322
- }
323
- if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
324
- if (transformed.changed)
325
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
326
- return {
327
- content: transformed.content,
328
- result: {
329
- ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
330
- status: 'no-op',
331
- changed: false,
332
- error: null
333
- },
334
- lockAlreadyHeld: options.lockAlreadyHeld
335
- };
336
- }
337
- if (!recovery && !request.dryRun && options.startRecovery) {
338
- try {
339
- recovery = beginArtifactRecovery({
340
- taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
341
- requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
342
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
343
- }
344
- catch (error) {
345
- return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests);
346
- }
347
- }
348
209
  return {
349
210
  content: transformed.content,
350
211
  result: {
@@ -354,65 +215,26 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
354
215
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
355
216
  error: null
356
217
  },
357
- ...(recovery ? { recovery } : {}),
358
218
  lockAlreadyHeld: options.lockAlreadyHeld
359
219
  };
360
220
  }
361
221
  function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
362
- if (prepared.result.status === 'failed' || !prepared.recovery)
363
- return prepared.result;
222
+ return prepared.result;
223
+ }
224
+ function replaceCurrentArtifactAtomically(file, content) {
225
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
364
226
  try {
365
- const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
366
- if (!intent)
367
- throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
368
- let committed;
369
- if (intent.state === 'passed' || intent.state === 'consumed') {
370
- return {
371
- ...prepared.result,
372
- status: 'no-op',
373
- changed: false,
374
- artifactSha256: intent.finalArtifactSha256,
375
- semanticDigest: intent.finalSemanticDigest,
376
- error: null
377
- };
378
- }
379
- else if (intent.state === 'commit-started') {
380
- const reconciled = reconcileArtifactRecovery(prepared.recovery, {
381
- lockAlreadyHeld: prepared.lockAlreadyHeld,
382
- afterPublish: options.afterPublish
383
- });
384
- committed = reconciled.intent;
385
- }
386
- else if (intent.state === 'awaiting-preflight-recovery') {
387
- const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
388
- prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
389
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
390
- }
391
- else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
392
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
393
- }
394
- if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
395
- prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
396
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
227
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
228
+ fs.renameSync(temporary, file);
229
+ }
230
+ finally {
231
+ try {
232
+ fs.unlinkSync(temporary);
397
233
  }
398
- else if (intent.state === 'full-finalizer-ready') {
399
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
234
+ catch (error) {
235
+ if (error.code !== 'ENOENT')
236
+ throw error;
400
237
  }
401
- if (!committed || committed.state !== 'passed')
402
- throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
403
- return {
404
- ...prepared.result,
405
- artifactSha256: committed.finalArtifactSha256,
406
- semanticDigest: committed.finalSemanticDigest,
407
- error: null
408
- };
409
- }
410
- catch (error) {
411
- if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
412
- throw error;
413
- return { ...prepared.result, status: 'failed', error: {
414
- code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
415
- } };
416
238
  }
417
239
  }
418
240
  function finalizeReviewSummaryUnlocked(request, options = {}) {
@@ -437,10 +259,23 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
437
259
  const prepared = prepareReviewSummaryCandidate(request, content, options);
438
260
  if (request.dryRun)
439
261
  return prepared.result;
440
- return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
262
+ if (prepared.result.status === 'failed' || !prepared.result.changed)
263
+ return prepared.result;
264
+ try {
265
+ replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
266
+ return {
267
+ ...prepared.result,
268
+ status: 'applied',
269
+ artifactSha256: sha256Content(prepared.content),
270
+ semanticDigest: canonicalSemanticDigest(prepared.content)
271
+ };
272
+ }
273
+ catch (error) {
274
+ return failed(request, 'REVIEW_RECOVERY_COMMIT_FAILED', `cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`, resolved.taskId);
275
+ }
441
276
  }
442
277
  function finalizeReviewSummary(request, options = {}) {
443
- const effectiveOptions = { ...options, startRecovery: !request.dryRun };
278
+ const effectiveOptions = options;
444
279
  if (request.dryRun || options.lockAlreadyHeld)
445
280
  return finalizeReviewSummaryUnlocked(request, effectiveOptions);
446
281
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
@@ -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
  });
17
20
  export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS));
18
21
  //# sourceMappingURL=workflow-command.js.map
@@ -1,5 +1,8 @@
1
+ import fs from 'node:fs';
1
2
  import { reconcileTaskInvalidation } from './invalidation-command.js';
2
3
  import { applyLedgerIntent } from './ledger-intents.js';
4
+ import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
5
+ import { resolveTaskRef } from './resolve-ref.js';
3
6
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
4
7
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
5
8
  function failed(code, message) {
@@ -102,8 +105,27 @@ function ledgerIntent(operation, args) {
102
105
  }
103
106
  function warningIntent(args) {
104
107
  const [taskRef, kind] = args;
105
- if (!taskRef || kind !== 'add')
108
+ if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
106
109
  return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
110
+ }
111
+ if (kind === 'list') {
112
+ const values = options(args, { '--status': 'status' });
113
+ if (isFailure(values))
114
+ return values;
115
+ return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') } : {}) };
116
+ }
117
+ if (kind === 'set-status') {
118
+ const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
119
+ if (isFailure(values))
120
+ return values;
121
+ const missing = required(values, ['--id', '--status', '--resolution']);
122
+ if (missing)
123
+ return missing;
124
+ return {
125
+ kind, taskRef, id: value(values, '--id'), status: value(values, '--status'),
126
+ resolution: value(values, '--resolution'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
127
+ };
128
+ }
107
129
  const values = options(args, {
108
130
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
109
131
  '--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
@@ -119,6 +141,32 @@ function warningIntent(args) {
119
141
  action: value(values, '--action'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
120
142
  };
121
143
  }
144
+ function stageStatus(repoRoot, args) {
145
+ const [taskRef, kind] = args;
146
+ if (!taskRef || kind !== 'stage-status')
147
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
148
+ const values = options(args, { '--stage': 'stage' });
149
+ if (isFailure(values))
150
+ return values;
151
+ const stage = value(values, '--stage');
152
+ if (!stage || !isReviewStage(stage))
153
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
154
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
155
+ if (!resolved.ok)
156
+ return failed(resolved.code, resolved.message);
157
+ try {
158
+ const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
159
+ if (!ledger.present)
160
+ return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
161
+ const invalid = validateLedgerRows(ledger.rows);
162
+ if (invalid)
163
+ return failed(invalid.code, invalid.message);
164
+ return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
165
+ }
166
+ catch (error) {
167
+ return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
168
+ }
169
+ }
122
170
  /**
123
171
  * The caller has already selected and authorized the execution boundary.
124
172
  * This function deliberately invokes task domains in that process: starting a
@@ -143,11 +191,15 @@ export function dispatchWorkflowCommand(repoRoot, operation, args) {
143
191
  ...(values['--dry-run'] === true ? { dryRun: true } : {})
144
192
  }));
145
193
  }
146
- if (operation === 'warning-add') {
194
+ if (operation === 'ledger-stage-status')
195
+ return stageStatus(repoRoot, args);
196
+ if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
147
197
  const intent = warningIntent(args);
148
198
  if (isFailure(intent))
149
199
  return intent;
150
- return withTaskExecutionLock(repoRoot, taskRef, 'task-warning.add', () => applyWorkflowWarningIntent(intent, { repoRoot }));
200
+ if (intent.kind === 'list')
201
+ return applyWorkflowWarningIntent(intent, { repoRoot });
202
+ return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
151
203
  }
152
204
  const intent = ledgerIntent(operation, args);
153
205
  if (isFailure(intent))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.1",
4
- "sourceInputHash": "0d66fb32bbdc0982d38a04b86b0278768bb6091eb945ebd90a432f9bae363712",
5
- "internalExecutableBuildHash": "4b1edd20ed87d3d742e5e7975fd49b1ac1fb0ff14f837d52ee9ba3ab11318365"
3
+ "packageVersion": "0.11.2",
4
+ "sourceInputHash": "0a1ffa8b90a0d157f9b2aabea6a43f946d68a622bde337befe71059f63b3812b",
5
+ "internalExecutableBuildHash": "b6742e1b5d1b997bf7e21e644bfde2bafaa608f7ede7ca308ff61c2cd19da831"
6
6
  }
@@ -46,7 +46,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
46
46
  'platform-security': ['read', 'dismiss'],
47
47
  'platform-metadata': ['init-labels', 'init-milestones'],
48
48
  'platform-context': ['resolve'],
49
- 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
49
+ 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'stage-summary', 'sync'],
50
50
  'platform-issue': ['inspect', 'create', 'bind', 'sync'],
51
51
  'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
52
52
  'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
3
4
 
4
5
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
5
6
  import {
@@ -8,15 +9,19 @@ import {
8
9
  syncPlatformComment
9
10
  } from '../platform/issue-comments.ts';
10
11
  import type { CommentKind } from '../platform/issue-comments.ts';
12
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
13
+ import { platformResult } from '../platform/types.ts';
11
14
  import type { PlatformResult } from '../platform/types.ts';
12
15
  import { recoverTaskFromComments } from '../platform/task-recovery.ts';
13
16
  import { backfillCompletionComments } from '../platform/completion-backfill.ts';
14
17
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
18
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
15
19
 
16
20
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
17
21
  agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
18
22
  agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
19
23
  agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
24
+ agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
20
25
  agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
21
26
  `;
22
27
 
@@ -64,6 +69,7 @@ async function platformComment(args: string[] = []): Promise<void> {
64
69
  internalHandlerRoute('platform-comment', 'recover', operation),
65
70
  internalHandlerRoute('platform-comment', 'owner', operation),
66
71
  internalHandlerRoute('platform-comment', 'backfill', operation),
72
+ internalHandlerRoute('platform-comment', 'stage-summary', operation),
67
73
  internalHandlerRoute('platform-comment', 'sync', operation)
68
74
  ].some(Boolean)) { fail('a valid operation is required'); return; }
69
75
  const hasTaskRef = operation !== 'list' && operation !== 'recover';
@@ -144,6 +150,38 @@ async function platformComment(args: string[] = []): Promise<void> {
144
150
  finish(await backfillCompletionComments(taskRef!, { cwd, agent: normalizedAgent }));
145
151
  return;
146
152
  }
153
+ if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
154
+ const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
155
+ if (unexpected) { fail(`stage-summary does not accept '--${unexpected}'`); return; }
156
+ if (typeof parsed.values.bodyFile !== 'string') { fail('stage-summary requires --body-file'); return; }
157
+ let body: string;
158
+ try { body = readBodyFile(parsed.values.bodyFile, cwd); }
159
+ catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
160
+ const canonical = canonicalizeSummaryBody(body);
161
+ if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
162
+ body = canonical.value;
163
+ const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
164
+ if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
165
+ const target = path.join(resolved.taskDir, '.delivery-summary.json');
166
+ const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
167
+ const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
168
+ const next = `${JSON.stringify(value)}\n`;
169
+ if (previous === next) {
170
+ finish(platformResult('no-op'));
171
+ return;
172
+ }
173
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
174
+ try {
175
+ fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
176
+ fs.renameSync(temporary, target);
177
+ } catch (error) {
178
+ try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
179
+ fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
180
+ return;
181
+ }
182
+ finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
183
+ return;
184
+ }
147
185
  if (!internalHandlerRoute('platform-comment', 'sync', operation)) { fail('operation is not registered'); return; }
148
186
  const kind = parsed.values.kind;
149
187
  const agent = parsed.values.agent;