@cassiomc1/forgeloop 1.13.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 (211) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/DOCS_INDEX.md +35 -3
  3. package/ENG/nodejs-backend-development-eng.md +2 -2
  4. package/ENG/sec-code-eng.md +7 -7
  5. package/EXECUTION_STATE.md +12 -0
  6. package/LOOP_ENGINEERING.md +28 -2
  7. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  8. package/PROTOCOL_INTEGRATION.md +55 -2
  9. package/README.md +40 -25
  10. package/TERMINOLOGY.md +2 -0
  11. package/THREAT_MODEL.md +140 -1
  12. package/completions/_forgeloop +19 -1
  13. package/completions/forgeloop.bash +37 -1
  14. package/completions/forgeloop.fish +123 -1
  15. package/docs/ADVISORY_CONTEXT.md +25 -0
  16. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  17. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  18. package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
  19. package/docs/AGENT_SKILL.md +66 -0
  20. package/docs/ARTIFACT_REFERENCE.md +123 -0
  21. package/docs/AUDIT_UX.md +46 -0
  22. package/docs/BROWSER_VERIFICATION.md +136 -0
  23. package/docs/CLI_REFERENCE.md +366 -6
  24. package/docs/CODE_ATTESTATION.md +2 -2
  25. package/docs/DOCUMENTATION_GUIDE.md +32 -11
  26. package/docs/JEV_BENCHMARKS.md +31 -0
  27. package/docs/MODEL_ROUTING.md +37 -0
  28. package/docs/OPENSRC_ADAPTER.md +241 -0
  29. package/docs/PACKAGE_CONTENTS.md +35 -8
  30. package/docs/PROVIDERS.md +126 -0
  31. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  32. package/docs/RECIPES.md +9 -0
  33. package/docs/RELEASE_CHECKLIST.md +38 -5
  34. package/docs/SECURITY_REVIEW.md +71 -0
  35. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  36. package/docs/TEST_INTELLIGENCE.md +29 -0
  37. package/docs/TEST_PRUNING.md +14 -0
  38. package/docs/TROUBLESHOOTING.md +198 -1
  39. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  40. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  41. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  42. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  43. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  44. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  45. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  46. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  47. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  48. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  49. package/docs/diagrams/README.md +13 -9
  50. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  51. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  52. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  53. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  54. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  55. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  56. package/docs/documentation-manifest.json +750 -5
  57. package/docs/protocol-requirements.json +24 -0
  58. package/package.json +30 -3
  59. package/schemas/config.schema.json +14 -0
  60. package/schemas/context-plan.schema.json +18 -0
  61. package/schemas/semantic-decision.schema.json +46 -0
  62. package/schemas/test-utility.schema.json +44 -0
  63. package/scripts/CI_VALIDATORS.md +6 -6
  64. package/scripts/benchmark-jev.mjs +5 -0
  65. package/scripts/benchmark-test-intelligence.mjs +4 -0
  66. package/scripts/generate-agent-protocol-summary.mjs +4 -1
  67. package/scripts/generate-forgeloop-skill.mjs +133 -0
  68. package/scripts/jev-smoke.mjs +19 -0
  69. package/skills/forgeloop/README.md +9 -0
  70. package/skills/forgeloop/SKILL.md +77 -0
  71. package/skills/forgeloop/references/lifecycle.md +9 -0
  72. package/skills/forgeloop/references/recovery.md +7 -0
  73. package/skills/forgeloop/references/verification.md +7 -0
  74. package/src/adapters/agent-browser/assertions.js +47 -0
  75. package/src/adapters/agent-browser/commands.js +54 -0
  76. package/src/adapters/agent-browser/index.js +3 -0
  77. package/src/adapters/agent-browser/locator.js +40 -0
  78. package/src/adapters/agent-browser/process.js +215 -0
  79. package/src/adapters/agent-browser/provider.js +313 -0
  80. package/src/adapters/emulated-services/constants.js +24 -0
  81. package/src/adapters/emulated-services/index.js +7 -0
  82. package/src/adapters/emulated-services/process.js +162 -0
  83. package/src/adapters/emulated-services/provider.js +282 -0
  84. package/src/adapters/opensrc/normalize.js +90 -0
  85. package/src/adapters/opensrc/process.js +248 -0
  86. package/src/adapters/opensrc/provider.js +338 -0
  87. package/src/adapters/opensrc/search.js +264 -0
  88. package/src/adapters/typesafe/client.js +28 -0
  89. package/src/adapters/typesafe/engine.js +63 -0
  90. package/src/adapters/typesafe/normalize.js +41 -0
  91. package/src/cli.js +108 -0
  92. package/src/commands/checkpoint-revalidate.js +176 -0
  93. package/src/commands/context-plan.js +38 -0
  94. package/src/commands/contract-create.js +264 -0
  95. package/src/commands/contract-revise.js +236 -0
  96. package/src/commands/decision-show.js +14 -0
  97. package/src/commands/decision-status.js +22 -0
  98. package/src/commands/discover.js +41 -0
  99. package/src/commands/doctor.js +15 -0
  100. package/src/commands/gate-record.js +205 -0
  101. package/src/commands/gate-revalidate.js +137 -0
  102. package/src/commands/model-route.js +32 -0
  103. package/src/commands/route.js +146 -18
  104. package/src/commands/semantic-plan.js +17 -0
  105. package/src/commands/task-abandon.js +224 -0
  106. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  107. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  108. package/src/commands/test-inventory.js +5 -0
  109. package/src/commands/test-prune-plan.js +5 -0
  110. package/src/commands/test-prune-probe.js +5 -0
  111. package/src/commands/test-utility.js +5 -0
  112. package/src/commands/validate-protocol.js +10 -1
  113. package/src/core/artifact-registry.js +24 -0
  114. package/src/core/audit-ux.js +514 -0
  115. package/src/core/browser-verification/constants.js +149 -0
  116. package/src/core/browser-verification/normalize.js +254 -0
  117. package/src/core/browser-verification/provider.js +519 -0
  118. package/src/core/browser-verification/service.js +115 -0
  119. package/src/core/checkpoint-revalidation.js +319 -0
  120. package/src/core/cli-command-definitions.js +241 -0
  121. package/src/core/command-executors.js +110 -0
  122. package/src/core/command-input.js +115 -43
  123. package/src/core/completion-artifacts.js +14 -5
  124. package/src/core/completion.js +4 -6
  125. package/src/core/config.js +3 -0
  126. package/src/core/context-compiler/budget.js +9 -0
  127. package/src/core/context-compiler/candidates.js +39 -0
  128. package/src/core/context-compiler/compiler.js +63 -0
  129. package/src/core/context-compiler/fingerprint.js +11 -0
  130. package/src/core/context-compiler/policy.js +13 -0
  131. package/src/core/context-compiler/result.js +23 -0
  132. package/src/core/contract-bootstrap-recovery.js +655 -0
  133. package/src/core/contract-revision.js +210 -0
  134. package/src/core/decision/artifact.js +69 -0
  135. package/src/core/decision/benchmarks.js +103 -0
  136. package/src/core/decision/cache.js +27 -0
  137. package/src/core/decision/constants.js +58 -0
  138. package/src/core/decision/cutover.js +34 -0
  139. package/src/core/decision/engine.js +22 -0
  140. package/src/core/decision/errors.js +68 -0
  141. package/src/core/decision/events.js +101 -0
  142. package/src/core/decision/freshness.js +19 -0
  143. package/src/core/decision/normalizers/index.js +115 -0
  144. package/src/core/decision/policy.js +18 -0
  145. package/src/core/decision/projection.js +16 -0
  146. package/src/core/decision/question-registry.js +201 -0
  147. package/src/core/decision/request.js +26 -0
  148. package/src/core/decision/resolver.js +130 -0
  149. package/src/core/decision/result.js +58 -0
  150. package/src/core/decision/service.js +156 -0
  151. package/src/core/decision/state-builder.js +65 -0
  152. package/src/core/decision/task-bindings.js +30 -0
  153. package/src/core/decision/test-provider.js +32 -0
  154. package/src/core/decision/thresholds.js +15 -0
  155. package/src/core/error-codes.js +278 -0
  156. package/src/core/events.js +226 -57
  157. package/src/core/evidence-readiness.js +9 -0
  158. package/src/core/execution-prerequisites.js +14 -0
  159. package/src/core/execution-profile.js +63 -38
  160. package/src/core/gate-provenance.js +124 -0
  161. package/src/core/integration-invocation-policy.js +27 -4
  162. package/src/core/integration-resources.js +86 -61
  163. package/src/core/model-router/constants.js +10 -0
  164. package/src/core/model-router/policy.js +103 -0
  165. package/src/core/model-router/router.js +37 -0
  166. package/src/core/next-action-model.js +58 -0
  167. package/src/core/next-action-phases.js +130 -42
  168. package/src/core/next-action-refresh.js +43 -9
  169. package/src/core/next-action-review-phase.js +7 -2
  170. package/src/core/next-action.js +35 -7
  171. package/src/core/phase.js +128 -10
  172. package/src/core/preflight-consistency.js +23 -9
  173. package/src/core/preflight-loaders.js +37 -5
  174. package/src/core/protocol-info.js +65 -0
  175. package/src/core/protocol.js +20 -0
  176. package/src/core/reconcile-closure.js +128 -52
  177. package/src/core/recovery-history.js +1 -0
  178. package/src/core/resumability.js +154 -44
  179. package/src/core/route-artifact.js +15 -1
  180. package/src/core/router.js +67 -1
  181. package/src/core/runtime-context.js +118 -61
  182. package/src/core/schema-validation.js +3 -0
  183. package/src/core/security-review/constants.js +64 -0
  184. package/src/core/security-review/normalize.js +245 -0
  185. package/src/core/security-review/provider.js +204 -0
  186. package/src/core/security-review/service.js +134 -0
  187. package/src/core/semantic-planning/constants.js +19 -0
  188. package/src/core/semantic-planning/projection.js +94 -0
  189. package/src/core/semantic-planning/service.js +15 -0
  190. package/src/core/sources.js +37 -0
  191. package/src/core/task-claim-state.js +201 -1
  192. package/src/core/task-conflict-inspection.js +31 -5
  193. package/src/core/task-paths.js +13 -0
  194. package/src/core/task-recovery.js +1 -0
  195. package/src/core/templates.js +3 -0
  196. package/src/core/test-intelligence/benchmarks.js +68 -0
  197. package/src/core/test-intelligence/inventory.js +73 -0
  198. package/src/core/test-intelligence/prune.js +90 -0
  199. package/src/core/test-intelligence/semantic-state.js +15 -0
  200. package/src/core/test-intelligence/service.js +40 -0
  201. package/src/core/test-intelligence/utility.js +50 -0
  202. package/src/core/trace.js +11 -7
  203. package/src/core/transaction.js +1 -0
  204. package/src/integration.d.ts +492 -0
  205. package/src/integration.js +54 -0
  206. package/src/providers/README.md +47 -0
  207. package/src/providers/capabilities.js +46 -0
  208. package/src/providers/errors.js +15 -0
  209. package/src/providers/index.js +29 -0
  210. package/src/providers/json-snapshot.js +105 -0
  211. package/src/providers/registry.js +152 -0
