@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
@@ -13,11 +13,10 @@ function receiptIdentity(value, generation, requestId) {
13
13
  if (!binding || typeof binding !== 'object' || Array.isArray(binding))
14
14
  return null;
15
15
  const controlBinding = binding;
16
- if (receipt.version !== 3 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
16
+ if (receipt.version !== 4 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
17
17
  || !Number.isSafeInteger(receipt.revision) || receipt.revision < 0
18
18
  || receipt.lifecycle !== 'done'
19
19
  || !['pending', 'done', 'skipped'].includes(String(receipt.summary))
20
- || !['pending', 'done', 'skipped'].includes(String(receipt.postSummaryVerification))
21
20
  || controlBinding.generation !== generation
22
21
  || typeof controlBinding.requestId !== 'string'
23
22
  || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)
@@ -8,7 +8,6 @@ import { enumerateAllTaskDirs } from '../task/resolve-ref.js';
8
8
  import { resolveSandboxCleanupTarget } from './workspace-identity.js';
9
9
  import { readSandboxControlManifest } from './control/lifecycle.js';
10
10
  import { readJsonFile, readSandboxControlStatus } from './control/state.js';
11
- import { finalizationTerminalResponse } from './control/finalization-response.js';
12
11
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
13
12
  function matchingControlManifest(repoRoot, controlRoot, taskId, binding) {
14
13
  if (!controlRootIdentity(controlRoot))
@@ -77,11 +76,9 @@ function canonicalPath(input) {
77
76
  return path.resolve(input);
78
77
  }
79
78
  }
80
- function taskFinalizationReceiptComplete(receipt, taskId, binding) {
79
+ function taskFinalizationReceiptComplete(receipt, taskId) {
81
80
  return receipt.taskId === taskId
82
- && taskFinalizationReceiptState(receipt) === 'complete'
83
- && receipt.controlBinding?.generation === binding.generation
84
- && receipt.controlBinding.requestId === binding.requestId;
81
+ && taskFinalizationReceiptState(receipt) === 'complete';
85
82
  }
86
83
  function terminalControlBindingEvidence(repoRootInput, controlRootInput, taskId, binding) {
87
84
  const repoRoot = canonicalPath(repoRootInput);
@@ -105,26 +102,22 @@ function terminalControlBindingEvidence(repoRootInput, controlRootInput, taskId,
105
102
  return false;
106
103
  }
107
104
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
108
- if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId, binding)) {
105
+ if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId)) {
109
106
  return false;
110
107
  }
111
- const status = readSandboxControlStatus(manifest.publicStatusDir);
112
- const viewReceipt = status.taskView.receipt;
113
- const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
114
- || (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
115
- if (status.generation !== binding.generation || status.state !== 'healthy'
116
- || status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
117
- || !viewReceipt || viewReceipt.generation !== binding.generation
118
- || viewReceipt.requestId !== binding.requestId
119
- || viewReceipt.receiptId !== receipt.receiptId
120
- || viewReceipt.revision !== receipt.revision)
121
- return false;
122
108
  const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
123
109
  if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file'))
124
110
  return false;
125
111
  const actual = readJsonFile(responsePath);
126
- const expected = finalizationTerminalResponse(taskId, binding.requestId, receipt);
127
- return JSON.stringify(actual) === JSON.stringify(expected);
112
+ const response = actual;
113
+ if (response.version !== 2 || response.id !== binding.requestId || response.phase !== 'completed'
114
+ || response.exitCode !== 0 || typeof response.stdout !== 'string')
115
+ return false;
116
+ const envelope = JSON.parse(response.stdout);
117
+ const result = envelope.result;
118
+ return envelope.status === 'completed' && envelope.accepted === true
119
+ && result !== null && typeof result === 'object'
120
+ && result.status === 'completed' && result.taskId === taskId;
128
121
  }
129
122
  catch {
130
123
  return false;
@@ -153,7 +146,7 @@ function removedControlBindingEvidence(repoRootInput, journal, taskId, binding)
153
146
  return false;
154
147
  try {
155
148
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
156
- return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId, binding);
149
+ return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId);
157
150
  }
