@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -4,11 +4,16 @@ import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-
4
4
  import {
5
5
  buildArtifactLinkSection,
6
6
  artifactName,
7
+ inspectArtifactDirectory,
8
+ parseReviewedInputReference,
9
+ parseCodePlanInputReference,
7
10
  parseArtifactName,
8
11
  resolveArtifactContext,
9
12
  validateCompletedArtifact
10
13
  } from './artifact-lifecycle.ts';
11
14
  import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
15
+ import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
16
+ import type { ArtifactReceipt } from './artifact-receipts.ts';
12
17
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
13
18
  import {
14
19
  consumeImplementationInput,
@@ -35,7 +40,8 @@ const eventCatalog = [
35
40
  'review-plan.started', 'review-plan.completed',
36
41
  'code.started', 'code.completed',
37
42
  'review-code.started', 'review-code.completed',
38
- 'manual-validation.started', 'manual-validation.completed'
43
+ 'manual-validation.started', 'manual-validation.completed',
44
+ 'validation-run.started', 'validation-run.completed'
39
45
  ] as const;
40
46
  type TaskEventName = (typeof eventCatalog)[number];
41
47
  type Verdict = 'approved' | 'changes-requested' | 'rejected';
@@ -83,7 +89,9 @@ const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[]
83
89
  'review-code.started': { optional: ['round'] },
84
90
  'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
85
91
  'manual-validation.started': { optional: ['round'] },
86
- 'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] }
92
+ 'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] },
93
+ 'validation-run.started': { optional: ['round'] },
94
+ 'validation-run.completed': { required: ['artifact'], optional: ['round'] }
87
95
  };
88
96
 
89
97
  function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | null {
@@ -121,7 +129,8 @@ const FAMILY = {
121
129
  'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
122
130
  code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
123
131
  'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
124
- 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
132
+ 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
133
+ 'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
125
134
  } as const;
126
135
  type EventFamily = keyof typeof FAMILY;
127
136
 
@@ -217,6 +226,7 @@ function identity(request: TaskEventRequest) {
217
226
  else if (family === 'plan') note = `Plan completed, awaiting human review → ${request.artifact}`;
218
227
  else if (family === 'code' && request.fixFor) note = `Fixed ${request.blockers} blockers, ${request.major} major, ${request.minor} minor issues${request.manualValidation ? `, skipped ${request.manualValidation} manual-validation` : ''} → ${request.artifact}`;
219
228
  else if (family === 'code') note = `Code implemented, ${request.filesModified} files modified, ${request.testsPassed} tests passed → ${request.artifact}`;
229
+ else if (family === 'validation-run') note = `Validation evidence recorded → ${request.artifact}`;
220
230
  else {
221
231
  const verdict = request.verdict === 'approved' ? 'Approved' : request.verdict === 'changes-requested' ? 'Changes Requested' : 'Rejected';
222
232
  note = `Verdict: ${verdict}, blockers: ${request.blockers}, major: ${request.major}, minor: ${request.minor}, Manual-validation: ${request.manualValidation} → ${request.artifact}`;
@@ -268,6 +278,80 @@ function openStartedIdentity(rows: ReturnType<typeof pairEntries>, family: Event
268
278
  return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true as const } : null;
269
279
  }
270
280
 
281
+ function reviewInputFamily(family: EventFamily): ArtifactFamily {
282
+ return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
283
+ }
284
+
285
+ function buildCompletionReceipt(
286
+ taskDir: string,
287
+ family: EventFamily,
288
+ artifact: ArtifactIdentity,
289
+ completedAt: string,
290
+ frontmatter: Record<string, unknown>
291
+ ): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
292
+ if (family === 'code') {
293
+ let content: string;
294
+ try { content = fs.readFileSync(artifact.path, 'utf8'); }
295
+ catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
296
+ const input = parseCodePlanInputReference(content);
297
+ if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
298
+ const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
299
+ const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
300
+ if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
301
+ if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
302
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
303
+ if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
304
+ return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
305
+ }
306
+ try {
307
+ const inputSha256 = sha256File(plan.latest.path);
308
+ if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${input.name} changed after code.started` };
309
+ return {
310
+ ok: true,
311
+ receipt: {
312
+ event: 'code.completed', output: artifact.name, input: input.name,
313
+ inputSha256, completedAt
314
+ }
315
+ };
316
+ } catch (error) {
317
+ return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
318
+ }
319
+ }
320
+ if (!family.startsWith('review-')) return null;
321
+ const expectedFamily = reviewInputFamily(family);
322
+ let content: string;
323
+ try { content = fs.readFileSync(artifact.path, 'utf8'); }
324
+ catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
325
+ const input = parseReviewedInputReference(content, expectedFamily);
326
+ if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
327
+ const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
328
+ const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
329
+ if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
330
+ if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
331
+ const current = inspectArtifactDirectory(taskDir, expectedFamily);
332
+ if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
333
+ return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
334
+ }
335
+ const event = family === 'review-analysis'
336
+ ? 'review-analysis.completed' as const
337
+ : family === 'review-plan' ? 'review-plan.completed' as const : 'review-code.completed' as const;
338
+ try {
339
+ const stat = fs.lstatSync(current.latest.path);
340
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
341
+ const inputSha256 = sha256File(current.latest.path);
342
+ if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${input.name} changed after review.started` };
343
+ return {
344
+ ok: true,
345
+ receipt: {
346
+ event, output: artifact.name, input: input.name,
347
+ inputSha256, completedAt
348
+ }
349
+ };
350
+ } catch (error) {
351
+ return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
352
+ }
353
+ }
354
+
271
355
  function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
