@fitlab-ai/agent-infra 0.10.0 → 0.11.0

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 (95) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/platform-comment.js +60 -2
  7. package/dist/lib/internal/task-artifact.js +1 -1
  8. package/dist/lib/internal/task-finalization.js +2 -2
  9. package/dist/lib/internal/task-lifecycle.js +2 -2
  10. package/dist/lib/internal/task-operation-registry.js +22 -12
  11. package/dist/lib/internal/task-orchestration.js +2 -2
  12. package/dist/lib/internal/task-review.js +10 -5
  13. package/dist/lib/platform/activity-recovery.js +159 -0
  14. package/dist/lib/platform/issue-comments.js +84 -32
  15. package/dist/lib/platform/task-comment-projection.js +70 -0
  16. package/dist/lib/platform/task-recovery.js +55 -0
  17. package/dist/lib/platform/verification-sync.js +43 -9
  18. package/dist/lib/sandbox/control/executor.js +4 -2
  19. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  20. package/dist/lib/sandbox/control/workflow-executor.js +83 -21
  21. package/dist/lib/server/process-state.js +17 -5
  22. package/dist/lib/task/artifact-command.js +6 -3
  23. package/dist/lib/task/artifact-operations.js +1 -1
  24. package/dist/lib/task/artifact-recovery.js +114 -68
  25. package/dist/lib/task/artifact-repair-intent.js +25 -8
  26. package/dist/lib/task/control-authority.js +1 -1
  27. package/dist/lib/task/events.js +6 -6
  28. package/dist/lib/task/intermediate-cleanup.js +28 -3
  29. package/dist/lib/task/local-artifact-finalization.js +78 -14
  30. package/dist/lib/task/review-command.js +2 -2
  31. package/dist/lib/task/review-finalization.js +203 -10
  32. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  33. package/dist/lib/task/workflow-command.js +2 -0
  34. package/dist/lib/task/workflow-dispatch.js +161 -0
  35. package/dist/lifecycle-build-manifest.json +3 -3
  36. package/lib/internal/cli-route-inventory.ts +3 -4
  37. package/lib/internal/platform-comment.ts +53 -2
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +1 -1
  40. package/lib/internal/task-lifecycle.ts +1 -1
  41. package/lib/internal/task-operation-registry.ts +22 -12
  42. package/lib/internal/task-orchestration.ts +1 -1
  43. package/lib/internal/task-review.ts +9 -5
  44. package/lib/platform/activity-recovery.ts +179 -0
  45. package/lib/platform/issue-comments.ts +98 -31
  46. package/lib/platform/task-comment-projection.ts +79 -0
  47. package/lib/platform/task-recovery.ts +54 -0
  48. package/lib/platform/verification-sync.ts +46 -13
  49. package/lib/sandbox/control/executor.ts +4 -2
  50. package/lib/sandbox/control/protocol.ts +1 -1
  51. package/lib/sandbox/control/task-workflow.ts +1 -1
  52. package/lib/sandbox/control/workflow-executor.ts +102 -21
  53. package/lib/server/process-state.ts +26 -5
  54. package/lib/task/artifact-command.ts +6 -4
  55. package/lib/task/artifact-operations.ts +1 -1
  56. package/lib/task/artifact-recovery.ts +120 -67
  57. package/lib/task/artifact-repair-intent.ts +38 -11
  58. package/lib/task/control-authority.ts +1 -1
  59. package/lib/task/events.ts +6 -6
  60. package/lib/task/intermediate-cleanup.ts +18 -2
  61. package/lib/task/local-artifact-finalization.ts +85 -11
  62. package/lib/task/review-command.ts +1 -1
  63. package/lib/task/review-finalization.ts +203 -8
  64. package/lib/{host-control → task}/secure-fs.ts +10 -32
  65. package/lib/task/workflow-command.ts +2 -0
  66. package/lib/task/workflow-dispatch.ts +171 -0
  67. package/package.json +1 -1
  68. package/templates/.agents/rules/issue-sync.en.md +9 -1
  69. package/templates/.agents/rules/issue-sync.zh-CN.md +10 -3
  70. package/templates/.agents/rules/local-artifact-repair.en.md +7 -6
  71. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +10 -8
  72. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  73. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  74. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  75. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +9 -4
  76. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  77. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  78. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  79. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  80. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  81. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  82. package/dist/lib/host-control/audit.js +0 -19
  83. package/dist/lib/host-control/client.js +0 -142
  84. package/dist/lib/host-control/command.js +0 -49
  85. package/dist/lib/host-control/path.js +0 -160
  86. package/dist/lib/host-control/server.js +0 -159
  87. package/dist/lib/host-control/service.js +0 -112
  88. package/dist/lib/internal/host-control.js +0 -28
  89. package/lib/host-control/audit.ts +0 -22
  90. package/lib/host-control/client.ts +0 -172
  91. package/lib/host-control/command.ts +0 -60
  92. package/lib/host-control/path.ts +0 -167
  93. package/lib/host-control/server.ts +0 -180
  94. package/lib/host-control/service.ts +0 -133
  95. package/lib/internal/host-control.ts +0 -28
