@fitlab-ai/agent-infra 0.11.3 → 0.11.5

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/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -6
  4. package/dist/lib/decide.js +4 -41
  5. package/dist/lib/git/branch-worktree.js +45 -0
  6. package/dist/lib/internal/cli-route-inventory.js +1 -4
  7. package/dist/lib/internal/task-activity.js +7 -40
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-event.js +4 -37
  10. package/dist/lib/internal/task-finalization.js +48 -1
  11. package/dist/lib/internal/task-ledger.js +14 -44
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +2 -4
  14. package/dist/lib/internal/task-review.js +5 -37
  15. package/dist/lib/internal/task-verify.js +3 -47
  16. package/dist/lib/internal/task-warning.js +4 -40
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/verification-sync.js +12 -49
  20. package/dist/lib/sandbox/control/client.js +4 -1
  21. package/dist/lib/sandbox/control/executor.js +12 -4
  22. package/dist/lib/sandbox/control/protocol.js +2 -1
  23. package/dist/lib/sandbox/control/server.js +26 -143
  24. package/dist/lib/sandbox/removal.js +15 -8
  25. package/dist/lib/sandbox/task-cleanup.js +13 -20
  26. package/dist/lib/task/activity-intent.js +1 -2
  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 +37 -28
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +8 -7
  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 +12 -168
  36. package/dist/lib/task/decision-intents.js +14 -4
  37. package/dist/lib/task/delivery.js +1 -43
  38. package/dist/lib/task/events.js +68 -73
  39. package/dist/lib/task/finalization-handoff.js +92 -0
  40. package/dist/lib/task/finalization.js +423 -154
  41. package/dist/lib/task/intermediate-cleanup.js +9 -148
  42. package/dist/lib/task/ledger-intents.js +159 -20
  43. package/dist/lib/task/lifecycle-path.js +75 -0
  44. package/dist/lib/task/lifecycle.js +60 -11
  45. package/dist/lib/task/local-artifact-finalization.js +13 -47
  46. package/dist/lib/task/orchestration.js +17 -7
  47. package/dist/lib/task/review-command.js +1 -2
  48. package/dist/lib/task/review-finalization.js +4 -18
  49. package/dist/lib/task/rework-intent.js +70 -4
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +25 -30
  52. package/dist/lib/task/verification.js +2 -13
  53. package/dist/lib/task/workflow-warning-intents.js +23 -10
  54. package/dist/lib/task/workspace-snapshot.js +7 -2
  55. package/dist/lib/task/write.js +1 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/decide.ts +5 -35
  58. package/lib/git/branch-worktree.ts +41 -0
  59. package/lib/internal/cli-route-inventory.ts +1 -3
  60. package/lib/internal/task-activity.ts +6 -34
  61. package/lib/internal/task-artifact.ts +1 -1
  62. package/lib/internal/task-event.ts +4 -37
  63. package/lib/internal/task-finalization.ts +47 -1
  64. package/lib/internal/task-ledger.ts +15 -41
  65. package/lib/internal/task-lifecycle.ts +2 -2
  66. package/lib/internal/task-operation-registry.ts +2 -4
  67. package/lib/internal/task-review.ts +5 -31
  68. package/lib/internal/task-verify.ts +3 -39
  69. package/lib/internal/task-warning.ts +3 -32
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/pr-summary.ts +4 -25
  72. package/lib/platform/verification-sync.ts +13 -53
  73. package/lib/sandbox/control/client.ts +5 -1
  74. package/lib/sandbox/control/executor.ts +13 -4
  75. package/lib/sandbox/control/protocol.ts +3 -2
  76. package/lib/sandbox/control/server.ts +21 -146
  77. package/lib/sandbox/removal.ts +23 -7
  78. package/lib/sandbox/task-cleanup.ts +12 -20
  79. package/lib/task/activity-intent.ts +1 -2
  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 +59 -28
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +10 -7
  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 +17 -189
  89. package/lib/task/decision-intents.ts +11 -4
  90. package/lib/task/delivery.ts +1 -40
  91. package/lib/task/events.ts +73 -74
  92. package/lib/task/finalization-handoff.ts +104 -0
  93. package/lib/task/finalization.ts +388 -122
  94. package/lib/task/intermediate-cleanup.ts +9 -125
  95. package/lib/task/ledger-intents.ts +157 -20
  96. package/lib/task/lifecycle-path.ts +98 -0
  97. package/lib/task/lifecycle.ts +58 -13
  98. package/lib/task/local-artifact-finalization.ts +14 -56
  99. package/lib/task/orchestration.ts +26 -8
  100. package/lib/task/review-command.ts +2 -5
  101. package/lib/task/review-finalization.ts +4 -20
  102. package/lib/task/rework-intent.ts +75 -5
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +29 -30
  105. package/lib/task/verification.ts +3 -16
  106. package/lib/task/workflow-warning-intents.ts +20 -10
  107. package/lib/task/workspace-snapshot.ts +3 -2
  108. package/lib/task/write.ts +1 -4
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/review-handshake.en.md +4 -4
  111. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  112. package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
  113. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
  114. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  115. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  116. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  117. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  118. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  119. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  120. package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
  121. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
  122. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  125. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  128. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  129. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  130. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  131. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  132. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  137. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  141. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  143. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  144. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  145. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  146. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  147. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  148. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  149. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  150. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  151. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  152. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  153. package/templates/.agents/templates/task.en.md +2 -2
  154. package/templates/.agents/templates/task.zh-CN.md +2 -2
  155. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  156. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  157. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  158. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  159. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  160. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  161. package/dist/lib/internal/task-override.js +0 -138
  162. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  163. package/dist/lib/task/artifact-recovery.js +0 -476
  164. package/dist/lib/task/artifact-repair-intent.js +0 -181
  165. package/dist/lib/task/guard-override.js +0 -31
  166. package/dist/lib/task/human-override.js +0 -1259
  167. package/lib/internal/task-override.ts +0 -144
  168. package/lib/sandbox/control/finalization-response.ts +0 -35
  169. package/lib/task/artifact-recovery.ts +0 -572
  170. package/lib/task/artifact-repair-intent.ts +0 -227
  171. package/lib/task/guard-override.ts +0 -50
  172. package/lib/task/human-override.ts +0 -1522
