@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
@@ -0,0 +1,137 @@
1
+ import { lstat } from "node:fs/promises";
2
+
3
+ import { optionalConfig } from "../core/preflight-loaders.js";
4
+ import { readContract } from "../core/contract.js";
5
+ import { appendProtocolEvent, validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
6
+ import { ensureWithin, isPathWithin, readBytes, realpathWithTransientWindowsRetry } from "../core/filesystem.js";
7
+ import { sha256 } from "../core/manifest.js";
8
+ import { requiredGatesForGuides } from "../core/guide-metadata.js";
9
+ import { readPersistedRoute } from "../core/route-artifact.js";
10
+ import { staleReasons } from "../core/next-action-artifacts.js";
11
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
12
+ import { taskGatePath, taskArtifactPath } from "../core/task-paths.js";
13
+ import { withTaskMutation } from "../core/task-command.js";
14
+ import { readWorkState } from "../core/work-state.js";
15
+ import { stateIdentityErrors } from "../core/completion-relationships.js";
16
+ import { persistGate, readGateIfPresent, validateGateArtifacts } from "../core/gate-artifact.js";
17
+ import {
18
+ GATE_REVALIDATED_EVENT,
19
+ assertGateRevalidatedDetails,
20
+ } from "../core/gate-provenance.js";
21
+
22
+ const POST_EXECUTION_PHASES = new Set([
23
+ "EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING",
24
+ ]);
25
+
26
+ function revalidationError(message, code = "E_GATE_REVALIDATION_UNSAFE", artifacts = []) {
27
+ const error = new Error(message);
28
+ error.code = code;
29
+ error.artifacts = artifacts;
30
+ return error;
31
+ }
32
+
33
+ async function hashGateArtifact(target, relativePath) {
34
+ let artifactPath;
35
+ try {
36
+ artifactPath = ensureWithin(target, relativePath);
37
+ const stat = await lstat(artifactPath);
38
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("artifact must be a regular file");
39
+ const rootReal = await realpathWithTransientWindowsRetry(target);
40
+ const artifactReal = await realpathWithTransientWindowsRetry(artifactPath);
41
+ if (!isPathWithin(rootReal, artifactReal)) throw new Error("artifact resolves outside the project");
42
+ return { path: relativePath, sha256: sha256(await readBytes(artifactReal)) };
43
+ } catch (cause) {
44
+ throw revalidationError(`Cannot refresh gate artifact ${relativePath}: ${cause.message}`, "E_GATE_REVALIDATION_UNSAFE", [relativePath]);
45
+ }
46
+ }
47
+
48
+ async function assertCurrentGateBinding(target, packageRoot, taskId, state, gate) {
49
+ const contract = await readContract(target, packageRoot, { taskId });
50
+ const route = await readPersistedRoute(target, packageRoot, { taskId });
51
+ const identityErrors = stateIdentityErrors({ contract, route, state });
52
+ if (identityErrors.length > 0) {
53
+ throw revalidationError(
54
+ "Gate revalidation requires coherent current contract, route, state, and selected guides",
55
+ "E_GATE_REVALIDATION_UNSAFE",
56
+ [taskArtifactPath(taskId, "contract"), taskArtifactPath(taskId, "route"), taskArtifactPath(taskId, "state")],
57
+ );
58
+ }
59
+ const routeStale = staleReasons(state, contract, route);
60
+ if (routeStale.length > 0) {
61
+ throw revalidationError("Gate revalidation cannot repair stale contract or route identity", "E_GATE_REVALIDATION_UNSAFE", routeStale.flatMap((reason) => reason.artifacts));
62
+ }
63
+ const config = await optionalConfig(target, packageRoot, []);
64
+ const required = [...new Set([
65
+ ...(await requiredGatesForGuides(route.value.guides, packageRoot)),
66
+ ...(config.requiredGates ?? []),
67
+ ])];
68
+ if (!required.includes(gate)) {
69
+ throw revalidationError(`Gate is not required by the active route: ${gate}`, "E_GATE_NOT_REQUIRED", [taskArtifactPath(taskId, "route")]);
70
+ }
71
+ return { contract, route };
72
+ }
73
+
74
+ export async function runGateRevalidate({ target, packageRoot, taskId, gate, acknowledgeStale = false } = {}) {
75
+ if (!acknowledgeStale) {
76
+ throw revalidationError("gate-revalidate requires --acknowledge-stale", "E_GATE_REVALIDATION_ACK_REQUIRED");
77
+ }
78
+ if (typeof gate !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(gate)) {
79
+ throw revalidationError(`Invalid gate name: ${gate}`, "E_GATE_INVALID");
80
+ }
81
+ return withTaskMutation(target, { taskId, packageRoot }, "gate-revalidate", async (ctx) => {
82
+ const state = await readWorkState(target, { packageRoot, taskId: ctx.taskId });
83
+ if (!state || !POST_EXECUTION_PHASES.has(state.phase)) {
84
+ throw revalidationError(
85
+ `gate-revalidate is only available for post-execution repair phases; found ${state?.phase ?? "no phase"}`,
86
+ "E_GATE_REVALIDATION_PHASE_INVALID",
87
+ [taskArtifactPath(ctx.taskId, "state")],
88
+ );
89
+ }
90
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
91
+ if (!ledger.valid) throw revalidationError("Gate revalidation requires a valid append-only event ledger", "E_EVENT_INVALID", ledger.errors);
92
+ const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
93
+ if (coherenceErrors.length > 0) throw revalidationError("Gate revalidation requires coherent state and ledger", "E_PHASE_CHRONOLOGY_INVALID", coherenceErrors);
94
+ const ownership = await resolveTaskClaimState(target, { taskId: ctx.taskId, packageRoot });
95
+ if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
96
+ throw revalidationError("Gate revalidation cannot bypass active task claim ownership", "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT", ownership.ownershipErrors);
97
+ }
98
+ await assertCurrentGateBinding(target, packageRoot, ctx.taskId, state, gate);
99
+ const existing = await readGateIfPresent(target, gate, packageRoot, { taskId: ctx.taskId });
100
+ if (!existing || existing.value.status !== "satisfied") {
101
+ throw revalidationError("Only an existing satisfied gate can be revalidated", "E_GATE_UNVERIFIED", [taskGatePath(ctx.taskId, gate)]);
102
+ }
103
+ const stale = await validateGateArtifacts(target, existing.value, packageRoot);
104
+ if (stale.length === 0) {
105
+ throw revalidationError("Gate artifacts are already fresh; no revalidation event is required", "E_GATE_REVALIDATION_NOT_STALE", [existing.path]);
106
+ }
107
+ const refreshedArtifacts = [];
108
+ for (const artifact of existing.value.artifacts ?? []) refreshedArtifacts.push(await hashGateArtifact(target, artifact.path));
109
+ const refreshedGate = { ...existing.value, artifacts: refreshedArtifacts };
110
+ const persisted = await persistGate(target, refreshedGate, packageRoot, { taskId: ctx.taskId });
111
+ const details = {
112
+ gate,
113
+ previousGateFingerprint: existing.fingerprint,
114
+ gateFingerprint: persisted.fingerprint,
115
+ stalePaths: stale.map((entry) => entry.path).sort(),
116
+ };
117
+ assertGateRevalidatedDetails(details);
118
+ const event = await appendProtocolEvent(target, {
119
+ taskId: ctx.taskId,
120
+ event: GATE_REVALIDATED_EVENT,
121
+ details,
122
+ }, packageRoot, { taskId: ctx.taskId });
123
+ return {
124
+ taskId: ctx.taskId,
125
+ gate,
126
+ stalePaths: details.stalePaths,
127
+ gateFingerprint: persisted.fingerprint,
128
+ eventSeq: event.seq,
129
+ transactionCommitSeq: event.seq + 1,
130
+ path: persisted.path ?? persisted.relativePath ?? taskGatePath(ctx.taskId, gate),
131
+ };
132
+ });
133
+ }
134
+
135
+ export function formatGateRevalidateResult(result) {
136
+ return `${JSON.stringify(result, null, 2)}\n`;
137
+ }
@@ -0,0 +1,32 @@
1
+ import { projectModelRoute } from "../core/model-router/router.js";
2
+ import { resolveRequiredSemanticDecision } from "../core/decision/resolver.js";
3
+ import { readCurrentDecisionBindings } from "../core/decision/task-bindings.js";
4
+
5
+ export async function runModelRoute({ target, packageRoot, taskId, decisionId, workType, surfaces, risks, platforms, behaviorChange, executableChange, generationRequired, architectureChange, ambiguity, semanticRecommendation, contract } = {}) {
6
+ if (target && taskId) {
7
+ const bindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
8
+ const decision = await resolveRequiredSemanticDecision({
9
+ target, packageRoot, taskId, decisionId, decisionKind: "MODEL_ROUTE", currentBindings: bindings,
10
+ });
11
+ semanticRecommendation = decision.decision;
12
+ }
13
+ return projectModelRoute({
14
+ input: {
15
+ workType,
16
+ surfaces,
17
+ risks,
18
+ platforms,
19
+ behaviorChange,
20
+ executableChange,
21
+ generationRequired,
22
+ architectureChange,
23
+ ambiguity,
24
+ contract,
25
+ },
26
+ semanticRecommendation,
27
+ });
28
+ }
29
+
30
+ export function formatModelRouteResult(result) {
31
+ return `${JSON.stringify(result, null, 2)}\n`;
32
+ }
@@ -1,13 +1,104 @@
1
1
  import { evaluateRoute } from "../core/router.js";