@@ -10,8 +10,9 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
10
10
  import { allowsManualOverride } from './guard-override.js';
11
11
  import { getArtifactSchema } from './artifact-schema.js';
12
12
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
13
+ import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
13
14
  import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
14
- import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryCommit, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
15
+ import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
15
16
  const STAGES = {
16
17
  analysis: { family: 'review-analysis' },
17
18
  plan: { family: 'review-plan' },
@@ -37,6 +38,162 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
37
38
  ...extra
38
39
  };
39
40
  }
41
+ function preflightFailed(request, code, message, taskId = null, extra = {}) {
42
+ const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
43
+ return {
44
+ ...failed(request, code, message, taskId, null, finalizationExtra),
45
+ status: 'failed',
46
+ intent: 'preflight',
47
+ stageStatus: null
48
+ };
49
+ }
50
+ /** Validate and seal a review draft before any finding-ledger write. */
51
+ function preflightReviewSummaryUnlocked(request, options = {}) {
52
+ const stage = request.stage;
53
+ const spec = STAGES[stage];
54
+ if (!spec)
55
+ return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
56
+ if (!request.taskRef || !request.artifact)
57
+ return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
58
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
59
+ if (!resolved.ok)
60
+ return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
61
+ if (resolved.state !== 'active')
62
+ return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
63
+ const parsed = parseArtifactName(request.artifact);
64
+ if (!parsed || parsed.family !== spec.family)
65
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
66
+ const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
67
+ if (publicationError)
68
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
69
+ let taskContent;
70
+ let content;
71
+ let recovery;
72
+ let existingIntent;
73
+ try {
74
+ taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
75
+ if (request.recoveryId) {
76
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
77
+ if (!intent || intent.recoveryOperationId !== request.recoveryId)
78
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
79
+ if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
80
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
81
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
82
+ content = fs.existsSync(recovery.stagingPath)
83
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
84
+ : fs.readFileSync(recovery.formalPath, 'utf8');
85
+ }
86
+ else {
87
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
88
+ if (!validated.ok)
89
+ return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
90
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
91
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
92
+ existingIntent = intent;
93
+ if (intent?.state === 'preflight-ready')
94
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
95
+ if (intent?.state === 'consumed')
96
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
97
+ }
98
+ }
99
+ catch (error) {
100
+ return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
101
+ }
102
+ if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
103
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
104
+ const execution = validateLifecycleExecution(request.taskRef, {
105
+ mode: request.orchestrated ? 'orchestrated' : 'standalone',
106
+ identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
107
+ dryRun: request.dryRun
108
+ }, { repoRoot: options.repoRoot });
109
+ if (!execution.ok)
110
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
111
+ const artifactSha256 = sha256Content(content);
112
+ const semanticDigest = canonicalSemanticDigest(content);
113
+ const detail = inspectDecisionDetailDuplicates(content);
114
+ const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
115
+ const expected = expectedQualificationRelations(taskContent, spec.family);
116
+ if (!expected.ok)
117
+ return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
118
+ const qualification = validateQualificationAudit(taskContent, content, {
119
+ family: spec.family,
120
+ artifact: request.artifact,
121
+ expectedUpstreamRelations: expected.relations
122
+ });
123
+ const qualificationError = qualification.ok ? null : qualification;
124
+ if (!detail.ok || !structure.ok || qualificationError) {
125
+ if (!recovery && !request.dryRun) {
126
+ try {
127
+ recovery = beginArtifactRecovery({
128
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
129
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
130
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
131
+ }
132
+ catch (error) {
133
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
134
+ }
135
+ }
136
+ const diagnostics = [
137
+ ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
138
+ ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
139
+ ...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
140
+ ].join('; ');
141
+ return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
142
+ artifactSha256, semanticDigest,
143
+ ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
144
+ });
145
+ }
146
+ if (!recovery && existingIntent?.state === 'passed') {
147
+ return {
148
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
149
+ status: 'passed', changed: false, error: null
150
+ };
151
+ }
152
+ if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
153
+ return {
154
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
155
+ status: 'passed', changed: false, error: null
156
+ };
157
+ }
158
+ try {
159
+ recovery ??= beginArtifactRecovery({
160
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
161
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
162
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
163
+ const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
164
+ prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
165
+ return {
166
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
167
+ status: 'passed', changed: false, error: null
168
+ };
169
+ }
170
+ catch (error) {
171
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
172
+ }
173
+ }
174
+ function preflightReviewSummary(request, options = {}) {
175
+ if (request.dryRun || options.lockAlreadyHeld)
176
+ return preflightReviewSummaryUnlocked(request, options);
177
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
178
+ if (!resolved.ok)
179
+ return preflightReviewSummaryUnlocked(request, options);
180
+ try {
181
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.preflight', () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true }));
182
+ }
183
+ catch (error) {
184
+ if (!(error instanceof TaskExecutionLockError))
185
+ throw error;
186
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
187
+ }
188
+ }
189
+ function recoveryInfo(recovery) {
190
+ return {
191
+ recoveryId: recovery.recoveryId,
192
+ candidatePath: recovery.stagingPath,
193
+ baselineSha256: recovery.baselineSha256,
194
+ baselineSemanticDigest: recovery.baselineSemanticDigest
195
+ };
196
+ }
40
197
  /** Prepare domain validation, summary bytes and provenance before any publication. */
41
198
  function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
42
199
  let taskId = null;
@@ -76,7 +233,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
76
233
  const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
77
234
  if (!intent || intent.recoveryOperationId !== request.recoveryId)
78
235
  return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
79
- if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state))
236
+ if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
80
237
  return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
