@cassiomc1/forgeloop 1.12.0 → 1.14.0

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 (249) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/AGENT_COMPATIBILITY.md +8 -0
  4. package/CLAUDE.md +1 -1
  5. package/CONTRIBUTING.md +90 -0
  6. package/DOCS_INDEX.md +46 -12
  7. package/ENG/c-development-eng.md +112 -0
  8. package/ENG/cpp-development-eng.md +109 -0
  9. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  10. package/ENG/go-development-eng.md +103 -0
  11. package/ENG/java-development-eng.md +125 -0
  12. package/ENG/nodejs-backend-development-eng.md +605 -0
  13. package/ENG/php-development-eng.md +104 -0
  14. package/ENG/rust-development-eng.md +422 -0
  15. package/ENG/sec-code-eng.md +7 -7
  16. package/ENG/sql-development-eng.md +108 -0
  17. package/ENG/swift-development-eng.md +111 -0
  18. package/ENG/typescript-development-eng.md +108 -0
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +418 -9
  21. package/LOOP_ENGINEERING.md +28 -2
  22. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  23. package/PROTOCOL_INTEGRATION.md +55 -2
  24. package/QUALITY_SCORECARD.md +1 -0
  25. package/README.md +78 -52
  26. package/TERMINOLOGY.md +2 -0
  27. package/THIRD_PARTY_NOTICES.md +19 -7
  28. package/THREAT_MODEL.md +140 -1
  29. package/completions/_forgeloop +22 -4
  30. package/completions/forgeloop.bash +40 -4
  31. package/completions/forgeloop.fish +130 -1
  32. package/docs/ADVISORY_CONTEXT.md +25 -0
  33. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  34. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  35. package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
  36. package/docs/AGENT_SKILL.md +66 -0
  37. package/docs/ARTIFACT_REFERENCE.md +123 -0
  38. package/docs/AUDIT_UX.md +46 -0
  39. package/docs/BROWSER_VERIFICATION.md +136 -0
  40. package/docs/CLI_REFERENCE.md +392 -10
  41. package/docs/CODE_ATTESTATION.md +2 -2
  42. package/docs/DOCUMENTATION_GUIDE.md +34 -12
  43. package/docs/GETTING_STARTED.md +59 -0
  44. package/docs/JEV_BENCHMARKS.md +31 -0
  45. package/docs/MODEL_ROUTING.md +37 -0
  46. package/docs/OPENSRC_ADAPTER.md +241 -0
  47. package/docs/PACKAGE_CONTENTS.md +60 -19
  48. package/docs/PROVIDERS.md +126 -0
  49. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  50. package/docs/RECIPES.md +32 -0
  51. package/docs/RELEASE_CHECKLIST.md +66 -5
  52. package/docs/SECURITY_REVIEW.md +71 -0
  53. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  54. package/docs/TEST_INTELLIGENCE.md +29 -0
  55. package/docs/TEST_PRUNING.md +14 -0
  56. package/docs/TROUBLESHOOTING.md +298 -3
  57. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  58. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  59. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  61. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  62. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  64. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  65. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  67. package/docs/diagrams/README.md +13 -9
  68. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  69. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  70. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  71. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  72. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  73. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  74. package/docs/documentation-manifest.json +1397 -0
  75. package/docs/protocol-requirements.json +101 -0
  76. package/package.json +46 -4
  77. package/schemas/config.schema.json +14 -0
  78. package/schemas/context-plan.schema.json +18 -0
  79. package/schemas/routing-input.schema.json +1 -1
  80. package/schemas/semantic-decision.schema.json +46 -0
  81. package/schemas/test-utility.schema.json +44 -0
  82. package/scripts/CI_VALIDATORS.md +84 -11
  83. package/scripts/benchmark-jev.mjs +5 -0
  84. package/scripts/benchmark-test-intelligence.mjs +4 -0
  85. package/scripts/generate-agent-protocol-summary.mjs +40 -1
  86. package/scripts/generate-forgeloop-skill.mjs +133 -0
  87. package/scripts/jev-smoke.mjs +19 -0
  88. package/skills/forgeloop/README.md +9 -0
  89. package/skills/forgeloop/SKILL.md +77 -0
  90. package/skills/forgeloop/references/lifecycle.md +9 -0
  91. package/skills/forgeloop/references/recovery.md +7 -0
  92. package/skills/forgeloop/references/verification.md +7 -0
  93. package/src/adapters/agent-browser/assertions.js +47 -0
  94. package/src/adapters/agent-browser/commands.js +54 -0
  95. package/src/adapters/agent-browser/index.js +3 -0
  96. package/src/adapters/agent-browser/locator.js +40 -0
  97. package/src/adapters/agent-browser/process.js +215 -0
  98. package/src/adapters/agent-browser/provider.js +313 -0
  99. package/src/adapters/emulated-services/constants.js +24 -0
  100. package/src/adapters/emulated-services/index.js +7 -0
  101. package/src/adapters/emulated-services/process.js +162 -0
  102. package/src/adapters/emulated-services/provider.js +282 -0
  103. package/src/adapters/opensrc/normalize.js +90 -0
  104. package/src/adapters/opensrc/process.js +248 -0
  105. package/src/adapters/opensrc/provider.js +338 -0
  106. package/src/adapters/opensrc/search.js +264 -0
  107. package/src/adapters/typesafe/client.js +28 -0
  108. package/src/adapters/typesafe/engine.js +63 -0
  109. package/src/adapters/typesafe/normalize.js +41 -0
  110. package/src/cli.js +108 -0
  111. package/src/commands/checkpoint-revalidate.js +176 -0
  112. package/src/commands/context-plan.js +38 -0
  113. package/src/commands/contract-create.js +264 -0
  114. package/src/commands/contract-revise.js +236 -0
  115. package/src/commands/decision-show.js +14 -0
  116. package/src/commands/decision-status.js +22 -0
  117. package/src/commands/discover.js +41 -0
  118. package/src/commands/doctor.js +15 -0
  119. package/src/commands/gate-record.js +205 -0
  120. package/src/commands/gate-revalidate.js +137 -0
  121. package/src/commands/model-route.js +32 -0
  122. package/src/commands/next.js +19 -7
  123. package/src/commands/route.js +146 -18
  124. package/src/commands/semantic-plan.js +17 -0
  125. package/src/commands/task-abandon.js +224 -0
  126. package/src/commands/task-create.js +84 -25
  127. package/src/commands/task-list.js +22 -2
  128. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  129. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  130. package/src/commands/test-inventory.js +5 -0
  131. package/src/commands/test-prune-plan.js +5 -0
  132. package/src/commands/test-prune-probe.js +5 -0
  133. package/src/commands/test-utility.js +5 -0
  134. package/src/commands/validate-protocol.js +10 -1
  135. package/src/config/guides.json +44 -0
  136. package/src/core/artifact-registry.js +24 -0
  137. package/src/core/audit-ux.js +514 -0
  138. package/src/core/browser-verification/constants.js +149 -0
  139. package/src/core/browser-verification/normalize.js +254 -0
  140. package/src/core/browser-verification/provider.js +519 -0
  141. package/src/core/browser-verification/service.js +115 -0
  142. package/src/core/build-script.js +151 -0
  143. package/src/core/c-cpp-project.js +143 -0
  144. package/src/core/checkpoint-revalidation.js +319 -0
  145. package/src/core/cli-command-definitions.js +249 -1
  146. package/src/core/command-executors.js +115 -3
  147. package/src/core/command-input.js +212 -102
  148. package/src/core/completion-artifacts.js +14 -5
  149. package/src/core/completion.js +4 -6
  150. package/src/core/config.js +3 -0
  151. package/src/core/context-compiler/budget.js +9 -0
  152. package/src/core/context-compiler/candidates.js +39 -0
  153. package/src/core/context-compiler/compiler.js +63 -0
  154. package/src/core/context-compiler/fingerprint.js +11 -0
  155. package/src/core/context-compiler/policy.js +13 -0
  156. package/src/core/context-compiler/result.js +23 -0
  157. package/src/core/contract-bootstrap-recovery.js +655 -0
  158. package/src/core/contract-presets.js +82 -0
  159. package/src/core/contract-revision.js +210 -0
  160. package/src/core/decision/artifact.js +69 -0
  161. package/src/core/decision/benchmarks.js +103 -0
  162. package/src/core/decision/cache.js +27 -0
  163. package/src/core/decision/constants.js +58 -0
  164. package/src/core/decision/cutover.js +34 -0
  165. package/src/core/decision/engine.js +22 -0
  166. package/src/core/decision/errors.js +68 -0
  167. package/src/core/decision/events.js +101 -0
  168. package/src/core/decision/freshness.js +19 -0
  169. package/src/core/decision/normalizers/index.js +115 -0
  170. package/src/core/decision/policy.js +18 -0
  171. package/src/core/decision/projection.js +16 -0
  172. package/src/core/decision/question-registry.js +201 -0
  173. package/src/core/decision/request.js +26 -0
  174. package/src/core/decision/resolver.js +130 -0
  175. package/src/core/decision/result.js +58 -0
  176. package/src/core/decision/service.js +156 -0
  177. package/src/core/decision/state-builder.js +65 -0
  178. package/src/core/decision/task-bindings.js +30 -0
  179. package/src/core/decision/test-provider.js +32 -0
  180. package/src/core/decision/thresholds.js +15 -0
  181. package/src/core/error-codes.js +281 -3
  182. package/src/core/events.js +226 -57
  183. package/src/core/evidence-readiness.js +9 -0
  184. package/src/core/execution-prerequisites.js +14 -0
  185. package/src/core/execution-profile.js +63 -38
  186. package/src/core/filesystem.js +1 -10
  187. package/src/core/gate-provenance.js +124 -0
  188. package/src/core/go-project.js +206 -0
  189. package/src/core/integration-invocation-policy.js +27 -4
  190. package/src/core/integration-resources.js +86 -61
  191. package/src/core/java-project.js +403 -0
  192. package/src/core/model-router/constants.js +10 -0
  193. package/src/core/model-router/policy.js +103 -0
  194. package/src/core/model-router/router.js +37 -0
  195. package/src/core/multi-language-project.js +117 -0
  196. package/src/core/next-action-model.js +58 -0
  197. package/src/core/next-action-phases.js +130 -42
  198. package/src/core/next-action-refresh.js +43 -9
  199. package/src/core/next-action-review-phase.js +7 -2
  200. package/src/core/next-action.js +35 -7
  201. package/src/core/next-explanation.js +63 -0
  202. package/src/core/phase.js +128 -10
  203. package/src/core/php-project.js +85 -0
  204. package/src/core/preflight-consistency.js +23 -9
  205. package/src/core/preflight-loaders.js +37 -5
  206. package/src/core/project-detection.js +1760 -52
  207. package/src/core/protocol-info.js +65 -0
  208. package/src/core/protocol.js +20 -0
  209. package/src/core/reconcile-closure.js +132 -53
  210. package/src/core/recovery-history.js +1 -0
  211. package/src/core/resumability.js +154 -44
  212. package/src/core/route-artifact.js +15 -1
  213. package/src/core/router.js +223 -4
  214. package/src/core/runtime-context.js +118 -61
  215. package/src/core/rust-project.js +400 -0
  216. package/src/core/schema-validation.js +3 -0
  217. package/src/core/security-review/constants.js +64 -0
  218. package/src/core/security-review/normalize.js +245 -0
  219. package/src/core/security-review/provider.js +204 -0
  220. package/src/core/security-review/service.js +134 -0
  221. package/src/core/semantic-planning/constants.js +19 -0
  222. package/src/core/semantic-planning/projection.js +94 -0
  223. package/src/core/semantic-planning/service.js +15 -0
  224. package/src/core/sources.js +37 -0
  225. package/src/core/sql-project.js +141 -0
  226. package/src/core/swift-project.js +200 -0
  227. package/src/core/task-claim-state.js +201 -1
  228. package/src/core/task-conflict-inspection.js +31 -5
  229. package/src/core/task-paths.js +13 -0
  230. package/src/core/task-recovery.js +1 -0
  231. package/src/core/templates.js +3 -0
  232. package/src/core/test-intelligence/benchmarks.js +68 -0
  233. package/src/core/test-intelligence/inventory.js +73 -0
  234. package/src/core/test-intelligence/prune.js +90 -0
  235. package/src/core/test-intelligence/semantic-state.js +15 -0
  236. package/src/core/test-intelligence/service.js +40 -0
  237. package/src/core/test-intelligence/utility.js +50 -0
  238. package/src/core/trace.js +11 -7
  239. package/src/core/transaction.js +1 -0
  240. package/src/core/typescript-project.js +349 -0
  241. package/src/core/xml-structure.js +123 -0
  242. package/src/integration.d.ts +492 -0
  243. package/src/integration.js +54 -0
  244. package/src/providers/README.md +47 -0
  245. package/src/providers/capabilities.js +46 -0
  246. package/src/providers/errors.js +15 -0
  247. package/src/providers/index.js +29 -0
  248. package/src/providers/json-snapshot.js +105 -0
  249. package/src/providers/registry.js +152 -0