272
356
  const invalid = validateTaskEventRequest(request);
273
357
  if (invalid) return failed(request, invalid);
@@ -342,7 +426,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
342
426
  );
343
427
  if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
344
428
  let orchestrationCompletion: OrchestrationStageCompletion | null = null;
345
- if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation') {
429
+ if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
346
430
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
347
431
  const execution = validateLifecycleExecution(normalized.taskRef, {
348
432
  mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
@@ -366,18 +450,60 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
366
450
  let metadata;
367
451
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
368
452
  catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
453
+ let completionReceipt: ArtifactReceipt | null = null;
454
+ if (eventIdentity.phase === 'completed' && completedArtifact) {
455
+ const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
456
+ if (receipt && !receipt.ok) {
457
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
458
+ }
459
+ completionReceipt = receipt?.receipt ?? null;
460
+ }
369
461
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
370
462
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
371
463
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
372
464
  const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
465
+ let frontmatterRemove: string[] | undefined;
466
+ if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
467
+ const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
468
+ if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
469
+ try {
470
+ frontmatterSet.code_input_artifact = planInput.name;
471
+ frontmatterSet.code_input_sha256 = sha256File(planInput.path);
472
+ } catch (error) {
473
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
474
+ }
475
+ } else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
476
+ frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
477
+ } else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
478
+ const expectedFamily = reviewInputFamily(eventIdentity.family);
479
+ const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
480
+ if (!input) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
481
+ try {
482
+ frontmatterSet.review_input_artifact = input.name;
483
+ frontmatterSet.review_input_sha256 = sha256File(input.path);
484
+ } catch (error) {
485
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
486
+ }
487
+ } else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
488
+ frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
489
+ }
373
490
  if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
374
491
  const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
375
- { kind: 'frontmatter', set: frontmatterSet }
492
+ { kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
376
493
  ];
377
494
  if (completedArtifact) {
378
495
  const link = buildArtifactLinkSection(content, completedArtifact);
379
496
  mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
380
497
  }
498
+ if (completionReceipt) {
499
+ try {
500
+ const receiptSection = upsertArtifactReceipt(content, completionReceipt);
501
+ mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
502
+ } catch (error) {
503
+ const message = error instanceof ArtifactReceiptError ? error.message : String(error);
504
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
505
+ }
506
+ }
381
507
  if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
