@fitlab-ai/agent-infra 0.11.2 → 0.11.4

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 (162) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +7 -2
  4. package/dist/lib/agent-clients/adapter.js +3 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  8. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  9. package/dist/lib/internal/cli-route-inventory.js +1 -1
  10. package/dist/lib/internal/manual-validation.js +22 -27
  11. package/dist/lib/internal/sandbox-control.js +1 -43
  12. package/dist/lib/internal/task-artifact.js +1 -1
  13. package/dist/lib/internal/task-finalization.js +41 -1
  14. package/dist/lib/internal/task-ledger.js +13 -5
  15. package/dist/lib/internal/task-lifecycle.js +1 -1
  16. package/dist/lib/internal/task-operation-registry.js +7 -2
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/issue-comments.js +2 -31
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/sandbox/control/client.js +3 -44
  21. package/dist/lib/sandbox/control/executor.js +14 -79
  22. package/dist/lib/sandbox/control/protocol.js +5 -64
  23. package/dist/lib/sandbox/control/server.js +49 -343
  24. package/dist/lib/sandbox/control/state.js +1 -7
  25. package/dist/lib/sandbox/control/task-view.js +1 -2
  26. package/dist/lib/sandbox/task-cleanup.js +13 -20
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +1 -26
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +1 -0
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +37 -38
  36. package/dist/lib/task/control-recovery.js +3 -42
  37. package/dist/lib/task/decision-intents.js +12 -0
  38. package/dist/lib/task/delegation-receipts.js +2 -2
  39. package/dist/lib/task/delivery.js +1 -43
  40. package/dist/lib/task/events.js +80 -64
  41. package/dist/lib/task/finalization-artifacts.js +1 -3
  42. package/dist/lib/task/finalization-state.js +1 -1
  43. package/dist/lib/task/finalization.js +319 -234
  44. package/dist/lib/task/ledger-intents.js +157 -15
  45. package/dist/lib/task/lifecycle-path.js +75 -0
  46. package/dist/lib/task/lifecycle.js +11 -1
  47. package/dist/lib/task/local-artifact-finalization.js +1 -20
  48. package/dist/lib/task/manual-validation-receipt.js +8 -3
  49. package/dist/lib/task/orchestration.js +29 -13
  50. package/dist/lib/task/review-finalization.js +3 -16
  51. package/dist/lib/task/rework-intent.js +70 -4
  52. package/dist/lib/task/verification-engine.js +20 -30
  53. package/dist/lib/task/verification.js +1 -0
  54. package/dist/lib/task/workflow-warning-intents.js +21 -6
  55. package/dist/lib/task/workspace-snapshot.js +7 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/agent-clients/adapter.ts +46 -0
  58. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  60. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  61. package/lib/agent-clients/adapters/codex.ts +10 -0
  62. package/lib/internal/cli-route-inventory.ts +1 -1
  63. package/lib/internal/manual-validation.ts +22 -26
  64. package/lib/internal/sandbox-control.ts +0 -42
  65. package/lib/internal/task-artifact.ts +1 -1
  66. package/lib/internal/task-finalization.ts +40 -1
  67. package/lib/internal/task-ledger.ts +12 -5
  68. package/lib/internal/task-lifecycle.ts +2 -2
  69. package/lib/internal/task-operation-registry.ts +8 -4
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/issue-comments.ts +1 -35
  72. package/lib/platform/pr-summary.ts +10 -2
  73. package/lib/sandbox/control/client.ts +5 -68
  74. package/lib/sandbox/control/executor.ts +14 -81
  75. package/lib/sandbox/control/protocol.ts +7 -83
  76. package/lib/sandbox/control/server.ts +47 -362
  77. package/lib/sandbox/control/state.ts +1 -9
  78. package/lib/sandbox/control/task-view.ts +1 -2
  79. package/lib/sandbox/task-cleanup.ts +12 -20
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +1 -26
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +1 -0
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +41 -48
  89. package/lib/task/control-recovery.ts +3 -43
  90. package/lib/task/decision-intents.ts +9 -0
  91. package/lib/task/delegation-receipts.ts +2 -2
  92. package/lib/task/delivery.ts +1 -40
  93. package/lib/task/events.ts +82 -64
  94. package/lib/task/finalization-artifacts.ts +1 -3
  95. package/lib/task/finalization-state.ts +1 -1
  96. package/lib/task/finalization.ts +310 -216
  97. package/lib/task/ledger-intents.ts +155 -15
  98. package/lib/task/lifecycle-path.ts +98 -0
  99. package/lib/task/lifecycle.ts +15 -2
  100. package/lib/task/local-artifact-finalization.ts +2 -25
  101. package/lib/task/manual-validation-receipt.ts +8 -2
  102. package/lib/task/orchestration.ts +56 -27
  103. package/lib/task/review-finalization.ts +3 -16
  104. package/lib/task/rework-intent.ts +75 -5
  105. package/lib/task/verification-engine.ts +19 -30
  106. package/lib/task/verification.ts +2 -1
  107. package/lib/task/workflow-warning-intents.ts +18 -6
  108. package/lib/task/workspace-snapshot.ts +3 -2
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  111. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  112. package/templates/.agents/rules/review-handshake.en.md +4 -4
  113. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  114. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  115. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  116. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  117. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  118. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  119. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  120. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  121. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  122. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  123. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  124. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  125. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  128. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  129. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  130. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  131. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  132. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  135. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  136. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  137. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  138. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  139. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  140. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  141. package/templates/.agents/templates/task.en.md +2 -2
  142. package/templates/.agents/templates/task.zh-CN.md +2 -2
  143. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  144. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  145. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  146. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  147. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  148. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  149. package/dist/lib/sandbox/control/finalization-response.js +0 -35
  150. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  151. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  152. package/dist/lib/task/lifecycle-recovery.js +0 -549
  153. package/dist/lib/task/recovery-warning.js +0 -19
  154. package/dist/lib/task/workflow-command.js +0 -21
  155. package/dist/lib/task/workflow-dispatch.js +0 -213
  156. package/lib/sandbox/control/finalization-response.ts +0 -37
  157. package/lib/sandbox/control/task-workflow.ts +0 -95
  158. package/lib/sandbox/control/workflow-executor.ts +0 -156
  159. package/lib/task/lifecycle-recovery.ts +0 -691
  160. package/lib/task/recovery-warning.ts +0 -21
  161. package/lib/task/workflow-command.ts +0 -22
  162. package/lib/task/workflow-dispatch.ts +0 -214
