@fitlab-ai/agent-infra 0.9.12 → 0.9.13

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 (239) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/dist/bin/internal-cli.js +24 -19
  4. package/dist/lib/agent-clients/next-steps.js +6 -1
  5. package/dist/lib/decide.js +3 -3
  6. package/dist/lib/internal/platform-checks.js +3 -3
  7. package/dist/lib/internal/platform-comment.js +9 -9
  8. package/dist/lib/internal/platform-context.js +2 -2
  9. package/dist/lib/internal/platform-issue.js +8 -9
  10. package/dist/lib/internal/platform-pr-review.js +10 -10
  11. package/dist/lib/internal/platform-pr.js +63 -24
  12. package/dist/lib/internal/platform-release-notes.js +3 -3
  13. package/dist/lib/internal/pr-review-grade.js +11 -9
  14. package/dist/lib/internal/release-workflow.js +4 -4
  15. package/dist/lib/internal/sandbox-control.js +30 -4
  16. package/dist/lib/internal/task-activity.js +3 -3
  17. package/dist/lib/internal/task-artifact.js +76 -24
  18. package/dist/lib/internal/task-create.js +2 -2
  19. package/dist/lib/internal/task-event.js +6 -3
  20. package/dist/lib/internal/task-finalization.js +2 -2
  21. package/dist/lib/internal/task-invalidation.js +60 -0
  22. package/dist/lib/internal/task-ledger.js +9 -6
  23. package/dist/lib/internal/task-lifecycle.js +2 -2
  24. package/dist/lib/internal/task-override.js +4 -4
  25. package/dist/lib/internal/task-review.js +3 -3
  26. package/dist/lib/internal/task-verify.js +6 -6
  27. package/dist/lib/internal/task-warning.js +3 -3
  28. package/dist/lib/platform/adapters.js +1 -3
  29. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  30. package/dist/lib/platform/completion-backfill.js +6 -5
  31. package/dist/lib/platform/context.js +70 -130
  32. package/dist/lib/platform/github-client.js +1 -1
  33. package/dist/lib/platform/github-provider.js +903 -0
  34. package/dist/lib/platform/in-label-sync.js +143 -0
  35. package/dist/lib/platform/issue-comments.js +94 -41
  36. package/dist/lib/platform/issues.js +335 -189
  37. package/dist/lib/platform/metadata-labels.js +6 -2
  38. package/dist/lib/platform/none-provider.js +28 -0
  39. package/dist/lib/platform/pr-change-report.js +551 -0
  40. package/dist/lib/platform/pr-checks.js +119 -104
  41. package/dist/lib/platform/pr-review.js +88 -72
  42. package/dist/lib/platform/pr-summary.js +520 -50
  43. package/dist/lib/platform/provider-bridge.js +45 -0
  44. package/dist/lib/platform/provider-contract.d.ts +487 -0
  45. package/dist/lib/platform/provider-contract.js +95 -0
  46. package/dist/lib/platform/provider-loader.js +251 -0
  47. package/dist/lib/platform/provider-validation.js +504 -0
  48. package/dist/lib/platform/pull-request-metadata.js +8 -6
  49. package/dist/lib/platform/pull-requests.js +783 -537
  50. package/dist/lib/platform/release-notes.js +78 -55
  51. package/dist/lib/platform/releases.js +79 -50
  52. package/dist/lib/platform/resource-identity.d.ts +26 -0
  53. package/dist/lib/platform/resource-identity.js +92 -0
  54. package/dist/lib/platform/task-identities.js +31 -0
  55. package/dist/lib/platform/verification-required.js +11 -12
  56. package/dist/lib/platform/verification-sync.js +112 -238
  57. package/dist/lib/pr-review/evidence-grading.js +19 -6
  58. package/dist/lib/run/index.js +63 -23
  59. package/dist/lib/sandbox/control/client.js +96 -15
  60. package/dist/lib/sandbox/control/executor.js +3 -3
  61. package/dist/lib/sandbox/control/protocol.js +77 -0
  62. package/dist/lib/sandbox/control/server.js +398 -25
  63. package/dist/lib/sandbox/control/state.js +204 -1
  64. package/dist/lib/server/protocol.js +38 -1
  65. package/dist/lib/task/activity-log.js +8 -1
  66. package/dist/lib/task/artifact-lifecycle.js +33 -5
  67. package/dist/lib/task/capabilities.js +239 -0
  68. package/dist/lib/task/commands/status.js +32 -0
  69. package/dist/lib/task/commit-operation.js +12 -1
  70. package/dist/lib/task/control-authority.js +8 -5
  71. package/dist/lib/task/create-service.js +8 -8
  72. package/dist/lib/task/current-contract.js +5 -1
  73. package/dist/lib/task/decision-details.js +1 -1
  74. package/dist/lib/task/events.js +297 -17
  75. package/dist/lib/task/finalization.js +37 -13
  76. package/dist/lib/task/human-override.js +58 -36
  77. package/dist/lib/task/invalidation-command.js +86 -0
  78. package/dist/lib/task/invalidation.js +260 -0
  79. package/dist/lib/task/ledger-intents.js +37 -0
  80. package/dist/lib/task/local-artifact-finalization.js +374 -0
  81. package/dist/lib/task/orchestration.js +49 -93
  82. package/dist/lib/task/pr-delivery-fact.js +102 -101
  83. package/dist/lib/task/review-fingerprint.js +24 -2
  84. package/dist/lib/task/rework-intent.js +138 -0
  85. package/dist/lib/task/skill-scope.js +23 -0
  86. package/dist/lib/task/task-execution-lock.js +27 -17
  87. package/dist/lib/task/verification-engine.js +43 -16
  88. package/dist/lib/task/verification.js +3 -3
  89. package/dist/lib/task/workflow-warning-intents.js +1 -1
  90. package/dist/lib/task/write.js +11 -0
  91. package/dist/lib/version.js +11 -1
  92. package/dist/lifecycle-build-manifest.json +3 -3
  93. package/lib/agent-clients/next-steps.ts +6 -1
  94. package/lib/decide.ts +3 -3
  95. package/lib/init.ts +1 -1
  96. package/lib/internal/platform-checks.ts +3 -3
  97. package/lib/internal/platform-comment.ts +8 -8
  98. package/lib/internal/platform-context.ts +2 -2
  99. package/lib/internal/platform-issue.ts +7 -8
  100. package/lib/internal/platform-pr-review.ts +9 -9
  101. package/lib/internal/platform-pr.ts +47 -22
  102. package/lib/internal/platform-release-notes.ts +3 -3
  103. package/lib/internal/pr-review-grade.ts +10 -8
  104. package/lib/internal/release-workflow.ts +4 -4
  105. package/lib/internal/sandbox-control.ts +27 -3
  106. package/lib/internal/task-activity.ts +3 -3
  107. package/lib/internal/task-artifact.ts +65 -22
  108. package/lib/internal/task-create.ts +2 -2
  109. package/lib/internal/task-event.ts +6 -3
  110. package/lib/internal/task-finalization.ts +3 -3
  111. package/lib/internal/task-invalidation.ts +39 -0
  112. package/lib/internal/task-ledger.ts +11 -8
  113. package/lib/internal/task-lifecycle.ts +2 -2
  114. package/lib/internal/task-override.ts +6 -6
  115. package/lib/internal/task-review.ts +3 -3
  116. package/lib/internal/task-verify.ts +6 -6
  117. package/lib/internal/task-warning.ts +3 -3
  118. package/lib/platform/adapters.ts +3 -2
  119. package/lib/platform/change-request-git-evidence.ts +36 -9
  120. package/lib/platform/completion-backfill.ts +9 -8
  121. package/lib/platform/context.ts +97 -134
  122. package/lib/platform/github-client.ts +2 -2
  123. package/lib/platform/github-provider.ts +910 -0
  124. package/lib/platform/in-label-sync.ts +208 -0
  125. package/lib/platform/issue-comments.ts +97 -47
  126. package/lib/platform/issues.ts +344 -174
  127. package/lib/platform/metadata-labels.ts +5 -2
  128. package/lib/platform/none-provider.ts +35 -0
  129. package/lib/platform/pr-change-report.ts +645 -0
  130. package/lib/platform/pr-checks.ts +126 -106
  131. package/lib/platform/pr-review.ts +88 -85
  132. package/lib/platform/pr-summary.ts +560 -53
  133. package/lib/platform/provider-bridge.ts +82 -0
  134. package/lib/platform/provider-contract.ts +536 -0
  135. package/lib/platform/provider-loader.ts +307 -0
  136. package/lib/platform/provider-validation.ts +529 -0
  137. package/lib/platform/pull-request-metadata.ts +10 -7
  138. package/lib/platform/pull-requests.ts +826 -543
  139. package/lib/platform/release-notes.ts +76 -59
  140. package/lib/platform/releases.ts +77 -43
  141. package/lib/platform/resource-identity.ts +121 -0
  142. package/lib/platform/task-identities.ts +40 -0
  143. package/lib/platform/types.ts +4 -2
  144. package/lib/platform/verification-required.ts +11 -13
  145. package/lib/platform/verification-sync.ts +112 -292
  146. package/lib/pr-review/evidence-grading.ts +25 -8
  147. package/lib/run/index.ts +57 -22
  148. package/lib/sandbox/control/client.ts +105 -15
  149. package/lib/sandbox/control/executor.ts +4 -4
  150. package/lib/sandbox/control/protocol.ts +113 -0
  151. package/lib/sandbox/control/server.ts +431 -23
  152. package/lib/sandbox/control/state.ts +251 -1
  153. package/lib/server/protocol.ts +36 -1
  154. package/lib/task/activity-log.ts +8 -0
  155. package/lib/task/artifact-lifecycle.ts +32 -6
  156. package/lib/task/capabilities.ts +262 -0
  157. package/lib/task/commands/status.ts +38 -0
  158. package/lib/task/commit-operation.ts +12 -1
  159. package/lib/task/control-authority.ts +11 -8
  160. package/lib/task/create-service.ts +8 -8
  161. package/lib/task/current-contract.ts +6 -1
  162. package/lib/task/decision-details.ts +6 -2
  163. package/lib/task/events.ts +296 -16
  164. package/lib/task/finalization.ts +50 -14
  165. package/lib/task/human-override.ts +70 -46
  166. package/lib/task/invalidation-command.ts +86 -0
  167. package/lib/task/invalidation.ts +332 -0
  168. package/lib/task/ledger-intents.ts +35 -1
  169. package/lib/task/local-artifact-finalization.ts +563 -0
  170. package/lib/task/orchestration.ts +52 -85
  171. package/lib/task/pr-delivery-fact.ts +131 -148
  172. package/lib/task/review-fingerprint.ts +23 -2
  173. package/lib/task/rework-intent.ts +165 -0
  174. package/lib/task/skill-scope.ts +24 -0
  175. package/lib/task/task-execution-lock.ts +23 -14
  176. package/lib/task/verification-engine.ts +52 -17
  177. package/lib/task/verification.ts +4 -4
  178. package/lib/task/workflow-warning-intents.ts +1 -1
  179. package/lib/task/write.ts +19 -0
  180. package/lib/update.ts +1 -1
  181. package/lib/version.ts +15 -1
  182. package/package.json +7 -1
  183. package/templates/.agents/rules/README.en.md +1 -1
  184. package/templates/.agents/rules/README.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  186. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  187. package/templates/.agents/rules/issue-sync.en.md +2 -0
  188. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  189. package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
  190. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
  191. package/templates/.agents/rules/pr-sync.en.md +10 -4
  192. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  193. package/templates/.agents/rules/task-short-id.en.md +1 -4
  194. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  195. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
  196. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
  197. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  198. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  199. package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
  200. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
  201. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  202. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  203. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  204. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  205. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  206. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  207. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
  208. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
  209. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  210. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  211. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
  212. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
  213. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  214. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  215. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  216. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  217. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  218. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  219. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  220. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  221. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  222. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  223. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
  224. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
  225. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  226. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  227. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  229. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
  232. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
  233. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  236. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  237. package/templates/.agents/templates/task.en.md +18 -1
  238. package/templates/.agents/templates/task.zh-CN.md +18 -1
  239. package/templates/.github/workflows/pr-label.yml +7 -26