382
508
  let implementationRows;
383
509
  try {
@@ -16,7 +16,8 @@ const COMPLETION_BACKFILL_FAMILIES = [
16
16
  'review-plan',
17
17
  'code',
18
18
  'review-code',
19
- 'manual-validation'
19
+ 'manual-validation',
20
+ 'validation-run'
20
21
  ] as const satisfies readonly ArtifactFamily[];
21
22
 
22
23
  type CompletionArtifactResult = {
@@ -131,13 +131,9 @@ function finalizeImplementationInput(
131
131
  }
132
132
 
133
133
  function selectPendingImplementationInput(
134
- rows: readonly ImplementationInput[], reviewCompletedAt: string
134
+ rows: readonly ImplementationInput[]
135
135
  ): ImplementationInput | null {
136
- const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
137
- if (!Number.isFinite(reviewEpoch)) throw new Error('latest review completion time is invalid');
138
136
  const pending = rows.filter((row) => row.needsImplementation && row.status === 'pending');
139
- const stale = pending.find((row) => Date.parse(row.decidedAt.replace(' ', 'T')) <= reviewEpoch);
140
- if (stale) throw new Error(`implementation input ${stale.id} is not later than the latest approved review`);
141
137
  return [...pending].sort((left, right) => {
142
138
  const time = Date.parse(left.decidedAt.replace(' ', 'T')) - Date.parse(right.decidedAt.replace(' ', 'T'));
143
139
  return time || Number.parseInt(left.id.slice(3), 10) - Number.parseInt(right.id.slice(3), 10);
package/lib/task/index.ts CHANGED
@@ -10,6 +10,7 @@ Commands:
10
10
  ls [--all | --blocked | --completed] List tasks (default: active)
11
11
  show [--task <ref>] Print a task.md
12
12
  status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
13
+ validate <ref> [options] -- <command> Run host validation in snapshot or inplace scope
13
14
 
14
15
  Examples:
15
16
  ai task cat 11 analysis
@@ -29,6 +30,7 @@ Examples:
29
30
  ai task show 11
30
31
  ai task show TASK-20260612-162737
31
32
  ai task status 11
33
+ ai task validate 11 -- npm test
32
34
 
33
35
  Run 'ai task <command> --help' for details.`;
34
36
 
@@ -93,6 +95,12 @@ export async function runTask(args: string[]): Promise<void> {
93
95
  status(rest);
94
96
  break;
95
97
  }
98
+ case 'validate': {
99
+ const { validate } = await import('./commands/validate.ts');
100
+ const exitCode = await validate(rest);
101
+ if (exitCode !== 0) process.exitCode = exitCode;
102
+ break;
103
+ }
96
104
  default:
97
105
  process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
98
106
  process.stdout.write(`${USAGE}\n`);
@@ -61,7 +61,22 @@ function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError |
61
61
  for (const row of rows) {
62
62
  if (seen.has(row.id)) return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
63
63
  seen.add(row.id);
64
- if (/^PRC-[1-9]\d*$/.test(row.id)) continue;
64
+ if (/^PRC-[1-9]\d*$/.test(row.id)) {
65
+ if (
66
+ row.stage !== 'post-review-commit' ||
67
+ row.round !== '-' ||
68
+ row.severity !== '-' ||
69
+ row.status !== 'human-decided' ||
70
+ row.evidence === '' ||
71
+ /[\r\n]/.test(row.evidence)
72
+ ) {
73
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
74
+ }
75
+ continue;
76
+ }
77
+ if (row.stage === 'post-review-commit') {
78
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
79
+ }
65
80
  if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id)) return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
66
81
  if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage)) return { code: 'LEDGER_STAGE_INVALID', message: `ledger stage '${row.stage}' is invalid` };
67
82
  if (!FINDING_STATUSES.has(row.status)) return { code: 'LEDGER_STATUS_INVALID', message: `ledger status '${row.status}' is invalid` };