81
238
  recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
82
239
  artifactContent = fs.existsSync(recovery.stagingPath)
@@ -97,11 +254,15 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
97
254
  catch (error) {
98
255
  return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
99
256
  }
100
- if (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed') {
257
+ if (repairIntent?.state === 'consumed') {
101
258
  if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
102
259
  return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
103
260
  }
104
261
  }
262
+ if (repairIntent?.state === 'passed'
263
+ && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
264
+ repairIntent = undefined;
265
+ }
105
266
  if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
106
267
  return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
107
268
  }
@@ -159,6 +320,12 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
159
320
  if (!transformed.ok)
160
321
  return reject(transformed.code, transformed.message);
161
322
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
323
+ if (!recovery && repairIntent?.state === 'commit-started') {
324
+ if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
325
+ return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
326
+ }
327
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
328
+ }
162
329
  if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
163
330
  if (transformed.changed)
164
331
  return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
@@ -197,7 +364,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
197
364
  lockAlreadyHeld: options.lockAlreadyHeld
198
365
  };
199
366
  }
200
- function commitReviewSummaryProvenance(prepared, repoRoot) {
367
+ function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
201
368
  if (prepared.result.status === 'failed' || !prepared.recovery)
202
369
  return prepared.result;
203
370
  try {
@@ -205,16 +372,40 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
205
372
  if (!intent)
206
373
  throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
207
374
  let committed;
208
- if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
209
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
375
+ if (intent.state === 'passed' || intent.state === 'consumed') {
376
+ return {
377
+ ...prepared.result,
378
+ status: 'no-op',
379
+ changed: false,
380
+ artifactSha256: intent.finalArtifactSha256,
381
+ semanticDigest: intent.finalSemanticDigest,
382
+ error: null
383
+ };
210
384
  }
211
- else {
385
+ else if (intent.state === 'commit-started') {
386
+ const reconciled = reconcileArtifactRecovery(prepared.recovery, {
387
+ lockAlreadyHeld: prepared.lockAlreadyHeld,
388
+ afterPublish: options.afterPublish
389
+ });
390
+ committed = reconciled.intent;
391
+ }
392
+ else if (intent.state === 'awaiting-preflight-recovery') {
212
393
  const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
213
394
  prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
214
395
  committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
215
396
  }
216
- if (committed.state !== 'passed')
217
- throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
397
+ else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
398
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
399
+ }
400
+ if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
401
+ prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
402
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
403
+ }
404
+ else if (intent.state === 'full-finalizer-ready') {
405
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
406
+ }
407
+ if (!committed || committed.state !== 'passed')
408
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
218
409
  return {
219
410
  ...prepared.result,
220
411
  artifactSha256: committed.finalArtifactSha256,
@@ -223,6 +414,8 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
223
414
  };
224
415
  }