@@ -1,10 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
3
4
  import { declareImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
4
5
  import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
5
6
  import { resolveTaskRef } from './resolve-ref.js';
6
7
  import { writeTask } from './write.js';
7
8
  import { allowsManualOverride } from './guard-override.js';
9
+ import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
8
10
  const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
9
11
  const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
10
12
  const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
@@ -102,6 +104,41 @@ function applyLedgerIntent(intent, options = {}) {
102
104
  if (intent.kind === 'decision-next-id') {
103
105
  return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
104
106
  }
107
+ if (intent.kind === 'rework-intent-upsert') {
108
+ if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !['analysis', 'plan', 'code'].includes(intent.target) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
109
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, target, or source hash is invalid', resolved.taskId, intent.intentId);
110
+ }
111
+ const finding = rows.find((row) => row.id === intent.findingId);
112
+ if (!finding || finding.severity === 'decision')
113
+ return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
114
+ const sourceFromEvidence = finding.evidence.split('#')[0];
115
+ if (sourceFromEvidence !== intent.sourceArtifact || !REVIEW_ARTIFACT[finding.stage]?.test(intent.sourceArtifact)) {
116
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
117
+ }
118
+ try {
119
+ const actualHash = createHash('sha256').update(fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact))).digest('hex');
120
+ if (actualHash !== intent.sourceSha256)
121
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
122
+ const parsed = parseReworkIntentDocument(content);
123
+ if (!parsed.ok)
124
+ return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
125
+ const nextIntent = {
126
+ intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
127
+ sourceSha256: intent.sourceSha256, target: intent.target, status: 'pending',
128
+ declaredAt: new Date().toISOString(), consumedAt: ''
129
+ };
130
+ const next = upsertReworkIntent(parsed.intents, nextIntent);
131
+ if (!next.changed)
132
+ return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
133
+ const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
134
+ if (result.status === 'failed')
135
+ return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
136
+ return { status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId: intent.intentId, before: null, after: null, operations: result.operations, error: null };
137
+ }
138
+ catch (error) {
139
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
140
+ }
141
+ }
105
142
  let before = null;