@@ -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
 
@@ -313,6 +313,64 @@ function reasonForSignal(prefix, signal) {
313
313
  return `${prefix}_${signal.toUpperCase().replaceAll("-", "_")}`;
314
314
  }
315
315
 
316
+ const SECURITY_TRUST_BOUNDARY_RISKS = Object.freeze([
317
+ "untrusted-input", "personal-data", "secrets", "external-service", "publication",
318
+ ]);
319
+
320
+ // Canonical deterministic safety floor. These are the route reason codes the
321
+ // router assigns when a trust-boundary signal selects the security guide. The
322
+ // semantic exclusion policy reads protection from this same source, so a
323
+ // mandatory safety guide can never drift out of protection (for example the
324
+ // previously missing external-service boundary).
325
+ const MANDATORY_SAFETY_REASONS = Object.freeze(new Set([
326
+ "SURFACE_AUTH",
327
+ ...SECURITY_TRUST_BOUNDARY_RISKS.map((risk) => reasonForSignal("RISK", risk)),
328
+ ]));
329
+
330
+ export function isMandatorySafetyReason(reason) {
331
+ return MANDATORY_SAFETY_REASONS.has(reason);
332
+ }
333
+
334
+ export function isMandatorySafetyGuide(guide, reasons) {
335
+ return guide === "security"
336
+ && Array.isArray(reasons)
337
+ && reasons.some((reason) => isMandatorySafetyReason(reason));
338
+ }
339
+
340
+ function applySemanticGuideRecommendation(selected, excluded, recommendation) {
341
+ const guideDecision = recommendation?.guideRelevance
342
+ ?? (recommendation?.rankedIds || recommendation?.excludedIds ? recommendation : null);
343
+ if (!guideDecision) return;
344
+ const excludedIds = new Set(Array.isArray(guideDecision.excludedIds) ? guideDecision.excludedIds : []);
345
+ const confidenceById = guideDecision.confidenceById ?? {};
346
+ for (const guide of [...selected.keys()]) {
347
+ const reasons = selected.get(guide);
348
+ if (isMandatorySafetyGuide(guide, reasons)) {
349
+ if (!reasons.includes("MANDATORY_SAFETY_GUIDE")) reasons.push("MANDATORY_SAFETY_GUIDE");
350
+ continue;
351
+ }
352
+ if (!excludedIds.has(guide)) {
353
+ if (!reasons.includes("JEV_RELEVANT")) reasons.push("JEV_RELEVANT");
354
+ continue;
355
+ }
356
+ if ((confidenceById[guide] ?? 0) < 0.75) {
357
+ if (!reasons.includes("JEV_LOW_CONFIDENCE_RETAINED")) reasons.push("JEV_LOW_CONFIDENCE_RETAINED");
358
+ continue;
359
+ }
360
+ selected.delete(guide);
361
+ excluded[guide] = ["JEV_EXCLUDED"];
362
+ }
363
+ const ranked = Array.isArray(guideDecision.rankedIds) ? guideDecision.rankedIds : [];
364
+ const order = new Map(ranked.map((guide, index) => [guide, index]));
365
+ const entries = [...selected.entries()].sort((left, right) => {
366
+ const leftRank = order.has(left[0]) ? order.get(left[0]) : ranked.length + [...selected.keys()].indexOf(left[0]);
367
+ const rightRank = order.has(right[0]) ? order.get(right[0]) : ranked.length + [...selected.keys()].indexOf(right[0]);
368
+ return leftRank - rightRank;
369
+ });
370
+ selected.clear();
371
+ for (const [guide, reasons] of entries) selected.set(guide, reasons);
372
+ }
373
+
316
374
  function normalizeArray(value, name, allowed) {
317
375
  if (value === undefined) return [];
318
376
  if (!Array.isArray(value)) throw new RouteInputError(`${name} must be an array`);
@@ -440,7 +498,7 @@ export function evaluateRoute(input = {}, profileOptions = {}) {
440
498
  }
441
499
 
442
500
  for (const risk of normalized.risks) {
443
- if (["untrusted-input", "personal-data", "secrets", "external-service", "publication"].includes(risk)) {
501
+ if (SECURITY_TRUST_BOUNDARY_RISKS.includes(risk)) {
444
502
  add("security", reasonForSignal("RISK", risk));
445
503
  }
446
504
  if (["critical-path", "performance"].includes(risk)) {
@@ -481,12 +539,19 @@ export function evaluateRoute(input = {}, profileOptions = {}) {
481
539
  excluded[guide] = [exclusionReasonForGuide(guide, projectEvidence, normalized.workType)];
482
540
  }
483
541
 
542
+ applySemanticGuideRecommendation(
543
+ selected,
544
+ excluded,
545
+ profileOptions.semanticRecommendation ?? null,
546
+ );
547
+
484
548
  const guides = [...selected.keys()];
485
549
  const result = {
486
550
  schemaVersion: ROUTING_SCHEMA_VERSION,
487
551
  protocolVersion: PROTOCOL_VERSION,
488
552
  input: normalized,
489
553
  primary: Object.prototype.hasOwnProperty.call(PRIMARY_GUIDES, normalized.workType)
554
+ && guides.includes(PRIMARY_GUIDES[normalized.workType])
490
555
  ? PRIMARY_GUIDES[normalized.workType]
491
556
  : guides[0] ?? null,
492
557
  guides,
@@ -499,6 +564,7 @@ export function evaluateRoute(input = {}, profileOptions = {}) {
499
564
  configuredProfile: profileOptions.configuredProfile ?? input.configuredProfile ?? "auto",
500
565
  requestedProfile: profileOptions.requestedProfile
501
566
  ?? (Object.prototype.hasOwnProperty.call(input, "executionProfile") ? input.executionProfile : null),
567
+ semanticRecommendation: profileOptions.semanticRecommendation ?? null,
502
568
  }),
503
569
  };
504
570
  return assertRouteInvariants(result);
@@ -4,8 +4,22 @@ import {
4
4
  normalizeVerificationExecutionPolicy,
5
5
  } from "./verification-execution.js";
6
6
  import { STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN } from "./structural-quality/constants.js";
7
- import { E_ADVISORY_CONTEXT_PROVIDER_INVALID } from "./error-codes.js";
7
+ import {
8
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
9
+ E_BROWSER_VERIFICATION_PROVIDER_INVALID,
10
+ E_SECURITY_REVIEW_PROVIDER_INVALID,
11
+ } from "./error-codes.js";
8
12
  import { assertAdvisoryContextProviderIdentity } from "./advisory-context/provider.js";
13
+ import {
14
+ BROWSER_VERIFICATION_PROVIDER_ID_PATTERN,
15
+ assertBrowserVerificationProvider,
16
+ assertBrowserVerificationProviderIdentity,
17
+ } from "./browser-verification/provider.js";
18
+ import {
19
+ SECURITY_REVIEW_PROVIDER_ID_PATTERN,
20
+ assertSecurityReviewProvider,
21
+ assertSecurityReviewProviderIdentity,
22
+ } from "./security-review/provider.js";
9
23
 
10
24
  export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
11
25
 
@@ -83,6 +97,100 @@ export function resolveAuthorityContext(options = {}) {
83
97
  return hasDirectAuthority ? createAuthorityContext({ ...source, trustMode: "NONE" }) : createAuthorityContext();
84
98
  }
85
99
 
100
+ function isProviderObject(value) {
101
+ return value && typeof value === "object" && !Array.isArray(value);
102
+ }
103
+
104
+ function providerEntries(configured, label, code) {
105
+ if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
106
+ const error = new Error(`${label} must be an object or Map`);
107
+ error.code = code;
108
+ throw error;
109
+ }
110
+ return configured instanceof Map ? [...configured.entries()] : Object.entries(configured);
111
+ }
112
+
113
+ function registerProviders(configured, { label, code, idError, validateId, providerError, validateProvider }) {
114
+ const providers = {};
115
+ for (const [id, provider] of providerEntries(configured, label, code)) {
116
+ if (!validateId(id)) {
117
+ const error = new Error(idError(id));
118
+ error.code = code;
119
+ throw error;
120
+ }
121
+ if (typeof provider !== "function" && !isProviderObject(provider)) {
122
+ const error = new Error(providerError(id));
123
+ error.code = code;
124
+ throw error;
125
+ }
126
+ if (typeof provider !== "function") validateProvider(provider, id);
127
+ providers[id] = provider;
128
+ }
129
+ return Object.freeze(providers);
130
+ }
131
+
132
+ function configureUsageProvider(context, provider) {
133
+ if (!provider
134
+ || typeof provider !== "object"
135
+ || Array.isArray(provider)
136
+ || typeof provider.getTaskUsage !== "function") {
137
+ const error = new Error("Usage provider must expose getTaskUsage({ projectPath, taskId })");
138
+ error.code = "E_USAGE_INVALID";
139
+ throw error;
140
+ }
141
+ context.usageProvider = provider;
142
+ }
143
+
144
+ function configureStructuralQualityProviders(context, configured) {
145
+ context.structuralQualityProviders = registerProviders(configured, {
146
+ label: "structuralQualityProviders",
147
+ code: "E_STRUCTURAL_QUALITY_PROVIDER_INVALID",
148
+ idError: id => `Invalid or reserved structural-quality provider ID: ${id}`,
149
+ validateId: id => STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) && id !== "sentrux",
150
+ providerError: id => `Structural-quality provider ${id} must be an object or factory`,
151
+ validateProvider: (provider, id) => {},
152
+ });
153
+ }
154
+
155
+ function configureAdvisoryContextProviders(context, configured) {
156
+ context.advisoryContextProviders = registerProviders(configured, {
157
+ label: "advisoryContextProviders",
158
+ code: E_ADVISORY_CONTEXT_PROVIDER_INVALID,
159
+ idError: id => `Invalid advisory-context provider ID: ${id}`,
160
+ validateId: id => /^[a-z0-9][a-z0-9_-]*$/.test(id),
161
+ providerError: id => `Advisory-context provider ${id} must be an object or factory`,
162
+ validateProvider: assertAdvisoryContextProviderIdentity,
163
+ });
164
+ }
165
+
166
+ function configureBrowserVerificationProviders(context, configured) {
167
+ context.browserVerificationProviders = registerProviders(configured, {
168
+ label: "browserVerificationProviders",
169
+ code: E_BROWSER_VERIFICATION_PROVIDER_INVALID,
170
+ idError: id => `Invalid browser-verification provider ID: ${id}`,
171
+ validateId: id => BROWSER_VERIFICATION_PROVIDER_ID_PATTERN.test(id),
172
+ providerError: id => `Browser-verification provider ${id} must be an object with verify() or a factory`,
173
+ validateProvider: (provider, id) => {
174
+ assertBrowserVerificationProviderIdentity(provider, { expectedId: id });
175
+ assertBrowserVerificationProvider(provider, { label: `browser-verification provider "${id}"` });
176
+ },
177
+ });
178
+ }
179
+
180
+ function configureSecurityReviewProviders(context, configured) {
181
+ context.securityReviewProviders = registerProviders(configured, {
182
+ label: "securityReviewProviders",
183
+ code: E_SECURITY_REVIEW_PROVIDER_INVALID,
184
+ idError: id => `Invalid security-review provider ID: ${id}`,
185
+ validateId: id => SECURITY_REVIEW_PROVIDER_ID_PATTERN.test(id),
186
+ providerError: id => `Security-review provider ${id} must be an object with review() or a factory`,
187
+ validateProvider: (provider, id) => {
188
+ assertSecurityReviewProviderIdentity(provider, { expectedId: id });
189
+ assertSecurityReviewProvider(provider, { label: `security-review provider "${id}"` });
190
+ },
191
+ });
192
+ }
193
+
86
194
  export function createForgeLoopContext(options = {}) {
87
195
  const authorityContext = createAuthorityContext(options);
88
196
  const context = { authorityContext };
@@ -98,70 +206,19 @@ export function createForgeLoopContext(options = {}) {
98
206
  context.verificationExecutionPolicy = normalizeVerificationExecutionPolicy(options.verificationExecutionPolicy);
99
207
  }
100
208
  if (options?.usageProvider !== undefined) {
101
- if (!options.usageProvider
102
- || typeof options.usageProvider !== "object"
103
- || Array.isArray(options.usageProvider)
104
- || typeof options.usageProvider.getTaskUsage !== "function") {
105
- const error = new Error("Usage provider must expose getTaskUsage({ projectPath, taskId })");
106
- error.code = "E_USAGE_INVALID";
107
- throw error;
108
- }
109
- context.usageProvider = options.usageProvider;
209
+ configureUsageProvider(context, options.usageProvider);
110
210
  }
111
211
  if (options?.structuralQualityProviders !== undefined) {
112
- const configured = options.structuralQualityProviders instanceof Map
113
- ? Object.fromEntries(options.structuralQualityProviders.entries())
114
- : options.structuralQualityProviders;
115
- if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
116
- const error = new Error("structuralQualityProviders must be an object or Map");
117
- error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
118
- throw error;
119
- }
120
- const providers = {};
121
- for (const [id, provider] of Object.entries(configured)) {
122
- if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
123
- const error = new Error(`Invalid or reserved structural-quality provider ID: ${id}`);
124
- error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
125
- throw error;
126
- }
127
- if (typeof provider !== "function"
128
- && (!provider || typeof provider !== "object" || Array.isArray(provider))) {
129
- const error = new Error(`Structural-quality provider ${id} must be an object or factory`);
130
- error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
131
- throw error;
132
- }
133
- providers[id] = provider;
134
- }
135
- context.structuralQualityProviders = Object.freeze(providers);
212
+ configureStructuralQualityProviders(context, options.structuralQualityProviders);
136
213
  }
