@cassiomc1/forgeloop 1.12.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -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 +81 -3
- 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 +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- 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 +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -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 +298 -3
- 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 +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -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/next.js +19 -7
- 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-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- 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/config/guides.json +44 -0
- 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/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- 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-presets.js +82 -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 +281 -3
- 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/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- 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/multi-language-project.js +117 -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/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- 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 +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- 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/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -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/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -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,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
|
+
}
|
|
@@ -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 {
|