106
143
  let after;
107
144
  if (intent.kind === 'finding-upsert') {
@@ -0,0 +1,374 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { inspectDecisionDetailDuplicates, scanVisibleMarkdown } from './decision-details.js';
5
+ import { parseArtifactName, validateCompletedArtifact } from './artifact-lifecycle.js';
6
+ import { resolveTaskRef } from './resolve-ref.js';
7
+ const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
8
+ analysis: [
9
+ '需求来源', '需求理解', '相关文件', '影响评估', '技术风险',
10
+ '工作量和复杂度评估', '状态核对'
11
+ ],
12
+ plan: [
13
+ '问题理解', '约束条件', '方案对比', '技术方法', '实施步骤',
14
+ '文件清单', '验证策略', '状态核对'
15
+ ],
16
+ code: [
17
+ '实现输入', '变更文件', '关键代码说明', '测试结果', '与方案的差异',
18
+ '供审查关注的内容', '状态核对', '证据原文'
19
+ ]
20
+ };
21
+ const LOCAL_ARTIFACT_REQUIRED_PATTERNS = ['^\\$ '];
22
+ function finalizationIntentRoot(repoRoot) {
23
+ return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
24
+ }
25
+ function finalizationIntentPath(repoRoot, taskId, family, artifact) {
26
+ return path.join(finalizationIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
27
+ }
28
+ function isFinalizationIntent(value) {
29
+ if (!value || typeof value !== 'object' || Array.isArray(value))
30
+ return false;
31
+ const intent = value;
32
+ return intent.version === 1
33
+ && typeof intent.taskId === 'string' && intent.taskId.length > 0
34
+ && (intent.family === 'analysis' || intent.family === 'plan' || intent.family === 'code')
35
+ && typeof intent.artifact === 'string' && intent.artifact.length > 0
36
+ && (intent.state === 'awaiting-repair' || intent.state === 'passed' || intent.state === 'consumed')
37
+ && (intent.baselineSemanticDigest === null || (typeof intent.baselineSemanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.baselineSemanticDigest)))
38
+ && typeof intent.artifactSha256 === 'string' && /^[a-f0-9]{64}$/.test(intent.artifactSha256)
39
+ && typeof intent.semanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.semanticDigest)
40
+ && (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
41
+ }
42
+ function readLocalArtifactFinalizationIntent(repoRoot, taskId, family, artifact) {
43
+ const target = finalizationIntentPath(repoRoot, taskId, family, artifact);
44
+ if (!fs.existsSync(target))
45
+ return null;
46
+ const value = JSON.parse(fs.readFileSync(target, 'utf8'));
47
+ if (!isFinalizationIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
48
+ throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
49
+ }
50
+ return value;
51
+ }
52
+ function writeLocalArtifactFinalizationIntent(repoRoot, value) {
53
+ if (!isFinalizationIntent(value))
54
+ throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
55
+ const directory = finalizationIntentRoot(repoRoot);
56
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
57
+ const target = finalizationIntentPath(repoRoot, value.taskId, value.family, value.artifact);
58
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
59
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
60
+ try {
61
+ fs.renameSync(temporary, target);
62
+ }
63
+ catch (error) {
64
+ try {
65
+ fs.unlinkSync(temporary);
66
+ }
67
+ catch { /* preserve primary error */ }
68
+ throw error;
69
+ }
70
+ }
71
+ function consumeLocalArtifactFinalizationIntent(repoRoot, intent) {
72
+ if (intent.state === 'consumed')
73
+ return intent;
74
+ if (intent.state !== 'passed')
75
+ throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
76
+ const consumed = { ...intent, state: 'consumed' };
77
+ writeLocalArtifactFinalizationIntent(repoRoot, consumed);
78
+ return consumed;
79
+ }
80
+ function requiredSections(family) {
81
+ return LOCAL_ARTIFACT_REQUIRED_SECTIONS[family];
82
+ }
83
+ function lineNumber(content, offset) {
84
+ return content.slice(0, offset).split('\n').length;
85
+ }
86
+ function sectionBody(content, heading) {
87
+ const next = scanVisibleMarkdown(content).headings.find((candidate) => (candidate.start > heading.start && candidate.level <= 2));
88
+ return content.slice(heading.end, next?.start ?? content.length);
89
+ }
90
+ function semanticDigest(content, candidate) {
91
+ let normalized = content;
92
+ if (candidate) {
93
+ const raw = content.slice(candidate.start, candidate.end);
94
+ const marker = raw.match(/([::])\s*$/);
95
+ if (marker?.index !== undefined) {
96
+ const canonical = raw.slice(0, marker.index) + raw.slice(marker.index + 1);
97
+ normalized = `${content.slice(0, candidate.start)}${canonical}${content.slice(candidate.end)}`;
98
+ }
99
+ }
100
+ return createHash('sha256').update(normalized, 'utf8').digest('hex');
101
+ }
102
+ function sha256Content(content) {
103
+ return createHash('sha256').update(content, 'utf8').digest('hex');
104
+ }
105
+ function visibleSectionHeadings(content, section) {
106
+ return scanVisibleMarkdown(content).headings.filter((heading) => (heading.level === 2 && heading.text === section));
107
+ }
108
+ function trailingPunctuationCandidates(content, section) {
109
+ return scanVisibleMarkdown(content).headings.filter((heading) => (heading.level === 2 && (heading.text === `${section}:` || heading.text === `${section}:`)));
110
+ }
111
+ function diagnostic(code, message, content, heading, extra = {}) {
112
+ return {
113
+ code, message, repairable: false,
114
+ line: heading ? lineNumber(content, heading.start) : null,
115
+ ...extra
116
+ };
117
+ }
118
+ function isStatusPattern(pattern) {
119
+ return pattern === '^\\$ ';
120
+ }
121
+ function validateLocalArtifact(content, options) {
122
+ const sections = options.requiredSections ?? requiredSections(options.family);
123
+ const patterns = options.requiredPatterns ?? LOCAL_ARTIFACT_REQUIRED_PATTERNS;
124
+ const diagnostics = [];
125
+ const candidates = [];
126
+ const scanned = scanVisibleMarkdown(content);
127
+ if (!content.trim()) {
128
+ diagnostics.push(diagnostic('LOCAL_ARTIFACT_EMPTY', 'artifact is empty', content));
129
+ }
130
+ for (const section of sections) {
131
+ const exact = scanned.headings.filter((heading) => heading.level === 2 && heading.text === section);
132
+ const punctuated = trailingPunctuationCandidates(content, section);
133
+ if (exact.length > 1 || punctuated.length > 0 && exact.length > 0 || punctuated.length > 1) {
134
+ const first = exact[1] ?? punctuated[0] ?? exact[0];
135
+ diagnostics.push(diagnostic('LOCAL_ARTIFACT_DUPLICATE_SECTION', `required section '${section}' is duplicated or has an ambiguous punctuation variant`, content, first));
136
+ }
137
+ else if (exact.length === 0 && punctuated.length === 1) {
138
+ candidates.push(punctuated[0]);
139
+ }
140
+ else if (exact.length === 0) {
141
+ diagnostics.push(diagnostic('LOCAL_ARTIFACT_MISSING_SECTION', `required section '${section}' is missing`, content));
142
+ }
143
+ }
144
+ const statusSection = sections.find((section) => section === '状态核对' || section.toLowerCase() === 'state check');
145
+ if (statusSection) {
146
+ const statusHeading = visibleSectionHeadings(content, statusSection)[0]
147
+ ?? candidates.find((heading) => heading.text === `${statusSection}:` || heading.text === `${statusSection}:`);
148
+ if (statusHeading) {
149
+ const body = sectionBody(content, statusHeading);
150
+ for (const pattern of patterns.filter(isStatusPattern)) {
151
+ if (!new RegExp(pattern, 'm').test(body)) {
152
+ diagnostics.push(diagnostic('LOCAL_STATUS_COMMAND_MISSING', `status section '${statusSection}' is missing required command output`, content, statusHeading));
153
+ }
154
+ }
155
+ }
156
+ }
157
+ for (const pattern of patterns.filter((item) => !isStatusPattern(item))) {
158
+ if (!new RegExp(pattern, 'm').test(content)) {
159
+ diagnostics.push(diagnostic('LOCAL_REQUIRED_PATTERN_MISSING', `artifact is missing required pattern '${pattern}'`, content));
160
+ }
161
+ }
162
+ const decisionDetails = inspectDecisionDetailDuplicates(content);
163
+ if (!decisionDetails.ok) {
164
+ diagnostics.push(diagnostic('LOCAL_DECISION_DETAIL_DUPLICATE', decisionDetails.message, content, decisionDetails.duplicates[0]?.blocks[0]
165
+ ? scanned.headings.find((heading) => heading.start === decisionDetails.duplicates[0].blocks[0].start)
166
+ : undefined));
167
+ }
168
+ if (candidates.length === 1 && diagnostics.length === 0) {
169
+ const candidate = candidates[0];
170
+ const section = candidate.text.slice(0, -1);
171
+ const repair = diagnostic('LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION', `visible required H2 '${candidate.text}' may be normalized to '${section}'`, content, candidate, {
172
+ repairable: true,
173
+ from: candidate.text,
174
+ to: section,
175
+ operation: 'replace-line'
176
+ });
177
+ return {
178
+ ok: false,
179
+ family: options.family,
180
+ semanticDigest: semanticDigest(content, candidate),
181
+ repairable: true,
182
+ diagnostics: [repair]
183
+ };
184
+ }
185
+ if (candidates.length > 0) {
186
+ for (const candidate of candidates) {
187
+ const section = candidate.text.slice(0, -1);
188
+ diagnostics.push(diagnostic('LOCAL_ARTIFACT_MISSING_SECTION', `required section '${section}' is missing and cannot be repaired while other structural defects exist`, content, candidate));
189
+ }
190
+ }
191
+ return {
192
+ ok: diagnostics.length === 0,
193
+ family: options.family,
194
+ semanticDigest: semanticDigest(content),
195
+ repairable: false,
196
+ diagnostics
197
+ };
198
+ }
199
+ function failedFinalization(request, error, extra = {}) {
200
+ return {
201
+ status: 'failed', changed: false,
202
+ taskId: null, taskDir: null,
203
+ family: request.family, artifact: request.artifact,
204
+ artifactSha256: null, semanticDigest: null,
205
+ repairable: false, diagnostics: [], error, ...extra
206
+ };
207
+ }
208
+ function provenanceFailure(request, resolved, content, artifactSha256, semanticDigestValue, code, message) {
209
+ return failedFinalization(request, { code, message }, {
210
+ taskId: resolved.taskId,
211
+ taskDir: resolved.taskDir,
212
+ artifactSha256,
213
+ semanticDigest: semanticDigestValue,
214
+ diagnostics: [diagnostic(code, message, content)]
215
+ });
216
+ }
217
+ function finalizeLocalArtifact(request) {
218
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
219
+ if (!resolved.ok)
220
+ return failedFinalization(request, { code: resolved.code, message: resolved.message });
221
+ const parsed = parseArtifactName(request.artifact);
222
+ if (!parsed || parsed.family !== request.family) {
223
+ return failedFinalization(request, {
224
+ code: 'ARTIFACT_IDENTITY_INVALID',
225
+ message: `artifact '${request.artifact}' does not match ${request.family}`
226
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir });
227
+ }
228
+ const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
229
+ if (!validated.ok) {
230
+ return failedFinalization(request, validated.error, { taskId: resolved.taskId, taskDir: resolved.taskDir });
231
+ }
232
+ let content;
233
+ try {
234
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
235
+ }
236
+ catch (error) {
237
+ return failedFinalization(request, { code: 'ARTIFACT_NOT_READABLE', message: String(error) }, {
238
+ taskId: resolved.taskId, taskDir: resolved.taskDir
239
+ });
240
+ }
241
+ const result = validateLocalArtifact(content, {
242
+ family: request.family,
243
+ requiredSections: request.requiredSections,
244
+ requiredPatterns: request.requiredPatterns
245
+ });
246
+ const artifactSha256 = sha256Content(content);
247
+ let intent;
248
+ try {
249
+ intent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
250
+ }
251
+ catch (error) {
252
+ return failedFinalization(request, {
253
+ code: 'LOCAL_FINALIZATION_INTENT_INVALID',
254
+ message: error instanceof Error ? error.message : String(error)
255
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
256
+ }
257
+ if (result.repairable) {
258
+ if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== result.semanticDigest)) {
259
+ return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_PROVENANCE_CONFLICT', 'a different local repair baseline is already recorded for this artifact');
260
+ }
261
+ if (!intent) {
262
+ try {
263
+ writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
264
+ version: 1,
265
+ taskId: resolved.taskId,
266
+ family: request.family,
267
+ artifact: request.artifact,
268
+ state: 'awaiting-repair',
269
+ baselineSemanticDigest: result.semanticDigest,
270
+ artifactSha256,
271
+ semanticDigest: result.semanticDigest
272
+ });
273
+ }
274
+ catch (error) {
275
+ return failedFinalization(request, {
276
+ code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
277
+ message: error instanceof Error ? error.message : String(error)
278
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
279
+ }
280
+ }
281
+ return {
282
+ status: 'failed',
283
+ changed: false,
284
+ taskId: resolved.taskId,
285
+ taskDir: resolved.taskDir,
286
+ family: request.family,
287
+ artifact: request.artifact,
288
+ artifactSha256,
289
+ semanticDigest: result.semanticDigest,
290
+ repairable: true,
291
+ diagnostics: result.diagnostics,
292
+ error: {
293
+ code: 'LOCAL_ARTIFACT_INVALID',
294
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
295
+ }
296
+ };
297
+ }
298
+ if (!result.ok) {
299
+ return {
300
+ status: 'failed',
301
+ changed: false,
302
+ taskId: resolved.taskId,
303
+ taskDir: resolved.taskDir,
304
+ family: request.family,
305
+ artifact: request.artifact,
306
+ artifactSha256,
307
+ semanticDigest: result.semanticDigest,
308
+ repairable: false,
309
+ diagnostics: result.diagnostics,
310
+ error: {
311
+ code: 'LOCAL_ARTIFACT_INVALID',
312
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
313
+ }
314
+ };
315
+ }
316
+ if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== result.semanticDigest) {
317
+ return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
318
+ }
319
+ if ((intent?.state === 'passed' || intent?.state === 'consumed')
320
+ && (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== result.semanticDigest)) {
321
+ return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_PROVENANCE_CONFLICT', 'the artifact changed after its finalization provenance was recorded');
322
+ }
323
+ if (intent?.state === 'consumed') {
324
+ return {
325
+ status: 'passed',
326
+ changed: false,
327
+ taskId: resolved.taskId,
328
+ taskDir: resolved.taskDir,
329
+ family: request.family,
330
+ artifact: request.artifact,
331
+ artifactSha256,
332
+ semanticDigest: result.semanticDigest,
333
+ repairable: false,
334
+ diagnostics: result.diagnostics,
335
+ error: null
336
+ };
337
+ }
338
+ try {
339
+ writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
340
+ version: 1,
341
+ taskId: resolved.taskId,
342
+ family: request.family,
343
+ artifact: request.artifact,
344
+ state: 'passed',
345
+ baselineSemanticDigest: intent?.baselineSemanticDigest ?? null,
346
+ artifactSha256,
347
+ semanticDigest: result.semanticDigest
348
+ });
349
+ }
350
+ catch (error) {
351
+ return failedFinalization(request, {
352
+ code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
353
+ message: error instanceof Error ? error.message : String(error)
354
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
355
+ }
356
+ return {
357
+ status: result.ok ? 'passed' : 'failed',
358
+ changed: false,
359
+ taskId: resolved.taskId,
360
+ taskDir: resolved.taskDir,
361
+ family: request.family,
362
+ artifact: request.artifact,
363
+ artifactSha256,
364
+ semanticDigest: result.semanticDigest,
365
+ repairable: result.repairable,
366
+ diagnostics: result.diagnostics,
367
+ error: result.ok ? null : {
368
+ code: 'LOCAL_ARTIFACT_INVALID',
369
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
370
+ }
371
+ };
372
+ }
373
+ export { LOCAL_ARTIFACT_REQUIRED_PATTERNS, LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, readLocalArtifactFinalizationIntent, semanticDigest, sha256Content, validateLocalArtifact };
374
+ //# sourceMappingURL=local-artifact-finalization.js.map
@@ -3,8 +3,7 @@ import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
5
  import { validateCurrentTaskContract } from './current-contract.js';
6
- import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
7
- import { inspectArtifactDirectory } from './artifact-lifecycle.js';
6
+ import { parseReviewSummary } from './review-artifacts.js';
8
7
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
9
8
  import { resolveTaskRef } from './resolve-ref.js';
10
9
  import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
@@ -18,6 +17,9 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
18
17
  import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.js';
19
18
  import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.js';
20
19
  import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
20
+ import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js';
21
+ import { hasOpenLifecycleExecution } from './activity-log.js';
22
+ import { invalidationBlocks } from './invalidation.js';
21
23
  function supportsLifecycleDelegation(client) {
22
24
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
23
25
  && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
@@ -377,86 +379,21 @@ function validateSavedCurrentDiffBase(repoRoot, metadata, taskDir, reviewedHead)
377
379
  }
378
380
  return null;
379
381
  }
