@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,176 @@
1
+ import { canonicalFingerprint } from "../core/artifacts.js";
2
+ import { readContract } from "../core/contract.js";
3
+ import {
4
+ appendProtocolEvent,
5
+ validateEventLedger,
6
+ validateStateLedgerCoherence,
7
+ } from "../core/events.js";
8
+ import { readPersistedRoute } from "../core/route-artifact.js";
9
+ import { currentRepositoryFingerprint } from "../core/repository.js";
10
+ import { resolveTaskContext } from "../core/task-context.js";
11
+ import { taskArtifactPath } from "../core/task-paths.js";
12
+ import { assertTaskMutationAllowed } from "../core/task-claim-state.js";
13
+ import { withTaskTransaction } from "../core/transaction.js";
14
+ import {
15
+ CHECKPOINT_REVALIDATED_EVENT,
16
+ assertCheckpointRevalidatedDetails,
17
+ assertRepositoryFingerprint,
18
+ } from "../core/checkpoint-revalidation.js";
19
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
20
+ import { sameCanonicalGuideList, resolveEffectiveContractBootstrapRepairAnchor } from "../core/contract-bootstrap-recovery.js";
21
+ import {
22
+ classifyLoadedWorkState,
23
+ createWorkState,
24
+ mutateWorkState,
25
+ readWorkState,
26
+ } from "../core/work-state.js";
27
+
28
+ function revalidationError(message, details = {}) {
29
+ const error = new Error(message);
30
+ error.code = "E_CHECKPOINT_REVALIDATION_UNSAFE";
31
+ Object.assign(error, details);
32
+ return error;
33
+ }
34
+
35
+ function sameRepositoryFingerprint(left, right) {
36
+ return left?.branch === right?.branch && left?.head === right?.head;
37
+ }
38
+
39
+ function assertRouteIdentity(state, contract, route) {
40
+ if (!route || route.value?.contractFingerprint !== contract.fingerprint
41
+ || state.routeFingerprint !== route.fingerprint
42
+ || !sameCanonicalGuideList(state.selectedGuides, route.value.guides)) {
43
+ throw revalidationError("ROUTED checkpoint route identity cannot be proven safe", {
44
+ artifacts: [taskArtifactPath(state.taskId, "state"), taskArtifactPath(state.taskId, "route")],
45
+ });
46
+ }
47
+ }
48
+
49
+ async function inspectEligibility(target, packageRoot, taskId) {
50
+ const state = await readWorkState(target, { packageRoot, taskId });
51
+ if (!state) throw revalidationError("A persisted work-state checkpoint is required");
52
+ if (state.phase !== "ROUTED") throw revalidationError(`Checkpoint revalidation supports phase ROUTED, found ${state.phase}`);
53
+
54
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
55
+ if (!ledger.valid) throw revalidationError("Checkpoint revalidation requires a structurally valid event ledger", { errors: ledger.errors });
56
+ if (ledger.events.some((event) => event.event === "EXECUTION_STARTED")) {
57
+ throw revalidationError("Checkpoint revalidation is unavailable after execution has started");
58
+ }
59
+ const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
60
+ if (coherenceErrors.length > 0) throw revalidationError("Checkpoint revalidation requires coherent state and ledger", { errors: coherenceErrors });
61
+ const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
62
+ if (!ownership.ownershipValid || !ownership.mutationAllowed) {
63
+ throw revalidationError("Checkpoint revalidation cannot bypass invalid task ownership", { errors: ownership.ownershipErrors });
64
+ }
65
+ const contract = await readContract(target, packageRoot, { taskId });
66
+ if (state.contractFingerprint !== contract.fingerprint) {
67
+ throw revalidationError("Checkpoint contract identity changed; use the contract revision path");
68
+ }
69
+ const route = await readPersistedRoute(target, packageRoot, { taskId });
70
+ assertRouteIdentity(state, contract, route);
71
+
72
+ const freshness = await classifyLoadedWorkState({
73
+ target,
74
+ state,
75
+ contractFile: taskArtifactPath(taskId, "contract"),
76
+ });
77
+ const repository = freshness.repository;
78
+ assertRepositoryFingerprint(repository, "current repository fingerprint", { requireHead: true });
79
+ const reasons = [...new Set(freshness.reasons)].sort();
80
+ if (reasons.length === 0) {
81
+ return { state, contract, route, ledger, repository, freshness, alreadyFresh: true };
82
+ }
83
+ if (reasons.length !== 1 || reasons[0] !== "REPOSITORY_CHANGED") {
84
+ throw revalidationError(`Checkpoint freshness is not repository-only: ${reasons.join(", ")}`);
85
+ }
86
+ const repairAnchor = resolveEffectiveContractBootstrapRepairAnchor(ledger.events);
87
+ if (repairAnchor && (repairAnchor.details.contractFingerprint !== contract.fingerprint
88
+ || repairAnchor.details.routeFingerprint !== route.fingerprint)) {
89
+ throw revalidationError("Repaired checkpoint identity does not match the current contract and route");
90
+ }
91
+ return { state, contract, route, ledger, repository, freshness, repairAnchor, alreadyFresh: false };
92
+ }
93
+
94
+ function resultFor(taskId, state, repository, event = null, transaction = null, alreadyFresh = false) {
95
+ return {
96
+ taskId,
97
+ revalidated: !alreadyFresh,
98
+ alreadyFresh,
99
+ phase: state.phase,
100
+ previousRevision: event ? event.details.previousStateRevision : state.revision,
101
+ revalidatedRevision: event ? event.details.revalidatedStateRevision : state.revision,
102
+ previousRepositoryFingerprint: event?.details.previousRepositoryFingerprint ?? repository,
103
+ repositoryFingerprint: event?.details.repositoryFingerprint ?? repository,
104
+ eventSeq: event?.seq ?? null,
105
+ transactionCommitSeq: transaction?.seq ?? null,
106
+ };
107
+ }
108
+
109
+ async function revalidateLocked(target, packageRoot, taskId, transaction) {
110
+ const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
111
+ if (!ownership.ownershipValid || !ownership.mutationAllowed) {
112
+ throw revalidationError("Checkpoint revalidation cannot bypass invalid task ownership", { errors: ownership.ownershipErrors });
113
+ }
114
+ await assertTaskMutationAllowed(target, { taskId, packageRoot });
115
+ const inspected = await inspectEligibility(target, packageRoot, taskId);
116
+ if (inspected.alreadyFresh) return resultFor(taskId, inspected.state, inspected.repository, null, null, true);
117
+
118
+ const before = inspected.state;
119
+ const previousStateFingerprint = canonicalFingerprint(before);
120
+ const previousRepositoryFingerprint = structuredClone(before.repositoryFingerprint);
121
+ const repository = await currentRepositoryFingerprint(target);
122
+ assertRepositoryFingerprint(repository, "current repository fingerprint", { requireHead: true });
123
+ if (!sameRepositoryFingerprint(repository, inspected.repository)) {
124
+ throw revalidationError("Repository identity changed during checkpoint revalidation");
125
+ }
126
+ const next = await mutateWorkState(target, {
127
+ expectedRevision: before.revision,
128
+ packageRoot,
129
+ taskId,
130
+ }, (current) => createWorkState({
131
+ ...current,
132
+ repositoryFingerprint: repository,
133
+ lastUpdated: new Date().toISOString(),
134
+ }));
135
+ const details = {
136
+ phase: next.phase,
137
+ previousRepositoryFingerprint,
138
+ repositoryFingerprint: structuredClone(repository),
139
+ contractFingerprint: next.contractFingerprint,
140
+ routeFingerprint: next.routeFingerprint,
141
+ previousStateRevision: before.revision,
142
+ revalidatedStateRevision: next.revision,
143
+ previousStateFingerprint,
144
+ revalidatedStateFingerprint: canonicalFingerprint(next),
145
+ };
146
+ assertCheckpointRevalidatedDetails(details);
147
+ const event = await appendProtocolEvent(target, {
148
+ taskId,
149
+ event: CHECKPOINT_REVALIDATED_EVENT,
150
+ details,
151
+ }, packageRoot, { taskId });
152
+ const commitEvent = await appendProtocolEvent(target, {
153
+ taskId,
154
+ event: "TRANSACTION_COMMITTED",
155
+ details: { transactionId: transaction.transactionId, operation: "checkpoint-revalidate" },
156
+ }, packageRoot, { taskId });
157
+ return resultFor(taskId, next, repository, event, commitEvent, false);
158
+ }
159
+
160
+ export async function runCheckpointRevalidate({ target, packageRoot, taskId } = {}) {
161
+ const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
162
+ const effectiveTaskId = context.taskId;
163
+ const initial = await inspectEligibility(target, packageRoot, effectiveTaskId);
164
+ if (initial.alreadyFresh) return resultFor(effectiveTaskId, initial.state, initial.repository, null, null, true);
165
+ return withTaskTransaction({
166
+ target,
167
+ taskId: effectiveTaskId,
168
+ operation: "checkpoint-revalidate",
169
+ packageRoot,
170
+ recordCommitEvent: false,
171
+ }, async (transaction) => revalidateLocked(target, packageRoot, effectiveTaskId, transaction));
172
+ }
173
+
174
+ export function formatCheckpointRevalidateResult(result) {
175
+ return `${JSON.stringify(result, null, 2)}\n`;
176
+ }
@@ -0,0 +1,38 @@
1
+ import { compileContext } from "../core/context-compiler/compiler.js";
2
+ import { normalizeContextCandidates } from "../core/context-compiler/candidates.js";
3
+ import { canonicalFingerprint } from "../core/artifacts.js";
4
+ import { buildContextQuestionSet } from "../core/decision/question-registry.js";
5
+ import { ensureSemanticDecision } from "../core/decision/service.js";
6
+
7
+ export async function runContextPlan({ target, packageRoot, taskId, decisionId, profile = "balanced", candidates = [], mandatoryIds = [], provider = null, readOnly = false } = {}) {
8
+ if (readOnly) return compileContext({ profile, candidates, mandatoryIds });
9
+ if (target && taskId) {
10
+ const normalized = normalizeContextCandidates(candidates);
11
+ const candidateSetFingerprint = canonicalFingerprint(normalized.map((candidate) => ({
12
+ id: candidate.id, sourceRef: candidate.sourceRef, summary: candidate.summary,
13
+ required: candidate.required, mandatory: candidate.mandatory,
14
+ })));
15
+ const questionSet = buildContextQuestionSet(normalized);
16
+ const decision = await ensureSemanticDecision({
17
+ target, packageRoot, taskId, decisionId, provider, allowNetwork: !readOnly,
18
+ request: {
19
+ decisionKind: "CONTEXT_PLAN", questionSetId: questionSet.id, questionSet,
20
+ state: { candidates: normalized.map(({ id, sourceRef, kind, summary, required, mandatory, risk, trust }) => ({ id, sourceRef, kind, summary, required, mandatory, risk, trust })) },
21
+ candidateIds: normalized.map((candidate) => candidate.id), candidateSetFingerprint,
22
+ },
23
+ });
24
+ return compileContext({ profile, candidates, mandatoryIds, semanticRanker: () => ({ rankedIds: decision.artifact.decision?.rankedIds ?? [], excludedIds: decision.artifact.decision?.excludedIds ?? [] }) });
25
+ }
26
+ return compileContext({ profile, candidates, mandatoryIds });
27
+ }
28
+
29
+ export function formatContextPlanResult(result) {
30
+ return [
31
+ `Context plan: ${result.selectedItems}/${result.candidateItems} items`,
32
+ `Profile: ${result.profile}`,
33
+ `Selected chars: ${result.selectedChars}/${result.budget.maxChars}`,
34
+ `Prompt-injection candidates: ${result.promptInjectionIds.length}`,
35
+ `Fingerprint: ${result.decisionFingerprint}`,
36
+ "",
37
+ ].join("\n");
38
+ }
@@ -0,0 +1,264 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { createPresetContract } from "../core/contract-presets.js";
4
+ import { contractFingerprint, readContract, validateContract, writeContract } from "../core/contract.js";
5
+ import { appendProtocolEvent, readEvents, validateEventLedger } from "../core/events.js";
6
+ import { isContractBootstrapRepairCandidate } from "../core/contract-bootstrap-recovery.js";
7
+ import { currentRepositoryFingerprint } from "../core/repository.js";
8
+ import { createWorkState, initializeWorkState, readWorkState } from "../core/work-state.js";
9
+ import { ensureWithin, fileExists } from "../core/filesystem.js";
10
+ import { withTaskMutation } from "../core/task-command.js";
11
+ import { readPersistedRoute } from "../core/route-artifact.js";
12
+ import {
13
+ buildResumableWorkStateFields,
14
+ deriveResumePhaseFromLedger,
15
+ } from "../core/resumability.js";
16
+ import { taskArtifactPath } from "../core/task-paths.js";
17
+ import { ensureSemanticDecision } from "../core/decision/service.js";
18
+
19
+ function bootstrapInconsistent(message, candidate = false) {
20
+ const error = new Error(message);
21
+ error.code = candidate ? "E_CONTRACT_BOOTSTRAP_REPAIR_AVAILABLE" : "E_CONTRACT_BOOTSTRAP_INCONSISTENT";
22
+ return error;
23
+ }
24
+
25
+ async function hasValidatedContractHistory(target, packageRoot, taskId) {
26
+ if (!target || !packageRoot || !taskId) return false;
27
+ const events = await readEvents(target, packageRoot, { taskId });
28
+ return events.some((event) => event.event === "CONTRACT_VALIDATED");
29
+ }
30
+
31
+ /**
32
+ * Resolves the reconstruction projection for a task with a validated ledger
33
+ * whose work-state checkpoint is missing.
34
+ *
35
+ * Ownership rule: the existing canonical contract plus the validated
36
+ * CONTRACT_VALIDATED event are authoritative. Caller-supplied --preset or
37
+ * --contract-file material is only used to prove fingerprint equivalence; the
38
+ * preset generator never redefines historical truth. Reconstruction is
39
+ * recovery, not contract creation.
40
+ *
41
+ * Fail-closed rule: the ledger proves which lifecycle milestones happened.
42
+ * A ROUTE_VALIDATED event with a missing, malformed, or differently-bound
43
+ * route artifact cannot be downgraded to CONTRACT_READY; that would regress
44
+ * the lifecycle and allow a later route command to append a duplicate
45
+ * ROUTE_VALIDATED milestone.
46
+ */
47
+ async function loadReconstructionEvidence(target, packageRoot, taskId) {
48
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
49
+ if (!ledger.valid) {
50
+ const candidate = isContractBootstrapRepairCandidate(ledger.events, ledger.errors, taskId);
51
+ const error = bootstrapInconsistent(
52
+ candidate
53
+ ? "The exact contract bootstrap defect requires task-repair-contract-bootstrap"
54
+ : "Existing contract has an inconsistent ledger; cannot overwrite",
55
+ !!candidate,
56
+ );
57
+ error.next = candidate
58
+ ? `forgeloop task-repair-contract-bootstrap --task ${taskId} --acknowledge-repair --json`
59
+ : undefined;
60
+ throw error;
61
+ }
62
+ const contract = await readContract(target, packageRoot, { taskId });
63
+ if (!ledger.events.some((event) => event.event === "CONTRACT_VALIDATED"
64
+ && event.details?.contractFingerprint === contract.fingerprint)) {
65
+ throw bootstrapInconsistent("Existing contract is not bound to a valid CONTRACT_VALIDATED event");
66
+ }
67
+
68
+ const routeValidated = ledger.events.some((event) => event.event === "ROUTE_VALIDATED");
69
+ const route = await readPersistedRoute(target, packageRoot, { taskId }).catch((error) => ({
70
+ __reconstructionError: error,
71
+ }));
72
+ if (routeValidated) {
73
+ if (route?.__reconstructionError || !route?.fingerprint) {
74
+ throw bootstrapInconsistent(
75
+ "Ledger contains ROUTE_VALIDATED but the route artifact is missing or malformed; reconstruction must not downgrade the lifecycle",
76
+ );
77
+ }
78
+ if (route.value?.contractFingerprint !== undefined
79
+ && route.value.contractFingerprint !== contract.fingerprint) {
80
+ throw bootstrapInconsistent(
81
+ "Persisted route artifact is bound to a different contract than the validated contract",
82
+ );
83
+ }
84
+ }
85
+ const scopedEvents = ledger.events.filter((event) => event.taskId === taskId);
86
+ const resumedPhase = routeValidated
87
+ ? (await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED")
88
+ : "CONTRACT_READY";
89
+ const projection = buildResumableWorkStateFields({ events: scopedEvents, resumedPhase });
90
+ return { ledger, contract, route: routeValidated ? route : null, projection };
91
+ }
92
+
93
+ async function reconstructFromExistingContract(target, packageRoot, taskId, requestedFingerprint = null) {
94
+ const { contract, route, projection } = await loadReconstructionEvidence(target, packageRoot, taskId);
95
+ if (requestedFingerprint !== null && requestedFingerprint !== contract.fingerprint) {
96
+ throw bootstrapInconsistent("A different contract already exists for this task; contract-create is not contract-revise");
97
+ }
98
+ const repositoryFingerprint = await currentRepositoryFingerprint(target);
99
+ const reconstructedState = createWorkState({
100
+ taskId,
101
+ contractFingerprint: contract.fingerprint,
102
+ ...(route ? { routeFingerprint: route.fingerprint } : {}),
103
+ repositoryFingerprint,
104
+ phase: projection.phase,
105
+ selectedGuides: route ? [...route.value.guides] : [],
106
+ requiredGates: [],
107
+ satisfiedGates: [],
108
+ completedSteps: projection.completedSteps,
109
+ pendingSteps: projection.pendingSteps,
110
+ ...(projection.verificationCycle !== undefined ? { verificationCycle: projection.verificationCycle } : {}),
111
+ requiredArtifacts: [],
112
+ checks: [],
113
+ failures: [],
114
+ blockers: [],
115
+ verificationEvidence: [],
116
+ });
117
+ await initializeWorkState(target, reconstructedState, { packageRoot, taskId });
118
+ return {
119
+ taskId,
120
+ phase: projection.phase,
121
+ idempotent: true,
122
+ contractFingerprint: contract.fingerprint,
123
+ reconstructed: true,
124
+ };
125
+ }
126
+
127
+ export async function runContractCreate({ target, packageRoot, taskId, task, contractFile = null, preset = null, semanticProvider = null } = {}) {
128
+ if (!preset && !contractFile) throw new Error("contract-create requires --preset or --contract-file");
129
+ if (preset && contractFile) throw new Error("contract-create accepts either --preset or --contract-file, not both");
130
+
131
+ const effectiveTaskId = taskId ?? task;
132
+ const hasCanonicalContract = await hasValidatedContractHistory(target, packageRoot, effectiveTaskId);
133
+ if (target && packageRoot && effectiveTaskId && !hasCanonicalContract) {
134
+ await ensureSemanticDecision({
135
+ target, packageRoot, taskId: effectiveTaskId, provider: semanticProvider,
136
+ request: {
137
+ decisionKind: "CONTRACT_APPLICABILITY", questionSetId: "contract-v1",
138
+ state: { source: preset ? "preset" : "contract-file", preset: preset ?? null },
139
+ },
140
+ });
141
+ }
142
+ return withTaskMutation(target, { taskId: effectiveTaskId, packageRoot }, "contract-create", async (ctx) => {
143
+ const events = await readEvents(target, packageRoot, { taskId: ctx.taskId });
144
+ if (!events.some((event) => event.event === "DISCOVERY_STARTED")) {
145
+ const error = new Error("Contract creation requires completed discovery");
146
+ error.code = "E_DISCOVERY_REQUIRED";
147
+ throw error;
148
+ }
149
+ const existingState = await readWorkState(target, { packageRoot, taskId: ctx.taskId });
150
+ if (existingState) {
151
+ if (existingState.phase !== "CONTRACT_READY") throw new Error("Contract already exists beyond the contract-ready phase");
152
+ const existingContract = await readContract(target, packageRoot, { taskId: ctx.taskId });
153
+ const existingLedger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
154
+ if (!existingLedger.valid) {
155
+ const candidate = isContractBootstrapRepairCandidate(existingLedger.events, existingLedger.errors, ctx.taskId);
156
+ const error = bootstrapInconsistent(
157
+ candidate
158
+ ? "The exact contract bootstrap defect requires task-repair-contract-bootstrap"
159
+ : "Existing contract checkpoint has an invalid event ledger",
160
+ !!candidate,
161
+ );
162
+ error.next = candidate ? `forgeloop task-repair-contract-bootstrap --task ${ctx.taskId} --acknowledge-repair --json` : undefined;
163
+ throw error;
164
+ }
165
+ if (existingState.contractFingerprint !== existingContract.fingerprint
166
+ || !existingLedger.events.some((event) => event.event === "CONTRACT_VALIDATED"
167
+ && event.details?.contractFingerprint === existingContract.fingerprint)) {
168
+ throw bootstrapInconsistent("Existing contract checkpoint is not bound to its validated contract event");
169
+ }
170
+ return { taskId: ctx.taskId, phase: existingState.phase, idempotent: true, contractFingerprint: existingContract.fingerprint };
171
+ }
172
+
173
+ // Reconstruction precedes preset generation: when the task already has a
174
+ // canonical contract, recover from it without generating a replacement
175
+ // candidate first. The requested preset/file contract is only compared by
176
+ // fingerprint to prove idempotent equivalence; it never redefines the
177
+ // canonical historical contract.
178
+ const existingContractPath = taskArtifactPath(ctx.taskId, "contract");
179
+ if (await fileExists(ensureWithin(target, existingContractPath)).catch(() => false)) {
180
+ const existingContract = await readContract(target, packageRoot, { taskId: ctx.taskId });
181
+ const existingLedger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
182
+ const validatedContractEvents = existingLedger.events.filter(
183
+ (event) => event.event === "CONTRACT_VALIDATED",
184
+ );
185
+
186
+ // task-create persists the proposed contract before contract-create has
187
+ // validated it. That artifact is not yet canonical history, so preserve
188
+ // ordinary first-time contract creation when no CONTRACT_VALIDATED event
189
+ // exists. Once validation history exists, reconstruction must be
190
+ // authoritative and fail closed on any binding mismatch.
191
+ if (validatedContractEvents.length > 0) {
192
+ if (!validatedContractEvents.some(
193
+ (event) => event.details?.contractFingerprint === existingContract.fingerprint,
194
+ )) {
195
+ throw bootstrapInconsistent("Existing contract is not bound to a valid CONTRACT_VALIDATED event");
196
+ }
197
+
198
+ let requestedFingerprint = null;
199
+ if (contractFile) {
200
+ const source = ensureWithin(target, contractFile);
201
+ if (await fileExists(source).catch(() => false)) {
202
+ try {
203
+ const candidate = { ...JSON.parse(await readFile(source, "utf8")), taskId: ctx.taskId };
204
+ await validateContract(candidate, packageRoot);
205
+ requestedFingerprint = contractFingerprint(candidate);
206
+ } catch {
207
+ requestedFingerprint = undefined;
208
+ }
209
+ }
210
+ }
211
+ // A preset selector is not historical contract authority. If the
212
+ // caller supplied a replacement file, compare it; otherwise recover
213
+ // the persisted contract independently of current preset generation.
214
+ return reconstructFromExistingContract(
215
+ target,
216
+ packageRoot,
217
+ ctx.taskId,
218
+ requestedFingerprint ?? null,
219
+ );
220
+ }
221
+ }
222
+
223
+ let contract;
224
+ if (preset) {
225
+ contract = createPresetContract({ taskId: ctx.taskId, preset, claims: ctx.descriptor.writeClaims });
226
+ } else {
227
+ const source = ensureWithin(target, contractFile);
228
+ if (!(await fileExists(source))) throw new Error(`Specified contract file not found: ${contractFile}`);
229
+ contract = JSON.parse(await readFile(source, "utf8"));
230
+ contract = { ...contract, taskId: ctx.taskId };
231
+ }
232
+ await validateContract(contract, packageRoot);
233
+ const fingerprint = contractFingerprint(contract);
234
+
235
+ await writeContract(target, contract, packageRoot, { taskId: ctx.taskId });
236
+ const repositoryFingerprint = await currentRepositoryFingerprint(target);
237
+ await initializeWorkState(target, createWorkState({
238
+ taskId: ctx.taskId,
239
+ contractFingerprint: fingerprint,
240
+ repositoryFingerprint,
241
+ phase: "CONTRACT_READY",
242
+ selectedGuides: [],
243
+ requiredGates: [],
244
+ satisfiedGates: [],
245
+ completedSteps: ["contract"],
246
+ pendingSteps: ["route", "planning", "implementation", "verification"],
247
+ requiredArtifacts: [],
248
+ checks: [],
249
+ failures: [],
250
+ blockers: [],
251
+ verificationEvidence: [],
252
+ }), { packageRoot, taskId: ctx.taskId });
253
+ await appendProtocolEvent(target, {
254
+ taskId: ctx.taskId,
255
+ event: "CONTRACT_VALIDATED",
256
+ details: { contractFingerprint: fingerprint },
257
+ }, packageRoot, { taskId: ctx.taskId });
258
+ return { taskId: ctx.taskId, phase: "CONTRACT_READY", idempotent: false, contractFingerprint: fingerprint };
259
+ });
260
+ }
261
+
262
+ export function formatContractCreateResult(result) {
263
+ return `phase: ${result.phase}\n${result.idempotent ? "idempotent: true\n" : ""}`;
264
+ }