@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.
- package/AGENT_COMPATIBILITY.md +8 -0
- package/DOCS_INDEX.md +35 -3
- package/ENG/nodejs-backend-development-eng.md +2 -2
- package/ENG/sec-code-eng.md +7 -7
- package/EXECUTION_STATE.md +12 -0
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +40 -25
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +19 -1
- package/completions/forgeloop.bash +37 -1
- package/completions/forgeloop.fish +123 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +366 -6
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +32 -11
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +35 -8
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +38 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +750 -5
- package/docs/protocol-requirements.json +24 -0
- package/package.json +30 -3
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +4 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +241 -0
- package/src/core/command-executors.js +110 -0
- package/src/core/command-input.js +115 -43
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/gate-provenance.js +124 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/phase.js +128 -10
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +128 -52
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +118 -61
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- 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
|
+
}
|