380
- function hasReviewAfterArtifact(taskDir, family, reviewFamily) {
381
- const artifact = inspectArtifactDirectory(taskDir, family);
382
- const review = inspectArtifactDirectory(taskDir, reviewFamily);
383
- return artifact.status === 'ready'
384
- && review.status === 'ready'
385
- && artifact.latest !== null
386
- && review.reviewedInput?.name === artifact.latest.name;
387
- }
388
- function latestReviewApproved(taskDir, family) {
389
- const round = highestRound(taskDir, family);
390
- if (round === 0)
391
- return { approved: false, error: null };
392
- let content;
393
- try {
394
- content = fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8');
395
- }
396
- catch (error) {
397
- return { approved: false, error: String(error) };
398
- }
399
- const parsed = parseReviewSummary(content);
400
- if (!parsed.ok)
401
- return { approved: false, error: `${parsed.code}: ${parsed.message}` };
402
- const verdict = resolveCanonicalVerdict(parsed.summary);
403
- if (!verdict.ok)
404
- return { approved: false, error: `${verdict.code}: ${verdict.message}` };
405
- return { approved: verdict.verdict === 'Approved', error: null };
406
- }
407
- function routeFromFacts(taskDir) {
408
- const analysisRound = highestRound(taskDir, 'analysis');
409
- const analysisReviewRound = highestRound(taskDir, 'review-analysis');
410
- const planRound = highestRound(taskDir, 'plan');
411
- const planReviewRound = highestRound(taskDir, 'review-plan');
412
- const codeRound = highestRound(taskDir, 'code');
413
- const codeReviewRound = highestRound(taskDir, 'review-code');
414
- if (analysisRound === 0) {
415
- const round = analysisRound + 1;
416
- return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
417
- }
418
- if (!hasReviewAfterArtifact(taskDir, 'analysis', 'review-analysis')) {
419
- const round = analysisReviewRound + 1;
420
- return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
421
- }
422
- const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
423
- if (analysisVerdict.error)
424
- return { error: analysisVerdict.error };
425
- if (!analysisVerdict.approved) {
426
- const round = analysisRound + 1;
427
- return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
428
- }
429
- if (planRound === 0) {
430
- const round = planRound + 1;
431
- return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
432
- }
433
- if (!hasReviewAfterArtifact(taskDir, 'plan', 'review-plan')) {
434
- const round = planReviewRound + 1;
435
- return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
436
- }
437
- const planVerdict = latestReviewApproved(taskDir, 'review-plan');
438
- if (planVerdict.error)
439
- return { error: planVerdict.error };
440
- if (!planVerdict.approved) {
441
- const round = planRound + 1;
442
- return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
443
- }
444
- if (codeRound === 0) {
445
- const round = codeRound + 1;
446
- return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
447
- }
448
- if (!hasReviewAfterArtifact(taskDir, 'code', 'review-code')) {
449
- const round = codeReviewRound + 1;
450
- return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
451
- }
452
- const codeVerdict = latestReviewApproved(taskDir, 'review-code');
453
- if (codeVerdict.error)
454
- return { error: codeVerdict.error };
455
- if (!codeVerdict.approved) {
456
- const round = codeRound + 1;
457
- return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
458
- }
459
- return { action: 'commit', role: 'executor', stage: 'commit', round: 1, artifact: 'commit' };
382
+ function routeFromFacts(facts) {
383
+ const recommendation = recommendNext(facts);
384
+ if (!recommendation.action)
385
+ return { completion: true };
386
+ const action = recommendation.action;
387
+ if (action === 'manual-validation' || action === 'validation-run')
388
+ return null;
389
+ const family = action === 'analysis' ? 'analysis' : action;
390
+ const round = Math.max(0, ...[...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])].map((name) => {
391
+ const match = /-r(\d+)\.md$/.exec(name);
392
+ return match ? Number(match[1]) : 1;
393
+ })) + 1;
394
+ const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
395
+ const role = action.startsWith('review-') ? 'reviewer' : 'executor';
396
+ return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
460
397
  }
