@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,236 @@
|
|
|
1
|
+
import { createPresetContract } from "../core/contract-presets.js";
|
|
2
|
+
import {
|
|
3
|
+
contractFingerprint,
|
|
4
|
+
readContract,
|
|
5
|
+
validateContract,
|
|
6
|
+
writeContract,
|
|
7
|
+
} from "../core/contract.js";
|
|
8
|
+
import {
|
|
9
|
+
appendProtocolEvent,
|
|
10
|
+
validateEventLedger,
|
|
11
|
+
validateStateLedgerCoherence,
|
|
12
|
+
} from "../core/events.js";
|
|
13
|
+
import { canonicalFingerprint, readJsonArtifact } from "../core/artifacts.js";
|
|
14
|
+
import { resolveEffectiveContractBootstrapRepairAnchor } from "../core/contract-bootstrap-recovery.js";
|
|
15
|
+
import {
|
|
16
|
+
CONTRACT_REVISED_EVENT,
|
|
17
|
+
assertContractRevisedDetails,
|
|
18
|
+
} from "../core/contract-revision.js";
|
|
19
|
+
import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
|
|
20
|
+
import { assertTaskMutationAllowed, resolveTaskClaimState } from "../core/task-claim-state.js";
|
|
21
|
+
import { assertWorkspaceBinding } from "../core/workspace-binding.js";
|
|
22
|
+
import { withTaskTransaction } from "../core/transaction.js";
|
|
23
|
+
import { mutateWorkState, readWorkState } from "../core/work-state.js";
|
|
24
|
+
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
25
|
+
import { assertStateIdentity } from "../core/completion-relationships.js";
|
|
26
|
+
|
|
27
|
+
const REVISION_PHASES = new Set(["CONTRACT_READY", "ROUTED", "PLANNED"]);
|
|
28
|
+
|
|
29
|
+
function revisionError(message, artifacts = []) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.code = "E_CONTRACT_REVISION_UNSAFE";
|
|
32
|
+
if (artifacts.length > 0) error.artifacts = artifacts;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadCandidate({ target, packageRoot, taskId, contractFile, preset, claims }) {
|
|
37
|
+
if (preset) return createPresetContract({ taskId, preset, claims });
|
|
38
|
+
let artifact;
|
|
39
|
+
try {
|
|
40
|
+
artifact = await readJsonArtifact(target, contractFile, "current-contract", packageRoot);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw revisionError(`Unable to read replacement contract: ${error.message}`, error.artifacts ?? [contractFile]);
|
|
43
|
+
}
|
|
44
|
+
if (artifact.value.taskId !== taskId) {
|
|
45
|
+
throw revisionError(
|
|
46
|
+
`Replacement contract taskId "${artifact.value.taskId}" does not match requested taskId "${taskId}"`,
|
|
47
|
+
[contractFile],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return artifact.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function revisionStateProjection(state, contractFingerprintValue, phase) {
|
|
54
|
+
const next = {
|
|
55
|
+
...state,
|
|
56
|
+
contractFingerprint: contractFingerprintValue,
|
|
57
|
+
phase,
|
|
58
|
+
requiredGates: [],
|
|
59
|
+
satisfiedGates: [],
|
|
60
|
+
requiredArtifacts: [],
|
|
61
|
+
checks: [],
|
|
62
|
+
failures: [],
|
|
63
|
+
blockers: [],
|
|
64
|
+
verificationEvidence: [],
|
|
65
|
+
completedSteps: phase === "CONTRACT_READY" ? ["contract"] : ["contract", "route"],
|
|
66
|
+
pendingSteps: phase === "CONTRACT_READY"
|
|
67
|
+
? ["route", "planning", "implementation", "verification"]
|
|
68
|
+
: ["planning", "implementation", "verification"],
|
|
69
|
+
};
|
|
70
|
+
delete next.evidenceCoverage;
|
|
71
|
+
delete next.lastCompletionAttempt;
|
|
72
|
+
delete next.diagnosedHypothesis;
|
|
73
|
+
// The revision event carries the explicit PLANNED -> ROUTED rewind proof.
|
|
74
|
+
// Do not feed the old lifecycle transition marker back into createWorkState:
|
|
75
|
+
// the global transition table must remain forward-only.
|
|
76
|
+
delete next.previousPhase;
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function inspectRevisionEligibility({ target, packageRoot, taskId, context }) {
|
|
81
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
82
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
83
|
+
if (!state) throw revisionError("Contract revision requires a current work-state checkpoint");
|
|
84
|
+
if (state.taskId !== taskId) throw revisionError("Work-state task identity does not match the selected task");
|
|
85
|
+
if (!REVISION_PHASES.has(state.phase)) {
|
|
86
|
+
throw revisionError(`Contract revision is unavailable in phase ${state.phase}`);
|
|
87
|
+
}
|
|
88
|
+
if (state.contractFingerprint !== contract.fingerprint) {
|
|
89
|
+
throw revisionError("Current work-state is not bound to the current contract artifact");
|
|
90
|
+
}
|
|
91
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
92
|
+
if (!ledger.valid) throw revisionError("Contract revision requires a valid event ledger", ledger.errors);
|
|
93
|
+
if (ledger.events.some((event) => event.event === "EXECUTION_STARTED")) {
|
|
94
|
+
throw revisionError("Contract revision is unavailable after execution has started");
|
|
95
|
+
}
|
|
96
|
+
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
97
|
+
if (coherenceErrors.length > 0) {
|
|
98
|
+
throw revisionError("Contract revision cannot repair an inconsistent task", coherenceErrors);
|
|
99
|
+
}
|
|
100
|
+
const repairAnchor = resolveEffectiveContractBootstrapRepairAnchor(ledger.events);
|
|
101
|
+
if (repairAnchor) {
|
|
102
|
+
throw revisionError("Contract revision is unavailable for tasks with a contract-bootstrap repair or migration anchor");
|
|
103
|
+
}
|
|
104
|
+
const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
|
|
105
|
+
if (!ownership.ownershipValid || !ownership.mutationAllowed) {
|
|
106
|
+
throw revisionError("Contract revision cannot bypass invalid task ownership", ownership.ownershipErrors);
|
|
107
|
+
}
|
|
108
|
+
if (!context?.descriptor || context.descriptor.taskId !== taskId) {
|
|
109
|
+
throw revisionError("A valid task descriptor is required for contract revision");
|
|
110
|
+
}
|
|
111
|
+
return { state, contract, ledger, ownership };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function assertPreRevisionRouteIdentity({ target, packageRoot, taskId, state, contract }) {
|
|
115
|
+
if (state.phase === "CONTRACT_READY") return;
|
|
116
|
+
try {
|
|
117
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
118
|
+
assertStateIdentity({ contract, route, state });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw revisionError(
|
|
121
|
+
`Contract revision requires a valid current route: ${error.message}`,
|
|
122
|
+
error.artifacts ?? [],
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function reviseLocked({ target, packageRoot, taskId, context, contractFile, preset, transaction }) {
|
|
128
|
+
await assertTaskMutationAllowed(target, { taskId, packageRoot });
|
|
129
|
+
await assertWorkspaceBinding(target, { taskId, packageRoot, operation: "contract-revise" });
|
|
130
|
+
const inspected = await inspectRevisionEligibility({ target, packageRoot, taskId, context });
|
|
131
|
+
const candidate = await loadCandidate({
|
|
132
|
+
target,
|
|
133
|
+
packageRoot,
|
|
134
|
+
taskId,
|
|
135
|
+
contractFile,
|
|
136
|
+
preset,
|
|
137
|
+
claims: context.descriptor.writeClaims,
|
|
138
|
+
});
|
|
139
|
+
await validateContract(candidate, packageRoot);
|
|
140
|
+
const nextFingerprint = contractFingerprint(candidate);
|
|
141
|
+
if (nextFingerprint === inspected.contract.fingerprint) {
|
|
142
|
+
return {
|
|
143
|
+
taskId,
|
|
144
|
+
revised: false,
|
|
145
|
+
alreadyCurrent: true,
|
|
146
|
+
phase: inspected.state.phase,
|
|
147
|
+
contractFingerprint: inspected.contract.fingerprint,
|
|
148
|
+
revision: inspected.state.revision,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await assertPreRevisionRouteIdentity({
|
|
153
|
+
target,
|
|
154
|
+
packageRoot,
|
|
155
|
+
taskId,
|
|
156
|
+
state: inspected.state,
|
|
157
|
+
contract: inspected.contract,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const before = inspected.state;
|
|
161
|
+
const nextPhase = before.phase === "PLANNED" ? "ROUTED" : before.phase;
|
|
162
|
+
const previousRouteFingerprint = before.routeFingerprint ?? null;
|
|
163
|
+
const projected = revisionStateProjection(before, nextFingerprint, nextPhase);
|
|
164
|
+
await writeContract(target, candidate, packageRoot, { taskId, operation: "contract-revise" });
|
|
165
|
+
const next = await mutateWorkState(target, {
|
|
166
|
+
expectedRevision: before.revision,
|
|
167
|
+
packageRoot,
|
|
168
|
+
taskId,
|
|
169
|
+
}, () => projected);
|
|
170
|
+
const details = {
|
|
171
|
+
previousContractFingerprint: inspected.contract.fingerprint,
|
|
172
|
+
contractFingerprint: nextFingerprint,
|
|
173
|
+
previousPhase: before.phase,
|
|
174
|
+
phase: next.phase,
|
|
175
|
+
previousStateRevision: before.revision,
|
|
176
|
+
revisedStateRevision: next.revision,
|
|
177
|
+
previousStateFingerprint: canonicalFingerprint(before),
|
|
178
|
+
revisedStateFingerprint: canonicalFingerprint(next),
|
|
179
|
+
previousRouteFingerprint,
|
|
180
|
+
};
|
|
181
|
+
assertContractRevisedDetails(details);
|
|
182
|
+
const event = await appendProtocolEvent(target, {
|
|
183
|
+
taskId,
|
|
184
|
+
event: CONTRACT_REVISED_EVENT,
|
|
185
|
+
details,
|
|
186
|
+
}, packageRoot, { taskId });
|
|
187
|
+
const commit = await appendProtocolEvent(target, {
|
|
188
|
+
taskId,
|
|
189
|
+
event: "TRANSACTION_COMMITTED",
|
|
190
|
+
details: { transactionId: transaction.transactionId, operation: "contract-revise" },
|
|
191
|
+
}, packageRoot, { taskId });
|
|
192
|
+
return {
|
|
193
|
+
taskId,
|
|
194
|
+
revised: true,
|
|
195
|
+
alreadyCurrent: false,
|
|
196
|
+
phase: next.phase,
|
|
197
|
+
previousContractFingerprint: inspected.contract.fingerprint,
|
|
198
|
+
contractFingerprint: nextFingerprint,
|
|
199
|
+
previousPhase: before.phase,
|
|
200
|
+
revision: next.revision,
|
|
201
|
+
eventSeq: event.seq,
|
|
202
|
+
transactionCommitSeq: commit.seq,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function runContractRevise({ target, packageRoot, taskId, contractFile = null, preset = null } = {}) {
|
|
207
|
+
if (!taskId) throw revisionError("contract-revise requires --task");
|
|
208
|
+
if ((!contractFile && !preset) || (contractFile && preset)) {
|
|
209
|
+
throw revisionError("contract-revise requires exactly one of --contract-file or --preset");
|
|
210
|
+
}
|
|
211
|
+
const context = await resolveTaskContext(target, {
|
|
212
|
+
taskId,
|
|
213
|
+
packageRoot,
|
|
214
|
+
explicitRequired: true,
|
|
215
|
+
selectionMode: TASK_SELECTION_MODES.MUTATION,
|
|
216
|
+
});
|
|
217
|
+
return withTaskTransaction({
|
|
218
|
+
target,
|
|
219
|
+
taskId: context.taskId,
|
|
220
|
+
packageRoot,
|
|
221
|
+
operation: "contract-revise",
|
|
222
|
+
recordCommitEvent: false,
|
|
223
|
+
}, async (transaction) => reviseLocked({
|
|
224
|
+
target,
|
|
225
|
+
packageRoot,
|
|
226
|
+
taskId: context.taskId,
|
|
227
|
+
context,
|
|
228
|
+
contractFile,
|
|
229
|
+
preset,
|
|
230
|
+
transaction,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatContractReviseResult(result) {
|
|
235
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
236
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readDecisionArtifact } from "../core/decision/artifact.js";
|
|
2
|
+
|
|
3
|
+
export async function runDecisionShow({ target, packageRoot, taskId, decisionId } = {}) {
|
|
4
|
+
if (typeof taskId !== "string" || !taskId || typeof decisionId !== "string" || !decisionId) {
|
|
5
|
+
const error = new Error("decision-show requires --task and --decision");
|
|
6
|
+
error.code = "E_DECISION_REQUEST_INVALID";
|
|
7
|
+
throw error;
|
|
8
|
+
}
|
|
9
|
+
return (await readDecisionArtifact(target, taskId, decisionId, packageRoot)).value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatDecisionShowResult(result) {
|
|
13
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readConfig } from "../core/config.js";
|
|
2
|
+
import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "../core/decision/constants.js";
|
|
3
|
+
import { normalizeDecisionPolicy } from "../core/decision/policy.js";
|
|
4
|
+
import { createTypesafeEngine } from "../adapters/typesafe/engine.js";
|
|
5
|
+
import { hasTypesafeCredentials } from "../adapters/typesafe/client.js";
|
|
6
|
+
|
|
7
|
+
export async function runDecisionStatus({ target, packageRoot, health = false } = {}) {
|
|
8
|
+
let policy = DECISION_DEFAULT_POLICY;
|
|
9
|
+
try {
|
|
10
|
+
const config = await readConfig(target, packageRoot);
|
|
11
|
+
policy = normalizeDecisionPolicy(config.decisionEngine ?? DECISION_DEFAULT_POLICY);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
14
|
+
}
|
|
15
|
+
const result = { required: true, engine: DECISION_ENGINE_ID, model: PINNED_JEV_MODEL, credentials: hasTypesafeCredentials() ? "configured" : "missing", status: hasTypesafeCredentials() ? "configured" : "missing" };
|
|
16
|
+
if (health) result.health = await createTypesafeEngine({ policy }).health();
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatDecisionStatusResult(result) {
|
|
21
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { appendProtocolEvent, readEvents, validateEventLedger } from "../core/events.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
function initialHistory(events, taskId) {
|
|
5
|
+
if (events.length < 2) return false;
|
|
6
|
+
return events[0]?.taskId === taskId
|
|
7
|
+
&& events[0]?.event === "TASK_RECEIVED"
|
|
8
|
+
&& events[1]?.taskId === taskId
|
|
9
|
+
&& events[1]?.event === "TRANSACTION_COMMITTED"
|
|
10
|
+
&& events[1]?.details?.operation === "task-create";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runDiscover({ target, packageRoot, taskId, task } = {}) {
|
|
14
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "discover", async (ctx) => {
|
|
15
|
+
const events = await readEvents(target, packageRoot, { taskId: ctx.taskId });
|
|
16
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
|
|
17
|
+
if (!ledger.valid) {
|
|
18
|
+
const error = new Error("Initial discovery requires a valid task event ledger");
|
|
19
|
+
error.code = "E_LEDGER_INVALID";
|
|
20
|
+
error.artifacts = ledger.errors.flatMap((entry) => entry.artifacts ?? []);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!initialHistory(events, ctx.taskId)) {
|
|
24
|
+
const error = new Error("Initial discovery requires the canonical task-create history");
|
|
25
|
+
error.code = "E_INITIAL_LIFECYCLE_INVALID";
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const existing = events.find((event) => event.event === "DISCOVERY_STARTED");
|
|
29
|
+
if (existing) return { taskId: ctx.taskId, phase: "DISCOVERING", idempotent: true, eventSeq: existing.seq };
|
|
30
|
+
const event = await appendProtocolEvent(target, {
|
|
31
|
+
taskId: ctx.taskId,
|
|
32
|
+
event: "DISCOVERY_STARTED",
|
|
33
|
+
details: { source: "canonical-discover" },
|
|
34
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
35
|
+
return { taskId: ctx.taskId, phase: "DISCOVERING", idempotent: false, eventSeq: event.seq };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatDiscoverResult(result) {
|
|
40
|
+
return `phase: ${result.phase}\n${result.idempotent ? "idempotent: true\n" : ""}`;
|
|
41
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -9,6 +9,19 @@ import { isRepositoryCandidate } from "../repository-index/lifecycle.js";
|
|
|
9
9
|
import { getRepositoryIndexStatus, sanitizeRepositoryIndexStatus } from "../repository-index/status.js";
|
|
10
10
|
import { searchRepository } from "../repository-index/search.js";
|
|
11
11
|
import { getPersistentTransportStatus } from "../persistent-transport/client.js";
|
|
12
|
+
import { hasTypesafeCredentials } from "../adapters/typesafe/client.js";
|
|
13
|
+
import { PINNED_JEV_MODEL } from "../core/decision/constants.js";
|
|
14
|
+
|
|
15
|
+
function semanticDecisionPlaneFinding() {
|
|
16
|
+
const credentialState = hasTypesafeCredentials() ? "credentials configured" : "credentials missing";
|
|
17
|
+
return finding(
|
|
18
|
+
"semantic-decision-plane",
|
|
19
|
+
"info",
|
|
20
|
+
".forgeloop/config.json",
|
|
21
|
+
`Semantic decision plane: ${credentialState}; pinned model ${PINNED_JEV_MODEL}.`,
|
|
22
|
+
"Set TYPESAFE_API_KEY in the host environment before invoking a new semantic decision; recovery and inspection remain local.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
function finding(code, severity, relativePath, message, remediation = null, evidence = null) {
|
|
14
27
|
const evidenceRecord = evidence && typeof evidence === "object"
|
|
@@ -392,6 +405,8 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
392
405
|
findings,
|
|
393
406
|
});
|
|
394
407
|
|
|
408
|
+
findings.push(semanticDecisionPlaneFinding());
|
|
409
|
+
|
|
395
410
|
const ok = findings.every((item) => item.severity !== "error")
|
|
396
411
|
&& (!strict || findings.every((item) => item.severity !== "warning"));
|
|
397
412
|
return {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { lstat, readFile as readBytesFromFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { sha256 } from "../core/manifest.js";
|
|
5
|
+
import { ensureWithin, isPathWithin, realpathWithTransientWindowsRetry } from "../core/filesystem.js";
|
|
6
|
+
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
7
|
+
import { readContract } from "../core/contract.js";
|
|
8
|
+
import { readGuideMetadata, requiredGatesForGuides } from "../core/guide-metadata.js";
|
|
9
|
+
import { optionalConfig } from "../core/preflight-loaders.js";
|
|
10
|
+
import { persistGate } from "../core/gate-artifact.js";
|
|
11
|
+
import { readWorkState } from "../core/work-state.js";
|
|
12
|
+
import { taskArtifactPath, taskGatePath } from "../core/task-paths.js";
|
|
13
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
14
|
+
import { PROTOCOL_VERSION } from "../core/protocol.js";
|
|
15
|
+
import { staleReasons } from "../core/next-action-artifacts.js";
|
|
16
|
+
import { ARTIFACT_PATHS } from "../core/artifacts.js";
|
|
17
|
+
import { appendProtocolEvent, validateEventLedger } from "../core/events.js";
|
|
18
|
+
|
|
19
|
+
const STATUSES = new Set(["satisfied", "unverified", "blocked"]);
|
|
20
|
+
const PRE_EXECUTION_PHASES = new Set(["ROUTED", "DESIGNING", "PLANNED"]);
|
|
21
|
+
const MAX_TEXT = 2000;
|
|
22
|
+
const MAX_REPEATABLE_ITEMS = 32;
|
|
23
|
+
const MAX_GATE_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
24
|
+
const MAX_GATE_EVIDENCE_BYTES = 64 * 1024;
|
|
25
|
+
const CONFIG_REQUIRED_GATES_MARKER = "config.requiredGates";
|
|
26
|
+
|
|
27
|
+
const TASK_ARTIFACT_KEYS_BY_PATH = Object.freeze({
|
|
28
|
+
[ARTIFACT_PATHS.state]: "state",
|
|
29
|
+
[ARTIFACT_PATHS.contract]: "contract",
|
|
30
|
+
[ARTIFACT_PATHS.route]: "route",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function gateError(message, options = {}) {
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
error.code = options.code ?? "E_GATE_INVALID";
|
|
36
|
+
if (options.cause !== undefined) error.cause = options.cause;
|
|
37
|
+
if (options.artifacts !== undefined) error.artifacts = options.artifacts;
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function taskScopedFreshnessPath(taskId, artifactPath) {
|
|
42
|
+
const key = TASK_ARTIFACT_KEYS_BY_PATH[artifactPath];
|
|
43
|
+
return key ? taskArtifactPath(taskId, key) : artifactPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function bounded(values, name, options = {}) {
|
|
47
|
+
const maximum = options.maximum ?? MAX_REPEATABLE_ITEMS;
|
|
48
|
+
if (!Array.isArray(values)) {
|
|
49
|
+
throw gateError(`${name} must be an array`);
|
|
50
|
+
}
|
|
51
|
+
if (values.length > maximum) {
|
|
52
|
+
throw gateError(`${name} entries must not exceed ${maximum}`);
|
|
53
|
+
}
|
|
54
|
+
if (values.some((value) => typeof value !== "string" || value.trim().length === 0 || value.length > MAX_TEXT)) {
|
|
55
|
+
throw gateError(`${name} entries must be non-empty strings of at most ${MAX_TEXT} characters`);
|
|
56
|
+
}
|
|
57
|
+
return values.map((value) => value.trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function hashArtifact(target, artifactPath) {
|
|
61
|
+
if (typeof artifactPath !== "string" || !artifactPath || path.isAbsolute(artifactPath)) {
|
|
62
|
+
throw gateError(`Gate artifact must be a project-relative path: ${artifactPath}`);
|
|
63
|
+
}
|
|
64
|
+
let candidate;
|
|
65
|
+
try {
|
|
66
|
+
candidate = ensureWithin(target, artifactPath);
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
throw gateError(`Gate artifact escapes the project: ${artifactPath}`, { cause });
|
|
69
|
+
}
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = await lstat(candidate);
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
throw gateError(`Gate artifact is unavailable: ${artifactPath}`, { cause });
|
|
75
|
+
}
|
|
76
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
77
|
+
throw gateError(`Gate artifact must be a regular file: ${artifactPath}`);
|
|
78
|
+
}
|
|
79
|
+
if (stat.size > MAX_GATE_ARTIFACT_BYTES) {
|
|
80
|
+
throw gateError(`Gate artifact exceeds the maximum size of ${MAX_GATE_ARTIFACT_BYTES} bytes: ${artifactPath}`);
|
|
81
|
+
}
|
|
82
|
+
const rootReal = await realpathWithTransientWindowsRetry(target);
|
|
83
|
+
const candidateReal = await realpathWithTransientWindowsRetry(candidate).catch(() => null);
|
|
84
|
+
if (!candidateReal || !isPathWithin(rootReal, candidateReal)) {
|
|
85
|
+
throw gateError(`Gate artifact escapes the project: ${artifactPath}`);
|
|
86
|
+
}
|
|
87
|
+
return { path: artifactPath, sha256: sha256(await readBytesFromFile(candidateReal)) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readEvidenceFile(target, evidencePath) {
|
|
91
|
+
if (typeof evidencePath !== "string" || !evidencePath || path.isAbsolute(evidencePath)) {
|
|
92
|
+
throw gateError(`Gate evidence must be a project-relative path: ${evidencePath}`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const candidate = ensureWithin(target, evidencePath);
|
|
96
|
+
const stat = await lstat(candidate);
|
|
97
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("not a regular file");
|
|
98
|
+
if (stat.size > MAX_GATE_EVIDENCE_BYTES) throw new Error(`file exceeds ${MAX_GATE_EVIDENCE_BYTES} bytes`);
|
|
99
|
+
const rootReal = await realpathWithTransientWindowsRetry(target);
|
|
100
|
+
const candidateReal = await realpathWithTransientWindowsRetry(candidate);
|
|
101
|
+
if (!isPathWithin(rootReal, candidateReal)) throw new Error("outside project");
|
|
102
|
+
const bytes = await readBytesFromFile(candidateReal);
|
|
103
|
+
if (bytes.length > MAX_GATE_EVIDENCE_BYTES) throw new Error(`file exceeds ${MAX_GATE_EVIDENCE_BYTES} bytes`);
|
|
104
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
105
|
+
return { path: evidencePath, value };
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
throw gateError(`Gate evidence file is invalid: ${evidencePath}`, { cause });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function assertCurrentRoute(target, packageRoot, taskId, state, route) {
|
|
112
|
+
let contract;
|
|
113
|
+
try {
|
|
114
|
+
contract = await readContract(target, packageRoot, { taskId });
|
|
115
|
+
} catch (cause) {
|
|
116
|
+
throw gateError(`Cannot verify the active route against the current contract: ${cause.message}`, {
|
|
117
|
+
code: "E_ROUTE_STALE",
|
|
118
|
+
cause,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const freshness = staleReasons(state, contract, route);
|
|
122
|
+
if (freshness.length === 0) return;
|
|
123
|
+
const reason = freshness.find((entry) => entry.code === "E_ROUTE_STALE") ?? freshness[0];
|
|
124
|
+
throw gateError(reason.message, {
|
|
125
|
+
code: reason.code,
|
|
126
|
+
artifacts: reason.artifacts.map((artifactPath) => taskScopedFreshnessPath(taskId, artifactPath)),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasCurrentEpochGateEvent(events, taskId, gate) {
|
|
131
|
+
const latestRevisionSeq = events.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
|
|
132
|
+
return events.some((event) => event.event === "GATE_SATISFIED"
|
|
133
|
+
&& event.taskId === taskId
|
|
134
|
+
&& event.details?.gate === gate
|
|
135
|
+
&& event.seq > latestRevisionSeq);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runGateRecord({ target, packageRoot, taskId, gate, status, artifacts = [], decisions = [], unknowns = [], assumptions = [], evidenceFile = null } = {}) {
|
|
139
|
+
if (!STATUSES.has(status)) {
|
|
140
|
+
throw gateError(`Invalid gate status: ${status}`);
|
|
141
|
+
}
|
|
142
|
+
return withTaskMutation(target, { taskId, packageRoot }, "gate-record", async (ctx) => {
|
|
143
|
+
const state = await readWorkState(target, { packageRoot, taskId: ctx.taskId });
|
|
144
|
+
if (!state || !PRE_EXECUTION_PHASES.has(state.phase)) {
|
|
145
|
+
throw gateError(`gate-record is only available before execution; found ${state?.phase ?? "no phase"}`, { code: "E_PHASE_FREEZE" });
|
|
146
|
+
}
|
|
147
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId: ctx.taskId });
|
|
148
|
+
await assertCurrentRoute(target, packageRoot, ctx.taskId, state, route);
|
|
149
|
+
const config = await optionalConfig(target, packageRoot, []);
|
|
150
|
+
const required = [...new Set([...(await requiredGatesForGuides(route.value.guides, packageRoot)), ...(config.requiredGates ?? [])])];
|
|
151
|
+
if (!required.includes(gate)) {
|
|
152
|
+
throw gateError(`Gate is not required by the active route: ${gate}`, { code: "E_GATE_NOT_REQUIRED" });
|
|
153
|
+
}
|
|
154
|
+
const artifactPaths = bounded(artifacts, "artifact");
|
|
155
|
+
const normalizedDecisions = bounded(decisions, "decision");
|
|
156
|
+
const normalizedUnknowns = bounded(unknowns, "unknown");
|
|
157
|
+
const approvedAssumptions = bounded(assumptions, "assumption");
|
|
158
|
+
const fileEvidence = evidenceFile ? await readEvidenceFile(target, evidenceFile) : null;
|
|
159
|
+
if (status === "satisfied" && (normalizedUnknowns.length > 0 || (normalizedDecisions.length === 0 && !fileEvidence))) {
|
|
160
|
+
throw gateError("A satisfied gate requires no unknowns and at least one decision");
|
|
161
|
+
}
|
|
162
|
+
const hashedArtifacts = [];
|
|
163
|
+
for (const artifactPath of artifactPaths) hashedArtifacts.push(await hashArtifact(target, artifactPath));
|
|
164
|
+
const guideMetadata = await readGuideMetadata(packageRoot);
|
|
165
|
+
const requiredBy = [
|
|
166
|
+
...route.value.guides.filter((guide) => (guideMetadata[guide]?.requiresGates ?? []).includes(gate)),
|
|
167
|
+
...((config.requiredGates ?? []).includes(gate) ? [CONFIG_REQUIRED_GATES_MARKER] : []),
|
|
168
|
+
];
|
|
169
|
+
const value = {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
172
|
+
taskId: ctx.taskId,
|
|
173
|
+
gate,
|
|
174
|
+
status,
|
|
175
|
+
requiredBy,
|
|
176
|
+
artifacts: hashedArtifacts,
|
|
177
|
+
decisions: normalizedDecisions,
|
|
178
|
+
unknowns: normalizedUnknowns,
|
|
179
|
+
approvedAssumptions,
|
|
180
|
+
evidence: [
|
|
181
|
+
...normalizedDecisions.map((decision) => ({ kind: "CALLER_RECORDED", authority: "CALLER", text: decision })),
|
|
182
|
+
...(fileEvidence ? [{ kind: "CALLER_RECORDED", authority: "CALLER", path: fileEvidence.path, details: fileEvidence.value }] : []),
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
const persisted = await persistGate(target, value, packageRoot, { taskId: ctx.taskId });
|
|
186
|
+
if (status === "satisfied") {
|
|
187
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
|
|
188
|
+
if (!ledger.valid) {
|
|
189
|
+
throw gateError("gate-record requires a valid event ledger", { code: "E_EVENT_INVALID", artifacts: ledger.errors });
|
|
190
|
+
}
|
|
191
|
+
if (!hasCurrentEpochGateEvent(ledger.events, ctx.taskId, gate)) {
|
|
192
|
+
await appendProtocolEvent(target, {
|
|
193
|
+
taskId: ctx.taskId,
|
|
194
|
+
event: "GATE_SATISFIED",
|
|
195
|
+
details: { gate },
|
|
196
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { taskId: ctx.taskId, gate, status, path: persisted.path ?? persisted.relativePath ?? taskGatePath(ctx.taskId, gate), artifacts: hashedArtifacts };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatGateRecordResult(result) {
|
|
204
|
+
return `gate: ${result.gate}\nstatus: ${result.status}\n`;
|
|
205
|
+
}
|