@fitlab-ai/agent-infra 0.11.9 → 0.11.10

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 (124) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/git/workflow.js +1 -1
  3. package/dist/lib/internal/cli-route-inventory.js +0 -3
  4. package/dist/lib/internal/sandbox-control.js +30 -5
  5. package/dist/lib/internal/task-finalization.js +14 -23
  6. package/dist/lib/internal/task-operation-registry.js +2 -3
  7. package/dist/lib/internal/task-platform-recovery.js +9 -4
  8. package/dist/lib/platform/completion-backfill.js +2 -1
  9. package/dist/lib/platform/context.js +4 -2
  10. package/dist/lib/platform/github-client.js +20 -17
  11. package/dist/lib/platform/github-provider.js +42 -22
  12. package/dist/lib/platform/github-release-notes.js +4 -2
  13. package/dist/lib/platform/in-label-sync.js +2 -1
  14. package/dist/lib/platform/issue-comments.js +13 -9
  15. package/dist/lib/platform/operation-coordinator.js +1 -1
  16. package/dist/lib/platform/pr-review.js +2 -1
  17. package/dist/lib/platform/provider-bridge.js +4 -5
  18. package/dist/lib/platform/provider-contract.d.ts +3 -6
  19. package/dist/lib/platform/provider-contract.js +3 -15
  20. package/dist/lib/platform/provider-validation.js +9 -4
  21. package/dist/lib/platform/pull-requests.js +4 -2
  22. package/dist/lib/sandbox/control/client.js +54 -6
  23. package/dist/lib/sandbox/control/executor.js +8 -3
  24. package/dist/lib/sandbox/removal.js +0 -1
  25. package/dist/lib/task/artifact-command.js +11 -1
  26. package/dist/lib/task/artifact-lifecycle.js +119 -62
  27. package/dist/lib/task/artifact-selection.js +1 -1
  28. package/dist/lib/task/capabilities.js +55 -21
  29. package/dist/lib/task/commands/status.js +0 -27
  30. package/dist/lib/task/commit-identity.js +22 -7
  31. package/dist/lib/task/commit-intent.js +67 -26
  32. package/dist/lib/task/commit-operation.js +19 -8
  33. package/dist/lib/task/completion-facts.js +32 -10
  34. package/dist/lib/task/control-authority.js +2 -4
  35. package/dist/lib/task/current-contract.js +1 -5
  36. package/dist/lib/task/events.js +8 -274
  37. package/dist/lib/task/execution-inputs.js +14 -0
  38. package/dist/lib/task/finalization-envelope.js +4 -0
  39. package/dist/lib/task/finalization.js +10 -36
  40. package/dist/lib/task/frontmatter.js +1 -1
  41. package/dist/lib/task/ledger-intents.js +4 -7
  42. package/dist/lib/task/orchestration.js +11 -10
  43. package/dist/lib/task/platform-operation-journal.js +17 -10
  44. package/dist/lib/task/platform-operation-recovery.js +95 -28
  45. package/dist/lib/task/review-fingerprint.js +4 -26
  46. package/dist/lib/task/verification-engine.js +95 -6
  47. package/dist/lib/task/write.js +0 -11
  48. package/dist/lifecycle-build-manifest.json +3 -3
  49. package/lib/git/workflow.ts +1 -1
  50. package/lib/internal/cli-route-inventory.ts +0 -2
  51. package/lib/internal/sandbox-control.ts +39 -6
  52. package/lib/internal/task-finalization.ts +16 -24
  53. package/lib/internal/task-operation-registry.ts +2 -3
  54. package/lib/internal/task-platform-recovery.ts +9 -4
  55. package/lib/platform/completion-backfill.ts +2 -1
  56. package/lib/platform/context.ts +8 -4
  57. package/lib/platform/github-client.ts +24 -20
  58. package/lib/platform/github-provider.ts +47 -26
  59. package/lib/platform/github-release-notes.ts +5 -3
  60. package/lib/platform/in-label-sync.ts +2 -1
  61. package/lib/platform/issue-comments.ts +14 -9
  62. package/lib/platform/operation-coordinator.ts +1 -1
  63. package/lib/platform/pr-review.ts +2 -1
  64. package/lib/platform/provider-bridge.ts +10 -9
  65. package/lib/platform/provider-contract.ts +6 -21
  66. package/lib/platform/provider-loader.ts +7 -6
  67. package/lib/platform/provider-validation.ts +30 -8
  68. package/lib/platform/pull-requests.ts +4 -2
  69. package/lib/platform/repository-metadata.ts +3 -2
  70. package/lib/platform/security-alerts.ts +1 -1
  71. package/lib/platform/types.ts +2 -0
  72. package/lib/sandbox/control/client.ts +57 -6
  73. package/lib/sandbox/control/executor.ts +8 -3
  74. package/lib/sandbox/removal.ts +2 -10
  75. package/lib/task/artifact-command.ts +10 -1
  76. package/lib/task/artifact-lifecycle.ts +100 -52
  77. package/lib/task/artifact-selection.ts +1 -1
  78. package/lib/task/capabilities.ts +47 -24
  79. package/lib/task/commands/status.ts +0 -31
  80. package/lib/task/commit-identity.ts +24 -7
  81. package/lib/task/commit-intent.ts +58 -24
  82. package/lib/task/commit-operation.ts +17 -8
  83. package/lib/task/completion-facts.ts +31 -9
  84. package/lib/task/control-authority.ts +1 -4
  85. package/lib/task/current-contract.ts +1 -6
  86. package/lib/task/events.ts +8 -247
  87. package/lib/task/execution-inputs.ts +25 -0
  88. package/lib/task/finalization-envelope.ts +21 -0
  89. package/lib/task/finalization.ts +10 -35
  90. package/lib/task/frontmatter.ts +1 -1
  91. package/lib/task/ledger-intents.ts +4 -7
  92. package/lib/task/orchestration.ts +11 -17
  93. package/lib/task/platform-operation-journal.ts +18 -11
  94. package/lib/task/platform-operation-recovery.ts +90 -31
  95. package/lib/task/review-fingerprint.ts +4 -26
  96. package/lib/task/verification-engine.ts +103 -9
  97. package/lib/task/write.ts +0 -19
  98. package/package.json +1 -1
  99. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  100. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  101. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  102. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  104. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
  105. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  106. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  107. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  108. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  109. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  110. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  113. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/templates/task.zh-CN.md +0 -12
  116. package/templates/.github/workflows/pr-label.yml +1 -2
  117. package/dist/lib/internal/task-invalidation.js +0 -63
  118. package/dist/lib/task/intermediate-cleanup.js +0 -327
  119. package/dist/lib/task/invalidation-command.js +0 -88
  120. package/dist/lib/task/invalidation.js +0 -242
  121. package/lib/internal/task-invalidation.ts +0 -41
  122. package/lib/task/intermediate-cleanup.ts +0 -393
  123. package/lib/task/invalidation-command.ts +0 -88
  124. package/lib/task/invalidation.ts +0 -313
