@fitlab-ai/agent-infra 0.11.9 → 0.11.11

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 (219) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/agent-clients/adapter.js +20 -0
  3. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  4. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  5. package/dist/lib/git/workflow.js +1 -1
  6. package/dist/lib/internal/cli-route-inventory.js +0 -3
  7. package/dist/lib/internal/manual-validation.js +2 -2
  8. package/dist/lib/internal/platform-pr.js +4 -5
  9. package/dist/lib/internal/sandbox-control.js +30 -5
  10. package/dist/lib/internal/task-artifact.js +1 -1
  11. package/dist/lib/internal/task-create.js +15 -1
  12. package/dist/lib/internal/task-finalization.js +14 -25
  13. package/dist/lib/internal/task-operation-registry.js +6 -141
  14. package/dist/lib/internal/task-platform-recovery.js +9 -4
  15. package/dist/lib/platform/completion-backfill.js +2 -1
  16. package/dist/lib/platform/context.js +4 -2
  17. package/dist/lib/platform/github-client.js +20 -17
  18. package/dist/lib/platform/github-provider.js +42 -22
  19. package/dist/lib/platform/github-release-notes.js +4 -2
  20. package/dist/lib/platform/in-label-sync.js +2 -1
  21. package/dist/lib/platform/issue-comments.js +13 -9
  22. package/dist/lib/platform/operation-coordinator.js +1 -1
  23. package/dist/lib/platform/pr-review.js +2 -1
  24. package/dist/lib/platform/pr-summary.js +45 -8
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +83 -16
  30. package/dist/lib/platform/verification-sync.js +17 -14
  31. package/dist/lib/sandbox/commands/create.js +18 -2
  32. package/dist/lib/sandbox/commands/enter.js +47 -33
  33. package/dist/lib/sandbox/control/client.js +99 -17
  34. package/dist/lib/sandbox/control/executor.js +12 -14
  35. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  36. package/dist/lib/sandbox/control/protocol.js +2 -3
  37. package/dist/lib/sandbox/control/server.js +42 -2
  38. package/dist/lib/sandbox/environment.js +150 -0
  39. package/dist/lib/sandbox/recovery.js +8 -0
  40. package/dist/lib/sandbox/removal.js +0 -1
  41. package/dist/lib/sandbox/workspace-identity.js +0 -14
  42. package/dist/lib/sandbox/workspace-view.js +33 -6
  43. package/dist/lib/task/artifact-command.js +18 -3
  44. package/dist/lib/task/artifact-lifecycle.js +121 -63
  45. package/dist/lib/task/artifact-selection.js +1 -1
  46. package/dist/lib/task/capabilities.js +59 -22
  47. package/dist/lib/task/commands/status.js +0 -27
  48. package/dist/lib/task/commit-identity.js +22 -7
  49. package/dist/lib/task/commit-intent.js +67 -26
  50. package/dist/lib/task/commit-operation.js +19 -8
  51. package/dist/lib/task/completion-facts.js +32 -10
  52. package/dist/lib/task/control-authority.js +2 -6
  53. package/dist/lib/task/current-contract.js +1 -5
  54. package/dist/lib/task/events.js +9 -275
  55. package/dist/lib/task/execution-inputs.js +14 -0
  56. package/dist/lib/task/finalization-envelope.js +4 -0
  57. package/dist/lib/task/finalization.js +43 -86
  58. package/dist/lib/task/frontmatter.js +25 -2
  59. package/dist/lib/task/ledger-intents.js +6 -8
  60. package/dist/lib/task/orchestration.js +11 -10
  61. package/dist/lib/task/platform-operation-journal.js +29 -10
  62. package/dist/lib/task/platform-operation-recovery.js +109 -35
  63. package/dist/lib/task/resolve-ref.js +8 -3
  64. package/dist/lib/task/review-fingerprint.js +4 -26
  65. package/dist/lib/task/verification-engine.js +95 -6
  66. package/dist/lib/task/write.js +0 -11
  67. package/dist/lifecycle-build-manifest.json +3 -3
  68. package/lib/agent-clients/adapter.ts +22 -0
  69. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  70. package/lib/agent-clients/adapters/codex.ts +4 -0
  71. package/lib/git/workflow.ts +1 -1
  72. package/lib/internal/cli-route-inventory.ts +0 -2
  73. package/lib/internal/manual-validation.ts +2 -2
  74. package/lib/internal/platform-pr.ts +3 -4
  75. package/lib/internal/sandbox-control.ts +39 -6
  76. package/lib/internal/task-artifact.ts +1 -1
  77. package/lib/internal/task-create.ts +15 -2
  78. package/lib/internal/task-finalization.ts +16 -26
  79. package/lib/internal/task-operation-registry.ts +6 -153
  80. package/lib/internal/task-platform-recovery.ts +9 -4
  81. package/lib/platform/completion-backfill.ts +2 -1
  82. package/lib/platform/context.ts +8 -4
  83. package/lib/platform/github-client.ts +24 -20
  84. package/lib/platform/github-provider.ts +47 -26
  85. package/lib/platform/github-release-notes.ts +5 -3
  86. package/lib/platform/in-label-sync.ts +2 -1
  87. package/lib/platform/issue-comments.ts +14 -9
  88. package/lib/platform/operation-coordinator.ts +1 -1
  89. package/lib/platform/pr-review.ts +2 -1
  90. package/lib/platform/pr-summary.ts +43 -7
  91. package/lib/platform/provider-bridge.ts +10 -9
  92. package/lib/platform/provider-contract.ts +6 -21
  93. package/lib/platform/provider-loader.ts +7 -6
  94. package/lib/platform/provider-validation.ts +30 -8
  95. package/lib/platform/pull-requests.ts +78 -15
  96. package/lib/platform/repository-metadata.ts +3 -2
  97. package/lib/platform/security-alerts.ts +1 -1
  98. package/lib/platform/types.ts +2 -0
  99. package/lib/platform/verification-sync.ts +18 -14
  100. package/lib/sandbox/commands/create.ts +24 -2
  101. package/lib/sandbox/commands/enter.ts +42 -39
  102. package/lib/sandbox/control/client.ts +107 -19
  103. package/lib/sandbox/control/executor.ts +13 -15
  104. package/lib/sandbox/control/host-recovery.ts +195 -0
  105. package/lib/sandbox/control/protocol.ts +4 -5
  106. package/lib/sandbox/control/server.ts +42 -1
  107. package/lib/sandbox/environment.ts +162 -0
  108. package/lib/sandbox/recovery.ts +10 -0
  109. package/lib/sandbox/removal.ts +2 -10
  110. package/lib/sandbox/tool-types.ts +1 -0
  111. package/lib/sandbox/workspace-identity.ts +0 -26
  112. package/lib/sandbox/workspace-view.ts +33 -8
  113. package/lib/task/artifact-command.ts +19 -3
  114. package/lib/task/artifact-lifecycle.ts +102 -53
  115. package/lib/task/artifact-selection.ts +1 -1
  116. package/lib/task/capabilities.ts +52 -26
  117. package/lib/task/commands/status.ts +0 -31
  118. package/lib/task/commit-identity.ts +24 -7
  119. package/lib/task/commit-intent.ts +58 -24
  120. package/lib/task/commit-operation.ts +17 -8
  121. package/lib/task/completion-facts.ts +31 -9
  122. package/lib/task/control-authority.ts +1 -8
  123. package/lib/task/current-contract.ts +1 -6
  124. package/lib/task/events.ts +9 -248
  125. package/lib/task/execution-inputs.ts +25 -0
  126. package/lib/task/finalization-envelope.ts +21 -0
  127. package/lib/task/finalization.ts +40 -82
  128. package/lib/task/frontmatter.ts +26 -2
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/orchestration.ts +11 -17
  131. package/lib/task/platform-operation-journal.ts +47 -13
  132. package/lib/task/platform-operation-recovery.ts +102 -37
  133. package/lib/task/resolve-ref.ts +8 -3
  134. package/lib/task/review-fingerprint.ts +4 -26
  135. package/lib/task/verification-engine.ts +103 -9
  136. package/lib/task/write.ts +0 -19
  137. package/package.json +1 -1
  138. package/templates/.agents/rules/README.en.md +1 -1
  139. package/templates/.agents/rules/README.zh-CN.md +1 -1
  140. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  141. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  142. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  143. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  144. package/templates/.agents/rules/issue-sync.en.md +1 -1
  145. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  146. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  147. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  148. package/templates/.agents/rules/review-handshake.en.md +3 -0
  149. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  150. package/templates/.agents/rules/task-management.en.md +1 -1
  151. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  152. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  158. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  159. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  160. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  163. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
  164. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  166. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  168. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  169. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  170. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  172. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  173. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  176. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  177. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  178. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  179. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  180. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  181. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  182. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  183. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  184. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  197. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  198. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  199. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  200. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
  201. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
  202. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  205. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  206. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  207. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  208. package/templates/.agents/templates/task.zh-CN.md +0 -12
  209. package/templates/.github/workflows/pr-label.yml +1 -2
  210. package/dist/lib/internal/task-invalidation.js +0 -63
  211. package/dist/lib/task/finalization-handoff.js +0 -93
  212. package/dist/lib/task/intermediate-cleanup.js +0 -327
  213. package/dist/lib/task/invalidation-command.js +0 -88
  214. package/dist/lib/task/invalidation.js +0 -242
  215. package/lib/internal/task-invalidation.ts +0 -41
  216. package/lib/task/finalization-handoff.ts +0 -105
  217. package/lib/task/intermediate-cleanup.ts +0 -393
  218. package/lib/task/invalidation-command.ts +0 -88
  219. package/lib/task/invalidation.ts +0 -313
