@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,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
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { optionalConfig } from "../core/preflight-loaders.js";
|
|
4
|
+
import { readContract } from "../core/contract.js";
|
|
5
|
+
import { appendProtocolEvent, validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
|
|
6
|
+
import { ensureWithin, isPathWithin, readBytes, realpathWithTransientWindowsRetry } from "../core/filesystem.js";
|
|
7
|
+
import { sha256 } from "../core/manifest.js";
|
|
8
|
+
import { requiredGatesForGuides } from "../core/guide-metadata.js";
|
|
9
|
+
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
10
|
+
import { staleReasons } from "../core/next-action-artifacts.js";
|
|
11
|
+
import { resolveTaskClaimState } from "../core/task-claim-state.js";
|
|
12
|
+
import { taskGatePath, taskArtifactPath } from "../core/task-paths.js";
|
|
13
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
14
|
+
import { readWorkState } from "../core/work-state.js";
|
|
15
|
+
import { stateIdentityErrors } from "../core/completion-relationships.js";
|
|
16
|
+
import { persistGate, readGateIfPresent, validateGateArtifacts } from "../core/gate-artifact.js";
|
|
17
|
+
import {
|
|
18
|
+
GATE_REVALIDATED_EVENT,
|
|
19
|
+
assertGateRevalidatedDetails,
|
|
20
|
+
} from "../core/gate-provenance.js";
|
|
21
|
+
|
|
22
|
+
const POST_EXECUTION_PHASES = new Set([
|
|
23
|
+
"EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function revalidationError(message, code = "E_GATE_REVALIDATION_UNSAFE", artifacts = []) {
|
|
27
|
+
const error = new Error(message);
|
|
28
|
+
error.code = code;
|
|
29
|
+
error.artifacts = artifacts;
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function hashGateArtifact(target, relativePath) {
|
|
34
|
+
let artifactPath;
|
|
35
|
+
try {
|
|
36
|
+
artifactPath = ensureWithin(target, relativePath);
|
|
37
|
+
const stat = await lstat(artifactPath);
|
|
38
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("artifact must be a regular file");
|
|
39
|
+
const rootReal = await realpathWithTransientWindowsRetry(target);
|
|
40
|
+
const artifactReal = await realpathWithTransientWindowsRetry(artifactPath);
|
|
41
|
+
if (!isPathWithin(rootReal, artifactReal)) throw new Error("artifact resolves outside the project");
|
|
42
|
+
return { path: relativePath, sha256: sha256(await readBytes(artifactReal)) };
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
throw revalidationError(`Cannot refresh gate artifact ${relativePath}: ${cause.message}`, "E_GATE_REVALIDATION_UNSAFE", [relativePath]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function assertCurrentGateBinding(target, packageRoot, taskId, state, gate) {
|
|
49
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
50
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
51
|
+
const identityErrors = stateIdentityErrors({ contract, route, state });
|
|
52
|
+
if (identityErrors.length > 0) {
|
|
53
|
+
throw revalidationError(
|
|
54
|
+
"Gate revalidation requires coherent current contract, route, state, and selected guides",
|
|
55
|
+
"E_GATE_REVALIDATION_UNSAFE",
|
|
56
|
+
[taskArtifactPath(taskId, "contract"), taskArtifactPath(taskId, "route"), taskArtifactPath(taskId, "state")],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const routeStale = staleReasons(state, contract, route);
|
|
60
|
+
if (routeStale.length > 0) {
|
|
61
|
+
throw revalidationError("Gate revalidation cannot repair stale contract or route identity", "E_GATE_REVALIDATION_UNSAFE", routeStale.flatMap((reason) => reason.artifacts));
|
|
62
|
+
}
|
|
63
|
+
const config = await optionalConfig(target, packageRoot, []);
|
|
64
|
+
const required = [...new Set([
|
|
65
|
+
...(await requiredGatesForGuides(route.value.guides, packageRoot)),
|
|
66
|
+
...(config.requiredGates ?? []),
|
|
67
|
+
])];
|
|
68
|
+
if (!required.includes(gate)) {
|
|
69
|
+
throw revalidationError(`Gate is not required by the active route: ${gate}`, "E_GATE_NOT_REQUIRED", [taskArtifactPath(taskId, "route")]);
|
|
70
|
+
}
|
|
71
|
+
return { contract, route };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runGateRevalidate({ target, packageRoot, taskId, gate, acknowledgeStale = false } = {}) {
|
|
75
|
+
if (!acknowledgeStale) {
|
|
76
|
+
throw revalidationError("gate-revalidate requires --acknowledge-stale", "E_GATE_REVALIDATION_ACK_REQUIRED");
|
|
77
|
+
}
|
|
78
|
+
if (typeof gate !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(gate)) {
|
|
79
|
+
throw revalidationError(`Invalid gate name: ${gate}`, "E_GATE_INVALID");
|
|
80
|
+
}
|
|
81
|
+
return withTaskMutation(target, { taskId, packageRoot }, "gate-revalidate", async (ctx) => {
|
|
82
|
+
const state = await readWorkState(target, { packageRoot, taskId: ctx.taskId });
|
|
83
|
+
if (!state || !POST_EXECUTION_PHASES.has(state.phase)) {
|
|
84
|
+
throw revalidationError(
|
|
85
|
+
`gate-revalidate is only available for post-execution repair phases; found ${state?.phase ?? "no phase"}`,
|
|
86
|
+
"E_GATE_REVALIDATION_PHASE_INVALID",
|
|
87
|
+
[taskArtifactPath(ctx.taskId, "state")],
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
|
|
91
|
+
if (!ledger.valid) throw revalidationError("Gate revalidation requires a valid append-only event ledger", "E_EVENT_INVALID", ledger.errors);
|
|
92
|
+
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
93
|
+
if (coherenceErrors.length > 0) throw revalidationError("Gate revalidation requires coherent state and ledger", "E_PHASE_CHRONOLOGY_INVALID", coherenceErrors);
|
|
94
|
+
const ownership = await resolveTaskClaimState(target, { taskId: ctx.taskId, packageRoot });
|
|
95
|
+
if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
|
|
96
|
+
throw revalidationError("Gate revalidation cannot bypass active task claim ownership", "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT", ownership.ownershipErrors);
|
|
97
|
+
}
|
|
98
|
+
await assertCurrentGateBinding(target, packageRoot, ctx.taskId, state, gate);
|
|
99
|
+
const existing = await readGateIfPresent(target, gate, packageRoot, { taskId: ctx.taskId });
|
|
100
|
+
if (!existing || existing.value.status !== "satisfied") {
|
|
101
|
+
throw revalidationError("Only an existing satisfied gate can be revalidated", "E_GATE_UNVERIFIED", [taskGatePath(ctx.taskId, gate)]);
|
|
102
|
+
}
|
|
103
|
+
const stale = await validateGateArtifacts(target, existing.value, packageRoot);
|
|
104
|
+
if (stale.length === 0) {
|
|
105
|
+
throw revalidationError("Gate artifacts are already fresh; no revalidation event is required", "E_GATE_REVALIDATION_NOT_STALE", [existing.path]);
|
|
106
|
+
}
|
|
107
|
+
const refreshedArtifacts = [];
|
|
108
|
+
for (const artifact of existing.value.artifacts ?? []) refreshedArtifacts.push(await hashGateArtifact(target, artifact.path));
|
|
109
|
+
const refreshedGate = { ...existing.value, artifacts: refreshedArtifacts };
|
|
110
|
+
const persisted = await persistGate(target, refreshedGate, packageRoot, { taskId: ctx.taskId });
|
|
111
|
+
const details = {
|
|
112
|
+
gate,
|
|
113
|
+
previousGateFingerprint: existing.fingerprint,
|
|
114
|
+
gateFingerprint: persisted.fingerprint,
|
|
115
|
+
stalePaths: stale.map((entry) => entry.path).sort(),
|
|
116
|
+
};
|
|
117
|
+
assertGateRevalidatedDetails(details);
|
|
118
|
+
const event = await appendProtocolEvent(target, {
|
|
119
|
+
taskId: ctx.taskId,
|
|
120
|
+
event: GATE_REVALIDATED_EVENT,
|
|
121
|
+
details,
|
|
122
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
123
|
+
return {
|
|
124
|
+
taskId: ctx.taskId,
|
|
125
|
+
gate,
|
|
126
|
+
stalePaths: details.stalePaths,
|
|
127
|
+
gateFingerprint: persisted.fingerprint,
|
|
128
|
+
eventSeq: event.seq,
|
|
129
|
+
transactionCommitSeq: event.seq + 1,
|
|
130
|
+
path: persisted.path ?? persisted.relativePath ?? taskGatePath(ctx.taskId, gate),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatGateRevalidateResult(result) {
|
|
136
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { projectModelRoute } from "../core/model-router/router.js";
|
|
2
|
+
import { resolveRequiredSemanticDecision } from "../core/decision/resolver.js";
|
|
3
|
+
import { readCurrentDecisionBindings } from "../core/decision/task-bindings.js";
|
|
4
|
+
|
|
5
|
+
export async function runModelRoute({ target, packageRoot, taskId, decisionId, workType, surfaces, risks, platforms, behaviorChange, executableChange, generationRequired, architectureChange, ambiguity, semanticRecommendation, contract } = {}) {
|
|
6
|
+
if (target && taskId) {
|
|
7
|
+
const bindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
|
|
8
|
+
const decision = await resolveRequiredSemanticDecision({
|
|
9
|
+
target, packageRoot, taskId, decisionId, decisionKind: "MODEL_ROUTE", currentBindings: bindings,
|
|
10
|
+
});
|
|
11
|
+
semanticRecommendation = decision.decision;
|
|
12
|
+
}
|
|
13
|
+
return projectModelRoute({
|
|
14
|
+
input: {
|
|
15
|
+
workType,
|
|
16
|
+
surfaces,
|
|
17
|
+
risks,
|
|
18
|
+
platforms,
|
|
19
|
+
behaviorChange,
|
|
20
|
+
executableChange,
|
|
21
|
+
generationRequired,
|
|
22
|
+
architectureChange,
|
|
23
|
+
ambiguity,
|
|
24
|
+
contract,
|
|
25
|
+
},
|
|
26
|
+
semanticRecommendation,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatModelRouteResult(result) {
|
|
31
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
32
|
+
}
|
package/src/commands/next.js
CHANGED
|
@@ -2,8 +2,9 @@ import { getNextAction } from "../core/next-action.js";
|
|
|
2
2
|
import { withResolvedTask } from "../core/task-command.js";
|
|
3
3
|
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
4
4
|
import { projectExecutionProfile } from "../core/execution-profile.js";
|
|
5
|
+
import { explainNextAction } from "../core/next-explanation.js";
|
|
5
6
|
|
|
6
|
-
export async function runNext({ target, packageRoot, taskId, task, authorityContext, runtimeContext, compact = false }) {
|
|
7
|
+
export async function runNext({ target, packageRoot, taskId, task, authorityContext, runtimeContext, compact = false, explain = false }) {
|
|
7
8
|
return withResolvedTask(target, { taskId: taskId ?? task, packageRoot }, async (ctx) => {
|
|
8
9
|
const result = await getNextAction({
|
|
9
10
|
target,
|
|
@@ -12,7 +13,8 @@ export async function runNext({ target, packageRoot, taskId, task, authorityCont
|
|
|
12
13
|
authorityContext,
|
|
13
14
|
runtimeContext,
|
|
14
15
|
});
|
|
15
|
-
|
|
16
|
+
const explained = explain ? { ...result, explanation: explainNextAction(result) } : result;
|
|
17
|
+
if (!compact) return explained;
|
|
16
18
|
const compactTaskId = ctx?.taskId ?? (result.taskId && result.taskId !== "unknown" ? result.taskId : null);
|
|
17
19
|
let profile = null;
|
|
18
20
|
try {
|
|
@@ -23,12 +25,13 @@ export async function runNext({ target, packageRoot, taskId, task, authorityCont
|
|
|
23
25
|
}
|
|
24
26
|
return {
|
|
25
27
|
taskId: compactTaskId ?? result.taskId,
|
|
26
|
-
phase:
|
|
28
|
+
phase: explained.currentPhase,
|
|
27
29
|
profile,
|
|
28
|
-
nextAction:
|
|
29
|
-
command:
|
|
30
|
-
terminal:
|
|
31
|
-
errors:
|
|
30
|
+
nextAction: explained.nextAction,
|
|
31
|
+
command: explained.commandSpecs?.[0]?.argv ?? [],
|
|
32
|
+
terminal: explained.terminal,
|
|
33
|
+
errors: explained.reasonCodes ?? explained.reasons?.map((reason) => reason.code) ?? [],
|
|
34
|
+
...(explained.explanation ? { explanation: explained.explanation } : {}),
|
|
32
35
|
};
|
|
33
36
|
});
|
|
34
37
|
}
|
|
@@ -68,6 +71,15 @@ export function formatNextActionResult(result) {
|
|
|
68
71
|
lines.push(...result.missingArtifacts.map((artifact) => `- ${artifact}`));
|
|
69
72
|
}
|
|
70
73
|
if (result.terminal) lines.push("STATE: TERMINAL");
|
|
74
|
+
if (result.explanation) {
|
|
75
|
+
lines.push("EXPLANATION (BOUNDED, READ-ONLY):");
|
|
76
|
+
lines.push(`- ${result.explanation.summary}`);
|
|
77
|
+
for (const item of result.explanation.reasons) {
|
|
78
|
+
lines.push(`- ${item.code}: ${item.requiredChange}`);
|
|
79
|
+
if (item.safeArtifacts.length > 0) lines.push(` ARTIFACTS: ${item.safeArtifacts.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`- ACTION KIND: ${result.explanation.actionKind}`);
|
|
82
|
+
}
|
|
71
83
|
return `${lines.join("\n")}\n`;
|
|
72
84
|
}
|
|
73
85
|
|
package/src/commands/route.js
CHANGED
|
@@ -1,13 +1,104 @@
|
|
|
1
1
|
import { evaluateRoute } from "../core/router.js";
|
|
2
|
-
import { persistRoute } from "../core/route-artifact.js";
|
|
2
|
+
import { persistRoute, readPersistedRoute } from "../core/route-artifact.js";
|
|
3
3
|
import { readContract } from "../core/contract.js";
|
|
4
4
|
import { readConfig } from "../core/config.js";
|
|
5
|
+
import { appendProtocolEvent, readEvents } from "../core/events.js";
|
|
5
6
|
import { detectProjectEvidence } from "../core/project-detection.js";
|
|
6
|
-
import { withTaskMutation } from "../core/task-command.js";
|
|
7
|
+
import { withResolvedTask, withTaskMutation } from "../core/task-command.js";
|
|
8
|
+
import { advanceWorkState } from "../core/phase.js";
|
|
9
|
+
import { mutateWorkState, readWorkState } from "../core/work-state.js";
|
|
10
|
+
import { buildExecutionProfileQuestionSet, buildRouteQuestionSet } from "../core/decision/question-registry.js";
|
|
11
|
+
import { ensureSemanticDecision } from "../core/decision/service.js";
|
|
12
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveEffectiveContractBootstrapRepairAnchor,
|
|
15
|
+
} from "../core/contract-bootstrap-recovery.js";
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
function canAppendRepairedRouteWitness({ stateBefore, marker, previousPersistedRouteFingerprint, persistedRoute }) {
|
|
18
|
+
if (previousPersistedRouteFingerprint === null) {
|
|
19
|
+
return stateBefore.phase === "CONTRACT_READY"
|
|
20
|
+
&& marker.details.reconstructedPhase === "CONTRACT_READY"
|
|
21
|
+
&& marker.details.routeFingerprint === null;
|
|
22
|
+
}
|
|
23
|
+
return persistedRoute.fingerprint !== previousPersistedRouteFingerprint
|
|
24
|
+
&& persistedRoute.value.contractFingerprint === marker.details.contractFingerprint;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canAppendCanonicalRouteWitness({ stateBefore, previousPersistedRouteFingerprint, persistedRoute }) {
|
|
28
|
+
return stateBefore?.phase === "ROUTED"
|
|
29
|
+
&& previousPersistedRouteFingerprint !== null
|
|
30
|
+
&& persistedRoute.fingerprint !== previousPersistedRouteFingerprint
|
|
31
|
+
&& stateBefore.contractFingerprint === persistedRoute.value.contractFingerprint;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function appendRouteWitness({ target, packageRoot, taskId, stateBefore, previousPersistedRouteFingerprint, persistedRoute }) {
|
|
35
|
+
if (!stateBefore) return;
|
|
36
|
+
const events = await readEvents(target, packageRoot, { taskId });
|
|
37
|
+
const anchor = resolveEffectiveContractBootstrapRepairAnchor(events);
|
|
38
|
+
const canAppend = anchor
|
|
39
|
+
? canAppendRepairedRouteWitness({
|
|
40
|
+
stateBefore,
|
|
41
|
+
marker: anchor.sourceMarker,
|
|
42
|
+
previousPersistedRouteFingerprint,
|
|
43
|
+
persistedRoute,
|
|
44
|
+
})
|
|
45
|
+
: canAppendCanonicalRouteWitness({ stateBefore, previousPersistedRouteFingerprint, persistedRoute });
|
|
46
|
+
if (!canAppend) return;
|
|
47
|
+
await appendProtocolEvent(target, {
|
|
48
|
+
taskId,
|
|
49
|
+
event: "ROUTE_REBOUND",
|
|
50
|
+
details: {
|
|
51
|
+
routeFingerprint: persistedRoute.fingerprint,
|
|
52
|
+
contractFingerprint: persistedRoute.value.contractFingerprint,
|
|
53
|
+
previousRouteFingerprint: previousPersistedRouteFingerprint,
|
|
54
|
+
},
|
|
55
|
+
}, packageRoot, { taskId });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function persistRoutedState({ target, packageRoot, taskId, route, transaction }) {
|
|
59
|
+
const stateBefore = await readWorkState(target, { packageRoot, taskId });
|
|
60
|
+
let previousPersistedRouteFingerprint = null;
|
|
61
|
+
try {
|
|
62
|
+
previousPersistedRouteFingerprint = (await readPersistedRoute(target, packageRoot, { taskId })).fingerprint;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
65
|
+
}
|
|
66
|
+
let contractFingerprint;
|
|
67
|
+
try {
|
|
68
|
+
contractFingerprint = (await readContract(target, packageRoot, { taskId })).fingerprint;
|
|
69
|
+
} catch {
|
|
70
|
+
contractFingerprint = undefined;
|
|
71
|
+
}
|
|
72
|
+
const persistedRoute = await persistRoute(target, route, packageRoot, { contractFingerprint, taskId });
|
|
73
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
74
|
+
if (state?.phase === "CONTRACT_READY") {
|
|
75
|
+
await mutateWorkState(target, {
|
|
76
|
+
packageRoot,
|
|
77
|
+
taskId,
|
|
78
|
+
expectedRevision: state.revision ?? 0,
|
|
79
|
+
}, () => ({
|
|
80
|
+
...state,
|
|
81
|
+
routeFingerprint: persistedRoute.fingerprint,
|
|
82
|
+
selectedGuides: [...persistedRoute.value.guides],
|
|
83
|
+
}));
|
|
84
|
+
await advanceWorkState(target, "ROUTED", { packageRoot, taskId });
|
|
85
|
+
}
|
|
86
|
+
if (transaction?.operation === "route") {
|
|
87
|
+
await appendRouteWitness({
|
|
88
|
+
target,
|
|
89
|
+
packageRoot,
|
|
90
|
+
taskId,
|
|
91
|
+
stateBefore,
|
|
92
|
+
previousPersistedRouteFingerprint,
|
|
93
|
+
persistedRoute,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runRoute({ target, packageRoot, workType, surfaces, risks, platforms, behaviorChange, executableChange, executionProfile = null, taskId, task, semanticProvider = null, provider = null }) {
|
|
99
|
+
return withResolvedTask(target, { taskId: taskId ?? task, packageRoot }, async (context) => {
|
|
100
|
+
const effectiveTaskId = context?.taskId ?? null;
|
|
101
|
+
const effectiveSemanticProvider = semanticProvider ?? provider;
|
|
11
102
|
let contract = null;
|
|
12
103
|
try {
|
|
13
104
|
contract = (await readContract(target, packageRoot, { taskId: effectiveTaskId })).value;
|
|
@@ -21,9 +112,9 @@ export async function runRoute({ target, packageRoot, workType, surfaces, risks,
|
|
|
21
112
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
22
113
|
}
|
|
23
114
|
const projectEvidence = target
|
|
24
|
-
? await detectProjectEvidence(target, { claims:
|
|
115
|
+
? await detectProjectEvidence(target, { claims: context?.descriptor?.writeClaims ?? [] })
|
|
25
116
|
: null;
|
|
26
|
-
const
|
|
117
|
+
const routeInput = {
|
|
27
118
|
workType,
|
|
28
119
|
surfaces,
|
|
29
120
|
risks,
|
|
@@ -31,22 +122,59 @@ export async function runRoute({ target, packageRoot, workType, surfaces, risks,
|
|
|
31
122
|
behaviorChange,
|
|
32
123
|
executableChange,
|
|
33
124
|
...(projectEvidence ? { projectEvidence } : {}),
|
|
34
|
-
}
|
|
125
|
+
};
|
|
126
|
+
const deterministicRoute = evaluateRoute(routeInput, {
|
|
35
127
|
contract,
|
|
36
|
-
taskDescriptor:
|
|
128
|
+
taskDescriptor: context?.descriptor ?? null,
|
|
37
129
|
configuredProfile,
|
|
38
130
|
requestedProfile: executionProfile,
|
|
39
131
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
132
|
+
let semanticRecommendation = null;
|
|
133
|
+
if (target && packageRoot && effectiveTaskId) {
|
|
134
|
+
await ensureSemanticDecision({
|
|
135
|
+
target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
|
|
136
|
+
request: { decisionKind: "INTAKE", questionSetId: "intake-v1", state: { routeInput: deterministicRoute.input } },
|
|
137
|
+
});
|
|
138
|
+
const questionSet = buildRouteQuestionSet(deterministicRoute.guides);
|
|
139
|
+
const routeDecision = await ensureSemanticDecision({
|
|
140
|
+
target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
|
|
141
|
+
request: {
|
|
142
|
+
decisionKind: "ROUTE", questionSetId: questionSet.id, questionSet,
|
|
143
|
+
state: { routeInput: deterministicRoute.input, eligibleGuides: deterministicRoute.guides.map((guide) => ({ id: guide, reasons: deterministicRoute.reasons[guide] })) },
|
|
144
|
+
candidateIds: deterministicRoute.guides,
|
|
145
|
+
candidateSetFingerprint: canonicalFingerprint(deterministicRoute.guides),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const profileQuestionSet = buildExecutionProfileQuestionSet();
|
|
149
|
+
const profileDecision = await ensureSemanticDecision({
|
|
150
|
+
target, packageRoot, taskId: effectiveTaskId, provider: effectiveSemanticProvider,
|
|
151
|
+
request: {
|
|
152
|
+
decisionKind: "EXECUTION_PROFILE", questionSetId: profileQuestionSet.id, questionSet: profileQuestionSet,
|
|
153
|
+
state: {
|
|
154
|
+
routeInput: deterministicRoute.input,
|
|
155
|
+
deterministicFloor: deterministicRoute.executionProfile.floor,
|
|
156
|
+
requestedProfile: deterministicRoute.executionProfile.requested,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
semanticRecommendation = {
|
|
161
|
+
guideRelevance: routeDecision.artifact.decision,
|
|
162
|
+
executionProfile: profileDecision.artifact.decision,
|
|
163
|
+
};
|
|
48
164
|
}
|
|
49
|
-
|
|
165
|
+
const route = evaluateRoute(routeInput, {
|
|
166
|
+
contract,
|
|
167
|
+
taskDescriptor: context?.descriptor ?? null,
|
|
168
|
+
configuredProfile,
|
|
169
|
+
requestedProfile: executionProfile,
|
|
170
|
+
semanticRecommendation,
|
|
171
|
+
});
|
|
172
|
+
return withTaskMutation(target, { taskId: effectiveTaskId, packageRoot }, "route", async (ctx) => {
|
|
173
|
+
if (target && packageRoot) {
|
|
174
|
+
await persistRoutedState({ target, packageRoot, taskId: effectiveTaskId, route, transaction: ctx?.transaction });
|
|
175
|
+
}
|
|
176
|
+
return route;
|
|
177
|
+
});
|
|
50
178
|
});
|
|
51
179
|
}
|
|
52
180
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runSemanticPlan as projectPlan } from "../core/semantic-planning/service.js";
|
|
2
|
+
import { resolveRequiredSemanticDecision } from "../core/decision/resolver.js";
|
|
3
|
+
import { readCurrentDecisionBindings } from "../core/decision/task-bindings.js";
|
|
4
|
+
|
|
5
|
+
export async function runSemanticPlan({ target, packageRoot, taskId, decisionId, kind, input, semanticRecommendation } = {}) {
|
|
6
|
+
if (target && taskId) {
|
|
7
|
+
const decisionKind = { failure: "FAILURE_TRIAGE", diagnosis: "DIAGNOSIS_PRIORITY", review: "REVIEW_PLAN" }[kind];
|
|
8
|
+
const bindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
|
|
9
|
+
const decision = await resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionId, decisionKind, currentBindings: bindings });
|
|
10
|
+
semanticRecommendation = decision.decision;
|
|
11
|
+
}
|
|
12
|
+
return projectPlan({ kind, input, semanticRecommendation });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatSemanticPlanResult(result) {
|
|
16
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
17
|
+
}
|