@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,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
3
4
  import { buildArtifactLinkSection, artifactName, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
4
- import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
5
+ import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
5
6
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
6
7
  import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
7
8
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
@@ -13,6 +14,11 @@ import { commitOrchestrationStageCompletion } from './orchestration.js';
13
14
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
14
15
  import { captureTaskWriteMetadata, writeTask } from './write.js';
15
16
  import { allowsManualOverride } from './guard-override.js';
17
+ import { LOCAL_ARTIFACT_REQUIRED_PATTERNS, LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
18
+ import { loadVerificationConfig } from './verification-config.js';
19
+ import { buildLifecycleFacts, canStart } from './capabilities.js';
20
+ import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
21
+ import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
16
22
  const eventCatalog = [
17
23
  'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
18
24
  'review-analysis.started', 'review-analysis.completed',
@@ -23,19 +29,19 @@ const eventCatalog = [
23
29
  'manual-validation.started', 'manual-validation.completed',
24
30
  'validation-run.started', 'validation-run.completed'
25
31
  ];
26
- const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']);
32
+ const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
27
33
  const SCHEMAS = {
28
34
  'analyze.started': { optional: ['round'] },
29
35
  'analyze.awaiting-input': { required: ['question'] },
30
- 'analyze.completed': { required: ['artifact'], optional: ['round', 'orchestrated'] },
36
+ 'analyze.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'orchestrated'] },
31
37
  'review-analysis.started': { optional: ['round'] },
32
38
  'review-analysis.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
33
39
  'plan.started': { optional: ['round'] },
34
- 'plan.completed': { required: ['artifact'], optional: ['round', 'orchestrated'] },
40
+ 'plan.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'orchestrated'] },
35
41
  'review-plan.started': { optional: ['round'] },
36
42
  'review-plan.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
37
43
  'code.started': { optional: ['round', 'fixFor', 'implementationInput'] },
38
- 'code.completed': { required: ['artifact'], optional: ['round', 'fixFor', 'implementationInput', 'filesModified', 'testsPassed', 'blockers', 'major', 'minor', 'manualValidation', 'orchestrated'] },
44
+ 'code.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'fixFor', 'implementationInput', 'filesModified', 'testsPassed', 'blockers', 'major', 'minor', 'manualValidation', 'orchestrated'] },
39
45
  'review-code.started': { optional: ['round'] },
40
46
  'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
41
47
  'manual-validation.started': { optional: ['round'] },
@@ -43,6 +49,31 @@ const SCHEMAS = {
43
49
  'validation-run.started': { optional: ['round'] },
44
50
  'validation-run.completed': { required: ['artifact'], optional: ['round'] }
45
51
  };
52
+ function localArtifactValidationConfig(repoRoot, family) {
53
+ const fallback = {
54
+ requiredSections: LOCAL_ARTIFACT_REQUIRED_SECTIONS[family],
55
+ requiredPatterns: LOCAL_ARTIFACT_REQUIRED_PATTERNS
56
+ };
57
+ const skillName = family === 'analysis' ? 'analyze-task' : family === 'plan' ? 'plan-task' : 'code-task';
58
+ try {
59
+ const artifact = loadVerificationConfig(repoRoot, skillName).checks.artifact;
60
+ if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact))
61
+ return fallback;
62
+ const sections = artifact.required_sections;
63
+ const patterns = artifact.required_patterns;
64
+ return {
65
+ requiredSections: Array.isArray(sections)
66
+ ? sections.filter((value) => typeof value === 'string')
67
+ : fallback.requiredSections,
68
+ requiredPatterns: Array.isArray(patterns)
69
+ ? patterns.filter((value) => typeof value === 'string')
70
+ : fallback.requiredPatterns
71
+ };
72
+ }
73
+ catch {
74
+ return fallback;
75
+ }
76
+ }
46
77
  function validateTaskEventRequest(request) {
47
78
  if (!eventCatalog.includes(request.event))
48
79
  return { code: 'EVENT_UNKNOWN', message: `unknown task event '${request.event}'` };
@@ -65,6 +96,18 @@ function validateTaskEventRequest(request) {
65
96
  }
66
97
  if (request.verdict && !['approved', 'changes-requested', 'rejected'].includes(request.verdict))
67
98
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'verdict is invalid' };
99
+ if (request.initiator && !['human', 'model', 'orchestrator'].includes(request.initiator))
100
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'initiator is invalid' };
101
+ if (request.requestId !== undefined && (!request.requestId.trim() || /[\r\n]/.test(request.requestId)))
102
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'requestId must be a non-empty single line' };
103
+ if (request.reasonCode && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'].includes(request.reasonCode))
104
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'reasonCode is invalid' };
105
+ for (const [name, value] of [['sourceFinding', request.sourceFinding], ['sourceArtifact', request.sourceArtifact], ['sourceSha256', request.sourceSha256]]) {
106
+ if (value !== undefined && (!value.trim() || /[\r\n]/.test(value)))
107
+ return { code: 'EVENT_PAYLOAD_INVALID', message: `${name} must be a non-empty single line` };
108
+ }
109
+ if (request.sourceSha256 !== undefined && !/^[a-f0-9]{64}$/.test(request.sourceSha256))
110
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'sourceSha256 must be a sha256 digest' };
68
111
  if (request.event === 'code.completed') {
69
112
  const fix = request.fixFor !== undefined || ['blockers', 'major', 'minor', 'manualValidation'].some((key) => request[key] !== undefined);
70
113
  const modeRequired = fix ? ['fixFor', 'blockers', 'major', 'minor', 'manualValidation'] : ['filesModified', 'testsPassed'];
@@ -76,21 +119,30 @@ function validateTaskEventRequest(request) {
76
119
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'fixFor must reference a canonical review-code artifact' };
77
120
  if (request.implementationInput && !/^II-[1-9]\d*$/.test(request.implementationInput))
78
121
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'implementationInput must be a canonical II-N id' };
122
+ if (request.artifactSha256 !== undefined && !/^[0-9a-f]{64}$/i.test(request.artifactSha256))
123
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'artifactSha256 must be a 64-character hexadecimal digest' };
124
+ if (request.semanticDigest !== undefined && !/^[0-9a-f]{64}$/i.test(request.semanticDigest))
125
+ return { code: 'EVENT_PAYLOAD_INVALID', message: 'semanticDigest must be a 64-character hexadecimal digest' };
79
126
  if (request.fixFor && request.implementationInput)
