@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
package/src/core/phase.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  LIFECYCLE_MILESTONES,
6
6
  validateEventLedger,
7
7
  validateStateLedgerCoherence,
8
+ isRevisionEpochPlanRepeat,
8
9
  } from "./events.js";
9
10
  import { readPersistedRoute } from "./route-artifact.js";
10
11
  import { assertWorkPhase, isValidTransition } from "./protocol.js";
@@ -13,7 +14,7 @@ import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebin
13
14
  import { evaluateCompletion, runComplete as persistCompletion } from "./completion.js";
14
15
  import { evaluatePreflight } from "./preflight.js";
15
16
  import { requiredEvidenceForTarget } from "./completion-artifacts.js";
16
- import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
17
+ import { assertCompletionRelationships, assertStateIdentity, stateIdentityErrors } from "./completion-relationships.js";
17
18
  import { createReceipt, validateReceipt } from "./receipt.js";
18
19
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
19
20
  import { taskArtifactPath } from "./task-paths.js";
@@ -27,6 +28,13 @@ import {
27
28
  evaluateStructuredDiagnosticStall,
28
29
  } from "./information-gain-projection.js";
29
30
  import { assertStructuralQualityExecutionReady } from "./structural-quality/service.js";
31
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
32
+ import {
33
+ CONTRACT_BOOTSTRAP_REPAIR_EVENT,
34
+ ROUTE_CHECKPOINT_BOUND_EVENT,
35
+ resolveCanonicalPostRepairCheckpointBinding,
36
+ sameCanonicalGuideList,
37
+ } from "./contract-bootstrap-recovery.js";
30
38
 
31
39
  function phaseError(code, message, artifacts = []) {
32
40
  const error = new Error(message);
@@ -55,15 +63,13 @@ const LATE_PHASES = new Set([
55
63
  "COMPLETE",
56
64
  ]);
57
65
 
58
- async function assertPersistedStateIdentity(target, state, toPhase, packageRoot, options = {}) {
66
+ async function readPhaseIdentityArtifacts(target, state, toPhase, packageRoot, options, paths) {
59
67
  const scopedTaskId = options.taskId ?? null;
60
68
  const requireContract = LATE_PHASES.has(state.phase)
61
69
  || ["CONTRACT_READY", "ROUTED", "EXECUTING"].includes(toPhase);
62
- const requireRoute = LATE_PHASES.has(state.phase)
70
+ const requireRoute = state.phase === "ROUTED"
71
+ || LATE_PHASES.has(state.phase)
63
72
  || ["ROUTED", "EXECUTING"].includes(toPhase);
64
- const contractRel = options.contractPath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "contract") : ARTIFACT_PATHS.contract);
65
- const routeRel = options.routePath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "route") : ARTIFACT_PATHS.route);
66
-
67
73
  let contract = null;
68
74
  let route = null;
69
75
  try {
@@ -75,7 +81,7 @@ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot,
75
81
  throw phaseError(
76
82
  requireContract ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_CONTRACT_INVALID",
77
83
  `${requireContract ? `Phase ${toPhase} requires current contract` : "Unable to validate current contract"}: ${error.message}`,
78
- [contractRel],
84
+ [paths.contractRel],
79
85
  );
80
86
  }
81
87
  }
@@ -88,11 +94,69 @@ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot,
88
94
  throw phaseError(
89
95
  requireRoute ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_ROUTE_INVALID",
90
96
  `${requireRoute ? `Phase ${toPhase} requires persisted route` : "Unable to validate persisted route"}: ${error.message}`,
91
- [routeRel],
97
+ [paths.routeRel],
92
98
  );
93
99
  }
94
100
  }
