@cassiomc1/forgeloop 1.5.0 → 1.6.1
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/CONTRACT_COVERAGE.md +1 -0
- package/DOCS_INDEX.md +20 -10
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +103 -1
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +90 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +47 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +41 -1
- package/docs/ARTIFACT_REFERENCE.md +159 -0
- package/docs/CLI_REFERENCE.md +294 -3
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +22 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/MCP.md +34 -1
- package/docs/RECIPES.md +67 -0
- package/docs/RELEASE_CHECKLIST_1_6_1.md +121 -0
- package/docs/TROUBLESHOOTING.md +133 -2
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +13 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/execution.schema.json +15 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +94 -0
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +49 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/run-check.js +1 -0
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +21 -13
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +106 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +48 -0
- package/src/core/audit.js +25 -0
- package/src/core/bundles.js +15 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +210 -1
- package/src/core/command-executors.js +171 -15
- package/src/core/command-runtime.js +12 -1
- package/src/core/completion-artifacts.js +71 -14
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +363 -0
- package/src/core/events.js +41 -1
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +29 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +105 -7
- package/src/core/integration-invocation-policy.js +55 -0
- package/src/core/integration-resources.js +51 -0
- package/src/core/next-action-model.js +35 -1
- package/src/core/next-action.js +459 -3
- package/src/core/phase.js +40 -21
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +256 -0
- package/src/core/progress.js +41 -4
- package/src/core/protocol-info.js +56 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +15 -12
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/runtime-context.js +19 -1
- package/src/core/schema-validation.js +8 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-paths.js +28 -0
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +8 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +8 -0
- package/src/core/verification-execution.js +257 -0
- package/src/core/work-state.js +10 -5
- package/src/integration.js +8 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalFingerprint, writeJsonArtifact } from "./artifacts.js";
|
|
5
|
+
import { buildTaskTrace } from "./trace.js";
|
|
6
|
+
import { buildTrajectoryMetrics } from "./trajectory-metrics.js";
|
|
7
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
8
|
+
import { taskEvaluationPath } from "./task-paths.js";
|
|
9
|
+
import { appendProtocolEvent } from "./events.js";
|
|
10
|
+
import { assertSafePath, ensureWithin } from "./filesystem.js";
|
|
11
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
12
|
+
|
|
13
|
+
function evaluationError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
14
|
+
|
|
15
|
+
export async function evaluateTrajectory({ target, packageRoot, taskId, scenarioPath, evaluationId = `eval-${randomUUID()}` }) {
|
|
16
|
+
if (typeof scenarioPath !== "string" || !scenarioPath || path.isAbsolute(scenarioPath) || scenarioPath.includes("..")) {
|
|
17
|
+
throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "scenario path must be a relative project-local file");
|
|
18
|
+
}
|
|
19
|
+
let scenario;
|
|
20
|
+
try {
|
|
21
|
+
await assertSafePath(target, scenarioPath);
|
|
22
|
+
scenario = JSON.parse(await readFile(ensureWithin(target, scenarioPath), "utf8"));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", `unable to read scenario safely: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
assertSchema(scenario, await readSchema("trajectory-scenario", packageRoot), "trajectory scenario");
|
|
27
|
+
if (!/^eval-[A-Za-z0-9_-]+$/.test(evaluationId)) throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "evaluationId is invalid");
|
|
28
|
+
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
29
|
+
const metrics = await buildTrajectoryMetrics({ target, packageRoot, taskId });
|
|
30
|
+
const milestoneSet = new Set(trace.events.map((event) => event.type));
|
|
31
|
+
const missingMilestones = (scenario.requiredMilestones ?? []).filter((milestone) => !milestoneSet.has(milestone));
|
|
32
|
+
const completionEvent = trace.events.find((event) => event.type === "COMPLETION_VALIDATED");
|
|
33
|
+
const verificationEvent = trace.events.find((event) => event.type === "VERIFICATION_STARTED" || event.type === "VERIFICATION_RECORDED");
|
|
34
|
+
const completionBeforeVerification = Boolean(completionEvent && (!verificationEvent || completionEvent.sequence < verificationEvent.sequence));
|
|
35
|
+
// Canonical truth: ambiguous actions or unresolved trusted-readiness items
|
|
36
|
+
// block safety. A raw VERIFIED label alone never resolves a required action.
|
|
37
|
+
const unresolvedRequiredAction = (trace.actions?.ambiguous ?? 0) > 0
|
|
38
|
+
|| ((metrics.actions.unresolvedRequired ?? null) !== null
|
|
39
|
+
? metrics.actions.unresolvedRequired > 0
|
|
40
|
+
: (trace.actions?.required ?? 0) > (trace.actions?.verified ?? 0));
|
|
41
|
+
const limit = (name, actual, max) => ({ actual, ...(max === undefined ? {} : { max, pass: actual <= max }) });
|
|
42
|
+
const limits = {
|
|
43
|
+
verificationCycles: limit("verificationCycles", metrics.trajectory.verificationCycles, scenario.limits?.maxVerificationCycles),
|
|
44
|
+
nonInformativeInterventions: limit("nonInformativeInterventions", metrics.trajectory.interventions.nonInformative, scenario.limits?.maxNonInformativeInterventions),
|
|
45
|
+
ambiguousActions: limit("ambiguousActions", metrics.actions.ambiguous, scenario.limits?.maxAmbiguousActions),
|
|
46
|
+
};
|
|
47
|
+
const safetyValid = !(scenario.forbidden?.completionBeforeVerification && completionBeforeVerification)
|
|
48
|
+
&& !(scenario.forbidden?.unresolvedRequiredAction && unresolvedRequiredAction);
|
|
49
|
+
const completionValid = metrics.completion.validated && missingMilestones.length === 0;
|
|
50
|
+
const limitsValid = Object.values(limits).every((item) => item.pass !== false);
|
|
51
|
+
// Comparable steps have exactly one owner: buildTrajectoryMetrics.
|
|
52
|
+
const efficiency = Number.isInteger(scenario.reference?.comparableSteps) && scenario.reference.comparableSteps > 0
|
|
53
|
+
? { referenceComparableSteps: scenario.reference.comparableSteps, actualComparableSteps: metrics.comparableSteps,
|
|
54
|
+
ratio: scenario.reference.comparableSteps / Math.max(1, metrics.comparableSteps) }
|
|
55
|
+
: null;
|
|
56
|
+
const base = {
|
|
57
|
+
schemaVersion: 1, evaluationId, scenarioId: scenario.scenarioId,
|
|
58
|
+
scenarioFingerprint: canonicalFingerprint(scenario), taskId,
|
|
59
|
+
result: completionValid && safetyValid && limitsValid ? "PASS" : "FAIL",
|
|
60
|
+
completionValid, safetyValid, missingMilestones, limits, efficiency,
|
|
61
|
+
computedAt: new Date().toISOString(), source: "PROJECT_LOCAL_REFERENCE",
|
|
62
|
+
};
|
|
63
|
+
const evaluation = { ...base, evaluationFingerprint: canonicalFingerprint(base) };
|
|
64
|
+
assertSchema(evaluation, await readSchema("trajectory-evaluation", packageRoot), "trajectory evaluation");
|
|
65
|
+
await withTaskTransaction({ target, taskId, operation: "trajectory-evaluated" }, async () => {
|
|
66
|
+
await writeJsonArtifact(target, taskEvaluationPath(taskId, evaluationId), evaluation, "trajectory-evaluation", packageRoot);
|
|
67
|
+
await appendProtocolEvent(target, { taskId, event: "TRAJECTORY_EVALUATED", fingerprint: evaluation.evaluationFingerprint,
|
|
68
|
+
details: { evaluationId, scenarioId: scenario.scenarioId, evaluationFingerprint: evaluation.evaluationFingerprint } }, packageRoot, { taskId });
|
|
69
|
+
});
|
|
70
|
+
return evaluation;
|
|
71
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { buildTaskTrace } from "./trace.js";
|
|
2
|
+
import { buildTaskReflection } from "./reflection.js";
|
|
3
|
+
|
|
4
|
+
export const COMPARABLE_WORK_EVENTS = new Set([
|
|
5
|
+
"EXECUTION_STARTED", "VERIFICATION_RECORDED", "DIAGNOSTIC_CASE_RECORDED",
|
|
6
|
+
"INTERVENTION_RECORDED", "ACTION_STARTED", "ACTION_RECONCILED", "REVIEW_STARTED",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export async function buildTrajectoryMetrics({ target, packageRoot, taskId }) {
|
|
10
|
+
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
11
|
+
const reflection = await buildTaskReflection({ target, packageRoot, taskId });
|
|
12
|
+
const events = trace.events;
|
|
13
|
+
const verificationCycles = new Set(events.filter((event) => event.type === "VERIFICATION_STARTED")
|
|
14
|
+
.map((event) => event.data?.verificationCycle).filter(Number.isInteger));
|
|
15
|
+
const diagnosticCycles = new Set(trace.diagnostics.cases.map((item) => item.verificationCycle));
|
|
16
|
+
const firstEventAt = events.find((event) => event.timestampQuality === "authoritative")?.timestamp ?? null;
|
|
17
|
+
const lastEventAt = [...events].reverse().find((event) => event.timestampQuality === "authoritative")?.timestamp ?? null;
|
|
18
|
+
const firstMs = firstEventAt ? Date.parse(firstEventAt) : NaN;
|
|
19
|
+
const lastMs = lastEventAt ? Date.parse(lastEventAt) : NaN;
|
|
20
|
+
const interventions = reflection.interventions ?? { count: 0, informative: 0, nonInformative: 0 };
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
taskId,
|
|
24
|
+
completion: {
|
|
25
|
+
validated: Boolean(trace.completion.validatedAt),
|
|
26
|
+
phase: trace.task.phase,
|
|
27
|
+
},
|
|
28
|
+
trajectory: {
|
|
29
|
+
events: events.length,
|
|
30
|
+
verificationCycles: verificationCycles.size,
|
|
31
|
+
diagnosticCycles: diagnosticCycles.size,
|
|
32
|
+
strategyChanges: Math.max(0, (reflection.strategies?.length ?? 0) - 1),
|
|
33
|
+
oscillationDetected: Boolean(reflection.oscillation?.detected),
|
|
34
|
+
noEffectiveInformationGainCycles: reflection.informationGain?.cyclesWithoutEffectiveGain?.length ?? 0,
|
|
35
|
+
interventions: {
|
|
36
|
+
total: interventions.count ?? 0,
|
|
37
|
+
informative: interventions.informative ?? 0,
|
|
38
|
+
nonInformative: interventions.nonInformative ?? 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
actions: {
|
|
42
|
+
total: trace.actions?.total ?? 0,
|
|
43
|
+
verified: trace.actions?.verified ?? 0,
|
|
44
|
+
// Canonical trust counts come from the action-readiness projection;
|
|
45
|
+
// raw VERIFIED labels are observability only.
|
|
46
|
+
trustedSatisfied: await (async () => {
|
|
47
|
+
try {
|
|
48
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
49
|
+
const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
|
|
50
|
+
return readiness.satisfied;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
})(),
|
|
55
|
+
unresolvedRequired: await (async () => {
|
|
56
|
+
try {
|
|
57
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
58
|
+
const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
|
|
59
|
+
return readiness.unresolved;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
})(),
|
|
64
|
+
failed: trace.actions?.failed ?? 0,
|
|
65
|
+
ambiguous: trace.actions?.ambiguous ?? 0,
|
|
66
|
+
reconciliations: trace.actions?.reconciliationCount ?? 0,
|
|
67
|
+
},
|
|
68
|
+
executions: {
|
|
69
|
+
observedCommands: trace.executions.length,
|
|
70
|
+
failedCommands: trace.executions.filter((execution) => execution.status === "failed").length,
|
|
71
|
+
},
|
|
72
|
+
timing: {
|
|
73
|
+
firstEventAt,
|
|
74
|
+
lastEventAt,
|
|
75
|
+
wallClockMs: Number.isFinite(firstMs) && Number.isFinite(lastMs) ? Math.max(0, lastMs - firstMs) : null,
|
|
76
|
+
},
|
|
77
|
+
usage: { tokens: null, costUsd: null, source: "UNKNOWN" },
|
|
78
|
+
comparableSteps: events.filter((event) => COMPARABLE_WORK_EVENTS.has(event.type)).length,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/core/transaction.js
CHANGED
|
@@ -103,6 +103,13 @@ export async function withTaskTransaction({
|
|
|
103
103
|
recordCommitEvent = false,
|
|
104
104
|
} = {}, callback) {
|
|
105
105
|
if (!target || !taskId) throw new Error("target and taskId are required for a task transaction");
|
|
106
|
+
const activeTransaction = getActiveTaskTransaction();
|
|
107
|
+
if (activeTransaction) {
|
|
108
|
+
if (activeTransaction.taskId !== taskId) {
|
|
109
|
+
throw new Error(`cannot nest task transaction for ${taskId} inside ${activeTransaction.taskId}`);
|
|
110
|
+
}
|
|
111
|
+
return callback(activeTransaction);
|
|
112
|
+
}
|
|
106
113
|
const started = Date.now();
|
|
107
114
|
const runWithLock = async () => withTaskLock(target, lockTaskId, operation, async (lock) => {
|
|
108
115
|
const transactionId = `txn-${randomUUID()}`;
|
|
@@ -115,6 +122,7 @@ export async function withTaskTransaction({
|
|
|
115
122
|
await writeManifest(target, manifestPath, manifest);
|
|
116
123
|
const tx = {
|
|
117
124
|
transactionId,
|
|
125
|
+
taskId,
|
|
118
126
|
lock,
|
|
119
127
|
async readText(relativePath) {
|
|
120
128
|
await assertSafePath(target, relativePath);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
export const VERIFICATION_ISOLATION_MODES = Object.freeze([
|
|
2
|
+
"NATIVE_PROJECT",
|
|
3
|
+
"PROJECT_ISOLATED",
|
|
4
|
+
"SYSTEM_ISOLATED",
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export const VERIFICATION_EXECUTION_POLICY_MODES = Object.freeze([
|
|
8
|
+
"NONE",
|
|
9
|
+
...VERIFICATION_ISOLATION_MODES,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const E_VERIFICATION_ISOLATION_UNAVAILABLE = "E_VERIFICATION_ISOLATION_UNAVAILABLE";
|
|
13
|
+
export const E_VERIFICATION_EXECUTION_INVALID = "E_VERIFICATION_EXECUTION_INVALID";
|
|
14
|
+
|
|
15
|
+
const ISOLATION_RANK = Object.freeze({
|
|
16
|
+
NATIVE_PROJECT: 0,
|
|
17
|
+
PROJECT_ISOLATED: 1,
|
|
18
|
+
SYSTEM_ISOLATED: 2,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function executionError(code, message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = code;
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requiredString(value, field) {
|
|
32
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
33
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, `Verification execution ${field} must be a non-empty string`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nullableString(value, field) {
|
|
39
|
+
if (value !== null && typeof value !== "string") {
|
|
40
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, `Verification execution ${field} must be a string or null`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function outputBytes(value, field) {
|
|
46
|
+
if (value === undefined || value === null) return Buffer.alloc(0);
|
|
47
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
48
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
49
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, `Verification execution ${field} must be text or bytes`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nonNegativeInteger(value, field, fallback) {
|
|
53
|
+
const resolved = value === undefined ? fallback : value;
|
|
54
|
+
if (!Number.isInteger(resolved) || resolved < 0) {
|
|
55
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, `Verification execution ${field} must be a non-negative integer`);
|
|
56
|
+
}
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeVerificationExecutionPolicy(policy = {}) {
|
|
61
|
+
if (policy === undefined || policy === null) return Object.freeze({ requiredIsolation: "NONE" });
|
|
62
|
+
if (!isRecord(policy)) {
|
|
63
|
+
throw executionError(E_VERIFICATION_ISOLATION_UNAVAILABLE, "Verification execution isolation policy is invalid");
|
|
64
|
+
}
|
|
65
|
+
const requiredIsolation = policy.requiredIsolation ?? "NONE";
|
|
66
|
+
if (!VERIFICATION_EXECUTION_POLICY_MODES.includes(requiredIsolation)) {
|
|
67
|
+
throw executionError(E_VERIFICATION_ISOLATION_UNAVAILABLE, "Verification execution isolation policy is unsupported");
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({ requiredIsolation });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isVerificationExecutionAdapter(value) {
|
|
73
|
+
return isRecord(value) && typeof value.execute === "function";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readAdapter(runtimeContext) {
|
|
77
|
+
const value = isRecord(runtimeContext) ? runtimeContext.verificationExecutionAdapter : undefined;
|
|
78
|
+
if (value === undefined || value === null) return null;
|
|
79
|
+
if (!isVerificationExecutionAdapter(value)) {
|
|
80
|
+
throw executionError(E_VERIFICATION_ISOLATION_UNAVAILABLE, "Verification execution adapter is unavailable or invalid");
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertIsolationMetadataConsistency(isolation) {
|
|
86
|
+
if (isolation.mode === "NATIVE_PROJECT") {
|
|
87
|
+
if (
|
|
88
|
+
isolation.isolated !== false
|
|
89
|
+
|| isolation.liveProjectWritable !== true
|
|
90
|
+
) {
|
|
91
|
+
throw executionError(
|
|
92
|
+
E_VERIFICATION_EXECUTION_INVALID,
|
|
93
|
+
"NATIVE_PROJECT must report isolated=false and liveProjectWritable=true",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
isolation.isolated !== true
|
|
101
|
+
|| isolation.liveProjectWritable !== false
|
|
102
|
+
) {
|
|
103
|
+
throw executionError(
|
|
104
|
+
E_VERIFICATION_EXECUTION_INVALID,
|
|
105
|
+
`${isolation.mode} must report isolated=true and liveProjectWritable=false`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
isolation.mode === "SYSTEM_ISOLATED"
|
|
111
|
+
&& isolation.networkPolicy !== "DENIED"
|
|
112
|
+
) {
|
|
113
|
+
throw executionError(
|
|
114
|
+
E_VERIFICATION_EXECUTION_INVALID,
|
|
115
|
+
"SYSTEM_ISOLATED must report networkPolicy=DENIED",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeIsolation(value) {
|
|
121
|
+
if (!isRecord(value)) {
|
|
122
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution isolation metadata is missing");
|
|
123
|
+
}
|
|
124
|
+
const mode = value.mode;
|
|
125
|
+
if (!VERIFICATION_ISOLATION_MODES.includes(mode)) {
|
|
126
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution isolation mode is unsupported");
|
|
127
|
+
}
|
|
128
|
+
if (typeof value.isolated !== "boolean" || typeof value.liveProjectWritable !== "boolean") {
|
|
129
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution isolation booleans are invalid");
|
|
130
|
+
}
|
|
131
|
+
const isolation = {
|
|
132
|
+
mode,
|
|
133
|
+
isolated: value.isolated,
|
|
134
|
+
liveProjectWritable: value.liveProjectWritable,
|
|
135
|
+
networkPolicy: requiredString(value.networkPolicy, "networkPolicy"),
|
|
136
|
+
environmentPolicy: requiredString(value.environmentPolicy, "environmentPolicy"),
|
|
137
|
+
};
|
|
138
|
+
assertIsolationMetadataConsistency(isolation);
|
|
139
|
+
return isolation;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function normalizeVerificationExecutionResult(value, { defaultCwd = null } = {}) {
|
|
143
|
+
if (!isRecord(value)) {
|
|
144
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution adapter returned an invalid result");
|
|
145
|
+
}
|
|
146
|
+
const cwd = value.cwd ?? defaultCwd;
|
|
147
|
+
requiredString(cwd, "cwd");
|
|
148
|
+
if (value.exitCode !== null && !Number.isInteger(value.exitCode)) {
|
|
149
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution exitCode must be an integer or null");
|
|
150
|
+
}
|
|
151
|
+
const signal = nullableString(value.signal ?? null, "signal");
|
|
152
|
+
if (typeof value.timedOut !== "boolean") {
|
|
153
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution timedOut must be boolean");
|
|
154
|
+
}
|
|
155
|
+
if (typeof value.outputTruncated !== "boolean") {
|
|
156
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution outputTruncated must be boolean");
|
|
157
|
+
}
|
|
158
|
+
const stdout = outputBytes(value.stdout, "stdout");
|
|
159
|
+
const stderr = outputBytes(value.stderr, "stderr");
|
|
160
|
+
return {
|
|
161
|
+
exitCode: value.exitCode,
|
|
162
|
+
signal,
|
|
163
|
+
timedOut: value.timedOut,
|
|
164
|
+
stdout,
|
|
165
|
+
stderr,
|
|
166
|
+
stdoutBytes: nonNegativeInteger(value.stdoutBytes, "stdoutBytes", stdout.length),
|
|
167
|
+
stderrBytes: nonNegativeInteger(value.stderrBytes, "stderrBytes", stderr.length),
|
|
168
|
+
outputTruncated: value.outputTruncated,
|
|
169
|
+
spawnError: Boolean(value.spawnError),
|
|
170
|
+
cwd,
|
|
171
|
+
isolation: normalizeIsolation(value.isolation),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function satisfiesIsolationPolicy(isolation, requiredIsolation) {
|
|
176
|
+
if (requiredIsolation === "NONE") return true;
|
|
177
|
+
if (ISOLATION_RANK[isolation.mode] < ISOLATION_RANK[requiredIsolation]) return false;
|
|
178
|
+
if (requiredIsolation === "NATIVE_PROJECT") return true;
|
|
179
|
+
return isolation.isolated === true
|
|
180
|
+
&& isolation.liveProjectWritable === false
|
|
181
|
+
&& (requiredIsolation !== "SYSTEM_ISOLATED" || isolation.networkPolicy === "DENIED");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function assertVerificationIsolation(result, policy) {
|
|
185
|
+
const requiredIsolation = normalizeVerificationExecutionPolicy(policy).requiredIsolation;
|
|
186
|
+
if (!satisfiesIsolationPolicy(result.isolation, requiredIsolation)) {
|
|
187
|
+
throw executionError(
|
|
188
|
+
E_VERIFICATION_ISOLATION_UNAVAILABLE,
|
|
189
|
+
`Verification execution did not satisfy the required ${requiredIsolation} isolation boundary`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function executeVerificationProcess({
|
|
196
|
+
argv,
|
|
197
|
+
protocolProjectRoot,
|
|
198
|
+
taskId,
|
|
199
|
+
checkId,
|
|
200
|
+
requirement,
|
|
201
|
+
resolution,
|
|
202
|
+
timeoutMs,
|
|
203
|
+
runtimeContext,
|
|
204
|
+
nativeExecute,
|
|
205
|
+
}) {
|
|
206
|
+
const policy = normalizeVerificationExecutionPolicy(runtimeContext?.verificationExecutionPolicy);
|
|
207
|
+
const adapter = readAdapter(runtimeContext);
|
|
208
|
+
let rawResult;
|
|
209
|
+
if (adapter) {
|
|
210
|
+
try {
|
|
211
|
+
rawResult = await adapter.execute(Object.freeze({
|
|
212
|
+
argv: [...argv],
|
|
213
|
+
protocolProjectRoot,
|
|
214
|
+
taskId,
|
|
215
|
+
checkId,
|
|
216
|
+
requirement,
|
|
217
|
+
timeoutMs,
|
|
218
|
+
resolution,
|
|
219
|
+
}));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error?.code === E_VERIFICATION_ISOLATION_UNAVAILABLE) throw error;
|
|
222
|
+
throw executionError(E_VERIFICATION_EXECUTION_INVALID, "Verification execution adapter failed");
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
if (policy.requiredIsolation !== "NONE") {
|
|
226
|
+
throw executionError(
|
|
227
|
+
E_VERIFICATION_ISOLATION_UNAVAILABLE,
|
|
228
|
+
`Verification execution requires ${policy.requiredIsolation}, but no trusted adapter was supplied`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const nativeResult = await nativeExecute(argv, protocolProjectRoot, { timeoutMs });
|
|
232
|
+
rawResult = {
|
|
233
|
+
...nativeResult,
|
|
234
|
+
cwd: protocolProjectRoot,
|
|
235
|
+
isolation: {
|
|
236
|
+
mode: "NATIVE_PROJECT",
|
|
237
|
+
isolated: false,
|
|
238
|
+
liveProjectWritable: true,
|
|
239
|
+
networkPolicy: "INHERITED",
|
|
240
|
+
environmentPolicy: "INHERITED",
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const normalized = normalizeVerificationExecutionResult(rawResult, { defaultCwd: protocolProjectRoot });
|
|
246
|
+
if (
|
|
247
|
+
policy.requiredIsolation !== "NONE"
|
|
248
|
+
&& normalized.isolation.mode !== "NATIVE_PROJECT"
|
|
249
|
+
&& normalized.cwd === protocolProjectRoot
|
|
250
|
+
) {
|
|
251
|
+
throw executionError(
|
|
252
|
+
E_VERIFICATION_EXECUTION_INVALID,
|
|
253
|
+
"Isolated verification execution must use a cwd separate from the protocol project root",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return assertVerificationIsolation(normalized, policy);
|
|
257
|
+
}
|
package/src/core/work-state.js
CHANGED
|
@@ -538,13 +538,18 @@ export async function classifyLoadedWorkState({ target, state, contractFile = nu
|
|
|
538
538
|
};
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
export async function readAndClassifyWorkState({ target, packageRoot = getPackageRoot(), contractFile = null, maxAgeMs } = {}) {
|
|
541
|
+
export async function readAndClassifyWorkState({ target, packageRoot = getPackageRoot(), contractFile = null, maxAgeMs, taskId = null, stateFile = null, statePath = null } = {}) {
|
|
542
|
+
const effectiveStateRel = stateFile ?? statePath ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
|
|
542
543
|
let state = null;
|
|
543
544
|
try {
|
|
544
|
-
state = await readWorkState(target,
|
|
545
|
+
state = await readWorkState(target, {
|
|
546
|
+
packageRoot,
|
|
547
|
+
...(taskId ? { taskId } : {}),
|
|
548
|
+
...(stateFile || statePath ? { statePath: effectiveStateRel } : {}),
|
|
549
|
+
});
|
|
545
550
|
} catch (error) {
|
|
546
551
|
return {
|
|
547
|
-
path:
|
|
552
|
+
path: effectiveStateRel,
|
|
548
553
|
present: true,
|
|
549
554
|
status: "INVALID",
|
|
550
555
|
reasons: ["STATE_INVALID"],
|
|
@@ -565,7 +570,7 @@ export async function readAndClassifyWorkState({ target, packageRoot = getPackag
|
|
|
565
570
|
if (!state) {
|
|
566
571
|
const repository = await currentRepositoryFingerprint(target);
|
|
567
572
|
return {
|
|
568
|
-
path:
|
|
573
|
+
path: effectiveStateRel,
|
|
569
574
|
present: false,
|
|
570
575
|
status: "ABSENT",
|
|
571
576
|
reasons: ["NO_CHECKPOINT"],
|
|
@@ -589,7 +594,7 @@ export async function readAndClassifyWorkState({ target, packageRoot = getPackag
|
|
|
589
594
|
|
|
590
595
|
const classification = await classifyLoadedWorkState({ target, state, contractFile, maxAgeMs });
|
|
591
596
|
return {
|
|
592
|
-
path:
|
|
597
|
+
path: effectiveStateRel,
|
|
593
598
|
present: true,
|
|
594
599
|
...classification,
|
|
595
600
|
phase: state.phase,
|
package/src/integration.js
CHANGED
|
@@ -44,4 +44,12 @@ export {
|
|
|
44
44
|
CLI_COMMAND_DEFINITIONS,
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
export { createForgeLoopContext } from "./core/runtime-context.js";
|
|
48
|
+
export {
|
|
49
|
+
E_VERIFICATION_EXECUTION_INVALID,
|
|
50
|
+
E_VERIFICATION_ISOLATION_UNAVAILABLE,
|
|
51
|
+
VERIFICATION_EXECUTION_POLICY_MODES,
|
|
52
|
+
VERIFICATION_ISOLATION_MODES,
|
|
53
|
+
} from "./core/verification-execution.js";
|
|
54
|
+
|
|
47
55
|
export const FORGELOOP_INTEGRATION_API_VERSION = FORGELOOP_INTEGRATION_RUNTIME_VERSION;
|