80
127
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'fixFor and implementationInput are mutually exclusive' };
81
128
  if (request.summaryResult !== undefined && (!request.summaryResult.trim() || /[\r\n]/.test(request.summaryResult)))
82
129
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'summaryResult must be a non-empty single line' };
130
+ if (request.event !== 'analyze.awaiting-input' && /\.(?:started|completed)$/.test(request.event)) {
131
+ if (!request.initiator || !request.requestId || !request.reasonCode) {
132
+ return { code: 'EVENT_TRIGGER_REQUIRED', message: `${request.event} requires --initiator, --request-id, and --reason-code` };
133
+ }
134
+ }
83
135
  return null;
84
136
  }
85
137
  const FAMILY = {
86
- analyze: { artifact: 'analysis', started: ['requirement-analysis', 'requirement-analysis-review', 'code'], completed: ['requirement-analysis', 'requirement-analysis-review', 'code'], target: 'requirement-analysis', label: 'Analyze Task' },
87
- 'review-analysis': { artifact: 'review-analysis', started: ['requirement-analysis', 'requirement-analysis-review'], completed: ['requirement-analysis', 'requirement-analysis-review'], target: 'requirement-analysis-review', label: 'Review Analysis' },
88
- plan: { artifact: 'plan', started: ['requirement-analysis-review', 'technical-design-review', 'commit'], completed: ['requirement-analysis-review', 'technical-design-review', 'commit'], target: 'technical-design', label: 'Plan Task' },
89
- 'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
90
- code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
91
- 'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
92
- 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
93
- 'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
138
+ analyze: { artifact: 'analysis', target: 'requirement-analysis', label: 'Analyze Task' },
139
+ 'review-analysis': { artifact: 'review-analysis', target: 'requirement-analysis-review', label: 'Review Analysis' },
140
+ plan: { artifact: 'plan', target: 'technical-design', label: 'Plan Task' },
141
+ 'review-plan': { artifact: 'review-plan', target: 'technical-design-review', label: 'Review Plan' },
142
+ code: { artifact: 'code', target: 'code', label: 'Code Task' },
143
+ 'review-code': { artifact: 'review-code', target: 'code-review', label: 'Review Code' },
144
+ 'manual-validation': { artifact: 'manual-validation', target: null, label: 'Complete Manual Validation' },
145
+ 'validation-run': { artifact: 'validation-run', target: null, label: 'Run Manual Validation' }
94
146
  };