@@ -47,12 +47,35 @@ function serializeScalar(value) {
47
47
  }
48
48
  if (value === '')
49
49
  return '';
50
- const serialized = stringify(value).trimEnd();
50
+ const serialized = stringify(value, { lineWidth: 0 }).trimEnd();
51
51
  if (serialized.includes('\n') || serialized.includes('\r')) {
52
52
  throw new FrontmatterError('MUTATION_INVALID', 'frontmatter values must be scalar');
53
53
  }
54
54
  return serialized;
55
55
  }
56
+ function stripInlineComment(value) {
57
+ const first = value.search(/\S/u);
58
+ let activeQuote = first >= 0 && (value[first] === '"' || value[first] === "'") ? value[first] : null;
59
+ for (let index = activeQuote ? first + 1 : Math.max(first, 0); index < value.length; index += 1) {
60
+ const character = value[index];
61
+ if (activeQuote === '"' && character === '\\') {
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (activeQuote && character === activeQuote) {
66
+ if (activeQuote === "'" && value[index + 1] === "'") {
67
+ index += 1;
68
+ continue;
69
+ }
70
+ activeQuote = null;
71
+ continue;
72
+ }
73
+ if (!activeQuote && character === '#' && (index === 0 || /\s/u.test(value[index - 1]))) {
74
+ return value.slice(0, index).trimEnd();
75
+ }
76
+ }
77
+ return value;
78
+ }
56
79
  function parseTaskFrontmatter(content) {
57
80
  const result = {};
58
81
  if (!content.startsWith('---'))
@@ -69,7 +92,7 @@ function parseTaskFrontmatter(content) {
69
92
  if (colon === -1)
70
93
  continue;
71
94
  const key = line.slice(0, colon).trim();
72
- const value = line.slice(colon + 1).trim();
95
+ const value = stripInlineComment(line.slice(colon + 1).trim());
73
96
  if (key)
74
97
  result[key] = value;
75
98
  }
@@ -10,6 +10,7 @@ import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkInten
10
10
  import { extractSection, extractSubSection } from './sections.js';
11
11
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
12
  import { scanVisibleMarkdown } from './markdown.js';
13
+ import { inspectArtifactDirectory } from './artifact-lifecycle.js';
13
14
  const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
14
15
  const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
15
16
  const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
@@ -61,9 +62,8 @@ function findingEvidence(reviewContent, evidence) {
61
62
  return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
62
63
  }
63
64
  function taskFactDigest(taskDir, content) {
64
- const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
65
- const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
66
- const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
65
+ const latest = inspectArtifactDirectory(taskDir, 'analysis').latest;
66
+ const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(latest.path, 'utf8'), latest.name) : null;
67
67
  return semanticDigest(JSON.stringify({
68
68
  taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
69
69
  requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
@@ -220,10 +220,7 @@ function applyLedgerIntent(intent, options = {}) {
220
220
  }
221
221
  const sourceIdentity = parseArtifactName(intent.sourceArtifact);
222
222
  const expectedFamily = `review-${finding.stage}`;
223
- const latestSource = fs.readdirSync(resolved.taskDir)
224
- .map((name) => parseArtifactName(name))
225
- .filter((identity) => identity?.family === expectedFamily)
226
- .sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0];
223
+ const latestSource = inspectArtifactDirectory(resolved.taskDir, expectedFamily).latest;
227
224
  if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
228
225
  return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
229
226
  }
@@ -339,7 +336,8 @@ function applyLedgerIntent(intent, options = {}) {
339
336
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
340
337
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
341
338
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
342
- if (!sameRoundMinorClose && !allowed.has(intent.status)) {
339
+ const refreshOpenEvidence = before.status === 'open' && intent.status === 'open';
340
+ if (!sameRoundMinorClose && !refreshOpenEvidence && !allowed.has(intent.status)) {
343
341
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
344
342
  }
345
343
  after = { ...before, status: intent.status, evidence };
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
- import { artifactName, maxArtifactRound } from './artifact-name.js';
4
+ import { inspectArtifactDirectory } from './artifact-lifecycle.js';
5
5
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
6
6
  import { validateCurrentTaskContract } from './current-contract.js';
7
7
  import { parseReviewSummary } from './review-artifacts.js';
@@ -20,7 +20,6 @@ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead,
20
20
  import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
21
21
  import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js';
22
22
  import { hasOpenLifecycleExecution } from './activity-log.js';
23
- import { reconcileTaskInvalidation } from './invalidation-command.js';
24
23
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
25
24
  import { resolveArtifactContext } from './artifact-lifecycle.js';
26
25
  function supportsLifecycleDelegation(client) {
@@ -441,8 +440,11 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
441
440
  return { status: 'running', changed: true, taskId: resolved.taskId, run, next: null, error: null };
442
441
  }
443
442
  function validateSavedCurrentDiffBase(repoRoot, metadata, taskDir, reviewedHead) {
444
- const reviewRound = maxArtifactRound(fs.readdirSync(taskDir), 'review-code');
445
- const reviewContent = fs.readFileSync(path.join(taskDir, artifactName('review-code', reviewRound)), 'utf8');
443
+ const reviewInventory = inspectArtifactDirectory(taskDir, 'review-code');
444
+ if (reviewInventory.status !== 'ready' || !reviewInventory.latest) {
445
+ return { code: 'ORCHESTRATION_REVIEW_INVALID', message: 'latest completed review-code artifact is unavailable' };
446
+ }
447
+ const reviewContent = fs.readFileSync(reviewInventory.latest.path, 'utf8');
446
448
  const savedTargetHead = extractReviewTargetHead(reviewContent);
447
449
  const savedDiffBase = extractReviewDiffBase(reviewContent);
448
450
  const savedReviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
@@ -540,8 +542,11 @@ function routeOrchestration(taskRef, options = {}) {
540
542
  if (facts.facts.executionBusy && !run?.pendingDelegation) {
541
543
  return failed('ORCHESTRATION_EXECUTION_BUSY', 'clean completion requires no open lifecycle execution', resolved.taskId);
542
544
  }
543
- const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
544
- const review = parseReviewSummary(fs.readFileSync(path.join(resolved.taskDir, artifactName('review-code', reviewRound)), 'utf8'));
545
+ const reviewInventory = inspectArtifactDirectory(resolved.taskDir, 'review-code');
546
+ if (reviewInventory.status !== 'ready' || !reviewInventory.latest) {
547
+ return failed('ORCHESTRATION_REVIEW_INVALID', 'latest completed review-code artifact is unavailable', resolved.taskId);
548
+ }
549
+ const review = parseReviewSummary(fs.readFileSync(reviewInventory.latest.path, 'utf8'));
545
550
  if (!review.ok || review.summary.manualValidation === null) {
546
551
  return failed('ORCHESTRATION_REVIEW_INVALID', 'latest code review has no numeric manual-validation count', resolved.taskId);
547
552
  }
@@ -678,10 +683,6 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
678
683
  }
679
684
  if (run.stepCount >= run.maxSteps)
680
685
  return pauseOrchestration(taskRef, 'ORCHESTRATION_MAX_STEPS', 'maximum orchestration steps reached', true, options);
681
- const invalidation = reconcileTaskInvalidation(taskRef, { repoRoot: resolved.repoRoot });
682
- if (invalidation.status === 'failed') {
683
- return failed('ORCHESTRATION_INVALIDATION_RECONCILE_FAILED', `${invalidation.error?.code ?? 'UNKNOWN'}: ${invalidation.error?.message ?? ''}`, resolved.taskId);
684
- }
685
686
  const routed = routeOrchestration(taskRef, options);
686
687
  if (!routed.next)
687
688
  return routed;
@@ -4,7 +4,6 @@ import { createHash, randomUUID } from 'node:crypto';
4
4
  import { isResourceIdentity } from '../platform/resource-identity.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
6
  const JOURNAL_FILE = '.platform-operations.json';
7
- const MAX_ATTEMPTS = 3;
8
7
  function operationId(input) {
9
8
  return createHash('sha256')
10
9
  .update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
@@ -36,8 +35,7 @@ function parseJournal(file, taskId) {
36
35
  || !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
37
36
  || !['deferred', 'required'].includes(item.dependency)
38
37
  || !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
39
- || !Number.isSafeInteger(item.attempts) || item.attempts < 0 || item.attempts > MAX_ATTEMPTS
40
- || item.maxAttempts !== MAX_ATTEMPTS
38
+ || !Number.isSafeInteger(item.attempts) || item.attempts < 0
41
39
  || !(item.lastCode === null || typeof item.lastCode === 'string')
42
40
  || (item.kind === 'issue-metadata'
43
41
  ? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
@@ -71,6 +69,18 @@ function parseJournal(file, taskId) {
71
69
  ? !item.pullRequestSummary || typeof item.pullRequestSummary.body !== 'string'
72
70
  || !item.pullRequestSummary.body.trim() || typeof item.pullRequestSummary.changeReportFile !== 'string'
73
71
  || !item.pullRequestSummary.changeReportFile.trim()
72
+ || (item.pullRequestSummary.manualValidation !== undefined && (!item.pullRequestSummary.manualValidation
73
+ || typeof item.pullRequestSummary.manualValidation !== 'object'
74
+ || Array.isArray(item.pullRequestSummary.manualValidation)
75
+ || !['pending', 'final'].includes(item.pullRequestSummary.manualValidation.phase)
76
+ || typeof item.pullRequestSummary.manualValidation.transactionId !== 'string'
77
+ || !item.pullRequestSummary.manualValidation.transactionId.trim()
78
+ || (item.pullRequestSummary.manualValidation.receiptDigest !== undefined && !/^[a-f0-9]{64}$/u.test(item.pullRequestSummary.manualValidation.receiptDigest))
79
+ || (item.pullRequestSummary.manualValidation.prHeadSha !== undefined && !/^[a-f0-9]{40}$/u.test(item.pullRequestSummary.manualValidation.prHeadSha))
80
+ || (item.pullRequestSummary.manualValidation.authority !== undefined && item.pullRequestSummary.manualValidation.authority !== 'coordinator')
81
+ || (item.pullRequestSummary.manualValidation.phase === 'final' && (!/^[a-f0-9]{64}$/u.test(item.pullRequestSummary.manualValidation.receiptDigest ?? '')
82
+ || !/^[a-f0-9]{40}$/u.test(item.pullRequestSummary.manualValidation.prHeadSha ?? '')
83
+ || item.pullRequestSummary.manualValidation.authority !== 'coordinator'))))
74
84
  : item.pullRequestSummary !== undefined)
75
85
  || (item.kind === 'pull-request-review'
76
86
  ? !item.pullRequestReview || typeof item.pullRequestReview.prNumber !== 'string'
@@ -120,7 +130,13 @@ function recordPlatformOperation(input) {
120
130
  const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
121
131
  const journal = parseJournal(file, taskId);
122
132
  const id = operationId(input);
123
- const previous = journal.operations.find((item) => item.id === id);
133
+ const targetIndex = journal.operations.findIndex((item) => item.id === id);
134
+ const replacementIndex = input.replaceOperationId
135
+ ? journal.operations.findIndex((item) => item.id === input.replaceOperationId)
136
+ : -1;
137
+ const target = targetIndex >= 0 ? journal.operations[targetIndex] : undefined;
138
+ const replacement = replacementIndex >= 0 ? journal.operations[replacementIndex] : undefined;
139
+ const previousAttempts = Math.max(target?.attempts ?? 0, replacement?.attempts ?? 0);
124
140
  const next = {
125
141
  id,
126
142
  kind: input.kind,
@@ -128,8 +144,7 @@ function recordPlatformOperation(input) {
128
144
  expectedDigest: input.expectedDigest,
129
145
  dependency: input.dependency,
130
146
  state: input.state,
131
- attempts: Math.min(MAX_ATTEMPTS, (previous?.attempts ?? 0) + (input.state === 'pending' ? 1 : 0)),
132
- maxAttempts: MAX_ATTEMPTS,
147
+ attempts: previousAttempts + (input.state === 'pending' ? 1 : 0),
133
148
  lastCode: input.lastCode ?? null,
134
149
  ...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
135
150
  ...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
@@ -138,9 +153,13 @@ function recordPlatformOperation(input) {
138
153
  ...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
139
154
  updatedAt: new Date().toISOString()
140
155
  };
141
- const operations = previous
142
- ? journal.operations.map((item) => item.id === id ? next : item)
143
- : [...journal.operations, next];
156
+ const operations = replacementIndex >= 0
157
+ ? journal.operations.flatMap((item, index) => index === replacementIndex
158
+ ? [next]
159
+ : index === targetIndex ? [] : [item])
160
+ : targetIndex >= 0
161
+ ? journal.operations.map((item, index) => index === targetIndex ? next : item)
162
+ : [...journal.operations, next];
144
163
  writeJournal(file, { version: 1, taskId, operations });
145
164
  return next;
146
165
  }
@@ -148,5 +167,5 @@ function readPlatformOperationJournal(taskRef, cwd) {
148
167
  const { taskId, file } = resolveJournal(taskRef, cwd);
149
168
  return parseJournal(file, taskId);
150
169
  }
151
- export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, MAX_ATTEMPTS as PLATFORM_OPERATION_MAX_ATTEMPTS, operationId, recordPlatformOperation, readPlatformOperationJournal };
170
+ export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, operationId, recordPlatformOperation, readPlatformOperationJournal };
152
171
  //# sourceMappingURL=platform-operation-journal.js.map
@@ -18,6 +18,8 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
18
18
  import { platformResult } from '../platform/types.js';
19
19
  import { resolveTaskRef } from './resolve-ref.js';
20
20
  import { readPlatformOperationJournal, recordPlatformOperation } from './platform-operation-journal.js';
21
+ import { providerErrorRetryable } from '../platform/provider-validation.js';
22
+ const DEFAULT_RECOVERY_ATTEMPTS = 3;
21
23
  function labelsMatchOwnedPrefix(actual, expected, prefix) {
22
24
  return actual.filter((label) => label.startsWith(prefix)).sort().join('\0')
23
25
  === expected.filter((label) => label.startsWith(prefix)).sort().join('\0');
@@ -26,7 +28,7 @@ function fieldsMatchExpected(actual, expected) {
26
28
  return Object.entries(expected).every(([name, value]) => actual[name] === value);
27
29
  }
28
30
  function result(status, recovered, pending, error = null) {
29
- return { status, changed: recovered.length > 0, recovered, pending, error };
31
+ return { status, changed: recovered.length > 0, recovered, pending, error: error ? { code: error.code, message: error.message } : null };
30
32
  }
31
33
  function summaryPayload(taskDir, taskId) {
32
34
  const file = path.join(taskDir, '.delivery-summary.json');
@@ -191,6 +193,16 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
191
193
  catch {
192
194
  return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
193
195
  }
196
+ const summary = operation.pullRequestSummary;
197
+ if (!summary)
198
+ return platformResult('blocked', { error: {
199
+ code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
200
+ } });
201
+ const manualValidationHeading = /^###\s+(?:⏳\s+(?:Manual Validation Pending|人工验证待收尾)|✅\s+(?:Manual Validation Passed|人工验证已通过))\s*$/mu.test(summary.body);
202
+ if (manualValidationHeading && !summary.manualValidation)
203
+ return platformResult('blocked', { error: {
204
+ code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'Queued manual-validation summary has no persisted transaction context', retryable: false
205
+ } });
194
206
  const resolved = await resolvePlatformProviderContext({ cwd, client });
195
207
  if (!resolved.ok)
196
208
  return resolved.context;
@@ -198,20 +210,16 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
198
210
  ? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
199
211
  : unsupportedProviderOperation(resolved.value.provider, 'comments.list');
200
212
  if (!listed.ok)
201
- return platformResult(listed.error.retryable ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
213
+ return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
202
214
  const marker = `<!-- sync-pr:${taskId}:summary -->`;
203
215
  const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
204
216
  if (summaries.length > 1)
205
217
  return platformResult('blocked', { error: {
206
218
  code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
207
219
  } });
208
- if (summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
220
+ if (!summary.manualValidation && summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
209
221
  return platformResult('no-op');
210
222
  }
211
- if (!operation.pullRequestSummary)
212
- return platformResult('blocked', { error: {
213
- code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
214
- } });
215
223
  const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
216
224
  if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity))
217
225
  return platformResult('blocked', { error: bound.error ?? {
@@ -221,8 +229,9 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
221
229
  agent,
222
230
  cwd,
223
231
  client,
224
- body: operation.pullRequestSummary.body,
225
- changeReportFile: operation.pullRequestSummary.changeReportFile,
232
+ body: summary.body,
233
+ changeReportFile: summary.changeReportFile,
234
+ ...(summary.manualValidation ? { manualValidation: summary.manualValidation } : {}),
226
235
  primaryResult: 'no_op',
227
236
  strict: true,
228
237
  skipQueue: true,
@@ -293,7 +302,7 @@ async function replayPullRequest(taskId, operation, agent, cwd) {
293
302
  }
294
303
  return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
295
304
  }
296
- async function recoverPlatformOperations(taskRef, selection, options) {
305
+ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
297
306
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
298
307
  if (!resolved.ok)
299
308
  return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
@@ -307,34 +316,23 @@ async function recoverPlatformOperations(taskRef, selection, options) {
307
316
  }
308
317
  const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
309
318
  && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
310
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')))
311
- .filter((operation) => selection === 'all' || operation.dependency === selection)
312
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
319
+ || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
313
320
  if (!candidates.length)
314
321
  return result('no-op', [], []);
322
+ const head = candidates[0];
323
+ if (selection !== 'all' && head.dependency !== selection) {
324
+ return result('blocked', [], candidates.map((operation) => operation.id), {
325
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
326
+ message: 'The first unresolved platform operation belongs to another recovery selection',
327
+ retryable: true
328
+ });
329
+ }
315
330
  const limit = options.limit === undefined ? candidates.length : Math.max(1, options.limit);
316
331
  const recovered = [];
317
332
  const pending = [];
318
333
  const selected = candidates.slice(0, limit);
319
334
  for (const [index, operation] of selected.entries()) {
320
- if (operation.state === 'failed') {
321
- pending.push(operation.id);
322
- return result('blocked', recovered, pending, {
323
- code: operation.lastCode || 'PLATFORM_OPERATION_FAILED',
324
- message: 'A previous platform operation failed permanently; later writes remain queued',
325
- retryable: false
326
- });
327
- }
328
- if (operation.attempts >= operation.maxAttempts) {
329
- pending.push(operation.id, ...selected.slice(index + 1).map((next) => next.id));
330
- pending.push(...candidates.slice(limit).map((next) => next.id));
331
- return result('blocked', recovered, pending, {
332
- code: operation.lastCode || 'PLATFORM_OPERATION_RETRY_LIMIT_REACHED',
333
- message: 'A previous platform operation exhausted its recovery attempts; later writes remain queued',
334
- retryable: false
335
- });
336
- }
337
- if (operation.kind !== 'issue-create') {
335
+ {
338
336
  try {
339
337
  recordPlatformOperation({
340
338
  taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind,
@@ -368,8 +366,8 @@ async function recoverPlatformOperations(taskRef, selection, options) {
368
366
  if (current.id !== operation.id) {
369
367
  try {
370
368
  recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
371
- expectedDigest: operation.expectedDigest, issueMetadata: operation.issueMetadata, dependency: operation.dependency,
372
- state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
369
+ expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, dependency: operation.dependency,
370
+ state: 'queued', replaceOperationId: operation.id });
373
371
  targetOperation = { ...operation, expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, id: current.id };
374
372
  }
375
373
  catch (error) {
@@ -396,7 +394,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
396
394
  if (current.id !== operation.id) {
397
395
  try {
398
396
  recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
399
- expectedDigest: operation.expectedDigest, dependency: operation.dependency, state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
397
+ expectedDigest: current.expectedDigest, dependency: operation.dependency, state: 'queued', replaceOperationId: operation.id });
400
398
  targetOperation = { ...operation, expectedDigest: current.expectedDigest, id: current.id };
401
399
  }
402
400
  catch (error) {
@@ -437,7 +435,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
437
435
  ...(operation.pullRequestSummary ? { pullRequestSummary: operation.pullRequestSummary } : {}),
438
436
  ...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
439
437
  dependency: operation.dependency,
440
- state: succeeded ? 'succeeded' : remote.status === 'failed' && remote.error?.retryable === false ? 'failed' : 'unknown',
438
+ state: succeeded ? 'succeeded' : 'unknown',
441
439
  lastCode: remote.error?.code ?? null
442
440
  });
443
441
  }
@@ -455,5 +453,81 @@ async function recoverPlatformOperations(taskRef, selection, options) {
455
453
  pending.push(...remaining);
456
454
  return result(pending.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, pending, pending.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain pending after the recovery budget', retryable: true } : null);
457
455
  }
456
+ async function recoverPlatformOperations(taskRef, selection, options) {
457
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
458
+ if (!resolved.ok)
459
+ return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
460
+ let initial;
461
+ try {
462
+ initial = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
463
+ }
464
+ catch (error) {
465
+ const value = error;
466
+ return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
467
+ }
468
+ const eligible = (operation) => operation.id !== options.excludeId
469
+ && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
470
+ || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
471
+ const first = initial.operations.filter(eligible);
472
+ if (!first.length)
473
+ return result('no-op', [], []);
474
+ const operationLimit = options.limit === undefined ? first.length : Math.max(1, options.limit);
475
+ const attemptLimit = options.attempts ?? DEFAULT_RECOVERY_ATTEMPTS;
476
+ if (!Number.isSafeInteger(attemptLimit) || attemptLimit <= 0) {
477
+ return result('failed', [], [], { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'Recovery attempts must be a positive safe integer', retryable: false });
478
+ }
479
+ const recovered = [];
480
+ for (let operationIndex = 0; operationIndex < operationLimit; operationIndex += 1) {
481
+ let latest;
482
+ try {
483
+ latest = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
484
+ }
485
+ catch (error) {
486
+ const value = error;
487
+ return result('failed', recovered, [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
488
+ }
489
+ const head = latest.operations.find(eligible);
490
+ if (!head)
491
+ break;
492
+ if (selection !== 'all' && head.dependency !== selection) {
493
+ const pending = latest.operations.filter(eligible).map((operation) => operation.id);
494
+ return result('blocked', recovered, pending, {
495
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
496
+ message: 'The first unresolved platform operation belongs to another recovery selection',
497
+ retryable: true
498
+ });
499
+ }
500
+ let last = null;
501
+ for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
502
+ last = await recoverPlatformOperationsOnce(taskRef, selection, { ...options, limit: 1 });
503
+ recovered.push(...last.recovered.filter((id) => !recovered.includes(id)));
504
+ if (last.recovered.length > 0 || last.status === 'applied' || last.status === 'no-op')
505
+ break;
506
+ }
507
+ if (!last || (last.recovered.length === 0 && last.status !== 'applied' && last.status !== 'no-op')) {
508
+ let current;
509
+ try {
510
+ current = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
511
+ }
512
+ catch {
513
+ return result('blocked', recovered, [head.id], last?.error ?? null);
514
+ }
515
+ const pending = current.operations.filter(eligible).map((operation) => operation.id);
516
+ return result(last?.status === 'failed' ? 'failed' : 'blocked', recovered, pending, last?.error ?? {
517
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
518
+ message: 'The first queued platform operation remains unresolved after this recovery run',
519
+ retryable: true
520
+ });
521
+ }
522
+ }
523
+ let remaining;
524
+ try {
525
+ remaining = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.filter(eligible).map((operation) => operation.id);
526
+ }
527
+ catch {
528
+ remaining = [];
529
+ }
530
+ return result(remaining.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, remaining, remaining.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain queued after this recovery run', retryable: true } : null);
531
+ }
458
532
  export { fieldsMatchExpected, labelsMatchOwnedPrefix, recoverPlatformOperations };
459
533
  //# sourceMappingURL=platform-operation-recovery.js.map
@@ -98,16 +98,21 @@ function enumerateTaskDirs(repoRoot) {
98
98
  */
99
99
  function enumerateAllTaskDirs(repoRoot) {
100
100
  const out = [];
101
- const seen = new Set();
101
+ const seen = new Map();
102
102
  const add = (taskId, taskDir, state) => {
103
103
  if (!TASK_ID_RE.test(taskId))
104
104
  return;
105
105
  const stat = fs.lstatSync(taskDir);
106
106
  if (!stat.isDirectory() || stat.isSymbolicLink() || !fs.existsSync(path.join(taskDir, 'task.md')))
107
107
  return;
108
- if (seen.has(taskId))
108
+ const canonicalDir = fs.realpathSync.native(taskDir);
109
+ const previousDir = seen.get(taskId);
110
+ if (previousDir !== undefined) {
111
+ if (previousDir === canonicalDir)
112
+ return;
109
113
  throw new Error(`duplicate task identity: ${taskId}`);
110
- seen.add(taskId);
114
+ }
115
+ seen.set(taskId, canonicalDir);
111
116
  out.push({ taskId, taskDir, state });
112
117
  };
113
118
  for (const state of FLAT_WORKSPACE_DIRS) {
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { artifactName, maxArtifactRound } from "./artifact-name.js";
4
- import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.js";
3
+ import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
5
4
  const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
6
5
  export function parseReviewedGitTree(value) {
7
6
  return REVIEWED_TREE_RE.test(value) ? value : null;
@@ -28,32 +27,11 @@ export function resolvePostReviewGlobs(config = {}, reviewConfig = {}) {
28
27
  return [":/", ...excludes.map((p) => `:(top,exclude)${p}`)];
29
28
  }
30
29
  export function findAuthoritativeReviewCodeArtifact(taskDir) {
31
- const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
32
- const round = maxArtifactRound(entries, "review-code");
33
- if (round === 0) {
30
+ const inventory = inspectArtifactDirectory(taskDir, 'review-code');
31
+ if (inventory.status !== 'ready' || !inventory.latest) {
34
32
  return { ok: false, round: 0, fileName: null, path: null, error: null };
35
33
  }
36
- const fileName = artifactName("review-code", round);
37
- let taskContent;
38
- try {
39
- taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
40
- }
41
- catch (error) {
42
- return {
43
- ok: false, round, fileName, path: null,
44
- error: error instanceof Error ? error.message : String(error)
45
- };
46
- }
47
- const invalidation = parseInvalidationDocument(taskContent);
48
- if (!invalidation.ok) {
49
- return { ok: false, round, fileName, path: null, error: invalidation.message };
50
- }
51
- if (invalidationBlocks(invalidation.document)) {
52
- return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
53
- }
54
- if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
55
- return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
56
- }
34
+ const { round, name: fileName } = inventory.latest;
57
35
  return {
58
36
  ok: true,
59
37
  round,