@@ -6,19 +6,19 @@ import { resolveTaskRef } from './resolve-ref.ts';
6
6
  import type { ResolveTaskRefErrorCode, TaskWorkspaceState } from './resolve-ref.ts';
7
7
  import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
8
8
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
9
+ import { parseTypedTaskFrontmatter } from './frontmatter.ts';
9
10
  import { parseVerdict } from './review-artifacts.ts';
10
11
  import { extractSection, findSectionHeading } from './sections.ts';
11
- import { receiptForOutput, sha256File } from './artifact-receipts.ts';
12
- import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
12
+ import { receiptForOutput, receiptsForOutput, sha256File } from './artifact-receipts.ts';
13
+ import { artifactCompletionOrder, hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.ts';
13
14
  import { inspectReviewIdentity } from './review-identity.ts';
14
- import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
15
- import type { InvalidationDocument } from './invalidation.ts';
16
15
  import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
17
16
  import { parseLifecyclePathDecision } from './lifecycle-path.ts';
18
17
  import type { LifecyclePathState } from './lifecycle-path.ts';
19
18
  import { parseReworkIntentDocument } from './rework-intent.ts';
20
19
  import { selectArtifactDisposition } from './artifact-selection.ts';
21
20
  import type { ArtifactSelection } from './artifact-selection.ts';
21
+ import { executionInputsMatchLatest } from './execution-inputs.ts';
22
22
 
23
23
  const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
24
24
  const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
@@ -29,24 +29,37 @@ const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
29
29
 
30
30
  /** Init, repair and finalization share the same authoritative round identity. */
31
31
  export function hasOpenArtifactRound(content: string, family: string, round: number): boolean {
32
+ return openArtifactRounds(content, family).includes(round);
33
+ }
34
+
35
+ function openArtifactRounds(content: string, family: string): number[] {
32
36
  const activity = locateActivityLog(content);
33
37
  const step = ARTIFACT_STEPS[family];
34
- if (!activity || !step) return false;
38
+ if (!activity || !step) return [];
35
39
  const qualifier = family === 'code'
36
40
  ? '(?:, (?:fix for review-code(?:-r(?:[2-9]|[1-9]\\d+))?\\.md|decision II-[1-9]\\d*))?'
37
41
  : '';
38
- const expected = new RegExp(`^${step} \\(Round ${round}${qualifier}\\)$`);
39
- return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
42
+ const expected = new RegExp(`^${step} \\(Round ([1-9]\\d*)${qualifier}\\)$`);
43
+ return [...new Set(startedBackedRows(pairEntries(activity.entries))
44
+ .filter((row) => expected.test(row.step) && !row.done)
45
+ .map((row) => Number(expected.exec(row.step)?.[1])))].sort((left, right) => left - right);
40
46
  }
41
47
 
42
48
  function hasCompletedArtifactRound(content: string, family: string, round: number, name: string): boolean {
43
- const activity = locateActivityLog(content);
44
- const step = ARTIFACT_STEPS[family];
45
- if (!activity || !step) return false;
46
- const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
47
- return startedBackedRows(pairEntries(activity.entries)).some((row) =>
48
- expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`)
49
- );
49
+ const event = completionEvent(family);
50
+ return Boolean(event && parseArtifactName(name)?.round === round
51
+ && artifactCompletionOrder(content, name, event) !== null
52
+ && hasArtifactCompletionFact(content, name, event));
53
+ }
54
+
55
+ function completionEvent(family: string): string | null {
56
+ if (family === 'analysis') return 'analyze.completed';
57
+ if (family === 'review-analysis') return 'review-analysis.completed';
58
+ if (family === 'plan') return 'plan.completed';
59
+ if (family === 'review-plan') return 'review-plan.completed';
60
+ if (family === 'code') return 'code.completed';
61
+ if (family === 'review-code') return 'review-code.completed';
62
+ return null;
50
63
  }
51
64
 
52
65
  type ArtifactIdentity = {
@@ -60,7 +73,7 @@ type ArtifactIdentity = {
60
73
  type ArtifactDiagnosticCode =
61
74
  | 'NONCANONICAL_NAME' | 'ROUND_OUT_OF_RANGE' | 'MISSING_BASE' | 'ROUND_GAP'
62
75
  | 'DUPLICATE_LOGICAL_ROUND' | 'NON_REGULAR_FILE' | 'SYMBOLIC_LINK'
63
- | 'FILESYSTEM_RACE' | 'CATALOG_CONFLICT' | 'BROKEN_REFERENCE';
76
+ | 'FILESYSTEM_RACE' | 'CATALOG_CONFLICT' | 'BROKEN_REFERENCE' | 'MULTIPLE_OPEN_ROUNDS';
64
77
  type ArtifactDiagnostic = {
65
78
  code: ArtifactDiagnosticCode;
66
79
  family: ArtifactFamily;
@@ -74,7 +87,7 @@ type ArtifactErrorCode =
74
87
  | 'LIFECYCLE_PATH_INVALID'
75
88
  | 'ARTIFACT_IDENTITY_INVALID' | 'ARTIFACT_NOT_FOUND'
76
89
  | 'ARTIFACT_NOT_REGULAR' | 'ARTIFACT_NOT_READABLE' | 'ARTIFACT_VERDICT_INVALID'
77
- | 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
90
+ | 'ARTIFACT_MODE_REFUSED'
78
91
  | 'ARTIFACT_SELECTION_INDETERMINATE';
79
92
 
80
93
  type ArtifactError = { code: ArtifactErrorCode; message: string };
@@ -87,8 +100,10 @@ type ArtifactInventoryResult = {
87
100
  taskState: TaskWorkspaceState | null;
88
101
  family: ArtifactFamily | string;
89
102
  artifacts: readonly ArtifactIdentity[];
103
+ completed: readonly ArtifactIdentity[];
90
104
  latest: ArtifactIdentity | null;
91
105
  next: { round: number; name: string } | null;
106
+ openRounds: readonly number[];
92
107
  reviewedInput: ArtifactIdentity | null;
93
108
  diagnostics: readonly ArtifactDiagnostic[];
94
109
  error: ArtifactError | null;
@@ -122,7 +137,7 @@ type InspectOptions = {
122
137
  const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
123
138
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
124
139
  'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
125
- 'FILESYSTEM_RACE', 'CATALOG_CONFLICT'
140
+ 'FILESYSTEM_RACE', 'CATALOG_CONFLICT', 'MULTIPLE_OPEN_ROUNDS'
126
141
  ]);
127
142
 
128
143
  function familySpec(family: string): ArtifactFamilySpec | null {
@@ -136,7 +151,7 @@ function escapeRegExp(value: string): string {
136
151
  function failure(requestRef: string, family: string, error: ArtifactError, extra: Partial<ArtifactInventoryResult> = {}): ArtifactInventoryResult {
137
152
  return {
138
153
  status: 'failed', changed: false, requestRef, taskId: null, taskDir: null,
139
- taskState: null, family, artifacts: [], latest: null, next: null,
154
+ taskState: null, family, artifacts: [], completed: [], latest: null, next: null, openRounds: [],
140
155
  reviewedInput: null, diagnostics: [], error, ...extra
141
156
  };
142
157
  }
@@ -168,18 +183,11 @@ function inspectArtifactDirectory(
168
183
  taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
169
184
  });
170
185
  }
171
- let invalidation: InvalidationDocument = { operations: [], targets: [] };
186
+ let taskContent: string;
172
187
  try {
173
- const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
174
- const parsed = parseInvalidationDocument(taskContent);
175
- if (!parsed.ok) {
176
- return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: parsed.message }, {
177
- taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
178
- });
179
- }
180
- invalidation = parsed.document;
188
+ taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
181
189
  } catch (error) {
182
- return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: String(error) }, {
190
+ return failure(requestRef, family, { code: 'ARTIFACT_DIRECTORY_READ_FAILED', message: String(error) }, {
183
191
  taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
184
192
  });
185
193
  }
@@ -216,23 +224,34 @@ function inspectArtifactDirectory(
216
224
  }
217
225
  const identity = { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
218
226
  knownArtifacts.push(identity);
219
- if (!isArtifactInvalidated(invalidation, family, entry.name)) artifacts.push(identity);
227
+ artifacts.push(identity);
220
228
  } catch (error) {
221
229
  diagnostics.push(diagnostic('FILESYSTEM_RACE', family, entry.name, String(error)));
222
230
  }
223
231
  }
224
232
  artifacts.sort((left, right) => left.round - right.round);
225
233
  knownArtifacts.sort((left, right) => left.round - right.round);
234
+ const openRounds = openArtifactRounds(taskContent, family);
226
235
  const knownRounds = new Set(knownArtifacts.map((item) => item.round));
227
- if (knownArtifacts.length > 0 && !knownRounds.has(1)) diagnostics.push(diagnostic('MISSING_BASE', family, null, `${family}.md is missing`));
228
- const max = knownArtifacts.at(-1)?.round ?? 0;
236
+ for (const round of openRounds) knownRounds.add(round);
237
+ const max = Math.max(...knownRounds, 0);
238
+ if (max > 0 && !knownRounds.has(1)) diagnostics.push(diagnostic('MISSING_BASE', family, null, `${family}.md is missing`));
229
239
  for (let round = 1; round <= max; round += 1) {
230
240
  if (!knownRounds.has(round)) diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
231
241
  }
232
- const latest = artifacts.at(-1) ?? null;
242
+ const event = completionEvent(family);
243
+ const completed = event
244
+ ? artifacts.flatMap((artifact) => {
245
+ const order = artifactCompletionOrder(taskContent, artifact.name, event);
246
+ return order !== null && hasArtifactCompletionFact(taskContent, artifact.path, event)
247
+ ? [{ artifact, order }] : [];
248
+ }).sort((left, right) => left.order - right.order).map(({ artifact }) => artifact)
249
+ : [];
250
+ if (openRounds.length > 1) diagnostics.push(diagnostic('MULTIPLE_OPEN_ROUNDS', family, null, `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`));
251
+ const latest = completed.at(-1) ?? null;
233
252
  let hasReviewCodeReceipt = false;
234
253
  if (family === 'review-code' && latest) {
235
- try { hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name)); }
254
+ try { hasReviewCodeReceipt = Boolean(receiptForOutput(taskContent, latest.name)); }
236
255
  catch { hasReviewCodeReceipt = false; }
237
256
  }
238
257
  const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
@@ -240,9 +259,9 @@ function inspectArtifactDirectory(
240
259
  : null;
241
260
  return {
242
261
  status: 'ready', changed: false, requestRef, taskId: identity.taskId ?? null,
243
- taskDir, taskState: identity.taskState ?? null, family, artifacts, latest,
262
+ taskDir, taskState: identity.taskState ?? null, family, artifacts, completed, latest,
244
263
  next: { round: max + 1, name: artifactName(family, max + 1) },
245
- reviewedInput, diagnostics, error: null
264
+ openRounds, reviewedInput, diagnostics, error: null
246
265
  };
247
266
  }
248
267
 
@@ -262,9 +281,6 @@ function resolveReviewedInput(
262
281
  const abs = path.join(taskDir, input.name);
263
282
  const stat = fs.lstatSync(abs);
264
283
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
265
- const invalidation = parseInvalidationDocument(taskContent);
266
- if (!invalidation.ok) throw new Error(invalidation.message);
267
- if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name)) throw new Error(`${input.name} is invalidated`);
268
284
  const actualSha256 = sha256File(abs);
269
285
  if (actualSha256 !== receipt.inputSha256) {
270
286
  throw new Error(`${input.name} content digest does not match receipt`);
@@ -338,10 +354,22 @@ function hasSelectionEvidence(
338
354
  content: string,
339
355
  family: ArtifactFamily,
340
356
  options: InspectOptions,
341
- context: ArtifactContextResult
357
+ context: ArtifactContextResult,
358
+ pathState: LifecyclePathState
342
359
  ): boolean {
343
360
  if (options.sourceFinding && options.sourceArtifact && options.sourceSha256) return true;
344
361
  if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt') return true;
362
+ if ((family === 'plan' || family === 'code') && context.latest) {
363
+ const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
364
+ const capturedInputs = receiptsForOutput(content, context.latest.name)
365
+ .filter((receipt) => receipt.event === `${family}.completed`)
366
+ .filter((receipt) => {
367
+ try { return sha256File(path.join(context.taskDir!, receipt.input)) === receipt.inputSha256; }
368
+ catch { return false; }
369
+ })
370
+ .map((receipt) => receipt.input);
371
+ if (!executionInputsMatchLatest(family, pathState, latestInputs, capturedInputs)) return true;
372
+ }
345
373
  if (family.startsWith('review-')) {
346
374
  const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
347
375
  const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
@@ -398,16 +426,19 @@ function attachArtifactSelection(
398
426
  } catch (error) {
399
427
  return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
400
428
  }
401
- const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
402
- const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
403
- const open = openLatest || openNext;
404
- const selectedIdentity = openLatest ? context.latest! : context.next;
429
+ const openRounds = openArtifactRounds(taskContent, family);
430
+ if (openRounds.length > 1) return fail('ARTIFACT_TOPOLOGY_CONFLICT', `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`);
431
+ const openRound = openRounds[0] ?? null;
432
+ const selectedIdentity = openRound === null ? context.next : {
433
+ family, round: openRound, name: artifactName(family, openRound),
434
+ path: path.join(context.taskDir, artifactName(family, openRound)), size: 0, mtimeMs: 0
435
+ };
405
436
  try {
406
- const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
437
+ const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context, pathState);
407
438
  const selection = selectArtifactDisposition({
408
439
  next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
409
440
  latest: context.latest,
410
- open,
441
+ open: openRound !== null,
411
442
  hasChangeEvidence
412
443
  });
413
444
  return {
@@ -434,7 +465,8 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
434
465
  const selected = attachArtifactSelection(code, pathState, options);
435
466
  if (selected.status !== 'ready' || !selected.selection) return selected;
436
467
  if (selected.selection.disposition === 'reuse') return selected;
437
- if (selected.codeMode?.mode === 'error') {
468
+ if (selected.codeMode?.mode === 'error'
469
+ && selected.codeMode.message.includes('is not captured by the latest code input receipt')) {
438
470
  return {
439
471
  ...selected,
440
472
  status: 'ready',
@@ -490,23 +522,33 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
490
522
  const source = inspectTaskArtifacts(taskRef, inputFamily, options);
491
523
  if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
492
524
  const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
525
+ try {
526
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
527
+ const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
528
+ if (hasPendingImplementation && !reviewCode.latest) {
529
+ const candidate = reviewCode.artifacts.at(-1)?.name ?? 'review-code.md';
530
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `cannot find completed Activity Log identity for ${candidate}`);
531
+ }
532
+ } catch (error) {
533
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
534
+ }
493
535
  const latestCode = inventory.latest;
494
- const codeMax = latestCode?.round ?? 0;
536
+ const codeMax = inventory.next?.round ? inventory.next.round - 1 : latestCode?.round ?? 0;
495
537
  const reviewMax = reviewCode.latest?.round ?? 0;
496
538
  const inputs = [source.latest];
497
539
  const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
498
540
  const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
499
541
  if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name) inputs.push(sourceReview.latest);
500
- if (latestCode) {
542
+ if (inventory.openRounds.length > 0) {
501
543
  try {
502
544
  const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
503
- if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
545
+ const openRound = inventory.openRounds[0]!;
504
546
  const review = reviewCode.latest;
505
547
  const openInputs = review ? [...inputs, review] : inputs;
506
548
  const openVerdict = review ? parseVerdict(review.path) : null;
507
549
  const openRow = locateActivityLog(taskContent)
508
550
  ? startedBackedRows(pairEntries(locateActivityLog(taskContent)!.entries))
509
- .find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
551
+ .find((row) => row.step.startsWith(`Code Task (Round ${openRound}`) && !row.done)
510
552
  : null;
511
553
  const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
512
554
  const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
@@ -516,10 +558,9 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
516
558
  return withCodeMode(
517
559
  inventory, openInputs, 'ready', mode, codeMax, reviewMax,
518
560
  openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null,
519
- `Resuming open code round ${latestCode.round}.`,
561
+ `Resuming open code round ${openRound}.`,
520
562
  decisionInput
521
563
  );
522
- }
523
564
  } catch (error) {
524
565
  return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
525
566
  }
@@ -653,6 +694,13 @@ export function validateArtifactPublication(taskDir: string, family: ArtifactFam
653
694
  if (hasCompletedArtifactRound(task, family, existing.round, name)) {
654
695
  return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
655
696
  }
697
+ const event = completionEvent(family);
698
+ const frontmatter = parseTypedTaskFrontmatter(task);
699
+ const completionFactExists = event !== null && parseCompletionFacts(frontmatter.completion_facts)
700
+ .some((fact) => fact.event === event && fact.output === name);
701
+ if (event && completionFactExists && artifactCompletionOrder(task, name, event) !== null) {
702
+ return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completion evidence for '${name}' does not match its current content` };
703
+ }
656
704
  } catch (error) {
657
705
  return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
658
706
  }
@@ -32,7 +32,7 @@ function selectArtifactDisposition(request: ArtifactSelectionRequest): ArtifactS
32
32
  if (!request.latest) {
33
33
  return {
34
34
  disposition: 'create',
35
- reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
35
+ reasonCode: 'no-completed-history',
36
36
  artifact: request.next, writeRequired: true
37
37
  };
38
38
  }
@@ -2,8 +2,6 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { parseArtifactName } from './artifact-name.ts';
5
- import type { InvalidationDocument } from './invalidation.ts';
6
- import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
7
5
  import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.ts';
8
6
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
9
7
  import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
@@ -19,6 +17,8 @@ import type { LifecyclePathState } from './lifecycle-path.ts';
19
17
  import { parseImplementationInputs } from './implementation-inputs.ts';
20
18
  import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
21
19
  import { inspectReviewIdentity } from './review-identity.ts';
20
+ import { inspectArtifactDirectory } from './artifact-lifecycle.ts';
21
+ import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.ts';
22
22
 
23
23
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
24
24
 
@@ -43,11 +43,10 @@ type LifecycleFacts = {
43
43
  taskState: string;
44
44
  currentStep: string;
45
45
  artifacts: Partial<Record<LifecycleAction, readonly string[]>>;
46
- staleArtifacts?: Partial<Record<LifecycleAction, readonly string[]>>;
47
46
  reviewedInputs?: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', string>>;
47
+ executionInputs?: Partial<Record<'plan' | 'code', readonly string[]>>;
48
48
  artifactHashes: Readonly<Record<string, string>>;
49
49
  reviews: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', 'approved' | 'changes-requested' | 'rejected'>>;
50
- invalidation: InvalidationDocument;
51
50
  reworkIntents?: readonly ReworkIntent[];
52
51
  unresolvedLedger: Record<'analysis' | 'plan' | 'code', number>;
53
52
  executionBusy: boolean;
@@ -106,7 +105,6 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
106
105
  if (actual !== trigger.sourceSha256) return deny('SOURCE_ARTIFACT_HASH_MISMATCH', trigger.sourceArtifact);
107
106
  }
108
107
  if (facts.taskState !== 'active') return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
109
- if (invalidationBlocks(facts.invalidation)) return deny('INVALIDATION_INCOMPLETE');
110
108
  if (facts.executionBusy) return deny('EXECUTION_BUSY');
111
109
 
112
110
  const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
@@ -152,7 +150,8 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
152
150
  return allow('analysis-review-approved');
153
151
  }