158
151
  catch {
159
152
  return false;
@@ -13,7 +13,7 @@ export function parseArtifactCommand(args) {
13
13
  }
14
14
  const fields = {};
15
15
  const allowed = {
16
- inspect: ['--family'],
16
+ inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
17
17
  init: ['--family', '--artifact', '--locale'],
18
18
  'preflight': ['--family', '--artifact'],
19
19
  'finalize-local': ['--family', '--artifact']
@@ -37,8 +37,10 @@ export function parseArtifactCommand(args) {
37
37
  if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en')
38
38
  throw new Error("option '--locale' must be 'zh-CN' or 'en'");
39
39
  return {
40
- taskRef, operation: operation, family: fields['--family'],
41
- artifact: fields['--artifact'] ?? '', locale
40
+ taskRef, operation: operation, family: fields['--family'] ?? '',
41
+ artifact: fields['--artifact'] ?? '', locale,
42
+ sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
43
+ sourceSha256: fields['--source-sha256']
42
44
  };
43
45
  }
44
46
  /** Resolve authoritative task metadata separately from a trusted candidate directory. */
@@ -46,7 +48,12 @@ export function executeArtifactCommand(command, options = {}) {
46
48
  const { taskRef, operation, family, artifact, locale } = command;
47
49
  const fail = (code, message) => ({ status: 'failed', changed: false, error: { code, message } });
48
50
  if (operation === 'inspect') {
49
- const result = resolveArtifactContext(taskRef, family, { repoRoot: options.repoRoot });
51
+ const result = resolveArtifactContext(taskRef, family, {
52
+ repoRoot: options.repoRoot,
53
+ sourceFinding: command.sourceFinding,
54
+ sourceArtifact: command.sourceArtifact,
55
+ sourceSha256: command.sourceSha256
56
+ });
50
57
  return family === 'code' && result.codeMode ? {
51
58
  ...result, mode: result.codeMode.mode, code_max: result.codeMode.codeMax, rev_max: result.codeMode.reviewMax,
52
59
  verdict: result.codeMode.verdict, next_round: result.next?.round ?? null, next_artifact: result.next?.name ?? null,
@@ -8,7 +8,9 @@ import { parseVerdict } from './review-artifacts.js';
8
8
  import { extractSection, findSectionHeading } from './sections.js';
9
9
  import { receiptForOutput, sha256File } from './artifact-receipts.js';
10
10
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
11
- import { validateQualificationAudit } from './qualification-audit.js';
11
+ import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
+ import { parseReworkIntentDocument } from './rework-intent.js';
13
+ import { selectArtifactDisposition } from './artifact-selection.js';
12
14
  const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
13
15
  const ARTIFACT_STEPS = {
14
16
  analysis: 'Analyze Task', 'review-analysis': 'Review Analysis',
@@ -27,6 +29,14 @@ export function hasOpenArtifactRound(content, family, round) {
27
29
  const expected = new RegExp(`^${step} \\(Round ${round}${qualifier}\\)$`);
28
30
  return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
29
31
  }
32
+ function hasCompletedArtifactRound(content, family, round, name) {
33
+ const activity = locateActivityLog(content);
34
+ const step = ARTIFACT_STEPS[family];
35
+ if (!activity || !step)
36
+ return false;
37
+ const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
38
+ return startedBackedRows(pairEntries(activity.entries)).some((row) => expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`));
39
+ }
30
40
  const BLOCKING_DIAGNOSTICS = new Set([
31
41
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
32
42
  'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
@@ -205,15 +215,29 @@ function resolveCodeInputReceipt(taskDir, code) {
205
215
  }
206
216
  if (!receipt || receipt.event !== 'code.completed')
207
217
  return null;
208
- const plan = inspectArtifactDirectory(taskDir, 'plan');
209
- if (plan.status !== 'ready')
218
+ const identity = parseArtifactName(receipt.input);
219
+ if (!identity || !['analysis', 'plan'].includes(identity.family))
220
+ return null;
221
+ const source = inspectArtifactDirectory(taskDir, identity.family);
222
+ if (source.status !== 'ready')
210
223
  return null;
211
- const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
224
+ const input = source.artifacts.find((artifact) => artifact.name === receipt.input);
212
225
  return input ? { input, inputSha256: receipt.inputSha256 } : null;
213
226
  }
214
227
  function resolveCodePlanInput(taskDir, code) {
215
228
  return resolveCodeInputReceipt(taskDir, code);
216
229
  }
230
+ function lifecyclePathState(taskDir) {
231
+ const analysis = inspectArtifactDirectory(taskDir, 'analysis');
232
+ if (analysis.status !== 'ready' || !analysis.latest)
233
+ return { status: 'missing', decision: null, message: 'analysis artifact is missing' };
234
+ try {
235
+ return parseLifecyclePathDecision(fs.readFileSync(analysis.latest.path, 'utf8'), analysis.latest.name, sha256File(analysis.latest.path));
236
+ }
237
+ catch (error) {
238
+ return { status: 'invalid', decision: null, message: error instanceof Error ? error.message : String(error) };
239
+ }
240
+ }
217
241
  function assertWritableInventory(inventory) {
218
242
  if (inventory.status === 'failed')
219
243
  return inventory.error;
@@ -227,13 +251,87 @@ const REQUIRED_INPUT = {
227
251
  'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
228
252
  };
229
253
  const OPTIONAL_CONTEXT = {
230
- analysis: { family: 'review-analysis', requireReference: true },
231
- plan: { family: 'review-plan', requireReference: true },
254
+ analysis: { family: 'review-analysis' },
255
+ plan: { family: 'review-plan' },
232
256
  'review-code': { family: 'review-plan' },
233
257
  'manual-validation': { family: 'review-code' },
234
258
  'validation-run': { family: 'review-code' }
235
259
  };
236
- const QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS = new Set(['analysis', 'plan']);
260
+ function hasSelectionEvidence(content, family, options, context) {
261
+ if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
262
+ return true;
263
+ if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt')
264
+ return true;
265
+ if (family.startsWith('review-')) {
266
+ const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
267
+ const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
268
+ if (currentInput && context.reviewedInput?.name !== currentInput.name)
269
+ return true;
270
+ }
271
+ const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
272
+ if (target) {
273
+ const parsed = parseReworkIntentDocument(content);
274
+ if (!parsed.ok)
275
+ throw new Error(`${parsed.code}: ${parsed.message}`);
276
+ if (parsed.intents.some((intent) => intent.status === 'pending'
277
+ && (intent.target === target || family === 'analysis' && intent.target === 'plan')))
278
+ return true;
279
+ }
280
+ if (family === 'code') {
281
+ if (context.latest && context.codeMode?.mode === 'init')
282
+ return true;
283
+ if (parseImplementationInputs(content).rows.some((input) => input.needsImplementation && input.status === 'pending'))
284
+ return true;
285
+ }
286
+ return context.inputs.filter((candidate) => candidate.family.startsWith('review-')).some((input) => {
287
+ const verdict = parseVerdict(input.path);
288
+ if (!verdict.ok || verdict.verdict !== 'Changes Requested' || !context.latest)
289
+ return false;
290
+ const receipt = receiptForOutput(content, input.name);
291
+ return receipt?.event === reviewEventName(input.family)
292
+ && receipt.input === context.latest.name
293
+ && receipt.inputSha256 === sha256File(context.latest.path);
294
+ });
295
+ }
296
+ function attachArtifactSelection(context, pathState, options) {
297
+ if (context.status !== 'ready' || !context.taskDir || !context.taskId
298
+ || (context.family !== 'analysis' && pathState.status !== 'valid'))
299
+ return context;
300
+ const family = context.family;
301
+ const fail = (code, message) => ({
302
+ ...context, status: 'failed', selection: null, error: { code, message }
303
+ });
304
+ if (!context.next)
305
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', 'artifact inventory did not provide a writable identity');
306
+ let taskContent;
307
+ try {
308
+ taskContent = fs.readFileSync(path.join(context.taskDir, 'task.md'), 'utf8');
309
+ }
310
+ catch (error) {
311
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
312
+ }
313
+ const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
314
+ const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
315
+ const open = openLatest || openNext;
316
+ const selectedIdentity = openLatest ? context.latest : context.next;
317
+ try {
318
+ const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
319
+ const selection = selectArtifactDisposition({
320
+ next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
321
+ latest: context.latest,
322
+ open,
323
+ hasChangeEvidence
324
+ });
325
+ return {
326
+ ...context,
327
+ selection,
328
+ next: selection.writeRequired ? { round: selection.artifact.round, name: selection.artifact.name } : null
329
+ };
330
+ }
331
+ catch (error) {
332
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
333
+ }
334
+ }
237
335
  function resolveArtifactContext(taskRef, family, options = {}) {
238
336
  const inventory = inspectTaskArtifacts(taskRef, family, options);
239
337
  if (inventory.status === 'failed')
@@ -241,8 +339,43 @@ function resolveArtifactContext(taskRef, family, options = {}) {
241
339
  const writableError = assertWritableInventory(inventory);
242
340
  if (writableError)
243
341
  return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
244
- if (inventory.family === 'code')
245
- return resolveCodeContext(inventory, options);
342
+ const pathState = lifecyclePathState(inventory.taskDir);
343
+ if (inventory.family !== 'analysis' && pathState.status !== 'valid') {
344
+ return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
345
+ }
346
+ if (inventory.family === 'code') {
347
+ const code = resolveCodeContext(inventory, options);
348
+ const selected = attachArtifactSelection(code, pathState, options);
349
+ if (selected.status !== 'ready' || !selected.selection)
350
+ return selected;
351
+ if (selected.selection.disposition === 'reuse')
352
+ return selected;
353
+ if (selected.codeMode?.mode === 'error') {
354
+ return {
355
+ ...selected,
356
+ status: 'ready',
357
+ codeMode: {
358
+ ...selected.codeMode,
359
+ mode: 'init',
360
+ message: 'A new implementation round was requested.'
361
+ },
362
+ error: null
363
+ };
364
+ }
365
+ if (selected.codeMode?.mode === 'refused') {
366
+ return {
367
+ ...selected,
368
+ status: 'ready',
369
+ codeMode: {
370
+ ...selected.codeMode,
371
+ mode: 'init',
372
+ message: 'A new implementation round was requested.'
373
+ },
374
+ error: null
375
+ };
376
+ }
377
+ return selected;
378
+ }
246
379
  const required = REQUIRED_INPUT[inventory.family];
247
380
  const inputs = [];
248
381
  if (required) {
@@ -250,98 +383,71 @@ function resolveArtifactContext(taskRef, family, options = {}) {
250
383
  if (input.status === 'failed' || !input.latest) {
251
384
  return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
252
385
  }
253
- const qualificationError = qualificationErrorForArtifact(input.latest);
254
- if (qualificationError)
255
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
256
386
  inputs.push(input.latest);
257
387
  }
258
388
  const optional = OPTIONAL_CONTEXT[inventory.family];
259
389
  if (optional) {
260
390
  const context = inspectTaskArtifacts(taskRef, optional.family, options);
261
391
  if (context.status === 'ready' && context.latest) {
262
- if (optional.requireReference && !context.reviewedInput) {
263
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: `${context.latest.name} has no valid reviewed input` } };
264
- }
265
- const qualificationError = qualificationErrorForArtifact(context.latest);
266
- if (qualificationError && !QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS.has(inventory.family)) {
267
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
268
- }
269
392
  inputs.push(context.latest);
270
393
  }
271
394
  }
272
- return { ...inventory, inputs, codeMode: null };
273
- }
274
- function qualificationErrorForArtifact(artifact) {
275
- try {
276
- const taskContent = fs.readFileSync(path.join(path.dirname(artifact.path), 'task.md'), 'utf8');
277
- const artifactContent = fs.readFileSync(artifact.path, 'utf8');
278
- const result = validateQualificationAudit(taskContent, artifactContent);
279
- return result.ok ? null : `${result.code}: ${result.message}`;
280
- }
281
- catch (error) {
282
- return error instanceof Error ? error.message : String(error);
395
+ if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
396
+ return { ...inventory, inputs, codeMode: null };
283
397
  }
398
+ return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
284
399
  }
285
400
  function resolveCodeContext(inventory, options) {
286
401
  const taskRef = inventory.requestRef;
287
- const plan = inspectTaskArtifacts(taskRef, 'plan', options);
288
- if (plan.status === 'failed' || !plan.latest)
289
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest plan artifact is required');
290
- const reviewPlan = inspectTaskArtifacts(taskRef, 'review-plan', options);
402
+ const pathState = lifecyclePathState(inventory.taskDir);
403
+ if (pathState.status !== 'valid')
404
+ return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
405
+ const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
406
+ const source = inspectTaskArtifacts(taskRef, inputFamily, options);
407
+ if (source.status === 'failed' || !source.latest)
408
+ return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
291
409
  const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
292
410
  const latestCode = inventory.latest;
293
411
  const codeMax = latestCode?.round ?? 0;
294
412
  const reviewMax = reviewCode.latest?.round ?? 0;
295
- const inputs = [plan.latest];
296
- const planQualificationError = qualificationErrorForArtifact(plan.latest);
297
- if (planQualificationError)
298
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', planQualificationError);
413
+ const inputs = [source.latest];
299
414
  if (latestCode) {
300
- const qualificationError = qualificationErrorForArtifact(latestCode);
301
- if (qualificationError)
302
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, latestCode.name);
303
- }
304
- if (reviewCode.latest) {
305
- const qualificationError = qualificationErrorForArtifact(reviewCode.latest);
306
- if (qualificationError)
307
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewCode.latest.name);
308
- }
309
- if (!latestCode) {
310
- if (!reviewPlan.latest || reviewPlan.reviewedInput?.name !== plan.latest.name) {
311
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest plan '${plan.latest.name}' requires a matching approved review-plan`);
415
+ try {
416
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
417
+ if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
418
+ const review = reviewCode.latest;
419
+ const openInputs = review ? [...inputs, review] : inputs;
420
+ const openVerdict = review ? parseVerdict(review.path) : null;
421
+ const openRow = locateActivityLog(taskContent)
422
+ ? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
423
+ .find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
424
+ : null;
425
+ const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
426
+ const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
427
+ const mode = decisionInput ? 'decision'
428
+ : fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
429
+ : 'init';
430
+ return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${latestCode.round}.`, decisionInput);
431
+ }
312
432
  }
313
- const verdict = parseVerdict(reviewPlan.latest.path);
314
- if (!verdict.ok)
315
- return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
316
- if (verdict.verdict !== 'Approved') {
317
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `latest ${reviewPlan.latest.name} is not approved`, reviewPlan.latest.name);
433
+ catch (error) {
434
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
318
435
  }
436
+ }
437
+ if (!latestCode) {
319
438
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, 'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
320
439
  }
321
440
  const codePlanInput = resolveCodePlanInput(inventory.taskDir, latestCode);
322
441
  if (!codePlanInput) {
323
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
324
- }
325
- if (reviewPlan.latest && !reviewPlan.reviewedInput) {
326
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
327
- }
328
- if (reviewPlan.latest) {
329
- const qualificationError = qualificationErrorForArtifact(reviewPlan.latest);
330
- if (qualificationError)
331
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewPlan.latest.name);
332
- }
333
- if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
334
- const verdict = parseVerdict(reviewPlan.latest.path);
335
- if (!verdict.ok)
336
- return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
337
- const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
338
- if (verdict.ok && verdict.verdict === 'Approved' && (codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256)) {
339
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
340
- }
442
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null, `Code input receipt for ${latestCode.name} is unavailable.`);
443
+ }
444
+ const currentInputSha256 = sha256File(source.latest.path);
445
+ if (codePlanInput.input.name !== source.latest.name || codePlanInput.inputSha256 !== currentInputSha256) {
446
+ return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, `Latest ${source.latest.name} is not captured by the latest code input receipt. Entering replan-driven init.`);
341
447
  }
342
448
  if (!reviewCode.latest || reviewCode.reviewedInput?.name !== latestCode.name) {
343
449
  const expected = reviewCode.next?.name ?? artifactName('review-code', reviewMax + 1);
344
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `${expected} is required before another code round`, expected);
450
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, expected, `${expected} is required before another code round`);
345
451
  }
346
452
  const review = reviewCode.latest;
347
453
  if (!review)
@@ -349,18 +455,31 @@ function resolveCodeContext(inventory, options) {
349
455
  const verdict = parseVerdict(review.path);
350
456
  if (!verdict.ok)
351
457
  return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, review.name);
352
- if (verdict.verdict === 'Approved') {
353
- const decision = resolveDecisionImplementationInput(inventory, review);
354
- if ('error' in decision)
355
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
356
- if (decision.input) {
357
- return withCodeMode(inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax, verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`, decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence);
458
+ const decision = resolveDecisionImplementationInput(inventory, review);
459
+ if ('error' in decision)
460
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
461
+ if (decision.input) {
462
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax, verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`, decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence);
463
+ }
464
+ try {
465
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
466
+ const rework = parseReworkIntentDocument(taskContent);
467
+ const reviewSha256 = sha256File(review.path);
468
+ if (rework.ok && rework.intents.some((intent) => intent.status === 'pending'
469
+ && intent.target === 'code'
470
+ && intent.sourceArtifact === review.name
471
+ && intent.sourceSha256 === reviewSha256)) {
472
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name, `Pending rework evidence requires a new code round after ${review.name}.`);
358
473
  }
359
474
  }
360
- if (verdict.verdict === 'Approved' || verdict.verdict === 'Rejected') {
361
- return withCodeMode(inventory, [...inputs, review], 'refused', 'refused', codeMax, reviewMax, verdict.verdict, review.name, verdict.verdict === 'Approved'
362
- ? `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`
363
- : `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
475
+ catch (error) {
476
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error), review.name);
477
+ }
478
+ if (verdict.verdict === 'Approved') {
479
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`);
480
+ }
481
+ if (verdict.verdict === 'Rejected') {
482
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
364
483
  }
365
484
  return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} requires or permits fixes. Entering fix mode.`);
366
485
  }
@@ -439,8 +558,21 @@ export function validateArtifactPublication(taskDir, family, name) {
439
558
  const error = assertWritableInventory(inventory);
440
559
  if (error)
441
560
  return error;
442
- if (inventory.next?.name === name || inventory.artifacts.some((artifact) => artifact.name === name))
561
+ if (inventory.next?.name === name)
443
562
  return null;
563
+ const existing = inventory.artifacts.find((artifact) => artifact.name === name);
564
+ if (existing) {
565
+ try {
566
+ const task = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
567
+ if (hasCompletedArtifactRound(task, family, existing.round, name)) {
568
+ return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
569
+ }
570
+ }
571
+ catch (error) {
572
+ return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
573
+ }
574
+ return null;
575
+ }
444
576
  return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
445
577
  }
446
578
  function buildArtifactLinkSection(content, artifact) {
@@ -6,7 +6,6 @@ import { parseArtifactName } from './artifact-name.js';
6
6
  import { scanVisibleMarkdown } from './markdown.js';
7
7
  import { getArtifactSchema, renderArtifactSkeleton } from './artifact-schema.js';
8
8
  import { withTaskExecutionLock } from './task-execution-lock.js';
9
- import { buildQualificationAudit, expectedQualificationRelations, parseTaskQualification, renderQualificationAudit } from './qualification-audit.js';
10
9
  function sha256Content(content) {
11
10
  return createHash('sha256').update(content, 'utf8').digest('hex');
12
11
  }
@@ -194,27 +193,6 @@ function artifactRound(family, artifact) {
194
193
  const identity = parseArtifactName(artifact);
195
194
  return identity?.family === family ? identity.round : null;
196
195
  }
197
- function qualificationAuditForInit(request) {
198
- let taskContent;
199
- try {
200
- taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
201
- }
202
- catch (error) {
203
- return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
204
- }
205
- const qualification = parseTaskQualification(taskContent);
206
- if (!qualification.ok)
207
- return qualification;
208
- if (!qualification.qualification.present)
209
- return { ok: true, content: '' };
210
- const relations = expectedQualificationRelations(taskContent, request.family);
211
- if (!relations.ok)
212
- return relations;
213
- const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
214
- if (!audit.ok)
215
- return audit;
216
- return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
217
- }
218
196
  function initializeArtifactSkeleton(request) {
219
197
  const schema = getArtifactSchema(request.family);
220
198
  if (!schema)
@@ -244,10 +222,7 @@ function initializeArtifactSkeleton(request) {
244
222
  artifact: request.artifact,
245
223
  ...(request.locale ? { locale: request.locale } : {})
246
224
  });
247
- const audit = qualificationAuditForInit(request);
248
- if (!audit.ok)
249
- return resultFailure(audit.code, audit.message);
250
- const content = `${skeleton.trimEnd()}${audit.content}`;
225
+ const content = skeleton.trimEnd();
251
226
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
252
227
  try {
253
228
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
@@ -36,7 +36,10 @@ function validateReceiptShape(event, output, input) {
36
36
  if (!outputIdentity || !inputIdentity)
37
37
  throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
38
38
  const shape = RECEIPT_SHAPES[event];
39
- if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
39
+ const inputMatches = event === 'code.completed'
40
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
41
+ : inputIdentity.family === shape.input;
42
+ if (outputIdentity.family !== shape.output || !inputMatches) {
40
43
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
41
44
  }
42
45
  }
@@ -7,6 +7,7 @@ const LOCAL_SECTIONS = {
7
7
  ['impact', '影响评估', 'Impact Assessment'],
8
8
  ['risks', '技术风险', 'Technical Risks'],
9
9
  ['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
10
+ ['flow-decision', '流程裁定', 'Flow Decision'],
10
11
  ['state-check', '状态核对', 'State Check']
11
12
  ],
12
13
  plan: [
@@ -0,0 +1,25 @@
1
+ function selectArtifactDisposition(request) {
2
+ if (request.open) {
3
+ return {
4
+ disposition: 'resume', reasonCode: 'open-round', artifact: request.next,
5
+ writeRequired: true
6
+ };
7
+ }
8
+ if (!request.latest) {
9
+ return {
10
+ disposition: 'create',
11
+ reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
12
+ artifact: request.next, writeRequired: true
13
+ };
14
+ }
15
+ const reasonCode = request.hasChangeEvidence ? 'change-evidence' : 'history-matched';
16
+ const reuse = reasonCode === 'history-matched';
17
+ return {
18
+ disposition: reuse ? 'reuse' : 'create',
19
+ reasonCode,
20
+ artifact: reuse ? request.latest : request.next,
21
+ writeRequired: !reuse
22
+ };
23
+ }
24
+ export { selectArtifactDisposition };
25
+ //# sourceMappingURL=artifact-selection.js.map