@@ -1,6 +1,5 @@
1
1
  import { isCompletionEvidence, type CleanCompletionEvidence } from '../../task/orchestration.ts';
2
2
  import { parseControlOutput } from '../../task/control-recovery.ts';
3
- import { isRecoveryWarning } from '../../task/recovery-warning.ts';
4
3
  import { execFileSync } from 'node:child_process';
5
4
  import fs from 'node:fs';
6
5
  import path from 'node:path';
@@ -15,7 +14,6 @@ import type {
15
14
  SandboxControlPayload,
16
15
  SandboxControlReservation,
17
16
  SandboxControlResultEvidence,
18
- SandboxControlRecoveryWarning,
19
17
  SandboxControlStatus
20
18
  } from './protocol.ts';
21
19
  import {
@@ -115,7 +113,6 @@ export type SandboxControlTerminalResult = Readonly<{
115
113
  status: string;
116
114
  changed: boolean | null;
117
115
  completionEvidence: CleanCompletionEvidence | null;
118
- warning?: SandboxControlRecoveryWarning | null;
119
116
  }>;
120
117
 
121
118
  export function terminalResultPath(manifest: SandboxControlManifest, requestId: string): string {
@@ -133,7 +130,6 @@ export function createSandboxControlTerminalResult(
133
130
  ? nested.run as Record<string, unknown> : null;
134
131
  const rawCompletion = nested.completionEvidence ?? run?.completionEvidence;
135
132
  const operation = request.operation ?? (typeof nested.intent === 'string' ? nested.intent : null) ?? '';
136
- const warning = isRecoveryWarning(nested.warning) ? nested.warning : null;
137
133
  return {
138
134
  version: 1,
139
135
  requestId: request.id,
@@ -143,8 +139,7 @@ export function createSandboxControlTerminalResult(
143
139
  targetState: typeof nested.targetState === 'string' ? nested.targetState : null,
144
140
  status: typeof nested.status === 'string' ? nested.status : 'completed',
145
141
  changed: typeof nested.changed === 'boolean' ? nested.changed : null,
146
- completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null,
147
- ...(warning ? { warning } : {})
142
+ completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
148
143
  };
149
144
  }
150
145
 
@@ -178,9 +173,6 @@ export function readSandboxControlTerminalResult(filePath: string): SandboxContr
178
173
  if (value.completionEvidence !== null && !isCompletionEvidence(value.completionEvidence)) {
179
174
  throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
180
175
  }
181
- if (value.warning !== undefined && value.warning !== null && !isRecoveryWarning(value.warning)) {
182
- throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
183
- }
184
176
  if (value.changed === undefined) {
185
177
  throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
186
178
  }
@@ -56,11 +56,10 @@ function receiptIdentity(value: unknown, generation: string, requestId?: string)
56
56
  const binding = receipt.controlBinding;
57
57
  if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return null;
58
58
  const controlBinding = binding as Record<string, unknown>;
59
- if (receipt.version !== 3 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
59
+ if (receipt.version !== 4 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
60
60
  || !Number.isSafeInteger(receipt.revision) || (receipt.revision as number) < 0
61
61
  || receipt.lifecycle !== 'done'
62
62
  || !['pending', 'done', 'skipped'].includes(String(receipt.summary))
63
- || !['pending', 'done', 'skipped'].includes(String(receipt.postSummaryVerification))
64
63
  || controlBinding.generation !== generation
65
64
  || typeof controlBinding.requestId !== 'string'
66
65
  || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)
@@ -8,7 +8,6 @@ import { enumerateAllTaskDirs } from '../task/resolve-ref.ts';
8
8
  import { resolveSandboxCleanupTarget } from './workspace-identity.ts';
9
9
  import { readSandboxControlManifest } from './control/lifecycle.ts';
10
10
  import { readJsonFile, readSandboxControlStatus } from './control/state.ts';
11
- import { finalizationTerminalResponse } from './control/finalization-response.ts';
12
11
 
13
12
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
14
13
 
@@ -83,13 +82,10 @@ function canonicalPath(input: string): string {
83
82
 
84
83
  function taskFinalizationReceiptComplete(
85
84
  receipt: TaskFinalizationReceipt,
86
- taskId: string,
87
- binding: SandboxControlBinding
85
+ taskId: string
88
86
  ): boolean {
89
87
  return receipt.taskId === taskId
90
- && taskFinalizationReceiptState(receipt) === 'complete'
91
- && receipt.controlBinding?.generation === binding.generation
92
- && receipt.controlBinding.requestId === binding.requestId;
88
+ && taskFinalizationReceiptState(receipt) === 'complete';
93
89
  }
94
90
 
95
91
  function terminalControlBindingEvidence(
@@ -114,24 +110,20 @@ function terminalControlBindingEvidence(
114
110
  if (errorCode(error) !== 'ENOENT') return false;
115
111
  }
116
112
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
117
- if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId, binding)) {
113
+ if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId)) {
118
114
  return false;
119
115
  }
120
- const status = readSandboxControlStatus(manifest.publicStatusDir);
121
- const viewReceipt = status.taskView.receipt;
122
- const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
123
- || (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
124
- if (status.generation !== binding.generation || status.state !== 'healthy'
125
- || status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
126
- || !viewReceipt || viewReceipt.generation !== binding.generation
127
- || viewReceipt.requestId !== binding.requestId
128
- || viewReceipt.receiptId !== receipt.receiptId
129
- || viewReceipt.revision !== receipt.revision) return false;
130
116
  const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
131
117
  if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file')) return false;
132
118
  const actual = readJsonFile(responsePath);
133
- const expected = finalizationTerminalResponse(taskId, binding.requestId, receipt);
134
- return JSON.stringify(actual) === JSON.stringify(expected);
119
+ const response = actual as Record<string, unknown>;
120
+ if (response.version !== 2 || response.id !== binding.requestId || response.phase !== 'completed'
121
+ || response.exitCode !== 0 || typeof response.stdout !== 'string') return false;
122
+ const envelope = JSON.parse(response.stdout) as Record<string, unknown>;
123
+ const result = envelope.result as Record<string, unknown> | null;
124
+ return envelope.status === 'completed' && envelope.accepted === true
125
+ && result !== null && typeof result === 'object'
126
+ && result.status === 'completed' && result.taskId === taskId;
135
127
  } catch {
136
128
  return false;
137
129
  }
@@ -157,7 +149,7 @@ function removedControlBindingEvidence(
157
149
  if (frontmatter.id !== taskId || frontmatter.branch !== journal.target.branch) return false;
158
150
  try {
159
151
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
160
- return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId, binding);
152
+ return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId);
161
153
  } catch {
162
154
  return false;
163
155
  }
@@ -13,6 +13,9 @@ export type ArtifactCommand = Readonly<{
13
13
  family: string;
14
14
  artifact: string;
15
15
  locale?: 'zh-CN' | 'en';
16
+ sourceFinding?: string;
17
+ sourceArtifact?: string;
18
+ sourceSha256?: string;
16
19
  }>;
17
20
 
18
21
  /** One option contract for the CLI and trusted projection executor. */
@@ -23,7 +26,7 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
23
26
  }
24
27
  const fields: Record<string, string> = {};
25
28
  const allowed = {
26
- inspect: ['--family'],
29
+ inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
27
30
  init: ['--family', '--artifact', '--locale'],
28
31
  'preflight': ['--family', '--artifact'],
29
32
  'finalize-local': ['--family', '--artifact']
@@ -41,8 +44,10 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
41
44
  const locale = fields['--locale'];
42
45
  if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
43
46
  return {
44
- taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
45
- artifact: fields['--artifact'] ?? '', locale
47
+ taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'] ?? '',
48
+ artifact: fields['--artifact'] ?? '', locale,
49
+ sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
50
+ sourceSha256: fields['--source-sha256']
46
51
  };
47
52
  }
48
53
  /** Resolve authoritative task metadata separately from a trusted candidate directory. */
@@ -53,7 +58,12 @@ export function executeArtifactCommand(
53
58
  const { taskRef, operation, family, artifact, locale } = command;
54
59
  const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
55
60
  if (operation === 'inspect') {
56
- const result = resolveArtifactContext(taskRef, family, { repoRoot: options.repoRoot });
61
+ const result = resolveArtifactContext(taskRef, family, {
62
+ repoRoot: options.repoRoot,
63
+ sourceFinding: command.sourceFinding,
64
+ sourceArtifact: command.sourceArtifact,
65
+ sourceSha256: command.sourceSha256
66
+ });
57
67
  return family === 'code' && result.codeMode ? {
58
68
  ...result, mode: result.codeMode.mode, code_max: result.codeMode.codeMax, rev_max: result.codeMode.reviewMax,
59
69
  verdict: result.codeMode.verdict, next_round: result.next?.round ?? null, next_artifact: result.next?.name ?? null,
@@ -11,8 +11,12 @@ import { extractSection, findSectionHeading } from './sections.ts';
11
11
  import { receiptForOutput, sha256File } from './artifact-receipts.ts';
12
12
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
13
13
  import type { InvalidationDocument } from './invalidation.ts';
14
- import { validateQualificationAudit } from './qualification-audit.ts';
15
14
  import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
15
+ import { parseLifecyclePathDecision } from './lifecycle-path.ts';
16
+ import type { LifecyclePathState } from './lifecycle-path.ts';
17
+ import { parseReworkIntentDocument } from './rework-intent.ts';
18
+ import { selectArtifactDisposition } from './artifact-selection.ts';
19
+ import type { ArtifactSelection } from './artifact-selection.ts';
16
20
 
17
21
  const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
18
22
  const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
@@ -33,6 +37,16 @@ export function hasOpenArtifactRound(content: string, family: string, round: num
33
37
  return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
34
38
  }
35
39
 
40
+ function hasCompletedArtifactRound(content: string, family: string, round: number, name: string): boolean {
41
+ const activity = locateActivityLog(content);
42
+ const step = ARTIFACT_STEPS[family];
43
+ if (!activity || !step) return false;
44
+ const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
45
+ return startedBackedRows(pairEntries(activity.entries)).some((row) =>
46
+ expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`)
47
+ );
48
+ }
49
+
36
50
  type ArtifactIdentity = {
37
51
  family: ArtifactFamily;
38
52
  round: number;
@@ -55,9 +69,12 @@ type ArtifactErrorCode =
55
69
  | ResolveTaskRefErrorCode | 'ARTIFACT_FAMILY_UNKNOWN' | 'ARTIFACT_DIRECTORY_READ_FAILED'
56
70
  | 'ARTIFACT_TOPOLOGY_CONFLICT' | 'ARTIFACT_INPUT_MISSING'
57
71
  | 'ARTIFACT_REFERENCE_INVALID' | 'ARTIFACT_PATH_INVALID'
72
+ | 'LIFECYCLE_PATH_INVALID'
58
73
  | 'ARTIFACT_IDENTITY_INVALID' | 'ARTIFACT_NOT_FOUND'
59
74
  | 'ARTIFACT_NOT_REGULAR' | 'ARTIFACT_NOT_READABLE' | 'ARTIFACT_VERDICT_INVALID'
60
- | 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID';
75
+ | 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
76
+ | 'ARTIFACT_SELECTION_INDETERMINATE';
77
+
61
78
  type ArtifactError = { code: ArtifactErrorCode; message: string };
62
79
  type ArtifactInventoryResult = {
63
80
  status: 'ready' | 'failed';
@@ -79,6 +96,7 @@ type CodeArtifactMode = 'init' | 'fix' | 'decision' | 'refused' | 'error';
79
96
  type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
80
97
  status: ArtifactContextStatus;
81
98
  inputs: readonly ArtifactIdentity[];
99
+ selection?: ArtifactSelection | null;
82
100
  codeMode: {
83
101
  mode: CodeArtifactMode;
84
102
  codeMax: number;
@@ -91,7 +109,13 @@ type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
91
109
  message: string;
92
110
  } | null;
93
111
  };
94
- type InspectOptions = { repoRoot?: string };
112
+ type InspectOptions = {
113
+ repoRoot?: string;
114
+ reasonCode?: string;
115
+ sourceFinding?: string;
116
+ sourceArtifact?: string;
117
+ sourceSha256?: string;
118
+ };
95
119
 
96
120
  const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
97
121
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
@@ -260,9 +284,11 @@ function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { inp
260
284
  try { receipt = receiptForOutput(taskContent, code.name); }
261
285
  catch { return null; }
262
286
  if (!receipt || receipt.event !== 'code.completed') return null;
263
- const plan = inspectArtifactDirectory(taskDir, 'plan');
264
- if (plan.status !== 'ready') return null;
265
- const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
287
+ const identity = parseArtifactName(receipt.input);
288
+ if (!identity || !['analysis', 'plan'].includes(identity.family)) return null;
289
+ const source = inspectArtifactDirectory(taskDir, identity.family);
290
+ if (source.status !== 'ready') return null;
291
+ const input = source.artifacts.find((artifact) => artifact.name === receipt.input);
266
292
  return input ? { input, inputSha256: receipt.inputSha256 } : null;
267
293
  }
268
294
 
@@ -270,6 +296,16 @@ function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input:
270
296
  return resolveCodeInputReceipt(taskDir, code);
271
297
  }
272
298
 
299
+ function lifecyclePathState(taskDir: string): LifecyclePathState {
300
+ const analysis = inspectArtifactDirectory(taskDir, 'analysis');
301
+ if (analysis.status !== 'ready' || !analysis.latest) return { status: 'missing', decision: null, message: 'analysis artifact is missing' };
302
+ try {
303
+ return parseLifecyclePathDecision(fs.readFileSync(analysis.latest.path, 'utf8'), analysis.latest.name, sha256File(analysis.latest.path));
304
+ } catch (error) {
305
+ return { status: 'invalid', decision: null, message: error instanceof Error ? error.message : String(error) };
306
+ }
307
+ }
308
+
273
309
  function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
274
310
  if (inventory.status === 'failed') return inventory.error;
275
311
  const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
@@ -282,21 +318,127 @@ function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactEr
282
318
  const REQUIRED_INPUT: Partial<Record<ArtifactFamily, ArtifactFamily>> = {
283
319
  'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
284
320
  };
285
- const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily; requireReference?: boolean }>> = {
286
- analysis: { family: 'review-analysis', requireReference: true },
287
- plan: { family: 'review-plan', requireReference: true },
321
+ const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily }>> = {
322
+ analysis: { family: 'review-analysis' },
323
+ plan: { family: 'review-plan' },
288
324
  'review-code': { family: 'review-plan' },
289
325
  'manual-validation': { family: 'review-code' },
290
326
  'validation-run': { family: 'review-code' }
291
327
  };
292
- const QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS = new Set<ArtifactFamily>(['analysis', 'plan']);
328
+ function hasSelectionEvidence(
329
+ content: string,
330
+ family: ArtifactFamily,
331
+ options: InspectOptions,
332
+ context: ArtifactContextResult
333
+ ): boolean {
334
+ if (options.sourceFinding && options.sourceArtifact && options.sourceSha256) return true;
335
+ if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt') return true;
336
+ if (family.startsWith('review-')) {
337
+ const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
338
+ const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
339
+ if (currentInput && context.reviewedInput?.name !== currentInput.name) return true;
340
+ }
341
+ const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
342
+ if (target) {
343
+ const parsed = parseReworkIntentDocument(content);
344
+ if (!parsed.ok) throw new Error(`${parsed.code}: ${parsed.message}`);
345
+ if (parsed.intents.some((intent) => intent.status === 'pending'
346
+ && (intent.target === target || family === 'analysis' && intent.target === 'plan'))) return true;
347
+ }
348
+ if (family === 'code') {
349
+ if (context.latest && context.codeMode?.mode === 'init') return true;
350
+ if (parseImplementationInputs(content).rows.some((input) => input.needsImplementation && input.status === 'pending')) return true;
351
+ }
352
+ return context.inputs.filter((candidate) => candidate.family.startsWith('review-')).some((input) => {
353
+ const verdict = parseVerdict(input.path);
354
+ if (!verdict.ok || verdict.verdict !== 'Changes Requested' || !context.latest) return false;
355
+ const receipt = receiptForOutput(content, input.name);
356
+ return receipt?.event === reviewEventName(input.family)
357
+ && receipt.input === context.latest.name
358
+ && receipt.inputSha256 === sha256File(context.latest.path);
359
+ });
360
+ }
361
+
362
+ function attachArtifactSelection(
363
+ context: ArtifactContextResult,
364
+ pathState: LifecyclePathState,
365
+ options: InspectOptions
366
+ ): ArtifactContextResult {
367
+ if (context.status !== 'ready' || !context.taskDir || !context.taskId
368
+ || (context.family !== 'analysis' && pathState.status !== 'valid')) return context;
369
+ const family = context.family as ArtifactFamily;
370
+ const fail = (code: ArtifactErrorCode, message: string): ArtifactContextResult => ({
371
+ ...context, status: 'failed', selection: null, error: { code, message }
372
+ });
373
+ if (!context.next) return fail('ARTIFACT_SELECTION_INDETERMINATE', 'artifact inventory did not provide a writable identity');
374
+ let taskContent: string;
375
+ try {
376
+ taskContent = fs.readFileSync(path.join(context.taskDir, 'task.md'), 'utf8');
377
+ } catch (error) {
378
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
379
+ }
380
+ const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
381
+ const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
382
+ const open = openLatest || openNext;
383
+ const selectedIdentity = openLatest ? context.latest! : context.next;
384
+ try {
385
+ const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
386
+ const selection = selectArtifactDisposition({
387
+ next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
388
+ latest: context.latest,
389
+ open,
390
+ hasChangeEvidence
391
+ });
392
+ return {
393
+ ...context,
394
+ selection,
395
+ next: selection.writeRequired ? { round: selection.artifact.round, name: selection.artifact.name } : null
396
+ };
397
+ } catch (error) {
398
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
399
+ }
400
+ }
293
401
 
294
402
  function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
295
403
  const inventory = inspectTaskArtifacts(taskRef, family, options);
296
404
  if (inventory.status === 'failed') return { ...inventory, inputs: [], codeMode: null };
297
405
  const writableError = assertWritableInventory(inventory);
298
406
  if (writableError) return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
299
- if (inventory.family === 'code') return resolveCodeContext(inventory, options);
407
+ const pathState = lifecyclePathState(inventory.taskDir!);
408
+ if (inventory.family !== 'analysis' && pathState.status !== 'valid') {
409
+ return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
410
+ }
411
+ if (inventory.family === 'code') {
412
+ const code = resolveCodeContext(inventory, options);
413
+ const selected = attachArtifactSelection(code, pathState, options);
414
+ if (selected.status !== 'ready' || !selected.selection) return selected;
415
+ if (selected.selection.disposition === 'reuse') return selected;
416
+ if (selected.codeMode?.mode === 'error') {
417
+ return {
418
+ ...selected,
419
+ status: 'ready',
420
+ codeMode: {
421
+ ...selected.codeMode,
422
+ mode: 'init',
423
+ message: 'A new implementation round was requested.'
424
+ },
425
+ error: null
426
+ };
427
+ }
428
+ if (selected.codeMode?.mode === 'refused') {
429
+ return {
430
+ ...selected,
431
+ status: 'ready',
432
+ codeMode: {
433
+ ...selected.codeMode,
434
+ mode: 'init',
435
+ message: 'A new implementation round was requested.'
436
+ },
437
+ error: null
438
+ };
439
+ }
440
+ return selected;
441
+ }
300
442
  const required = REQUIRED_INPUT[inventory.family as ArtifactFamily];
301
443
  const inputs: ArtifactIdentity[] = [];
302
444
  if (required) {
@@ -304,116 +446,113 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
304
446
  if (input.status === 'failed' || !input.latest) {
305
447
  return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
306
448
  }
307
- const qualificationError = qualificationErrorForArtifact(input.latest);
308
- if (qualificationError) return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
309
449
  inputs.push(input.latest);
310
450
  }
311
451
  const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
312
452
  if (optional) {
313
453
  const context = inspectTaskArtifacts(taskRef, optional.family, options);
314
454
  if (context.status === 'ready' && context.latest) {
315
- if (optional.requireReference && !context.reviewedInput) {
316
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: `${context.latest.name} has no valid reviewed input` } };
317
- }
318
- const qualificationError = qualificationErrorForArtifact(context.latest);
319
- if (qualificationError && !QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS.has(inventory.family as ArtifactFamily)) {
320
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
321
- }
322
455
  inputs.push(context.latest);
323
456
  }
324
457
  }
325
- return { ...inventory, inputs, codeMode: null };
326
- }
327
-
328
- function qualificationErrorForArtifact(artifact: ArtifactIdentity): string | null {
329
- try {
330
- const taskContent = fs.readFileSync(path.join(path.dirname(artifact.path), 'task.md'), 'utf8');
331
- const artifactContent = fs.readFileSync(artifact.path, 'utf8');
332
- const result = validateQualificationAudit(taskContent, artifactContent);
333
- return result.ok ? null : `${result.code}: ${result.message}`;
334
- } catch (error) {
335
- return error instanceof Error ? error.message : String(error);
458
+ if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
459
+ return { ...inventory, inputs, codeMode: null };
336
460
  }
461
+ return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
337
462
  }
338
463
 
339
464
  function resolveCodeContext(inventory: ArtifactInventoryResult, options: InspectOptions): ArtifactContextResult {
340
465
  const taskRef = inventory.requestRef;
341
- const plan = inspectTaskArtifacts(taskRef, 'plan', options);
342
- if (plan.status === 'failed' || !plan.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest plan artifact is required');
343
- const reviewPlan = inspectTaskArtifacts(taskRef, 'review-plan', options);
466
+ const pathState = lifecyclePathState(inventory.taskDir!);
467
+ if (pathState.status !== 'valid') return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
468
+ const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
469
+ const source = inspectTaskArtifacts(taskRef, inputFamily, options);
470
+ if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
344
471
  const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
345
472
  const latestCode = inventory.latest;
346
473
  const codeMax = latestCode?.round ?? 0;
347
474
  const reviewMax = reviewCode.latest?.round ?? 0;
348
- const inputs = [plan.latest];
349
- const planQualificationError = qualificationErrorForArtifact(plan.latest);
350
- if (planQualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', planQualificationError);
475
+ const inputs = [source.latest];
351
476
  if (latestCode) {
352
- const qualificationError = qualificationErrorForArtifact(latestCode);
353
- if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, latestCode.name);
354
- }
355
- if (reviewCode.latest) {
356
- const qualificationError = qualificationErrorForArtifact(reviewCode.latest);
357
- if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewCode.latest.name);
477
+ try {
478
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
479
+ if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
480
+ const review = reviewCode.latest;
481
+ const openInputs = review ? [...inputs, review] : inputs;
482
+ const openVerdict = review ? parseVerdict(review.path) : null;
483
+ const openRow = locateActivityLog(taskContent)
484
+ ? startedBackedRows(pairEntries(locateActivityLog(taskContent)!.entries))
485
+ .find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
486
+ : null;
487
+ const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
488
+ const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
489
+ const mode = decisionInput ? 'decision'
490
+ : fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
491
+ : 'init';
492
+ return withCodeMode(
493
+ inventory, openInputs, 'ready', mode, codeMax, reviewMax,
494
+ openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null,
495
+ `Resuming open code round ${latestCode.round}.`,
496
+ decisionInput
497
+ );
498
+ }
499
+ } catch (error) {
500
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
501
+ }
358
502
  }
359
503
  if (!latestCode) {
360
- if (!reviewPlan.latest || reviewPlan.reviewedInput?.name !== plan.latest.name) {
361
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest plan '${plan.latest.name}' requires a matching approved review-plan`);
362
- }
363
- const verdict = parseVerdict(reviewPlan.latest.path);
364
- if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
365
- if (verdict.verdict !== 'Approved') {
366
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `latest ${reviewPlan.latest.name} is not approved`, reviewPlan.latest.name);
367
- }
368
504
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
369
505
  'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
370
506
  }
371
507
  const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
372
508
  if (!codePlanInput) {
373
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
509
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null,
510
+ `Code input receipt for ${latestCode.name} is unavailable.`);
374
511
  }
375
- if (reviewPlan.latest && !reviewPlan.reviewedInput) {
376
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
377
- }
378
- if (reviewPlan.latest) {
379
- const qualificationError = qualificationErrorForArtifact(reviewPlan.latest);
380
- if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewPlan.latest.name);
381
- }
382
- if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
383
- const verdict = parseVerdict(reviewPlan.latest.path);
384
- if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
385
- const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
386
- if (verdict.ok && verdict.verdict === 'Approved' && (
387
- codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
388
- )) {
389
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
390
- `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
391
- }
512
+ const currentInputSha256 = sha256File(source.latest.path);
513
+ if (codePlanInput.input.name !== source.latest.name || codePlanInput.inputSha256 !== currentInputSha256) {
514
+ return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
515
+ `Latest ${source.latest.name} is not captured by the latest code input receipt. Entering replan-driven init.`);
392
516
  }
393
517
  if (!reviewCode.latest || reviewCode.reviewedInput?.name !== latestCode.name) {
394
518
  const expected = reviewCode.next?.name ?? artifactName('review-code', reviewMax + 1);
395
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `${expected} is required before another code round`, expected);
519
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, expected,
520
+ `${expected} is required before another code round`);
396
521
  }
397
522
  const review = reviewCode.latest;
398
523
  if (!review) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest review-code artifact is required');
399
524
  const verdict = parseVerdict(review.path);
400
525
  if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, review.name);
401
- if (verdict.verdict === 'Approved') {
402
- const decision = resolveDecisionImplementationInput(inventory, review);
403
- if ('error' in decision) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
404
- if (decision.input) {
405
- return withCodeMode(
406
- inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax,
407
- verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`,
408
- decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence
409
- );
526
+ const decision = resolveDecisionImplementationInput(inventory, review);
527
+ if ('error' in decision) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
528
+ if (decision.input) {
529
+ return withCodeMode(
530
+ inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax,
531
+ verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`,
532
+ decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence
533
+ );
534
+ }
535
+ try {
536
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
537
+ const rework = parseReworkIntentDocument(taskContent);
538
+ const reviewSha256 = sha256File(review.path);
539
+ if (rework.ok && rework.intents.some((intent) => intent.status === 'pending'
540
+ && intent.target === 'code'
541
+ && intent.sourceArtifact === review.name
542
+ && intent.sourceSha256 === reviewSha256)) {
543
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
544
+ `Pending rework evidence requires a new code round after ${review.name}.`);
410
545
  }
546
+ } catch (error) {
547
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error), review.name);
411
548
  }
412
- if (verdict.verdict === 'Approved' || verdict.verdict === 'Rejected') {
413
- return withCodeMode(inventory, [...inputs, review], 'refused', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
414
- verdict.verdict === 'Approved'
415
- ? `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`
416
- : `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
549
+ if (verdict.verdict === 'Approved') {
550
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
551
+ `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`);
552
+ }
553
+ if (verdict.verdict === 'Rejected') {
554
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
555
+ `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
417
556
  }
418
557
  return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
419
558
  `Latest ${review.name} requires or permits fixes. Entering fix mode.`);
@@ -482,7 +621,19 @@ export function validateArtifactPublication(taskDir: string, family: ArtifactFam
482
621
  const inventory = inspectArtifactDirectory(taskDir, family);
483
622
  const error = assertWritableInventory(inventory);
484
623
  if (error) return error;
485
- if (inventory.next?.name === name || inventory.artifacts.some((artifact) => artifact.name === name)) return null;
624
+ if (inventory.next?.name === name) return null;
625
+ const existing = inventory.artifacts.find((artifact) => artifact.name === name);
626
+ if (existing) {
627
+ try {
628
+ const task = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
629
+ if (hasCompletedArtifactRound(task, family, existing.round, name)) {
630
+ return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
631
+ }
632
+ } catch (error) {
633
+ return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
634
+ }
635
+ return null;
636
+ }
486
637
  return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
487
638
  }
488
639