@@ -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
 
@@ -10,12 +10,6 @@ import {
10
10
  renderArtifactSkeleton
11
11
  } from './artifact-schema.ts';
12
12
  import { withTaskExecutionLock } from './task-execution-lock.ts';
13
- import {
14
- buildQualificationAudit,
15
- expectedQualificationRelations,
16
- parseTaskQualification,
17
- renderQualificationAudit
18
- } from './qualification-audit.ts';
19
13
  import type {
20
14
  ArtifactSchema,
21
15
  ArtifactSchemaFamily,
@@ -33,6 +27,7 @@ type ArtifactStructuralDiagnosticCode =
33
27
  | 'ARTIFACT_EMPTY_SECTION'
34
28
  | 'ARTIFACT_SECTION_ORDER_INVALID'
35
29
  | 'ARTIFACT_HEADING_TRAILING_PUNCTUATION'
30
+ | 'ARTIFACT_STATE_CHECK_PATTERN_MISSING'
36
31
  | 'ARTIFACT_REQUIRED_PATTERN_MISSING';
37
32
 
38
33
  type ArtifactStructuralDiagnostic = Readonly<{
@@ -260,13 +255,68 @@ function inspectArtifactPatterns(
260
255
  };
261
256
  }
262
257
 
258
+ function inspectArtifactStateCheck(
259
+ content: string,
260
+ schema: ArtifactSchema,
261
+ scopeToStateCheckSection: boolean
262
+ ): ArtifactStructureResult {
263
+ const stateCheck = schema.sections.find((section) => section.id === 'state-check');
264
+ if (!stateCheck || schema.stateCheckPatterns.length === 0) {
265
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
266
+ }
267
+
268
+ if (!scopeToStateCheckSection) {
269
+ const diagnostics = schema.stateCheckPatterns
270
+ .filter((pattern) => !new RegExp(pattern, 'm').test(content))
271
+ .map((pattern) => diagnostic(
272
+ 'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
273
+ `artifact is missing required state-check command '${pattern}'`,
274
+ stateCheck.id,
275
+ null
276
+ ));
277
+ return {
278
+ ok: diagnostics.length === 0,
279
+ family: schema.family,
280
+ semanticDigest: canonicalSemanticDigest(content),
281
+ diagnostics
282
+ };
283
+ }
284
+
285
+ const scanned = scanVisibleMarkdown(content);
286
+ const headings = scanned.headings.filter((heading) => (
287
+ heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)
288
+ ));
289
+ if (headings.length !== 1) {
290
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
291
+ }
292
+
293
+ const bodyBounds = sectionBodyBounds(content, headings[0]!, scanned);
294
+ const body = content.slice(bodyBounds.start, bodyBounds.end);
295
+ const diagnostics = schema.stateCheckPatterns
296
+ .filter((pattern) => !new RegExp(pattern, 'm').test(body))
297
+ .map((pattern) => diagnostic(
298
+ 'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
299
+ `state-check section is missing required command '${pattern}'`,
300
+ stateCheck.id,
301
+ lineNumber(content, headings[0]!.start)
302
+ ));
303
+ return {
304
+ ok: diagnostics.length === 0,
305
+ family: schema.family,
306
+ semanticDigest: canonicalSemanticDigest(content),
307
+ diagnostics
308
+ };
309
+ }
310
+
263
311
  function inspectArtifactContract(
264
312
  content: string,
265
- schema: ArtifactSchema
313
+ schema: ArtifactSchema,
314
+ options: { scopeStateCheckSection?: boolean } = {}
266
315
  ): ArtifactStructureResult {
267
316
  const structure = inspectArtifactStructure(content, schema);
317
+ const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
268
318
  const patterns = inspectArtifactPatterns(content, schema);
269
- const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
319
+ const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
270
320
  return {
271
321
  ok: diagnostics.length === 0,
272
322
  family: schema.family,
@@ -294,23 +344,6 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
294
344
  return identity?.family === family ? identity.round : null;
295
345
  }
296
346
 
297
- function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
298
- let taskContent: string;
299
- try {
300
- taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
301
- } catch (error) {
302
- return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
303
- }
304
- const qualification = parseTaskQualification(taskContent);
305
- if (!qualification.ok) return qualification;
306
- if (!qualification.qualification.present) return { ok: true, content: '' };
307
- const relations = expectedQualificationRelations(taskContent, request.family);
308
- if (!relations.ok) return relations;
309
- const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
310
- if (!audit.ok) return audit;
311
- return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
312
- }
313
-
314
347
  function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
315
348
  const schema = getArtifactSchema(request.family);
316
349
  if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
@@ -333,9 +366,7 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
333
366
  artifact: request.artifact,
334
367
  ...(request.locale ? { locale: request.locale } : {})
335
368
  });
336
- const audit = qualificationAuditForInit(request);
337
- if (!audit.ok) return resultFailure(audit.code, audit.message);
338
- const content = `${skeleton.trimEnd()}${audit.content}`;
369
+ const content = skeleton.trimEnd();
339
370
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
340
371
  try {
341
372
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
@@ -64,7 +64,10 @@ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input
64
64
  const inputIdentity = parseArtifactName(input);
65
65
  if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
66
66
  const shape = RECEIPT_SHAPES[event];
67
- if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
67
+ const inputMatches = event === 'code.completed'
68
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
69
+ : inputIdentity.family === shape.input;
70
+ if (outputIdentity.family !== shape.output || !inputMatches) {
68
71
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
69
72
  }
70
73
  }