95
147
  const REVIEW_LEDGER_STAGES = {
96
148
  'review-analysis': 'analysis',
@@ -248,6 +300,124 @@ function openStartedIdentity(rows, family) {
248
300
  function reviewInputFamily(family) {
249
301
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
250
302
  }
303
+ function lifecycleAction(family) {
304
+ return family === 'analyze' ? 'analysis' : family;
305
+ }
306
+ function eventTrigger(request, family) {
307
+ return {
308
+ initiator: request.initiator ?? (request.orchestrated ? 'orchestrator' : request.agent === 'human' ? 'human' : 'model'),
309
+ requestId: request.requestId ?? `${request.taskRef}:${request.event}:${request.round ?? 1}`,
310
+ requestedAction: lifecycleAction(family),
311
+ reasonCode: request.reasonCode ?? (request.fixFor || request.implementationInput ? 'review-finding' : 'user-request'),
312
+ ...(request.sourceFinding ? { sourceFinding: request.sourceFinding } : {}),
313
+ ...(request.sourceArtifact ? { sourceArtifact: request.sourceArtifact } : {}),
314
+ ...(request.sourceSha256 ? { sourceSha256: request.sourceSha256 } : {}),
315
+ ...(request.implementationInput ? { implementationInput: request.implementationInput } : {}),
316
+ explicitRequest: request.requestId !== undefined || request.reasonCode !== undefined || request.initiator !== undefined || request.orchestrated === true
317
+ };
318
+ }
319
+ function invalidationMutationForCompletion(content, taskDir, family, artifact, timestamp) {
320
+ if (!['analyze', 'plan', 'code'].includes(family))
321
+ return { mutation: null };
322
+ const parsed = parseInvalidationDocument(content);
323
+ if (!parsed.ok)
324
+ return { error: parsed.message };
325
+ const downstream = {
326
+ analyze: ['review-analysis', 'plan', 'review-plan', 'code', 'review-code'],
327
+ plan: ['review-plan', 'code', 'review-code'],
328
+ code: ['review-code']
329
+ };
330
+ const sourceFamily = family;
331
+ let receipts = [];
332
+ try {
333
+ receipts = parseArtifactReceipts(content).rows;
334
+ }
335
+ catch (error) {
336
+ return { error: error instanceof Error ? error.message : String(error) };
337
+ }
338
+ const targets = downstream[sourceFamily].flatMap((targetFamily) => {
339
+ const names = fs.readdirSync(taskDir).filter((name) => {
340
+ const pattern = targetFamily === 'review-analysis' ? /^review-analysis(?:-r\d+)?\.md$/
341
+ : targetFamily === 'review-plan' ? /^review-plan(?:-r\d+)?\.md$/
342
+ : targetFamily === 'review-code' ? /^review-code(?:-r\d+)?\.md$/
343
+ : new RegExp(`^${targetFamily}(?:-r\\d+)?\\.md$`);
344
+ return pattern.test(name);
345
+ });
346
+ return names.flatMap((name) => {
347
+ try {
348
+ const stat = fs.lstatSync(path.join(taskDir, name));
349
+ if (!stat.isFile() || stat.isSymbolicLink())
350
+ return [];
351
+ const targetRound = parseArtifactName(name)?.round ?? 1;
352
+ const targetSha256 = sha256File(path.join(taskDir, name));
353
+ const shapes = [
354
+ { targetKind: 'artifact', targetFamily, targetArtifact: name, targetRound, targetSha256 }
355
+ ];
356
+ const receipt = receipts.find((candidate) => candidate.output === name);
357
+ if (receipt)
358
+ shapes.push({
359
+ targetKind: 'receipt', targetFamily, targetArtifact: name, targetRound,
360
+ targetSha256: receipt.inputSha256
361
+ });
362
+ if (targetFamily.startsWith('review-')) {
363
+ shapes.push({ targetKind: 'approval', targetFamily, targetArtifact: name, targetRound, targetSha256 });
364
+ }
365
+ if (targetFamily === 'review-code') {
366
+ shapes.push({ targetKind: 'reviewed-snapshot', targetFamily, targetArtifact: name, targetRound, targetSha256 });
367
+ }
368
+ return shapes.map((targetShape) => ({
369
+ ...targetShape,
370
+ targetId: targetIdFor('pending', targetShape), operationId: 'pending', status: 'pending',
371
+ reasonCode: 'upstream-replaced', updatedAt: timestamp
372
+ }));
373
+ }
374
+ catch (error) {
375
+ throw new Error(`cannot inspect invalidation target '${name}': ${error instanceof Error ? error.message : String(error)}`);
376
+ }
377
+ });
378
+ });
379
+ if (targets.length === 0)
380
+ return { mutation: null };
381
+ const source = {
382
+ sourceFamily: lifecycleAction(family), sourceArtifact: artifact.name,
383
+ sourceRound: parseArtifactName(artifact.name)?.round ?? 1, sourceSha256: sha256File(artifact.path),
384
+ createdAt: timestamp, updatedAt: timestamp
385
+ };
386
+ const operation = createInvalidationOperation(source);
387
+ const normalizedTargets = targets.map((target) => ({
388
+ ...target, operationId: operation.operationId,
389
+ targetId: targetIdFor(operation.operationId, target)
390
+ }));
391
+ const operationWithTotal = createInvalidationOperation(source, normalizedTargets);
392
+ const next = upsertInvalidation(parsed.document, operationWithTotal, normalizedTargets);
393
+ if (!next.ok)
394
+ return { error: next.message };
395
+ return { mutation: next.changed ? invalidationMutation(content, next.document) : null };
396
+ }
397
+ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, timestamp) {
398
+ const parsed = parseReworkIntentDocument(content);
399
+ if (!parsed.ok)
400
+ return { error: parsed.message };
401
+ const hash = sha256File(artifact.path);
402
+ let next = parsed.intents;
403
+ if (family === 'analyze' || family === 'plan' || family === 'code') {
404
+ const hashes = Object.fromEntries(fs.readdirSync(taskDir).flatMap((name) => {
405
+ if (!/^review-(?:analysis|plan|code)(?:-r\d+)?\.md$/.test(name))
406
+ return [];
407
+ try {
408
+ return [[name, sha256File(path.join(taskDir, name))]];
409
+ }
410
+ catch {
411
+ return [];
412
+ }
413
+ }));
414
+ next = consumeReworkIntents(next, family === 'analyze' ? 'analysis' : family, hashes, timestamp).intents;
415
+ }
416
+ if (family === 'review-analysis' || family === 'review-plan' || family === 'review-code') {
417
+ next = supersedeReworkIntents(next, artifact.name, hash, timestamp).intents;
418
+ }
419
+ return { mutation: JSON.stringify(next) === JSON.stringify(parsed.intents) ? null : reworkIntentMutation(content, next) };
420
+ }
251
421
  function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
252
422
  if (family === 'code') {
253
423
  let content;
@@ -399,6 +569,7 @@ function applyTaskEventUnlocked(request, options = {}) {
399
569
  const completedRows = matchingRows.filter((item) => item.done);
400
570
  const row = manual ? openRows.at(-1) : matchingRows[0];
401
571
  let completedArtifact = null;
572
+ let localFinalizationIntent = null;
402
573
  if (eventIdentity.phase === 'started' && row) {
403
574
  if (row.agent !== normalized.agent)
404
575
  return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
@@ -418,14 +589,83 @@ function applyTaskEventUnlocked(request, options = {}) {
418
589
  if (!validated.ok)
419
590
  return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
420
591
  completedArtifact = validated.artifact;
592
+ if (eventIdentity.family === 'analyze' || eventIdentity.family === 'plan' || eventIdentity.family === 'code') {
593
+ const localFamily = eventIdentity.family === 'analyze'
594
+ ? 'analysis'
595
+ : eventIdentity.family === 'plan' ? 'plan' : 'code';
596
+ let artifactContent;
597
+ try {
598
+ artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
599
+ }
600
+ catch (error) {
601
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read ${completedArtifact.name}: ${String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
602
+ }
603
+ const validationConfig = localArtifactValidationConfig(resolved.repoRoot, localFamily);
604
+ const local = validateLocalArtifact(artifactContent, {
605
+ family: localFamily,
606
+ requiredSections: validationConfig.requiredSections,
607
+ requiredPatterns: validationConfig.requiredPatterns
608
+ });
609
+ if (!local.ok) {
610
+ return failed(normalized, {
611
+ code: 'EVENT_ARTIFACT_CONFLICT',
612
+ message: `LOCAL_ARTIFACT_INVALID: ${local.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')}`
613
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
614
+ }
615
+ const actualSha256 = sha256File(completedArtifact.path);
616
+ if (actualSha256 !== normalized.artifactSha256) {
617
+ return failed(normalized, {
618
+ code: 'EVENT_ARTIFACT_CONFLICT',
619
+ message: `artifact SHA-256 does not match finalizer result for ${completedArtifact.name}`
620
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
621
+ }
622
+ if (local.semanticDigest !== normalized.semanticDigest) {
623
+ return failed(normalized, {
624
+ code: 'EVENT_ARTIFACT_CONFLICT',
625
+ message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
626
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
627
+ }
628
+ try {
629
+ localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
630
+ }
631
+ catch (error) {
632
+ return failed(normalized, {
633
+ code: 'EVENT_ARTIFACT_CONFLICT',
634
+ message: `cannot read local finalizer provenance for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
635
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
636
+ }
637
+ if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
638
+ return failed(normalized, {
639
+ code: 'EVENT_ARTIFACT_CONFLICT',
640
+ message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
641
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
642
+ }
643
+ if (localFinalizationIntent.artifactSha256 !== actualSha256 || localFinalizationIntent.semanticDigest !== local.semanticDigest) {
644
+ return failed(normalized, {
645
+ code: 'EVENT_ARTIFACT_CONFLICT',
646
+ message: `local finalizer provenance does not match ${completedArtifact.name}`
647
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
648
+ }
649
+ }
421
650
  }
422
651
  if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
423
652
  const existing = section.entries.find((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note);
424
653
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, existing.time, frontmatter, artifactContext);
425
654
  }
426
- const allowed = FAMILY[eventIdentity.family][eventIdentity.phase === 'started' ? 'started' : 'completed'];
427
- if (!allowed.includes(currentStep) && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID'))
428
- return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${normalized.event} is not allowed from '${currentStep}'` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
655
+ if (eventIdentity.phase === 'started') {
656
+ const trigger = eventTrigger(normalized, eventIdentity.family);
657
+ const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state);
658
+ if (!facts.ok)
659
+ return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${facts.code}: ${facts.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
660
+ const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
661
+ const capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
662
+ ? stateOverride
663
+ : allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
664
+ if (!capability.allowed && !capabilityOverride) {
665
+ const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
666
+ return failed(normalized, { code, message: `${capability.reasonCode}: ${capability.evidence.join(', ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
667
+ }
668
+ }
429
669
  const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
430
670
  if (findingCountError)
431
671
  return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
@@ -459,12 +699,27 @@ function applyTaskEventUnlocked(request, options = {}) {
459
699
  return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
460
700
  }
461
701
  let completionReceipt = null;
702
+ let completionInvalidation = null;
703
+ let completionRework = null;
462
704
  if (eventIdentity.phase === 'completed' && completedArtifact) {
463
705
  const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
464
706
  if (receipt && !receipt.ok) {
465
707
  return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
466
708
  }
467
709
  completionReceipt = receipt?.receipt ?? null;
710
+ try {
711
+ const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
712
+ if ('error' in invalidation)
713
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: invalidation.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
714
+ completionInvalidation = invalidation.mutation;
715
+ const rework = reworkIntentMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
716
+ if ('error' in rework)
717
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: rework.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
718
+ completionRework = rework.mutation;
719
+ }
720
+ catch (error) {
721
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
722
+ }
468
723
  }
469
724
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
470
725
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
@@ -521,6 +776,10 @@ function applyTaskEventUnlocked(request, options = {}) {
521
776
  return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
522
777
  }
523
778
  }
779
+ if (completionInvalidation)
780
+ mutations.push(completionInvalidation);
781
+ if (completionRework)
782
+ mutations.push(completionRework);
524
783
  if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
525
784
  let implementationRows;
526
785
  try {
@@ -536,7 +795,28 @@ function applyTaskEventUnlocked(request, options = {}) {
536
795
  });
537
796
  }
538
797
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
539
- const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, metadataProvider: () => metadata });
798
+ if (!normalized.dryRun && localFinalizationIntent && completedArtifact && localFinalizationIntent.state === 'passed') {
799
+ try {
800
+ localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
801
+ }
802
+ catch (error) {
803
+ return failed(normalized, {
804
+ code: 'EVENT_ARTIFACT_CONFLICT',
805
+ message: `local finalizer provenance could not be consumed before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
806
+ }, {
807
+ taskId: resolved.taskId,
808
+ taskMdPath: resolved.taskMdPath,
809
+ fromStep: currentStep,
810
+ toStep: step,
811
+ action: eventIdentity.action,
812
+ phase: eventIdentity.phase,
813
+ artifactContext
814
+ });
815
+ }
816
+ }
817
+ const sourceCompletion = eventIdentity.phase === 'completed'
818
+ && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
819
+ const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
540
820
  if (result.status === 'failed')
541
821
  return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
542
822
  if (!normalized.dryRun && orchestrationCompletion) {
@@ -42,7 +42,7 @@ function failed(taskId, error, overrides = {}) {
42
42
  ...overrides
43
43
  };
44
44
  }
45
- function emptyReceipt(taskId) {
45
+ function emptyReceipt(taskId, controlBinding) {
46
46
  return {
47
47
  version: RECEIPT_VERSION,
48
48
  taskId,
@@ -54,6 +54,7 @@ function emptyReceipt(taskId) {
54
54
  verification: 'pending',
55
55
  warningProjection: 'done',
56
56
  warnings: [],
57
+ ...(controlBinding ? { controlBinding } : {}),
57
58
  updatedAt: now(),
58
59
  lastError: null
59
60
  };
@@ -79,6 +80,7 @@ function validateReceipt(value, taskId) {
79
80
  if (!value || typeof value !== 'object' || Array.isArray(value))
80
81
  throw new Error('receipt must be an object');
81
82
  const receipt = value;
83
+ const controlBinding = receipt.controlBinding;
82
84
  const states = ['pending', 'done', 'skipped'];
83
85
  if (receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
84
86
  || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
@@ -89,7 +91,11 @@ function validateReceipt(value, taskId) {
89
91
  || !['pending', 'done'].includes(String(receipt.warningProjection))
90
92
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
91
93
  || typeof receipt.updatedAt !== 'string'
92
- || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError))))
94
+ || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
95
+ || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
96
+ || Array.isArray(controlBinding)
97
+ || typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
98
+ || typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId))))
93
99
  throw new Error('receipt schema is invalid');
94
100
  return receipt;
95
101
  }
@@ -100,6 +106,13 @@ function readReceipt(repoRoot, taskId) {
100
106
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
101
107
  return validateReceipt(value, taskId);
102
108
  }
109
+ function readTaskFinalizationReceipt(repoRoot, taskId) {
110
+ return readReceipt(path.resolve(repoRoot), taskId);
111
+ }
112
+ function controlBindingMatches(receipt, binding) {
113
+ return receipt.controlBinding?.generation === binding.generation
114
+ && receipt.controlBinding.requestId === binding.requestId;
115
+ }
103
116
  function writeReceipt(repoRoot, receipt) {
104
117
  const directory = finalizationRoot(repoRoot);
105
118
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -346,7 +359,7 @@ function reconcileWarningProjection(repoRoot, taskId, receipt, consumed) {
346
359
  return receipt;
347
360
  }
348
361
  }
349
- function syncPendingTaskComment(input) {
362
+ async function syncPendingTaskComment(input) {
350
363
  const { repoRoot, taskId, agent, commentSync, consumedCapabilities } = input;
351
364
  let receipt = input.receipt;
352
365
  if (receipt.taskComment !== 'pending') {
@@ -358,7 +371,7 @@ function syncPendingTaskComment(input) {
358
371
  };
359
372
  }
360
373
  try {
361
- const result = commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
374
+ const result = await commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
362
375
  const step = commentStep(result);
363
376
  if (result.status === 'applied' || result.status === 'no-op') {
364
377
  const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
@@ -416,7 +429,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
416
429
  error: hardError ? error : postLifecyclePending ? null : error
417
430
  };
418
431
  }
419
- function applyUnderLock(request, taskId, options) {
432
+ async function applyUnderLock(request, taskId, options) {
420
433
  const repoRoot = path.resolve(options.repoRoot);
421
434
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
422
435
  const commentSync = options.commentSync ?? syncPlatformComment;
@@ -426,17 +439,28 @@ function applyUnderLock(request, taskId, options) {
426
439
  try {
427
440
  const file = receiptPath(repoRoot, taskId);
428
441
  const existed = fs.existsSync(file);
429
- receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId);
442
+ receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId, options.controlBinding);
430
443
  if (!existed)
431
444
  writeReceipt(repoRoot, receipt);
432
445
  }
433
446
  catch (error) {
434
447
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
435
448
  }
449
+ if (options.controlBinding && !controlBindingMatches(receipt, options.controlBinding)) {
450
+ const error = {
451
+ code: 'TASK_FINALIZATION_CONTROL_BINDING_CONFLICT',
452
+ message: 'finalization receipt belongs to a different sandbox request or generation',
453
+ retryable: false
454
+ };
455
+ return failed(taskId, error, {
456
+ completedSteps: completedSteps(receipt),
457
+ pendingSteps: pendingSteps(receipt)
458
+ });
459
+ }
436
460
  const preflightState = resolveTaskRef(taskId, { repoRoot });
437
461
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
438
462
  try {
439
- const preflight = options.preflight({ taskRef: taskId, event: 'complete-task.preflight' }, { repoRoot });
463
+ const preflight = await options.preflight({ taskRef: taskId, event: 'complete-task.preflight' }, { repoRoot });
440
464
  if (preflight.status !== 'pass') {
441
465
  const detail = verificationFailure(preflight);
442
466
  return failed(taskId, detail, {
@@ -510,7 +534,7 @@ function applyUnderLock(request, taskId, options) {
510
534
  }
511
535
  let taskComment = null;
512
536
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
513
- const initialComment = syncPendingTaskComment({
537
+ const initialComment = await syncPendingTaskComment({
514
538
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
515
539
  });
516
540
  receipt = initialComment.receipt;
@@ -526,7 +550,7 @@ function applyUnderLock(request, taskId, options) {
526
550
  verification = { status: 'no-op', changed: false, error: null };
527
551
  }
528
552
  else {
529
- const result = verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
553
+ const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
530
554
  verification = verificationStep(result);
531
555
  if (result.status === 'pass') {
532
556
  const capability = issueCapability(receipt, 'verification');
@@ -534,7 +558,7 @@ function applyUnderLock(request, taskId, options) {
534
558
  scope: 'verification', operation: 'succeeded'
535
559
  }, consumedCapabilities);
536
560
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
537
- const finalComment = syncPendingTaskComment({
561
+ const finalComment = await syncPendingTaskComment({
538
562
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
539
563
  });
540
564
  receipt = finalComment.receipt;
@@ -571,7 +595,7 @@ function applyUnderLock(request, taskId, options) {
571
595
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
572
596
  return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
573
597
  }
574
- function applyTaskFinalization(request, options) {
598
+ async function applyTaskFinalization(request, options) {
575
599
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
576
600
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
577
601
  }
@@ -580,7 +604,7 @@ function applyTaskFinalization(request, options) {
580
604
  if (!resolved.ok)
581
605
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
582
606
  try {
583
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
607
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
584
608
  }
585
609
  catch (error) {
586
610
  const detail = error instanceof TaskExecutionLockError
@@ -589,5 +613,5 @@ function applyTaskFinalization(request, options) {
589
613
  return failed(resolved.taskId, detail);
590
614
  }
591
615
  }
592
- export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability };
616
+ export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt };
593
617
  //# sourceMappingURL=finalization.js.map