154
152
  if (action === 'review-plan') {
155
- return hasArtifact(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_ARTIFACT_REQUIRED');
153
+ if (!hasArtifact(facts, 'plan')) return deny('PLAN_ARTIFACT_REQUIRED');
154
+ return executionInputsMatchLatest(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_INPUT_STALE');
156
155
  }
157
156
  if (action === 'code') {
158
157
  if (trigger.implementationInput) {
@@ -179,10 +178,12 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
179
178
  return allow('plan-review-approved');
180
179
  }
181
180
  if (action === 'review-code') {
181
+ if (hasArtifact(facts, 'code') && !executionInputsMatchLatest(facts, 'code')) return deny('CODE_INPUT_STALE');
182
182
  return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
183
183
  }
184
184
  if (action === 'manual-validation' || action === 'validation-run') {
185
185
  if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
186
+ if (!executionInputsMatchLatest(facts, 'code')) return deny('CODE_INPUT_STALE');
186
187
  if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
187
188
  if (!facts.reviewCodeIdentityValid) return deny('CODE_REVIEW_IDENTITY_INVALID');
188
189
  if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
@@ -193,8 +194,7 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
193
194
  }
194
195
 
195
196
  function latestArtifact(names: readonly string[]): string | null {
196
- return names.map(parseArtifactName).filter((identity) => identity !== null)
197
- .sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
197
+ return names.at(-1) ?? null;
198
198
  }
199
199
 
200
200
  function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan' | 'code', review: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
@@ -202,6 +202,16 @@ function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan'
202
202
  return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
203
203
  }
204
204
 
205
+ function executionInputsMatchLatest(facts: LifecycleFacts, stage: 'plan' | 'code'): boolean {
206
+ if (!facts.pathState || facts.pathState.status !== 'valid' || facts.executionInputs === undefined) return true;
207
+ return executionReceiptsMatchLatest(stage, facts.pathState, {
208
+ analysis: latestArtifact(facts.artifacts.analysis ?? []) ?? undefined,
209
+ 'review-analysis': latestArtifact(facts.artifacts['review-analysis'] ?? []) ?? undefined,
210
+ plan: latestArtifact(facts.artifacts.plan ?? []) ?? undefined,
211
+ 'review-plan': latestArtifact(facts.artifacts['review-plan'] ?? []) ?? undefined
212
+ }, facts.executionInputs[stage] ?? []);
213
+ }
214
+
205
215
  function qualificationCandidateSnapshotMatches(
206
216
  audit: QualificationAudit,
207
217
  qualification: TaskQualification
@@ -239,6 +249,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
239
249
  : { action: stage, reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
240
250
  }
241
251
  if (stage === 'plan' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
252
+ if (stage === 'plan' && !executionInputsMatchLatest(facts, stage)) return { action: stage, reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
242
253
  if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
243
254
  if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
244
255
  return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
@@ -248,6 +259,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
248
259
  : { action: stage, reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
249
260
  }
250
261
  if (stage === 'code' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
262
+ if (stage === 'code' && !executionInputsMatchLatest(facts, stage)) return { action: stage, reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
251
263
  if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
252
264
  if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
253
265
  return facts.resolvedHumanDecisions.code === 'implementation'
@@ -269,6 +281,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
269
281
  return { action: 'analysis', reasonCode: 'ANALYSIS_REWORK_REQUIRED', evidence: ['analysis review or ledger is not clear'] };
270
282
  }
271
283
  if (!hasArtifact(facts, 'plan')) return { action: 'plan', reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
284
+ if (!executionInputsMatchLatest(facts, 'plan')) return { action: 'plan', reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
272
285
  if (!hasArtifact(facts, 'review-plan') || !reviewMatchesLatest(facts, 'plan', 'review-plan')) {
273
286
  return { action: 'review-plan', reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
274
287
  }
@@ -276,6 +289,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
276
289
  return { action: 'plan', reasonCode: 'PLAN_REWORK_REQUIRED', evidence: ['plan review or ledger is not clear'] };
277
290
  }
278
291
  if (!hasArtifact(facts, 'code')) return { action: 'code', reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
292
+ if (!executionInputsMatchLatest(facts, 'code')) return { action: 'code', reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
279
293
  if (!hasArtifact(facts, 'review-code') || !reviewMatchesLatest(facts, 'code', 'review-code')) {
280
294
  return { action: 'review-code', reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
281
295
  }
@@ -288,8 +302,6 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
288
302
  function buildLifecycleFacts(taskDir: string, content: string, taskState = 'active', executionBusy = false): LifecycleFactsResult {
289
303
  try {
290
304
  const metadata = parseTypedTaskFrontmatter(content);
291
- const invalidation = parseInvalidationDocument(content);
292
- if (!invalidation.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalidation.message };
293
305
  const rework = parseReworkIntentDocument(content);
294
306
  if (!rework.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: rework.message };
295
307
  const ledger = parseLedgerDocument(content);
@@ -307,24 +319,36 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
307
319
  const family = parseArtifactName(name)?.family;
308
320
  return artifactFamilies.find((candidate) => candidate === family) ?? null;
309
321
  };
310
- const activeFiles = files.filter((name) => {
311
- const family = familyFor(name);
312
- return !family || !isArtifactInvalidated(invalidation.document, family, name);
313
- });
314
322
  const allArtifactHashes: Record<string, string> = {};
315
323
  for (const name of files) allArtifactHashes[name] = sha256File(path.join(taskDir, name));
316
324
  const artifactHashes: Record<string, string> = {};
317
- for (const name of activeFiles) artifactHashes[name] = allArtifactHashes[name]!;
325
+ for (const name of files) artifactHashes[name] = allArtifactHashes[name]!;
326
+ const completedByFamily = new Map<string, string[]>();
327
+ for (const family of artifactFamilies) {
328
+ const inventory = inspectArtifactDirectory(taskDir, family);
329
+ if (inventory.status !== 'ready') continue;
330
+ completedByFamily.set(family, inventory.completed.map((artifact) => artifact.name));
331
+ }
332
+ const completedFiles = new Set([...completedByFamily.values()].flat());
333
+ const activeFiles = [
334
+ ...files.filter((name) => !familyFor(name)),
335
+ ...artifactFamilies.flatMap((family) => completedByFamily.get(family) ?? [])
336
+ ];
318
337
  const artifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
319
338
  artifactFamilies.map((family) => [
320
339
  family, activeFiles.filter((name) => familyFor(name) === family)
321
340
  ])
322
341
  ) as Partial<Record<LifecycleAction, readonly string[]>>;
323
- const staleArtifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
324
- artifactFamilies.map((family) => [
325
- family, files.filter((name) => familyFor(name) === family && isArtifactInvalidated(invalidation.document, family, name))
326
- ])
327
- ) as Partial<Record<LifecycleAction, readonly string[]>>;
342
+ const receipts = parseArtifactReceipts(content).rows;
343
+ const executionInputs: NonNullable<LifecycleFacts['executionInputs']> = {};
344
+ for (const family of ['plan', 'code'] as const) {
345
+ const output = latestArtifact(artifacts[family] ?? []);
346
+ if (!output) continue;
347
+ executionInputs[family] = receipts.filter((receipt) => receipt.output === output
348
+ && receipt.event === `${family}.completed`
349
+ && completedFiles.has(receipt.input)
350
+ && artifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => receipt.input);
351
+ }
328
352
  const qualification = parseTaskQualification(content);
329
353
  if (!qualification.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: qualification.message };
330
354
  const qualificationStaleArtifacts: string[] = [];
@@ -377,9 +401,8 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
377
401
  if (verdict.ok) reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
378
402
  }
379
403
  const reworkClassificationRequired: Array<'analysis' | 'plan' | 'code'> = [];
380
- const receipts = parseArtifactReceipts(content).rows;
381
404
  for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']] as const) {
382
- const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && files.includes(receipt.output)
405
+ const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && completedFiles.has(receipt.output)
383
406
  && allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
384
407
  const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
385
408
  const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
@@ -426,8 +449,8 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
426
449
  }
427
450
  const facts: LifecycleFacts = {
428
451
  taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
429
- staleArtifacts, reviewedInputs, artifactHashes,
430
- invalidation: invalidation.document, reworkIntents: rework.intents,
452
+ reviewedInputs, executionInputs, artifactHashes,
453
+ reworkIntents: rework.intents,
431
454
  unresolvedLedger,
432
455
  executionBusy: executionBusy || hasOpenLifecycleExecution(content),
433
456
  recommendedAction: null,
@@ -12,8 +12,6 @@ import { parseActivityLog, pairEntries, startedBackedRows } from '../activity-lo
12
12
  import { readRun, type OrchestrationRun } from '../orchestration.ts';
13
13
  import type { DelegationReceipt } from '../delegation-receipts.ts';
14
14
  import { statusCard, type DisplayMessage } from '../../server/display.ts';
15
- import { invalidationBlocks, parseInvalidationDocument } from '../invalidation.ts';
16
- import { parseReworkIntentDocument } from '../rework-intent.ts';
17
15
  import { buildLifecycleFacts, recommendNext } from '../capabilities.ts';
18
16
 
19
17
  const USAGE = `Usage: ai task status [--task <ref> | -t <ref>]
@@ -384,7 +382,6 @@ type StatusModel = {
384
382
  workflow: WorkflowInfo;
385
383
  runtime: RuntimeInfo;
386
384
  orchestration: OrchestrationInfo;
387
- invalidation?: { status: string; processed: number; total: number; pendingIntents: number };
388
385
  recommendation?: string;
389
386
  recommendationReason?: string;
390
387
  lifecyclePath?: string;
@@ -436,16 +433,6 @@ function renderStatus(model: StatusModel): string[] {
436
433
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
437
434
  }
438
435
 
439
- lines.push(
440
- '',
441
- 'Invalidation',
442
- ...renderPairs([
443
- ['status', model.invalidation?.status ?? 'unknown'],
444
- ['processed', `${model.invalidation?.processed ?? 0}/${model.invalidation?.total ?? 0}`],
445
- ['pending_intents', String(model.invalidation?.pendingIntents ?? 0)]
446
- ])
447
- );
448
-
449
436
  lines.push('', 'Recommendation', ...renderPairs([
450
437
  ['path', model.lifecyclePath ?? DASH],
451
438
  ['action', model.recommendation ?? DASH],
@@ -517,8 +504,6 @@ function renderStatus(model: StatusModel): string[] {
517
504
  }
518
505
 
519
506
  function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
520
- if (model.invalidation?.status === 'invalid') return 'danger';
521
- if (model.invalidation?.status === 'pending' || model.invalidation?.status === 'in-progress') return 'warning';
522
507
  if (model.orchestration.status === 'paused') return 'warning';
523
508
  if (model.orchestration.status === 'running') return 'running';
524
509
  if (model.orchestration.status === 'completed') return 'success';
@@ -536,21 +521,6 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
536
521
  const run = input.run ?? makeRunner(input.repoRoot);
537
522
  const artifacts = enumerateArtifacts(input.taskDir);
538
523
  const workflow = collectWorkflow(content, input.now);
539
- const invalidation = parseInvalidationDocument(content);
540
- const rework = parseReworkIntentDocument(content);
541
- const pendingIntents = rework.ok ? rework.intents.filter((intent) => intent.status === 'pending').length : 0;
542
- const invalidationInfo = !invalidation.ok
543
- ? { status: 'invalid', processed: 0, total: 0, pendingIntents }
544
- : {
545
- status: invalidation.present
546
- ? invalidationBlocks(invalidation.document)
547
- ? invalidation.document.operations.some((operation) => operation.status === 'in-progress') ? 'in-progress' : 'pending'
548
- : 'completed'
549
- : 'none',
550
- processed: invalidation.document.operations.reduce((sum, operation) => sum + operation.processed, 0),
551
- total: invalidation.document.operations.reduce((sum, operation) => sum + operation.total, 0),
552
- pendingIntents
553
- };
554
524
  const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
555
525
  const next = facts.ok ? recommendNext(facts.facts) : null;
556
526
  const recommendation = next?.action ?? DASH;
@@ -565,7 +535,6 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
565
535
  workflow,
566
536
  runtime: collectRuntime(input.taskDir, workflow, run),
567
537
  orchestration: collectOrchestration(input.taskDir),
568
- invalidation: invalidationInfo,
569
538
  recommendation,
570
539
  recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
571
540
  lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,