225
416
  catch (error) {
417
+ if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
418
+ throw error;
226
419
  return { ...prepared.result, status: 'failed', error: {
227
420
  code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
228
421
  } };
@@ -268,5 +461,5 @@ function finalizeReviewSummary(request, options = {}) {
268
461
  return failed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
269
462
  }
270
463
  }
271
- export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
464
+ export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
272
465
  //# sourceMappingURL=review-finalization.js.map
@@ -1,6 +1,6 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
4
  const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
5
5
  const DIRECTORY = fs.constants.O_DIRECTORY ?? 0;
6
6
  export class SecureFileError extends Error {
@@ -15,11 +15,8 @@ function conflict(message) {
15
15
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', message);
16
16
  }
17
17
  function sameStat(left, right) {
18
- return left.dev === right.dev
19
- && left.ino === right.ino
20
- && left.size === right.size
21
- && left.mtimeNs === right.mtimeNs
22
- && left.ctimeNs === right.ctimeNs;
18
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size
19
+ && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
23
20
  }
24
21
  function assertNoSymlinkAncestors(candidate) {
25
22
  let current = path.dirname(candidate);
@@ -36,15 +33,12 @@ function assertNoSymlinkAncestors(candidate) {
36
33
  export async function readStableFile(candidate, options) {
37
34
  return readStableFileSync(candidate, options);
38
35
  }
39
- /** Shared bounded reader for synchronous domain commands and async workflow callers. */
40
36
  export function readStableFileSync(candidate, options) {
41
- if (!path.isAbsolute(candidate)) {
37
+ if (!path.isAbsolute(candidate))
42
38
  conflict('candidate path must be absolute and terminal');
43
- }
44
39
  assertNoSymlinkAncestors(candidate);
45
40
  let descriptor;
46
41
  try {
47
- // O_NONBLOCK lets fstat reject a FIFO even when it has no writer.
48
42
  descriptor = fs.openSync(candidate, fs.constants.O_RDONLY | NO_FOLLOW | (fs.constants.O_NONBLOCK ?? 0));
49
43
  const before = fs.fstatSync(descriptor, { bigint: true });
50
44
  if (!before.isFile())
@@ -63,18 +57,16 @@ export function readStableFileSync(candidate, options) {
63
57
  if (!sameStat(before, after))
64
58
  conflict('candidate identity or metadata changed during read');
65
59
  const sha256 = createHash('sha256').update(bytes).digest('hex');
66
- if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256) {
60
+ if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256)
67
61
  conflict('candidate digest does not match the expected digest');
68
- }
69
62
  return { bytes, sha256, stat: after };
70
63
  }
71
64
  catch (error) {
72
65
  if (error instanceof SecureFileError)
73
66
  throw error;
74
67
  const code = error.code;
75
- if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL') {
68
+ if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL')
76
69
  conflict('candidate terminal is not a stable non-symlink file');
77
- }
78
70
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', error instanceof Error ? error.message : String(error));
79
71
  }
80
72
  finally {
@@ -93,9 +85,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
93
85
  catch {
94
86
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is missing');
95
87
  }
96
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
88
+ if (!stat.isDirectory() || stat.isSymbolicLink())
97
89
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is not a real directory');
98
- }
99
90
  const next = path.dirname(current);
100
91
  if (next === current)
101
92
  break;
@@ -103,9 +94,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
103
94
  }
104
95
  try {
105
96
  const existing = await fs.promises.lstat(target);
106
- if (existing.isSymbolicLink() || !existing.isFile()) {
97
+ if (existing.isSymbolicLink() || !existing.isFile())
107
98
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative target is not a regular file');
108
- }
109
99
  }
110
100
  catch (error) {
111
101
  if (error.code !== 'ENOENT')
@@ -120,7 +110,6 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
120
110
  await handle.chmod(mode);
121
111
  await handle.close();
122
112
  handle = undefined;
123
- // Check after all asynchronous preparation; keep the final check/rename adjacent.
124
113
  if (expectedSha256 !== undefined)
125
114
  readStableFileSync(target, { maxBytes: 1024 * 1024, expectedSha256 });
126
115
  fs.renameSync(temporary, target);
@@ -1,7 +1,9 @@
1
1
  export const TASK_WORKFLOW_COMMANDS = Object.freeze({
2
2
  'artifact-inspect': ['task-artifact', 'inspect'],
3
3
  'artifact-init': ['task-artifact', 'init'],
4
+ 'artifact-preflight': ['task-artifact', 'preflight'],
4
5
  'artifact-finalize-local': ['task-artifact', 'finalize-local'],
6
+ 'review-preflight': ['task-review', 'preflight'],
5
7
  'review-finalize-summary': ['task-review', 'finalize-summary'],
6
8
  event: ['task-event', null],
7
9
  'ledger-finding-response': ['task-ledger', 'finding-respond'],
@@ -0,0 +1,161 @@
1
+ import { reconcileTaskInvalidation } from './invalidation-command.js';
2
+ import { applyLedgerIntent } from './ledger-intents.js';
3
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
4
+ import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
5
+ function failed(code, message) {
6
+ return { status: 'failed', changed: false, error: { code, message } };
7
+ }
8
+ function isFailure(value) {
9
+ return Boolean(value) && typeof value === 'object' && value.status === 'failed';
10
+ }
11
+ function options(args, allowed) {
12
+ const values = {};
13
+ for (let index = 2; index < args.length; index += 1) {
14
+ const flag = args[index];
15
+ if (flag === '--dry-run') {
16
+ if (!Object.hasOwn(allowed, flag) || values[flag] !== undefined)
17
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `invalid option '${flag}'`);
18
+ values[flag] = true;
19
+ continue;
20
+ }
21
+ const key = allowed[flag];
22
+ if (!key || values[flag] !== undefined)
23
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `invalid option '${flag}'`);
24
+ const value = args[++index];
25
+ if (value === undefined || value.startsWith('--'))
26
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `option '${flag}' requires a value`);
27
+ if (flag === '--needs-implementation' && value !== 'true' && value !== 'false') {
28
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', "option '--needs-implementation' must be true or false");
29
+ }
30
+ values[flag] = value;
31
+ }
32
+ return values;
33
+ }
34
+ function value(values, flag) {
35
+ const candidate = values[flag];
36
+ return typeof candidate === 'string' ? candidate : undefined;
37
+ }
38
+ function required(values, flags) {
39
+ const missing = flags.find((flag) => value(values, flag) === undefined);
40
+ return missing ? failed('TASK_WORKFLOW_PAYLOAD_INVALID', `option '${missing}' is required`) : null;
41
+ }
42
+ function resultForLock(error) {
43
+ if (error instanceof TaskExecutionLockError)
44
+ return failed(error.code, error.message);
45
+ throw error;
46
+ }
47
+ function ledgerIntent(operation, args) {
48
+ const [taskRef, kind] = args;
49
+ const mappings = {
50
+ 'ledger-finding-response': {
51
+ kind: 'finding-respond', required: ['--id', '--round', '--status', '--evidence'],
52
+ flags: { '--id': 'id', '--round': 'round', '--status': 'status', '--evidence': 'evidence', '--dry-run': 'dryRun' }
53
+ },
54
+ 'ledger-finding-review': {
55
+ kind: 'finding-review', required: ['--id', '--status', '--evidence'],
56
+ flags: { '--id': 'id', '--status': 'status', '--evidence': 'evidence', '--needs-implementation': 'needsImplementation', '--dry-run': 'dryRun' }
57
+ },
58
+ 'ledger-finding-upsert': {
59
+ kind: 'finding-upsert', required: ['--stage', '--review-artifact', '--ordinal', '--severity', '--evidence'],
60
+ flags: { '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal', '--severity': 'severity', '--evidence': 'evidence', '--dry-run': 'dryRun' }
61
+ },
62
+ 'decision-next-id': { kind: 'decision-next-id', required: [], flags: {} },
63
+ 'decision-upsert': {
64
+ kind: 'decision-upsert', required: ['--id', '--stage', '--artifact'],
65
+ flags: { '--id': 'id', '--stage': 'stage', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation', '--dry-run': 'dryRun' }
66
+ }
67
+ };
68
+ const mapping = mappings[operation];
69
+ if (!mapping || !taskRef || kind === undefined)
70
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
71
+ const values = options(args, mapping.flags);
72
+ if (isFailure(values))
73
+ return values;
74
+ const missing = required(values, mapping.required);
75
+ if (missing)
76
+ return missing;
77
+ const dryRun = values['--dry-run'] === true;
78
+ if (mapping.kind === 'decision-next-id')
79
+ return { kind: mapping.kind, taskRef };
80
+ if (mapping.kind === 'finding-respond')
81
+ return {
82
+ kind: mapping.kind, taskRef, id: value(values, '--id'), round: Number(value(values, '--round')),
83
+ status: value(values, '--status'), evidence: value(values, '--evidence'), ...(dryRun ? { dryRun } : {})
84
+ };
85
+ if (mapping.kind === 'finding-review')
86
+ return {
87
+ kind: mapping.kind, taskRef, id: value(values, '--id'), status: value(values, '--status'),
88
+ evidence: value(values, '--evidence'), ...(value(values, '--needs-implementation') === 'true' ? { needsImplementation: true } : {}),
89
+ ...(dryRun ? { dryRun } : {})
90
+ };
91
+ if (mapping.kind === 'finding-upsert')
92
+ return {
93
+ kind: mapping.kind, taskRef, stage: value(values, '--stage'), reviewArtifact: value(values, '--review-artifact'),
94
+ ordinal: Number(value(values, '--ordinal')), severity: value(values, '--severity'), evidence: value(values, '--evidence'),
95
+ ...(dryRun ? { dryRun } : {})
96
+ };
97
+ return {
98
+ kind: mapping.kind, taskRef, id: value(values, '--id'), stage: value(values, '--stage'),
99
+ artifact: value(values, '--artifact'), ...(value(values, '--needs-implementation') === 'true' ? { needsImplementation: true } : {}),
100
+ ...(dryRun ? { dryRun } : {})
101
+ };
102
+ }
103
+ function warningIntent(args) {
104
+ const [taskRef, kind] = args;
105
+ if (!taskRef || kind !== 'add')
106
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
107
+ const values = options(args, {
108
+ '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
109
+ '--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
110
+ });
111
+ if (isFailure(values))
112
+ return values;
113
+ const missing = required(values, ['--step', '--severity', '--code', '--target', '--message', '--action']);
114
+ if (missing)
115
+ return missing;
116
+ return {
117
+ kind: 'add', taskRef, step: value(values, '--step'), severity: value(values, '--severity'),
118
+ code: value(values, '--code'), target: value(values, '--target'), message: value(values, '--message'),
119
+ action: value(values, '--action'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
120
+ };
121
+ }
122
+ /**
123
+ * The caller has already selected and authorized the execution boundary.
124
+ * This function deliberately invokes task domains in that process: starting a
125
+ * second CLI process would discard the broker binding and could misclassify a
126
+ * sandbox process with cleared environment variables as direct-host.
127
+ */
128
+ export function dispatchWorkflowCommand(repoRoot, operation, args) {
129
+ const taskRef = args[0];
130
+ if (!taskRef)
131
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task ref is required');
132
+ try {
133
+ if (operation === 'invalidation-reconcile') {
134
+ const values = options(args, { '--max-targets': 'maxTargets', '--dry-run': 'dryRun' });
135
+ if (isFailure(values))
136
+ return values;
137
+ const maxTargets = value(values, '--max-targets');
138
+ if (maxTargets !== undefined && (!Number.isSafeInteger(Number(maxTargets)) || Number(maxTargets) < 1)) {
139
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', '--max-targets must be a positive integer');
140
+ }
141
+ return withTaskExecutionLock(repoRoot, taskRef, 'task-invalidation.reconcile', () => reconcileTaskInvalidation(taskRef, {
142
+ repoRoot, ...(maxTargets === undefined ? {} : { maxTargets: Number(maxTargets) }),
143
+ ...(values['--dry-run'] === true ? { dryRun: true } : {})
144
+ }));
145
+ }
146
+ if (operation === 'warning-add') {
147
+ const intent = warningIntent(args);
148
+ if (isFailure(intent))
149
+ return intent;
150
+ return withTaskExecutionLock(repoRoot, taskRef, 'task-warning.add', () => applyWorkflowWarningIntent(intent, { repoRoot }));
151
+ }
152
+ const intent = ledgerIntent(operation, args);
153
+ if (isFailure(intent))
154
+ return intent;
155
+ return withTaskExecutionLock(repoRoot, taskRef, `task-ledger.${intent.kind}`, () => applyLedgerIntent(intent, { repoRoot }));
156
+ }
157
+ catch (error) {
158
+ return resultForLock(error);
159
+ }
160
+ }
161
+ //# sourceMappingURL=workflow-dispatch.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.10.0",
4
- "sourceInputHash": "d7af8de4bd6c1996a920335dfc766e23b8d391cd4db2965d7d9dfaa2cb112059",
5
- "internalExecutableBuildHash": "63a6d2d9113dd353dfce165efccf82a5ea25398c2aad9f1b2c5543dc28e8b833"
3
+ "packageVersion": "0.11.0",
4
+ "sourceInputHash": "c67e66a46040d5246d67c751af605b796cd61913b98add01ff327829c01ce3db",
5
+ "internalExecutableBuildHash": "f6b4be06df12111b3a7614d335c9e802049c3cea194c6e7c040f3251188c694f"
6
6
  }
@@ -36,7 +36,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
36
36
  'task-create': ['input'],
37
37
  'task-qualification': ['proposal', 'confirm', 'supersede', 'revoke'],
38
38
  'sandbox-control': ['serve', 'execute', 'recover', 'client'],
39
- 'host-control': ['serve', 'status', 'install', 'uninstall'],
40
39
  'agent-client': ['next-steps', 'model-selection'],
41
40
  'codex-lifecycle': ['preflight', 'capability-arm', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
42
41
  'codex-sandbox-controller': ['verify-context', 'run'],
@@ -47,7 +46,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
47
46
  'platform-security': ['read', 'dismiss'],
48
47
  'platform-metadata': ['init-labels', 'init-milestones'],
49
48
  'platform-context': ['resolve'],
50
- 'platform-comment': ['list', 'owner', 'backfill', 'sync'],
49
+ 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
51
50
  'platform-issue': ['inspect', 'create', 'bind', 'sync'],
52
51
  'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
53
52
  'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
@@ -57,9 +56,9 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
57
56
  'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert'],
58
57
  'task-warning': ['list', 'add', 'set-status'],
59
58
  'task-activity': ['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate'],
60
- 'task-artifact': ['inspect', 'init', 'finalize-local'],
59
+ 'task-artifact': ['inspect', 'init', 'preflight', 'finalize-local'],
61
60
  'task-orchestration': ['status', 'progress'],
62
- 'task-review': ['finalize-summary'],
61
+ 'task-review': ['preflight', 'finalize-summary'],
63
62
  'task-event': ['event'],
64
63
  'task-invalidation': ['reconcile'],
65
64
  'task-override': ['diagnose', 'issue', 'consume'],