2
- import { persistRoute } from "../core/route-artifact.js";
2
+ import { persistRoute, readPersistedRoute } from "../core/route-artifact.js";
3
3
  import { readContract } from "../core/contract.js";
4
4
  import { readConfig } from "../core/config.js";
5
+ import { appendProtocolEvent, readEvents } from "../core/events.js";
5
6
  import { detectProjectEvidence } from "../core/project-detection.js";
6
- import { withTaskMutation } from "../core/task-command.js";
7
+ import { withResolvedTask, withTaskMutation } from "../core/task-command.js";
8
+ import { advanceWorkState } from "../core/phase.js";
9
+ import { mutateWorkState, readWorkState } from "../core/work-state.js";
10
+ import { buildExecutionProfileQuestionSet, buildRouteQuestionSet } from "../core/decision/question-registry.js";
11
+ import { ensureSemanticDecision } from "../core/decision/service.js";
12
+ import { canonicalFingerprint } from "../core/artifacts.js";
13
+ import {
14
+ resolveEffectiveContractBootstrapRepairAnchor,
15
+ } from "../core/contract-bootstrap-recovery.js";
7
16
 
8
- export async function runRoute({ target, packageRoot, workType, surfaces, risks, platforms, behaviorChange, executableChange, executionProfile = null, taskId, task }) {
9
- return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "route", async (ctx) => {
10
- const effectiveTaskId = ctx?.taskId ?? null;
17
+ function canAppendRepairedRouteWitness({ stateBefore, marker, previousPersistedRouteFingerprint, persistedRoute }) {
18
+ if (previousPersistedRouteFingerprint === null) {
19
+ return stateBefore.phase === "CONTRACT_READY"
20
+ && marker.details.reconstructedPhase === "CONTRACT_READY"
21
+ && marker.details.routeFingerprint === null;
22
+ }
23
+ return persistedRoute.fingerprint !== previousPersistedRouteFingerprint
24
+ && persistedRoute.value.contractFingerprint === marker.details.contractFingerprint;
25
+ }
26
+
27
+ function canAppendCanonicalRouteWitness({ stateBefore, previousPersistedRouteFingerprint, persistedRoute }) {
28
+ return stateBefore?.phase === "ROUTED"
29
+ && previousPersistedRouteFingerprint !== null
30
+ && persistedRoute.fingerprint !== previousPersistedRouteFingerprint
31
+ && stateBefore.contractFingerprint === persistedRoute.value.contractFingerprint;
32
+ }
33
+
34
+ async function appendRouteWitness({ target, packageRoot, taskId, stateBefore, previousPersistedRouteFingerprint, persistedRoute }) {
35
+ if (!stateBefore) return;
36
+ const events = await readEvents(target, packageRoot, { taskId });
37
+ const anchor = resolveEffectiveContractBootstrapRepairAnchor(events);
38
+ const canAppend = anchor
39
+ ? canAppendRepairedRouteWitness({
40
+ stateBefore,
41
+ marker: anchor.sourceMarker,
42
+ previousPersistedRouteFingerprint,
43
+ persistedRoute,
44
+ })
45
+ : canAppendCanonicalRouteWitness({ stateBefore, previousPersistedRouteFingerprint, persistedRoute });
46
+ if (!canAppend) return;
47
+ await appendProtocolEvent(target, {
48
+ taskId,
49
+ event: "ROUTE_REBOUND",
50
+ details: {
51
+ routeFingerprint: persistedRoute.fingerprint,
52
+ contractFingerprint: persistedRoute.value.contractFingerprint,
53
+ previousRouteFingerprint: previousPersistedRouteFingerprint,
54
+ },
55
+ }, packageRoot, { taskId });
56
+ }
57
+
58
+ async function persistRoutedState({ target, packageRoot, taskId, route, transaction }) {
59
+ const stateBefore = await readWorkState(target, { packageRoot, taskId });
60
+ let previousPersistedRouteFingerprint = null;
61
+ try {
62
+ previousPersistedRouteFingerprint = (await readPersistedRoute(target, packageRoot, { taskId })).fingerprint;
63
+ } catch (error) {
64
+ if (error.code !== "ARTIFACT_MISSING") throw error;
65
+ }
66
+ let contractFingerprint;
67
+ try {
68
+ contractFingerprint = (await readContract(target, packageRoot, { taskId })).fingerprint;
69
+ } catch {
70
+ contractFingerprint = undefined;
71
+ }
72
+ const persistedRoute = await persistRoute(target, route, packageRoot, { contractFingerprint, taskId });
73
+ const state = await readWorkState(target, { packageRoot, taskId });
74
+ if (state?.phase === "CONTRACT_READY") {
75
+ await mutateWorkState(target, {
76
+ packageRoot,
77
+ taskId,
78
+ expectedRevision: state.revision ?? 0,
79
+ }, () => ({
80
+ ...state,
81
+ routeFingerprint: persistedRoute.fingerprint,
82
+ selectedGuides: [...persistedRoute.value.guides],
83
+ }));
84
+ await advanceWorkState(target, "ROUTED", { packageRoot, taskId });
85
+ }
86
+ if (transaction?.operation === "route") {
87
+ await appendRouteWitness({
88
+ target,
89
+ packageRoot,
90
+ taskId,
91
+ stateBefore,
92
+ previousPersistedRouteFingerprint,
93
+ persistedRoute,
94
+ });
95
+ }
96
+ }
97
+
98
+ export async function runRoute({ target, packageRoot, workType, surfaces, risks, platforms, behaviorChange, executableChange, executionProfile = null, taskId, task, semanticProvider = null, provider = null }) {
99
+ return withResolvedTask(target, { taskId: taskId ?? task, packageRoot }, async (context) => {
100
+ const effectiveTaskId = context?.taskId ?? null;
101
+ const effectiveSemanticProvider = semanticProvider ?? provider;
11
102
  let contract = null;
12
103
  try {
13
104
  contract = (await readContract(target, packageRoot, { taskId: effectiveTaskId })).value;
@@ -21,9 +112,9 @@ export async function runRoute({ target, packageRoot, workType, surfaces, risks,
21
112
  if (error.code !== "ARTIFACT_MISSING") throw error;
22
113
  }
23
114
  const projectEvidence = target
24
- ? await detectProjectEvidence(target, { claims: ctx?.descriptor?.writeClaims ?? [] })
115
+ ? await detectProjectEvidence(target, { claims: context?.descriptor?.writeClaims ?? [] })
25
116
  : null;
26
- const route = evaluateRoute({
117
+ const routeInput = {
27
118
  workType,
28
119
  surfaces,
29
120
  risks,
@@ -31,22 +122,59 @@ export async function runRoute({ target, packageRoot, workType, surfaces, risks,
31
122
  behaviorChange,
32
123
  executableChange,
33
124
  ...(projectEvidence ? { projectEvidence } : {}),
34
- }, {
125
+ };
126
+ const deterministicRoute = evaluateRoute(routeInput, {
35
127
  contract,
36
- taskDescriptor: ctx?.descriptor ?? null,
128
+ taskDescriptor: context?.descriptor ?? null,
37
129
  configuredProfile,
38
130
  requestedProfile: executionProfile,
39
131
  });
40
- if (target && packageRoot) {
41
- let contractFingerprint;
42
- try {
43
- contractFingerprint = (await readContract(target, packageRoot, { taskId: effectiveTaskId })).fingerprint;
44
- } catch {
45
- contractFingerprint = undefined;
46
- }
47
- await persistRoute(target, route, packageRoot, { contractFingerprint, taskId: effectiveTaskId });
132
+ let semanticRecommendation = null;
133
+ if (target && packageRoot && effectiveTaskId) {
134
+ await ensureSemanticDecision({
135
+ target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
136
+ request: { decisionKind: "INTAKE", questionSetId: "intake-v1", state: { routeInput: deterministicRoute.input } },
137
+ });
138
+ const questionSet = buildRouteQuestionSet(deterministicRoute.guides);
139
+ const routeDecision = await ensureSemanticDecision({
140
+ target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
141
+ request: {
142
+ decisionKind: "ROUTE", questionSetId: questionSet.id, questionSet,
143
+ state: { routeInput: deterministicRoute.input, eligibleGuides: deterministicRoute.guides.map((guide) => ({ id: guide, reasons: deterministicRoute.reasons[guide] })) },
144
+ candidateIds: deterministicRoute.guides,
145
+ candidateSetFingerprint: canonicalFingerprint(deterministicRoute.guides),
146
+ },
147
+ });
148
+ const profileQuestionSet = buildExecutionProfileQuestionSet();
149
+ const profileDecision = await ensureSemanticDecision({
150
+ target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
151
+ request: {
152
+ decisionKind: "EXECUTION_PROFILE", questionSetId: profileQuestionSet.id, questionSet: profileQuestionSet,
153
+ state: {
154
+ routeInput: deterministicRoute.input,
155
+ deterministicFloor: deterministicRoute.executionProfile.floor,
156
+ requestedProfile: deterministicRoute.executionProfile.requested,
157
+ },
158
+ },
159
+ });
160
+ semanticRecommendation = {
161
+ guideRelevance: routeDecision.artifact.decision,
162
+ executionProfile: profileDecision.artifact.decision,
163
+ };
48
164
  }
49
- return route;
165
+ const route = evaluateRoute(routeInput, {
166
+ contract,
167
+ taskDescriptor: context?.descriptor ?? null,
168
+ configuredProfile,
169
+ requestedProfile: executionProfile,
170
+ semanticRecommendation,
171
+ });
172
+ return withTaskMutation(target, { taskId: effectiveTaskId, packageRoot }, "route", async (ctx) => {
173
+ if (target && packageRoot) {
174
+ await persistRoutedState({ target, packageRoot, taskId: effectiveTaskId, route, transaction: ctx?.transaction });
175
+ }
176
+ return route;
177
+ });
50
178
  });
51
179
  }
52
180
 
@@ -0,0 +1,17 @@
1
+ import { runSemanticPlan as projectPlan } from "../core/semantic-planning/service.js";
2
+ import { resolveRequiredSemanticDecision } from "../core/decision/resolver.js";
3
+ import { readCurrentDecisionBindings } from "../core/decision/task-bindings.js";
4
+
5
+ export async function runSemanticPlan({ target, packageRoot, taskId, decisionId, kind, input, semanticRecommendation } = {}) {
6
+ if (target && taskId) {
7
+ const decisionKind = { failure: "FAILURE_TRIAGE", diagnosis: "DIAGNOSIS_PRIORITY", review: "REVIEW_PLAN" }[kind];
8
+ const bindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
9
+ const decision = await resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionId, decisionKind, currentBindings: bindings });
10
+ semanticRecommendation = decision.decision;
11
+ }
12
+ return projectPlan({ kind, input, semanticRecommendation });
13
+ }
14
+
15
+ export function formatSemanticPlanResult(result) {
16
+ return `${JSON.stringify(result, null, 2)}\n`;
17
+ }
@@ -0,0 +1,224 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { resolveTaskContext } from "../core/task-context.js";
4
+ import { inspectTaskConflictState } from "../core/task-conflict-inspection.js";
5
+ import { appendProtocolEvent } from "../core/events.js";
6
+ import { withTaskTransaction } from "../core/transaction.js";
7
+ import { currentRepositoryFingerprint } from "../core/repository.js";
8
+ import { withProjectClaimsLock } from "../core/task-lock.js";
9
+ import { readTaskDescriptor } from "../core/task-descriptor.js";
10
+ import { createTaskRecovery, writeTaskRecovery } from "../core/task-recovery.js";
11
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
12
+ import {
13
+ E_TASK_ABANDON_AUTHORIZATION_REQUIRED,
14
+ E_TASK_ABANDON_INCONSISTENT,
15
+ E_TASK_ABANDON_INVALID_STATE,
16
+ E_TASK_ABANDON_UNSAFE,
17
+ E_TASK_ALREADY_ABANDONED,
18
+ } from "../core/error-codes.js";
19
+
20
+ const ABANDONMENT_REASON_CODES = Object.freeze(["CALLER_ABANDONED"]);
21
+
22
+ function abandonError(code, message, details = {}) {
23
+ const error = new Error(message);
24
+ error.code = code;
25
+ Object.assign(error, details);
26
+ return error;
27
+ }
28
+
29
+ function sameList(left, right) {
30
+ return Array.isArray(left)
31
+ && Array.isArray(right)
32
+ && left.length === right.length
33
+ && left.every((value, index) => value === right[index]);
34
+ }
35
+
36
+ function snapshot(inspection) {
37
+ return {
38
+ phase: inspection.evidence.phase,
39
+ revision: inspection.evidence.workStateRevision,
40
+ ledgerLastSeq: inspection.evidence.ledgerLastSeq,
41
+ claimState: inspection.evidence.claimState,
42
+ effectiveWriteClaims: [...inspection.evidence.effectiveWriteClaims],
43
+ };
44
+ }
45
+
46
+ function assertSnapshotUnchanged(before, after, taskId) {
47
+ const unchanged = before.phase === after.phase
48
+ && before.revision === after.revision
49
+ && before.ledgerLastSeq === after.ledgerLastSeq
50
+ && before.claimState === after.claimState
51
+ && sameList(before.effectiveWriteClaims, after.effectiveWriteClaims);
52
+ if (!unchanged) {
53
+ throw abandonError(
54
+ E_TASK_ABANDON_UNSAFE,
55
+ `Task ${taskId} changed during abandonment precondition validation`,
56
+ { before, after },
57
+ );
58
+ }
59
+ }
60
+
61
+ async function assertAbandonableOwnership(target, taskId, packageRoot) {
62
+ const projection = await resolveTaskClaimState(target, { taskId, packageRoot });
63
+ if (projection.phase === "COMPLETE" || projection.claimState === "RELEASED_BY_COMPLETION") {
64
+ throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is COMPLETE and cannot be abandoned`);
65
+ }
66
+ if (!projection.valid || !projection.ownershipValid) {
67
+ throw abandonError(
68
+ E_TASK_ABANDON_INCONSISTENT,
69
+ `Task ${taskId} claim ownership is inconsistent; abandonment is blocked`,
70
+ { reasonCodes: projection.reasonCodes, ownershipErrors: projection.ownershipErrors },
71
+ );
72
+ }
73
+ if (projection.claimState === "RELEASED_BY_RECOVERY") {
74
+ throw abandonError(
75
+ E_TASK_ALREADY_ABANDONED,
76
+ `Task ${taskId} is already abandoned/recovered`,
77
+ { recovery: projection.recovery },
78
+ );
79
+ }
80
+ if (projection.claimState !== "ACTIVE" || projection.mutationAllowed !== true) {
81
+ throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not an active mutable task`);
82
+ }
83
+ return projection;
84
+ }
85
+
86
+ function assertInspectionAbandonable(taskId, inspection) {
87
+ if (!inspection.evidence.healthy || !inspection.evidence.ledgerValid || !inspection.evidence.ownershipValid) {
88
+ throw abandonError(
89
+ E_TASK_ABANDON_INCONSISTENT,
90
+ `Task ${taskId} cannot be abandoned because canonical ownership or ledger validation failed`,
91
+ { reasonCodes: inspection.reasonCodes, ledgerErrors: inspection.evidence.ledgerErrors },
92
+ );
93
+ }
94
+ if (!inspection.evidence.phase || inspection.evidence.phase === "COMPLETE") {
95
+ throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not eligible for abandonment`);
96
+ }
97
+ if (inspection.evidence.claimState !== "ACTIVE" || inspection.evidence.mutationAllowed !== true) {
98
+ throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not an active mutable task`);
99
+ }
100
+ }
101
+
102
+ export async function runTaskAbandon({
103
+ target,
104
+ packageRoot,
105
+ taskId,
106
+ acknowledgeAbandonment = false,
107
+ } = {}) {
108
+ if (typeof taskId !== "string" || taskId.trim() === "") {
109
+ throw abandonError(E_TASK_ABANDON_INVALID_STATE, "task-abandon requires an explicit --task ID");
110
+ }
111
+ if (!acknowledgeAbandonment) {
112
+ throw abandonError(
113
+ E_TASK_ABANDON_AUTHORIZATION_REQUIRED,
114
+ "task-abandon requires explicit caller acknowledgement: re-run with --acknowledge-abandonment",
115
+ );
116
+ }
117
+
118
+ let context;
119
+ try {
120
+ context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
121
+ } catch (error) {
122
+ throw abandonError(
123
+ E_TASK_ABANDON_INCONSISTENT,
124
+ `Task ${taskId} cannot be abandoned because its canonical identity is unreadable`,
125
+ { causeCode: error.code, causeMessage: error.message },
126
+ );
127
+ }
128
+ const effectiveTaskId = context.taskId;
129
+ await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
130
+
131
+ return withProjectClaimsLock(target, "task-abandon", async () => {
132
+ await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
133
+ const inspectionBeforeLock = await inspectTaskConflictState(target, {
134
+ taskId: effectiveTaskId,
135
+ packageRoot,
136
+ });
137
+ assertInspectionAbandonable(effectiveTaskId, inspectionBeforeLock);
138
+ const before = snapshot(inspectionBeforeLock);
139
+
140
+ return withTaskTransaction({
141
+ target,
142
+ taskId: effectiveTaskId,
143
+ operation: "task-abandon",
144
+ packageRoot,
145
+ recordCommitEvent: true,
146
+ }, async (transaction) => {
147
+ const descriptor = await readTaskDescriptor(target, context.taskKey, packageRoot);
148
+ if (descriptor.value.taskId !== effectiveTaskId || descriptor.value.taskKey !== context.taskKey) {
149
+ throw abandonError(E_TASK_ABANDON_UNSAFE, `Task ${effectiveTaskId} identity changed during abandonment`);
150
+ }
151
+
152
+ await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
153
+ const inspection = await inspectTaskConflictState(target, {
154
+ taskId: effectiveTaskId,
155
+ packageRoot,
156
+ ignoredLockId: transaction.lock.lockId,
157
+ });
158
+ assertInspectionAbandonable(effectiveTaskId, inspection);
159
+ assertSnapshotUnchanged(before, snapshot(inspection), effectiveTaskId);
160
+
161
+ const repository = await currentRepositoryFingerprint(target);
162
+ const recoveryId = `recovery-${randomUUID()}`;
163
+ const abandonedAt = new Date().toISOString();
164
+ const releasedClaims = [...inspection.evidence.effectiveWriteClaims];
165
+ const details = {
166
+ recoveryId,
167
+ classification: "ABANDONED",
168
+ reasonCodes: [...ABANDONMENT_REASON_CODES],
169
+ previousPhase: inspection.evidence.phase,
170
+ previousRevision: inspection.evidence.workStateRevision,
171
+ previousHead: inspection.evidence.repositoryHead,
172
+ previousBranch: inspection.evidence.repositoryBranch,
173
+ currentHead: repository.head,
174
+ currentBranch: repository.branch,
175
+ releasedClaims,
176
+ authorityKind: "CALLER_ACKNOWLEDGED",
177
+ };
178
+
179
+ const abandonmentEvent = await appendProtocolEvent(target, {
180
+ taskId: effectiveTaskId,
181
+ event: "TASK_ABANDONED",
182
+ at: abandonedAt,
183
+ details,
184
+ }, packageRoot, { taskId: effectiveTaskId });
185
+
186
+ await writeTaskRecovery(target, createTaskRecovery({
187
+ taskId: effectiveTaskId,
188
+ recoveredAt: abandonedAt,
189
+ recoveryId,
190
+ recoveryEventSeq: abandonmentEvent.seq,
191
+ classificationAtRecovery: "ABANDONED",
192
+ reasonCodes: ABANDONMENT_REASON_CODES,
193
+ releasedClaims,
194
+ previousPhase: inspection.evidence.phase,
195
+ previousRevision: inspection.evidence.workStateRevision,
196
+ repositoryFingerprint: repository,
197
+ authority: { kind: "CALLER_ACKNOWLEDGED" },
198
+ }), packageRoot);
199
+
200
+ return {
201
+ taskId: effectiveTaskId,
202
+ taskKey: context.taskKey,
203
+ abandoned: true,
204
+ recovered: true,
205
+ recoveryId,
206
+ classification: "ABANDONED",
207
+ reasonCodes: [...ABANDONMENT_REASON_CODES],
208
+ phase: inspection.evidence.phase,
209
+ claimsReleased: true,
210
+ releasedClaims,
211
+ effectiveWriteClaims: [],
212
+ claimState: "RELEASED_BY_RECOVERY",
213
+ mutationAllowed: false,
214
+ authority: { kind: "CALLER_ACKNOWLEDGED" },
215
+ message: `Task ${effectiveTaskId} explicitly abandoned by caller acknowledgement; write claims released without completion claim`,
216
+ };
217
+ });
218
+ });
219
+ }
220
+
221
+ export function formatTaskAbandonResult(result) {
222
+ const claims = result.releasedClaims.length === 0 ? "none" : result.releasedClaims.join(", ");
223
+ return `${result.message}\nclassification: ${result.classification}\nphase: ${result.phase} (unchanged)\nreleased claims: ${claims}\n`;
224
+ }