137
214
  if (options?.advisoryContextProviders !== undefined) {
138
- const configured = options.advisoryContextProviders instanceof Map
139
- ? Object.fromEntries(options.advisoryContextProviders.entries())
140
- : options.advisoryContextProviders;
141
- if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
142
- const error = new Error("advisoryContextProviders must be an object or Map");
143
- error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
144
- throw error;
145
- }
146
- const providers = {};
147
- for (const [id, provider] of Object.entries(configured)) {
148
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) {
149
- const error = new Error(`Invalid advisory-context provider ID: ${id}`);
150
- error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
151
- throw error;
152
- }
153
- if (typeof provider !== "function"
154
- && (!provider || typeof provider !== "object" || Array.isArray(provider))) {
155
- const error = new Error(`Advisory-context provider ${id} must be an object or factory`);
156
- error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
157
- throw error;
158
- }
159
- if (typeof provider !== "function") {
160
- assertAdvisoryContextProviderIdentity(provider, id);
161
- }
162
- providers[id] = provider;
163
- }
164
- context.advisoryContextProviders = Object.freeze(providers);
215
+ configureAdvisoryContextProviders(context, options.advisoryContextProviders);
216
+ }
217
+ if (options?.browserVerificationProviders !== undefined) {
218
+ configureBrowserVerificationProviders(context, options.browserVerificationProviders);
219
+ }
220
+ if (options?.securityReviewProviders !== undefined) {
221
+ configureSecurityReviewProviders(context, options.securityReviewProviders);
165
222
  }