101
+ return { contract, route };
102
+ }
103
+
104
+ function assertRepairedCheckpointIdentity({ contract, route, state, events, contractRel, routeRel, stateRel }) {
105
+ const repairMarker = events.find((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
106
+ if (!repairMarker || state.phase === "ROUTED") return false;
107
+ const checkpoint = resolveCanonicalPostRepairCheckpointBinding(events, repairMarker);
108
+ if (!checkpoint) {
109
+ throw phaseError(
110
+ "E_PHASE_CHRONOLOGY_INVALID",
111
+ "Repaired late-phase state requires a canonical route checkpoint binding",
112
+ [contractRel, routeRel],
113
+ );
114
+ }
115
+ const identityErrors = [
116
+ ...stateIdentityErrors({ contract, state }),
117
+ ...stateIdentityErrors({ contract, route }),
118
+ ];
119
+ if (state.routeFingerprint !== checkpoint.routeFingerprint
120
+ || !sameCanonicalGuideList(state.selectedGuides, checkpoint.selectedGuides)) {
121
+ identityErrors.push({
122
+ code: "E_ROUTE_GUIDE_MISMATCH",
123
+ message: "Work state does not match the immutable route checkpoint identity",
124
+ artifacts: [routeRel, stateRel],
125
+ });
126
+ }
127
+ if (identityErrors.length > 0) {
128
+ const first = identityErrors[0];
129
+ throw phaseError(first.code, first.message, first.artifacts);
130
+ }
131
+ return true;
132
+ }
133
+
134
+ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot, options = {}) {
135
+ const scopedTaskId = options.taskId ?? null;
136
+ const events = options.events ?? [];
137
+ const contractRel = options.contractPath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "contract") : ARTIFACT_PATHS.contract);
138
+ const routeRel = options.routePath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "route") : ARTIFACT_PATHS.route);
139
+ const stateRel = options.statePath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "state") : ARTIFACT_PATHS.state);
140
+ const { contract, route } = await readPhaseIdentityArtifacts(
141
+ target,
142
+ state,
143
+ toPhase,
144
+ packageRoot,
145
+ options,
146
+ { contractRel, routeRel },
147
+ );
95
148
  if (!contract && !route) return;
149
+ if (toPhase !== "ROUTED" && assertRepairedCheckpointIdentity({
150
+ contract,
151
+ route,
152
+ state,
153
+ events,
154
+ contractRel,
155
+ routeRel,
156
+ stateRel,
157
+ })) {
158
+ return;
159
+ }
96
160
  try {
97
161
  assertStateIdentity({ contract, route, state });
98
162
  } catch (error) {
@@ -116,6 +180,11 @@ function reconcileImplementationStep(state, toPhase) {
116
180
  };
117
181
  }
118
182
 
183
+ function isRevisionPlanRepeat(events, eventType) {
184
+ return eventType === "PLAN_RECORDED"
185
+ && isRevisionEpochPlanRepeat(events, events.length, { event: eventType });
186
+ }
187
+
119
188
  async function assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext, options = {}) {
120
189
  const scopedTaskId = options.taskId ?? null;
121
190
  const contractRel = options.contractPath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "contract") : ARTIFACT_PATHS.contract);
@@ -160,8 +229,39 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot, aut
160
229
  }
161
230
  }
162
231
 