461
398
  function routeOrchestration(taskRef, options = {}) {
462
399
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
@@ -478,12 +415,14 @@ function routeOrchestration(taskRef, options = {}) {
478
415
  }
479
416
  let metadata;
480
417
  let currentLedger = null;
418
+ let currentInvalidation = { operations: [], targets: [] };
481
419
  if (resolved.state === 'active') {
482
420
  const contract = validateCurrentTaskContract(content);
483
421
  if (!contract.ok)
484
422
  return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
485
423
  metadata = contract.metadata;
486
424
  currentLedger = contract.ledger;
425
+ currentInvalidation = contract.invalidation;
487
426
  }
488
427
  else {
489
428
  try {
@@ -493,19 +432,23 @@ function routeOrchestration(taskRef, options = {}) {
493
432
  return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
494
433
  }
495
434
  }
496
- const routed = routeFromFacts(resolved.taskDir);
497
- if (routed && 'error' in routed)
498
- return failed('ORCHESTRATION_REVIEW_INVALID', routed.error, resolved.taskId);
435
+ if (invalidationBlocks(currentInvalidation)) {
436
+ return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
437
+ }
438
+ let run;
439
+ try {
440
+ run = readRun(resolved.taskDir);
441
+ }
442
+ catch (error) {
443
+ return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
444
+ }
445
+ const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state, hasOpenLifecycleExecution(content) || Boolean(run?.pendingDelegation));
446
+ if (!facts.ok)
447
+ return failed('ORCHESTRATION_CAPABILITY_FACTS_INVALID', facts.message, resolved.taskId);
448
+ const routed = routeFromFacts(facts.facts);
499
449
  if (!routed)
500
450
  return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
501
- const run = readRun(resolved.taskDir);
502
- const policy = run ? rolePolicy(run, routed.role) : null;
503
- const next = {
504
- ...routed,
505
- requestedModel: policy?.model ?? null,
506
- requestedReasoningEffort: policy?.reasoningEffort ?? null
507
- };
508
- if (next.stage === 'commit') {
451
+ if ('completion' in routed) {
509
452
  const reviewRound = highestRound(resolved.taskDir, 'review-code');
510
453
  const review = parseReviewSummary(fs.readFileSync(path.join(resolved.taskDir, artifactName('review-code', reviewRound)), 'utf8'));
511
454
  if (!review.ok || review.summary.manualValidation === null) {
@@ -581,6 +524,19 @@ function routeOrchestration(taskRef, options = {}) {
581
524
  saveRun(resolved.taskDir, completed);
582
525
  return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
583
526
  }
527
+ const action = routed.stage;
528
+ const capability = canStart(action, facts.facts, {
529
+ initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${routed.round}`,
530
+ requestedAction: action, reasonCode: 'user-request', explicitRequest: true
531
+ });
532
+ if (!capability.allowed)
533
+ return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
534
+ const policy = run ? rolePolicy(run, routed.role) : null;
535
+ const next = {
536
+ ...routed,
537
+ requestedModel: policy?.model ?? null,
538
+ requestedReasoningEffort: policy?.reasoningEffort ?? null
539
+ };
584
540
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
585
541
  }
586
542
  function statusOrchestration(taskRef, options = {}) {