166
223
  return Object.freeze(context);
167
224
  }
@@ -55,6 +55,9 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
55
55
  "execution-profile-benchmark-run",
56
56
  "execution-profile-benchmark-aggregate",
57
57
  "structural-quality",
58
+ "semantic-decision",
59
+ "context-plan",
60
+ "test-utility",
58
61
  ]);
59
62
 
60
63
  export class SchemaValidationError extends Error {
@@ -0,0 +1,64 @@
1
+ import { E_SECURITY_REVIEW_REQUEST_INVALID } from "../error-codes.js";
2
+
3
+ export const SECURITY_REVIEW_LIMITS = Object.freeze({
4
+ maxProjectPathChars: 4096,
5
+ maxTaskIdChars: 128,
6
+ maxReviewIdChars: 128,
7
+ maxRequirementChars: 2000,
8
+ maxRequirements: 64,
9
+ maxPaths: 128,
10
+ maxPathChars: 512,
11
+ maxCategories: 32,
12
+ maxCategoryChars: 64,
13
+ maxRevisionChars: 256,
14
+ maxFindings: 256,
15
+ maxFindingIdChars: 128,
16
+ maxSeverityChars: 16,
17
+ maxTitleChars: 512,
18
+ maxSummaryChars: 4096,
19
+ maxRuleIdChars: 128,
20
+ maxConfidenceChars: 16,
21
+ maxDiagnosticChars: 4000,
22
+ maxDiagnostics: 64,
23
+ maxResultChars: 524288,
24
+ maxSnapshotDepth: 32,
25
+ maxSnapshotNodes: 4096,
26
+ maxSnapshotChars: 524288,
27
+ maxDurationMs: 86_400_000,
28
+ defaultTimeoutMs: 30_000,
29
+ maxTimeoutMs: 120_000,
30
+ });
31
+
32
+ export const SECURITY_REVIEW_SCOPES = Object.freeze(["FULL", "CHANGED", "SELECTED"]);
33
+ export const SECURITY_REVIEW_SEVERITIES = Object.freeze(["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"]);
34
+ export const SECURITY_REVIEW_CONFIDENCE = Object.freeze(["LOW", "MEDIUM", "HIGH"]);
35
+ export const SECURITY_REVIEW_CATEGORIES = Object.freeze([
36
+ "DEPENDENCY", "SECRETS", "INJECTION", "AUTHENTICATION", "AUTHORIZATION",
37
+ "CRYPTOGRAPHY", "CONFIGURATION", "NETWORK", "FILESYSTEM", "SUPPLY_CHAIN",
38
+ "UNSAFE_EXECUTION", "OTHER",
39
+ ]);
40
+
41
+ export const SECURITY_REVIEW_TRUST = Object.freeze({
42
+ authority: "OBSERVATION",
43
+ evidenceAuthority: "NONE",
44
+ actionability: "NON_EXECUTABLE",
45
+ trustRole: "NON_EVIDENCE_SECURITY_REVIEW",
46
+ persisted: false,
47
+ lifecycleAuthority: false,
48
+ completionAuthority: false,
49
+ evidenceRequiresForgeLoopValidation: true,
50
+ });
51
+
52
+ export const SECURITY_REVIEW_BLOCKED_RESULT_FIELDS = Object.freeze([
53
+ "complete", "completed", "nextAction", "releaseClaims", "mutationAllowed", "taskPhase",
54
+ "lifecycleState", "receipt", "evidence", "evidenceStatus", "check", "gate", "satisfied",
55
+ "ownership", "writeClaims", "event", "events", "transaction", "installation", "command",
56
+ "commandArgv", "executable", "shell", "environment", "credentials", "secrets",
57
+ ]);
58
+
59
+ export function securityReviewRequestError(message, details = null) {
60
+ const error = new Error(message);
61
+ error.code = E_SECURITY_REVIEW_REQUEST_INVALID;
62
+ error.details = details;
63
+ return error;
64
+ }