232
+ async function appendRepairRouteCheckpoint({ target, packageRoot, taskId, state, events, stateRel, eventsRel }) {
233
+ const repairMarker = events.find((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
234
+ const checkpointAlreadyBound = events.some((event) => event.event === ROUTE_CHECKPOINT_BOUND_EVENT);
235
+ if (!repairMarker || state.phase !== "ROUTED" || checkpointAlreadyBound) return;
236
+ if (typeof state.routeFingerprint !== "string") {
237
+ throw phaseError("E_ROUTE_GUIDE_MISMATCH", "ROUTED state requires a route fingerprint before checkpoint binding", [stateRel, eventsRel]);
238
+ }
239
+ await appendProtocolEvent(target, {
240
+ taskId: state.taskId,
241
+ event: ROUTE_CHECKPOINT_BOUND_EVENT,
242
+ details: {
243
+ routeFingerprint: state.routeFingerprint,
244
+ contractFingerprint: state.contractFingerprint,
245
+ selectedGuides: [...state.selectedGuides].sort(),
246
+ },
247
+ }, packageRoot, { taskId, eventsPath: eventsRel });
248
+ }
249
+
163
250
  export async function advanceWorkState(target, toPhase, options = {}) {
164
251
  const normalizedOptions = typeof options === "string" ? { packageRoot: options } : options;
252
+ if (!normalizedOptions.taskId || await getTaskTransaction(target)) {
253
+ return advanceWorkStateInternal(target, toPhase, normalizedOptions);
254
+ }
255
+ return withTaskTransaction({
256
+ target,
257
+ taskId: normalizedOptions.taskId,
258
+ operation: "advance",
259
+ packageRoot: normalizedOptions.packageRoot,
260
+ recordCommitEvent: true,
261
+ }, async () => advanceWorkStateInternal(target, toPhase, normalizedOptions));
262
+ }
263
+
264
+ async function advanceWorkStateInternal(target, toPhase, normalizedOptions) {
165
265
  const {
166
266
  packageRoot,
167
267
  now = new Date().toISOString(),
@@ -211,7 +311,6 @@ export async function advanceWorkState(target, toPhase, options = {}) {
211
311
  }
212
312
 
213
313
  await assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext, { taskId, statePath, contractPath, routePath, receiptPath, eventsPath });
214
- await assertPersistedStateIdentity(target, state, toPhase, packageRoot, { taskId, contractPath, routePath });
215
314
 
216
315
  if (toPhase === "EXECUTING" && taskId) {
217
316
  // Multi-task checkout scope checks
@@ -259,6 +358,12 @@ export async function advanceWorkState(target, toPhase, options = {}) {
259
358
  if (coherenceErrors.length > 0) {
260
359
  throw phaseError(coherenceErrors[0].code, coherenceErrors[0].message, [stateRel, eventsRel]);
261
360
  }
361
+ await assertPersistedStateIdentity(target, state, toPhase, packageRoot, {
362
+ taskId,
363
+ contractPath,
364
+ routePath,
365
+ events: ledger.events,
366
+ });
262
367
  const eventType = PHASE_EVENTS[toPhase];
263
368
  const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
264
369
  if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
@@ -338,11 +443,15 @@ export async function advanceWorkState(target, toPhase, options = {}) {
338
443
  }
339
444
  const repeatedReview = state.phase === "VERIFYING" && toPhase === "REVIEWING"
340
445
  && ledger.events.some((event) => event.taskId === state.taskId && event.event === "REVIEW_STARTED");
446
+ const revisionPlanRepeat = isRevisionPlanRepeat(ledger.events, eventType);
341
447
  if (eventType && !reenteringVerification && !repeatedReview
448
+ && !revisionPlanRepeat
342
449
  && ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
343
450
  throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [eventsRel]);
344
451
  }
345
- const milestoneIndex = reenteringVerification || repeatedReview ? -1 : LIFECYCLE_MILESTONES.indexOf(eventType);
452
+ const milestoneIndex = reenteringVerification || repeatedReview || revisionPlanRepeat
453
+ ? -1
454
+ : LIFECYCLE_MILESTONES.indexOf(eventType);
346
455
  if (milestoneIndex >= 0) {
347
456
  const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
348
457
  if (lastMilestone !== milestoneIndex - 1) {
@@ -361,6 +470,15 @@ export async function advanceWorkState(target, toPhase, options = {}) {
361
470
  }
362
471
  if (reenteringVerification) delete next.lastCompletionAttempt;
363
472
  next.revision = (state.revision ?? 0) + 1;
473
+ await appendRepairRouteCheckpoint({
474
+ target,
475
+ packageRoot,
476
+ taskId,
477
+ state,
478
+ events: ledger.events,
479
+ stateRel,
480
+ eventsRel,
481
+ });
364
482
  let nextReceipt = null;
365
483
  try {
366
484
  const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
@@ -0,0 +1,85 @@
1
+ import { createLanguageProject, PHP_SOURCE_EXTENSIONS } from "./multi-language-project.js";
2
+
3
+ function isPlainObject(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ function invalidComposer() {
8
+ return {
9
+ valid: false,
10
+ name: null,
11
+ type: null,
12
+ autoload: false,
13
+ requirements: [],
14
+ };
15
+ }
16
+
17
+ function hasValidComposerSections(value) {
18
+ const requirementFields = ["require", "require-dev", "replace", "provide", "conflict"];
19
+ if (requirementFields.some((field) => value[field] !== undefined && !isPlainObject(value[field]))) return false;
20
+ const autoload = value.autoload === undefined || isPlainObject(value.autoload);
21
+ const autoloadDev = value["autoload-dev"] === undefined || isPlainObject(value["autoload-dev"]);
22
+ return autoload && autoloadDev;
23
+ }
24
+
25
+ function composerStringField(value, field) {
26
+ if (value[field] === undefined) return null;
27
+ return typeof value[field] === "string" && value[field].trim() !== "" ? value[field] : undefined;
28
+ }
29
+
30
+ function hasComposerIdentity(value) {
31
+ return value.name !== undefined
32
+ || value.require !== undefined
33
+ || value.autoload !== undefined
34
+ || value["autoload-dev"] !== undefined;
35
+ }
36
+
37
+ function parseComposerValue(value) {
38
+ if (!isPlainObject(value) || !hasValidComposerSections(value)) return invalidComposer();
39
+ const name = composerStringField(value, "name");
40
+ const type = composerStringField(value, "type");
41
+ if (name === undefined || type === undefined || !hasComposerIdentity(value)) return invalidComposer();
42
+ const hasPhpIdentity = name !== null || value.require !== undefined || value.autoload !== undefined || value["autoload-dev"] !== undefined;
43
+ if (!hasPhpIdentity) return invalidComposer();
44
+ return {
45
+ valid: true,
46
+ name,
47
+ type,
48
+ autoload: value.autoload !== undefined || value["autoload-dev"] !== undefined,
49
+ requirements: Object.keys(value.require ?? {}).sort((left, right) => left.localeCompare(right)),
50
+ };
51
+ }
52
+
53
+ export function parseComposerJson(text) {
54
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidComposer();
55
+ try {
56
+ return parseComposerValue(JSON.parse(text));
57
+ } catch {
58
+ return invalidComposer();
59
+ }
60
+ }
61
+
62
+ export function phpSourceLooksExecutable(text) {
63
+ return typeof text === "string" && /<\?(?:php(?:\s|$)|=)/iu.test(text);
64
+ }
65
+
66
+ export function inspectPhpProject({ projectRoot, manifestRelative, manifestText }) {
67
+ const parsed = parseComposerJson(manifestText);
68
+ const primary = parsed.valid;
69
+ const primarySignals = primary ? [`${manifestRelative}:composer`] : [];
70
+ const supportingSignals = [];
71
+ if (parsed.name) supportingSignals.push(`${manifestRelative}:name=${parsed.name}`);
72
+ if (parsed.type) supportingSignals.push(`${manifestRelative}:type=${parsed.type}`);
73
+ if (parsed.autoload) supportingSignals.push(`${manifestRelative}:autoload`);
74
+ for (const requirement of parsed.requirements) supportingSignals.push(`${manifestRelative}:require=${requirement}`);
75
+ return createLanguageProject({
76
+ kind: "php",
77
+ root: projectRoot,
78
+ manifest: manifestRelative,
79
+ frameworks: primary ? ["php"] : [],
80
+ primary,
81
+ primarySignals,
82
+ supportingSignals,
83
+ internal: { ...parsed, phpSourceExtensions: [...PHP_SOURCE_EXTENSIONS] },
84
+ });
85
+ }
@@ -80,6 +80,18 @@ function sameReadyPreflightEvent(event, result) {
80
80
  && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
81
81
  }
82
82
 
83
+ function latestContractRevisionSeq(events) {
84
+ return events.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
85
+ }
86
+
87
+ function hasCurrentRevisionGate(events, taskId, gate) {
88
+ const revisionSeq = latestContractRevisionSeq(events);
89
+ return events.some((event) => event.event === "GATE_SATISFIED"
90
+ && event.taskId === taskId
91
+ && event.details?.gate === gate
92
+ && event.seq > revisionSeq);
93
+ }
94
+
83
95
  export async function validateReadyProtocolConsistency({
84
96
  target,
85
97
  packageRoot,
@@ -141,9 +153,7 @@ export async function validateReadyProtocolConsistency({
141
153
  }
142
154
  }
143
155
  for (const gate of persisted.satisfiedGates ?? []) {
144
- if (!events.some((event) => event.event === "GATE_SATISFIED"
145
- && event.taskId === persisted.taskId
146
- && event.details?.gate === gate)) {
156
+ if (!hasCurrentRevisionGate(events, persisted.taskId, gate)) {
147
157
  errors.push(issue("E_PREFLIGHT_GATE_EVENT_MISSING", `READY preflight is missing lifecycle gate event: ${gate}`, [eventsRel, `${gatesRel}/${gate}.json`]));
148
158
  }
149
159
  }
@@ -189,9 +199,12 @@ export function assertExistingReadyLifecycleCompatibility(ledger, result) {
189
199
  const existingReady = events.findLast((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
190
200
  if (!existingReady) return;
191
201
  const latestOutcome = latestPreflightOutcomeEvent(events, result.taskId);
202
+ const revisedAfterReady = events.some((event) => event.event === "CONTRACT_REVISED"
203
+ && event.taskId === result.taskId
204
+ && event.seq > existingReady.seq);
192
205
  // A READY outcome superseded by a later BLOCKED outcome may be replaced by a
193
206
  // fresh READY with different details once the blocked preflight is resolved.
194
- if (latestOutcome?.event === "PREFLIGHT_BLOCKED") return;
207
+ if (latestOutcome?.event === "PREFLIGHT_BLOCKED" || revisedAfterReady) return;
195
208
  if (!sameReadyPreflightEvent(existingReady, result)) {
196
209
  throw preflightError(
197
210
  "E_PHASE_CHRONOLOGY_INVALID",
@@ -217,9 +230,7 @@ export async function appendActivationEvents(target, packageRoot, ledger, result
217
230
  await append({ taskId: result.taskId, event: "ROUTE_VALIDATED", fingerprint: result.fingerprints.routing });
218
231
  }
219
232
  for (const gate of result.satisfiedGates) {
220
- if (!events.some((event) => event.event === "GATE_SATISFIED"
221
- && event.taskId === result.taskId
222
- && event.details?.gate === gate)) {
233
+ if (!hasCurrentRevisionGate(events, result.taskId, gate)) {
223
234
  await append({ taskId: result.taskId, event: "GATE_SATISFIED", details: { gate } });
224
235
  }
225
236
  }
@@ -227,15 +238,18 @@ export async function appendActivationEvents(target, packageRoot, ledger, result
227
238
  const existingReady = events.findLast((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
228
239
  const latestOutcome = latestPreflightOutcomeEvent(events, result.taskId);
229
240
  const readySupersededByBlocked = existingReady && latestOutcome?.event === "PREFLIGHT_BLOCKED";
241
+ const readySupersededByContractRevision = existingReady && events.some((event) => event.event === "CONTRACT_REVISED"
242
+ && event.taskId === result.taskId
243
+ && event.seq > existingReady.seq);
230
244
  if (result.status === "READY") {
231
- if (existingReady && !readySupersededByBlocked && !sameReadyPreflightEvent(existingReady, result)) {
245
+ if (existingReady && !readySupersededByBlocked && !readySupersededByContractRevision && !sameReadyPreflightEvent(existingReady, result)) {
232
246
  throw preflightError(
233
247
  "E_PHASE_CHRONOLOGY_INVALID",
234
248
  "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
235
249
  [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
236
250
  );
237
251
  }
238
- if (!existingReady || readySupersededByBlocked) {
252
+ if (!existingReady || readySupersededByBlocked || readySupersededByContractRevision) {
239
253
  await append({
240
254
  taskId: result.taskId,
241
255
  event: "PREFLIGHT_READY",
@@ -2,13 +2,15 @@ import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
2
2
  import { readContract } from "./contract.js";
3
3
  import { readGateIfPresent, validateGateArtifacts } from "./gate-artifact.js";
4
4
  import { requiredGatesForGuides } from "./guide-metadata.js";
5
- import { assertSourceProvenance } from "./sources.js";
5
+ import { assertContractPresetRefs, assertContractSourceProvenance, externalContractSourceRefs } from "./sources.js";
6
6
  import { readPersistedRoute } from "./route-artifact.js";
7
7
  import { ensureWithin, readBytes } from "./filesystem.js";
8
8
  import { sha256 } from "./manifest.js";
9
9
  import { findProfilePath } from "./profile.js";
10
10
  import { issue } from "./preflight-model.js";
11
11
  import { taskGatePath, taskArtifactPath } from "./task-paths.js";
12
+ import { readEvents } from "./events.js";
13
+ import { hasCurrentGateEvidence } from "./gate-provenance.js";
12
14
 
13
15
  export async function readProfile(target) {
14
16
  const relativePath = await findProfilePath(target);
@@ -62,15 +64,25 @@ export async function loadRoute(target, packageRoot, errors, options = {}) {
62
64
 
63
65
  export async function loadSources(target, contract, packageRoot, errors) {
64
66
  if (!contract?.value?.sourceRefs?.length) return null;
65
- let registry;
67
+ const refs = contract.value.sourceRefs;
66
68
  try {
67
- registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
69
+ assertContractPresetRefs(refs);
68
70
  } catch (error) {
69
- errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_PROFILE_SOURCE_MISSING" : "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
71
+ errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
70
72
  return null;
71
73
  }
74
+ const externalRefs = externalContractSourceRefs(refs);
75
+ let registry;
76
+ if (externalRefs.length > 0) {
77
+ try {
78
+ registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
79
+ } catch (error) {
80
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_PROFILE_SOURCE_MISSING" : "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
81
+ return null;
82
+ }
83
+ }
72
84
  try {
73
- assertSourceProvenance(registry, contract.value.sourceRefs);
85
+ assertContractSourceProvenance(registry, refs);
74
86
  } catch (error) {
75
87
  errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
76
88
  }
@@ -84,6 +96,15 @@ export async function inspectGates(target, contract, route, packageRoot, errors,
84
96
  const satisfied = [];
85
97
  const records = {};
86
98
  const taskId = options.taskId ?? null;
99
+ let events = [];
100
+ if (taskId) {
101
+ try {
102
+ events = await readEvents(target, packageRoot, { taskId });
103
+ } catch {
104
+ events = [];
105
+ }
106
+ }
107
+ const latestContractRevisionSeq = events.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
87
108
  for (const gate of required) {
88
109
  let artifact;
89
110
  const defaultGateRel = taskId ? taskGatePath(taskId, gate) : `${ARTIFACT_PATHS.gates}/${gate}.json`;
@@ -111,6 +132,17 @@ export async function inspectGates(target, contract, route, packageRoot, errors,
111
132
  errors.push(issue("E_GATE_STALE", `Gate ${gate} references stale artifacts`, [artifact.path], { gate, stale }));
112
133
  continue;
113
134
  }
135
+ const currentEpochSatisfied = latestContractRevisionSeq === 0
136
+ || hasCurrentGateEvidence(events, taskId, gate, latestContractRevisionSeq);
137
+ if (!currentEpochSatisfied) {
138
+ errors.push(issue(
139
+ "E_GATE_UNVERIFIED",
140
+ `Required gate ${gate} was satisfied before the current contract revision`,
141
+ [defaultGateRel],
142
+ { gate },
143
+ ));
144
+ continue;
145
+ }
114
146
  satisfied.push(gate);
115
147
  }
116
148
  return { required, satisfied: satisfied.sort(), records };