@@ -4,6 +4,8 @@ import { PUBLIC_ERROR_REGISTRY } from "./error-codes.js";
4
4
  import { GUIDE_REGISTRY } from "./guide-registry.js";
5
5
  import { PROTOCOL_VERSION, WORK_PHASES, WORK_TRANSITIONS } from "./protocol.js";
6
6
  import { VERIFICATION_ISOLATION_MODES } from "./verification-execution.js";
7
+ import { PROVIDER_KINDS } from "../providers/capabilities.js";
8
+ import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "./decision/constants.js";
7
9
 
8
10
  export const SCHEMA_COMPATIBILITY_POLICY = Object.freeze({
9
11
  protocolVersion: PROTOCOL_VERSION,
@@ -36,6 +38,42 @@ export function protocolInfo({ packageVersion = null } = {}) {
36
38
  writesSchemaVersions: schemaVersions,
37
39
  compatibility: SCHEMA_COMPATIBILITY_POLICY,
38
40
  features: {
41
+ semanticDecisionPlane: {
42
+ version: 1,
43
+ supported: true,
44
+ requiredForNewSemanticDecisions: true,
45
+ engine: DECISION_ENGINE_ID,
46
+ model: PINNED_JEV_MODEL,
47
+ policyVersion: DECISION_DEFAULT_POLICY.policyVersion,
48
+ authority: "SEMANTIC_DECISION",
49
+ evidenceAuthority: "NONE",
50
+ lifecycleAuthority: false,
51
+ completionAuthority: false,
52
+ ownershipAuthority: false,
53
+ installationAuthority: false,
54
+ commands: ["decision-status", "decision-show", "context-plan", "model-route", "semantic-plan", "test-inventory", "test-utility", "test-prune-plan", "test-prune-probe"],
55
+ resource: "task/decisions",
56
+ contextPlanResource: "task/context-plan",
57
+ modelRouteResource: "task/model-route",
58
+ testUtilityResource: "task/test-utility",
59
+ testPruneResource: "task/test-utility",
60
+ offlineInspection: true,
61
+ completePerformsLiveRequest: false,
62
+ modelRouting: {
63
+ version: 1,
64
+ tiers: ["NONE", "FAST", "STANDARD", "PRIMARY"],
65
+ deterministicFloor: true,
66
+ advisoryEscalationOnly: true,
67
+ vendorSelection: false,
68
+ },
69
+ semanticPlanning: {
70
+ version: 1,
71
+ questionSets: ["intake-v1", "contract-v1", "route-v1", "context-v1", "model-route-v1", "failure-v1", "diagnosis-v1", "review-v1", "task-overlap-v1", "test-utility-v1", "test-prune-v1"],
72
+ authority: "SEMANTIC_DECISION",
73
+ evidenceAuthority: "NONE",
74
+ lifecycleAuthority: false,
75
+ },
76
+ },
39
77
  taskClaimRecovery: {
40
78
  version: 1,
41
79
  durableRecoveryState: true,
@@ -153,6 +191,18 @@ export function protocolInfo({ packageVersion = null } = {}) {
153
191
  commands: ["next", "task-show"],
154
192
  preservesDefaultOutput: true,
155
193
  },
194
+ auditUx: {
195
+ version: 1,
196
+ supported: true,
197
+ readOnly: true,
198
+ resource: "task/audit-view",
199
+ timeline: true,
200
+ lifecycleAuthority: false,
201
+ evidenceAuthority: false,
202
+ completionAuthority: false,
203
+ mutationAuthority: false,
204
+ externalExecution: false,
205
+ },
156
206
  usageTelemetry: {
157
207
  version: 1,
158
208
  supported: true,
@@ -205,6 +255,21 @@ export function protocolInfo({ packageVersion = null } = {}) {
205
255
  evidenceAuthority: false,
206
256
  executable: false,
207
257
  },
258
+ providerExtensions: {
259
+ version: 1,
260
+ supported: true,
261
+ providerNeutral: true,
262
+ maturity: "experimental",
263
+ publicRegistryApi: false,
264
+ packageSubpathExported: false,
265
+ autoInstall: false,
266
+ lifecycleAuthority: false,
267
+ completionAuthority: false,
268
+ evidenceAuthority: false,
269
+ providerKinds: [...PROVIDER_KINDS],
270
+ resultBoundary: "STRICT_JSON_SNAPSHOT",
271
+ cancellation: "COOPERATIVE_ABORT_SIGNAL",
272
+ },
208
273
  responsibilityConstraints: {
209
274
  version: 1,
210
275
  supported: true,
@@ -30,6 +30,9 @@ export const FAILURE_CODES = Object.freeze([
30
30
  "E_GATE_REQUIRED",
31
31
  "E_GATE_UNVERIFIED",
32
32
  "E_GATE_STALE",
33
+ "E_GATE_NOT_REQUIRED",
34
+ "E_GATE_INVALID",
35
+ "E_PHASE_FREEZE",
33
36
  "E_PHASE_TRANSITION_INVALID",
34
37
  "E_PHASE_PREREQUISITE_MISSING",
35
38
  "E_PHASE_CHRONOLOGY_INVALID",
@@ -113,6 +116,23 @@ export const FAILURE_CODES = Object.freeze([
113
116
  "E_INTERVENTION_HYPOTHESIS_MISSING",
114
117
  "E_INTERVENTION_REFERENCE_INVALID",
115
118
  "E_STRATEGY_OSCILLATION",
119
+ "E_DECISION_ENGINE_UNAVAILABLE",
120
+ "E_DECISION_ENGINE_AUTH_REQUIRED",
121
+ "E_DECISION_ENGINE_AUTH_INVALID",
122
+ "E_DECISION_MODEL_UNSUPPORTED",
123
+ "E_DECISION_REQUEST_INVALID",
124
+ "E_DECISION_RESULT_INVALID",
125
+ "E_DECISION_TIMEOUT",
126
+ "E_DECISION_RATE_LIMITED",
127
+ "E_DECISION_STATE_UNSAFE",
128
+ "E_DECISION_STATE_LIMIT",
129
+ "E_DECISION_LOW_CONFIDENCE",
130
+ "E_DECISION_STALE",
131
+ "E_DECISION_POLICY_INVALID",
132
+ "E_DECISION_CACHE_INVALID",
133
+ "E_DECISION_QUESTION_SET_UNKNOWN",
134
+ "E_DECISION_QUESTION_SET_STALE",
135
+ "E_DECISION_REQUIRED",
116
136
  "E_DECISION_CRITERION_INVALID",
117
137
  "E_DECISION_NOT_UNRESOLVED",
118
138
  ]);
@@ -1,17 +1,17 @@
1
1
  import { readContract } from "./contract.js";
2
2
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
- import { appendProtocolEvent, validateEventLedger } from "./events.js";
3
+ import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
4
4
  import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
5
5
  import { runCommandExecution } from "./execution.js";
6
6
  import { createReceipt } from "./receipt.js";
7
7
  import { currentRepositoryFingerprint } from "./repository.js";
8
8
  import { taskArtifactPath } from "./task-paths.js";
9
+ import { resolveTaskClaimState } from "./task-claim-state.js";
9
10
  import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
11
+ import { classifyRequirement } from "./evidence-readiness.js";
10
12
 
11
13
  export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
12
14
 
13
- const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
14
-
15
15
  const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
16
16
 
17
17
  function reconcileError(code, message, artifacts = []) {
@@ -21,14 +21,126 @@ function reconcileError(code, message, artifacts = []) {
21
21
  return error;
22
22
  }
23
23
 
24
+ function assertRepositoryOnlyFreshness(freshness, stateRel, contractRel) {
25
+ if (freshness.status !== "REVALIDATION_REQUIRED") {
26
+ throw reconcileError(
27
+ "E_RECONCILE_NOT_STALE",
28
+ `work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
29
+ [stateRel, contractRel],
30
+ );
31
+ }
32
+ if (freshness.reasons.length === 1 && freshness.reasons[0] === "REPOSITORY_CHANGED") return;
33
+ throw reconcileError(
34
+ "E_RECONCILE_UNSUPPORTED_DRIFT",
35
+ `reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${freshness.reasons.join(", ")}`,
36
+ [stateRel, contractRel],
37
+ );
38
+ }
39
+
40
+ async function assertFreshReviewingRecoveryIsAuthorized({
41
+ target,
42
+ packageRoot,
43
+ taskId,
44
+ state,
45
+ eventsRel,
46
+ receiptRel,
47
+ }) {
48
+ if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) return;
49
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
50
+ if (!ledger.valid) return;
51
+ let receipt = null;
52
+ try {
53
+ receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot)).value;
54
+ } catch (error) {
55
+ if (error.code !== "ARTIFACT_MISSING") throw error;
56
+ }
57
+ const recoveryAuth = validateCompletionRecoveryAuthorization({ state, receipt, events: ledger.events });
58
+ if (!recoveryAuth.authorized) {
59
+ const first = recoveryAuth.errors?.[0] ?? {};
60
+ throw reconcileError(
61
+ first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
62
+ `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
63
+ [eventsRel, receiptRel],
64
+ );
65
+ }
66
+ }
67
+
68
+ async function validateReconciliationCheckpoint({
69
+ target,
70
+ packageRoot,
71
+ taskId,
72
+ state,
73
+ stateRel,
74
+ contractRel,
75
+ eventsRel,
76
+ receiptRel,
77
+ authorityContext,
78
+ runtimeContext,
79
+ }) {
80
+ const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
81
+ if (freshness.status !== "REVALIDATION_REQUIRED") {
82
+ await assertFreshReviewingRecoveryIsAuthorized({
83
+ target,
84
+ packageRoot,
85
+ taskId,
86
+ state,
87
+ eventsRel,
88
+ receiptRel,
89
+ });
90
+ }
91
+ assertRepositoryOnlyFreshness(freshness, stateRel, contractRel);
92
+
93
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
94
+ if (!ledger.valid) {
95
+ const first = ledger.errors[0];
96
+ throw reconcileError(
97
+ "E_RECONCILE_LEDGER_INVALID",
98
+ `append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
99
+ [eventsRel],
100
+ );
101
+ }
102
+
103
+ const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
104
+ if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
105
+ const first = ownership.ownershipErrors?.[0] ?? ownership.errors?.[0] ?? {};
106
+ throw reconcileError(
107
+ first.code ?? "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT",
108
+ `reconcile-closure requires active, valid task claim ownership: ${first.message ?? ownership.claimState}`,
109
+ [stateRel, eventsRel],
110
+ );
111
+ }
112
+
113
+ if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) {
114
+ return state;
115
+ }
116
+
117
+ const recovery = await authorizeCompletionRecoveryOrRebind({
118
+ target,
119
+ packageRoot,
120
+ taskId,
121
+ authorityContext,
122
+ runtimeContext,
123
+ });
124
+ if (!recovery.recoveryAuth.authorized) {
125
+ const first = recovery.recoveryAuth.errors?.[0] ?? {};
126
+ throw reconcileError(
127
+ first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
128
+ `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
129
+ [stateRel, receiptRel],
130
+ );
131
+ }
132
+ return recovery.rebound ? recovery.state : state;
133
+ }
134
+
24
135
  /**
25
136
  * Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
26
137
  * objective is already satisfied in the current repository but whose
27
138
  * work-state checkpoint is stale because the repository fingerprint moved.
28
139
  *
29
140
  * The command refreshes the checkpoint repository fingerprint only after:
30
- * - the task is EXECUTING, VERIFYING, or (with authorized completion
31
- * recovery) REVIEWING,
141
+ * - the task is EXECUTING, VERIFYING, or REVIEWING (with either a
142
+ * persisted completion rejection or the narrow repository-only bootstrap
143
+ * path),
32
144
  * - classification requires revalidation and the only drift is
33
145
  * REPOSITORY_CHANGED,
34
146
  * - the append-only event ledger is valid,
@@ -80,60 +192,27 @@ export async function runReconcileClosure({
80
192
  [stateRel],
81
193
  );
82
194
  }
83
- if (state.phase === "REVIEWING") {
84
- const recovery = await authorizeCompletionRecoveryOrRebind({
85
- target,
86
- packageRoot,
87
- taskId,
88
- authorityContext,
89
- runtimeContext,
90
- });
91
- if (!recovery.recoveryAuth.authorized) {
92
- const first = recovery.recoveryAuth.errors?.[0] ?? {};
93
- throw reconcileError(
94
- first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
95
- `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
96
- [stateRel, receiptRel],
97
- );
98
- }
99
- if (recovery.rebound) {
100
- state = recovery.state;
101
- }
102
- }
103
-
104
- const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
105
- if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
106
- throw reconcileError(
107
- "E_RECONCILE_NOT_STALE",
108
- `work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
109
- [stateRel, contractRel],
110
- );
111
- }
112
- const unsupported = freshness.reasons.filter((reason) => !RECONCILABLE_DRIFT.has(reason));
113
- if (unsupported.length > 0) {
114
- throw reconcileError(
115
- "E_RECONCILE_UNSUPPORTED_DRIFT",
116
- `reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${unsupported.join(", ")}`,
117
- [stateRel, contractRel],
118
- );
119
- }
120
-
121
- const ledger = await validateEventLedger(target, packageRoot, { taskId });
122
- if (!ledger.valid) {
123
- const first = ledger.errors[0];
124
- throw reconcileError(
125
- "E_RECONCILE_LEDGER_INVALID",
126
- `append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
127
- [eventsRel],
128
- );
129
- }
195
+ state = await validateReconciliationCheckpoint({
196
+ target,
197
+ packageRoot,
198
+ taskId,
199
+ state,
200
+ stateRel,
201
+ contractRel,
202
+ eventsRel,
203
+ receiptRel,
204
+ authorityContext,
205
+ runtimeContext,
206
+ });
130
207
 
131
208
  const contract = await readContract(target, packageRoot, { taskId });
132
209
  const verificationItem = (contract.value.verification ?? []).find((item) => {
133
210
  if (typeof item === "string") {
134
211
  return item === requirement;
135
212
  }
136
- return item.type === "VERIFICATION" && item.id === checkId && item.text === requirement;
213
+ return classifyRequirement(item).type === "VERIFICATION"
214
+ && item.id === checkId
215
+ && item.text === requirement;
137
216
  });
138
217
  if (!verificationItem) {
139
218
  throw reconcileError(
@@ -6,6 +6,7 @@ import {
6
6
  const RECOVERY_EVENT_TYPES = new Set([
7
7
  "TASK_RECOVERY_RECORDED",
8
8
  "OPERATOR_RECOVERY_RECORDED",
9
+ "TASK_ABANDONED",
9
10
  LEGACY_RECOVERY_MIGRATION_EVENT,
10
11
  ]);
11
12
 
@@ -4,6 +4,12 @@ import { createWorkState, initializeWorkState, readWorkState, mutateWorkState }
4
4
 
5
5
  const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
6
6
 
7
+ const CURRENT_ROUTE_CHECKPOINT_PHASES = new Set(["ROUTED"]);
8
+
9
+ export function routeCheckpointMustMatchCurrentRoute(phase) {
10
+ return CURRENT_ROUTE_CHECKPOINT_PHASES.has(phase);
11
+ }
12
+
7
13
  /**
8
14
  * Resume phase derived from the highest lifecycle milestone already recorded in
9
15
  * a validated ledger. Recreating a checkpoint at ROUTED for a task whose ledger
@@ -16,9 +22,88 @@ const RESUME_PHASE_BY_MILESTONE = Object.freeze({
16
22
  EXECUTION_STARTED: "EXECUTING",
17
23
  VERIFICATION_STARTED: "VERIFYING",
18
24
  VERIFICATION_RECORDED: "VERIFYING",
25
+ REVIEW_STARTED: "REVIEWING",
26
+ });
27
+
28
+ const RESUME_STEPS_BY_PHASE = Object.freeze({
29
+ CONTRACT_READY: {
30
+ completedSteps: ["contract"],
31
+ pendingSteps: ["route", ...DEFAULT_PENDING_STEPS],
32
+ },
33
+ ROUTED: {
34
+ completedSteps: ["contract", "route"],
35
+ pendingSteps: [...DEFAULT_PENDING_STEPS],
36
+ },
37
+ PLANNED: {
38
+ completedSteps: ["contract", "route", "planning"],
39
+ pendingSteps: [...DEFAULT_PENDING_STEPS.filter((step) => step !== "planning")],
40
+ },
41
+ EXECUTING: {
42
+ completedSteps: ["contract", "route", "planning", "implementation"],
43
+ pendingSteps: ["verification"],
44
+ },
45
+ VERIFYING: {
46
+ completedSteps: ["contract", "route", "planning", "implementation"],
47
+ pendingSteps: ["verification"],
48
+ },
49
+ REVIEWING: {
50
+ completedSteps: ["contract", "route", "planning", "implementation", "verification"],
51
+ pendingSteps: [],
52
+ },
19
53
  });
20
54
 
21
- async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
55
+ function cycleEvents(events) {
56
+ const CYCLE_EVENT_NAMES = new Set([
57
+ "VERIFICATION_STARTED",
58
+ "VERIFICATION_RECORDED",
59
+ "DIAGNOSIS_RECORDED",
60
+ "DIAGNOSTIC_CASE_RECORDED",
61
+ ]);
62
+ const cycles = events
63
+ .filter((event) => CYCLE_EVENT_NAMES.has(event.event))
64
+ .map((event) => event.details?.verificationCycle)
65
+ .filter((cycle) => Number.isInteger(cycle) && cycle >= 1);
66
+ return cycles.at(-1);
67
+ }
68
+
69
+ /**
70
+ * Canonical reconstruction projection: for a resumed phase derived from a
71
+ * validated ledger, returns the single source of truth for completedSteps and
72
+ * pendingSteps. Unknown phases fall back to the ROUTED projection, matching the
73
+ * historical default resume checkpoint.
74
+ */
75
+ export function resumeStepsForPhase(phase) {
76
+ return RESUME_STEPS_BY_PHASE[phase] ?? RESUME_STEPS_BY_PHASE.ROUTED;
77
+ }
78
+
79
+ /**
80
+ * Canonical verification cycle derived from the ledger when history proves
81
+ * verification has started; undefined when the ledger has no cycle metadata.
82
+ */
83
+ export function resumeVerificationCycleForPhase(events) {
84
+ return cycleEvents(events);
85
+ }
86
+
87
+ /**
88
+ * Canonical work-state identity fields for reconstruction from a validated
89
+ * ledger: resumed phase, completedSteps, pendingSteps, and verificationCycle.
90
+ * This is the one projection shared by ensureResumableState and
91
+ * contract-create reconstruction so later phases cannot be reconstructed with
92
+ * contradictory steps.
93
+ */
94
+ export function buildResumableWorkStateFields({ events, resumedPhase }) {
95
+ const steps = resumeStepsForPhase(resumedPhase);
96
+ return {
97
+ phase: resumedPhase,
98
+ completedSteps: [...steps.completedSteps],
99
+ pendingSteps: [...steps.pendingSteps],
100
+ ...(events && events.length > 0 && cycleEvents(events) !== undefined
101
+ ? { verificationCycle: cycleEvents(events) }
102
+ : {}),
103
+ };
104
+ }
105
+
106
+ export async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
22
107
  let ledger;
23
108
  try {
24
109
  ledger = await validateEventLedger(target, packageRoot, { taskId });
@@ -32,71 +117,37 @@ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
32
117
  let derived = null;
33
118
  for (const event of scoped) {
34
119
  const phase = positions[event.event];
35
- if (!phase) continue;
36
- if (!derived) {
37
- derived = phase;
38
- continue;
39
- }
40
- if (phase === "VERIFYING") derived = "VERIFYING";
120
+ if (phase) derived = phase;
41
121
  }
42
122
  return derived;
43
123
  }
44
124
 
45
- function deriveVerificationCycleFromLedger(events) {
46
- const cycleEvents = new Set([
47
- "VERIFICATION_STARTED",
48
- "VERIFICATION_RECORDED",
49
- "DIAGNOSIS_RECORDED",
50
- "DIAGNOSTIC_CASE_RECORDED",
51
- ]);
52
- const cycles = events
53
- .filter((event) => cycleEvents.has(event.event))
54
- .map((event) => event.details?.verificationCycle)
55
- .filter((cycle) => Number.isInteger(cycle) && cycle >= 1);
56
- return cycles.at(-1);
57
- }
58
-
59
- function resumeSteps(phase) {
60
- if (phase === "EXECUTING" || phase === "VERIFYING") {
61
- return {
62
- completedSteps: ["contract", "route", "planning", "implementation"],
63
- pendingSteps: ["verification"],
64
- };
65
- }
66
- return {
67
- completedSteps: ["contract", "route"],
68
- pendingSteps: [...DEFAULT_PENDING_STEPS],
69
- };
70
- }
71
-
72
125
  export async function ensureResumableState({ target, packageRoot, contract, route, taskId, statePath }) {
73
126
  if (!contract || !route) return null;
74
127
  const existing = await readWorkState(target, { packageRoot, taskId, statePath });
75
128
  if (existing) return existing;
76
129
 
77
130
  const resumedPhase = await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED";
78
- let verificationCycle;
131
+ let events = [];
79
132
  try {
80
133
  const ledger = await validateEventLedger(target, packageRoot, { taskId });
81
134
  if (ledger.valid) {
82
- verificationCycle = deriveVerificationCycleFromLedger(
83
- (ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId),
84
- );
135
+ events = (ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId);
85
136
  }
86
137
  } catch {
87
- verificationCycle = undefined;
138
+ events = [];
88
139
  }
89
- const steps = resumeSteps(resumedPhase);
140
+ const projection = buildResumableWorkStateFields({ events, resumedPhase });
90
141
  const state = createWorkState({
91
142
  taskId: contract.value.taskId,
92
143
  contractFingerprint: contract.fingerprint,
93
144
  routeFingerprint: route.fingerprint,
94
145
  repositoryFingerprint: await currentRepositoryFingerprint(target),
95
- phase: resumedPhase,
146
+ phase: projection.phase,
96
147
  selectedGuides: route.value.guides,
97
- completedSteps: steps.completedSteps,
98
- pendingSteps: steps.pendingSteps,
99
- ...(verificationCycle !== undefined ? { verificationCycle } : {}),
148
+ completedSteps: projection.completedSteps,
149
+ pendingSteps: projection.pendingSteps,
150
+ ...(projection.verificationCycle !== undefined ? { verificationCycle: projection.verificationCycle } : {}),
100
151
  checks: [],
101
152
  failures: [],
102
153
  blockers: [],
@@ -105,6 +156,65 @@ export async function ensureResumableState({ target, packageRoot, contract, rout
105
156
  return initializeWorkState(target, state, { packageRoot, taskId, statePath });
106
157
  }
107
158
 
159
+ function routeSyncError(code, message) {
160
+ const error = new Error(message);
161
+ error.code = code;
162
+ return error;
163
+ }
164
+
165
+ function sameStringList(left, right) {
166
+ const a = [...(left ?? [])].sort();
167
+ const b = [...(right ?? [])].sort();
168
+ return a.length === b.length && a.every((value, index) => value === b[index]);
169
+ }
170
+
171
+ /**
172
+ * Rebinds an existing ROUTED checkpoint to an already-persisted route.
173
+ *
174
+ * Only the route-bound identity fields are updated; contract, repository,
175
+ * steps, checks, evidence, and phase are preserved. Same-identity reruns are
176
+ * a no-op. Every other situation fails closed.
177
+ */
178
+ export async function synchronizePersistedRouteState({ target, packageRoot, taskId, route, contract = null, statePath } = {}) {
179
+ if (!route || !route.value || typeof route.fingerprint !== "string") {
180
+ throw routeSyncError("E_ROUTE_STALE", "A persisted route artifact is required to synchronize checkpoint identity");
181
+ }
182
+ const state = await readWorkState(target, { packageRoot, taskId, statePath });
183
+ if (!state) return null;
184
+ if (state.phase !== "ROUTED") {
185
+ throw routeSyncError(
186
+ "E_ROUTE_PHASE_UNSUPPORTED",
187
+ `Route checkpoint synchronization supports phase ROUTED, found ${state.phase}`,
188
+ );
189
+ }
190
+ if (taskId && state.taskId !== taskId) {
191
+ throw routeSyncError("E_ROUTE_STALE", "Work state does not belong to the current route task");
192
+ }
193
+ if (contract && state.contractFingerprint !== contract.fingerprint) {
194
+ // Contract evolution with regenerated routing is a pre-existing flow whose
195
+ // stale checkpoint is recovered through the sanctioned clear-state and
196
+ // preflight recreation path. Never rebind route identity across a contract
197
+ // boundary here; leave the checkpoint untouched.
198
+ return state;
199
+ }
200
+ if (route.value.contractFingerprint !== undefined && state.contractFingerprint !== route.value.contractFingerprint) {
201
+ return state;
202
+ }
203
+ if (state.routeFingerprint === route.fingerprint && sameStringList(state.selectedGuides, route.value.guides)) {
204
+ return state;
205
+ }
206
+ return mutateWorkState(target, {
207
+ expectedRevision: state.revision ?? 0,
208
+ packageRoot,
209
+ taskId,
210
+ statePath,
211
+ }, () => ({
212
+ ...state,
213
+ routeFingerprint: route.fingerprint,
214
+ selectedGuides: [...route.value.guides],
215
+ }));
216
+ }
217
+
108
218
  export async function synchronizePreflightState({
109
219
  target,
110
220
  packageRoot,
@@ -1,9 +1,12 @@
1
1
  import { assertRouteInvariants } from "./router.js";
2
2
  import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
3
  import { readContract } from "./contract.js";
4
- import { ensureResumableState } from "./resumability.js";
4
+ import { ensureResumableState, synchronizePersistedRouteState } from "./resumability.js";
5
+ import { readWorkState } from "./work-state.js";
5
6
  import { taskArtifactPath } from "./task-paths.js";
6
7
 
8
+
9
+
7
10
  export async function persistRoute(target, route, packageRoot, options = {}) {
8
11
  assertRouteInvariants(route);
9
12
  const { contractFingerprint, ...writeOptions } = options;
@@ -21,6 +24,14 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
21
24
  : { ...route, contractFingerprint };
22
25
  assertRouteInvariants(value);
23
26
  const taskId = options.taskId ?? contractArtifact?.value?.taskId ?? null;
27
+ let existingState = null;
28
+ if (taskId) {
29
+ try {
30
+ existingState = await readWorkState(target, { packageRoot, taskId });
31
+ } catch {
32
+ existingState = null;
33
+ }
34
+ }
24
35
  const relPath = options.routePath ?? options.routeFile ?? options.relativePath ?? (taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
25
36
  const artifact = await writeJsonArtifact(
26
37
  target,
@@ -33,6 +44,9 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
33
44
  if (contractArtifact && contractArtifact.fingerprint === artifact.value.contractFingerprint) {
34
45
  await ensureResumableState({ target, packageRoot, contract: contractArtifact, route: artifact, taskId });
35
46
  }
47
+ if (existingState?.phase === "ROUTED") {
48
+ await synchronizePersistedRouteState({ target, packageRoot, taskId, route: artifact, contract: contractArtifact });
49
+ }
36
50
  return artifact;
37
51
  }
38
52