@cassiomc1/forgeloop 1.1.1 → 1.2.2
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/.cursor/rules/project-loop.mdc +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/DOCS_INDEX.md +3 -0
- package/ENG/design-code-eng.md +124 -0
- package/ENG/premium-sites-studio-eng.md +28 -0
- package/ENG/taste-frontend-eng.md +3 -2
- package/ENG/test-code-eng.md +45 -0
- package/LOOP_ENGINEERING.md +74 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +41 -6
- package/PROTOCOL_INTEGRATION.md +13 -0
- package/README.md +40 -6
- package/TERMINOLOGY.md +10 -0
- package/THIRD_PARTY_NOTICES.md +58 -1
- package/THREAT_MODEL.md +12 -1
- package/docs/ARTIFACT_REFERENCE.md +152 -2
- package/docs/CLI_REFERENCE.md +346 -30
- package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
- package/docs/DOCUMENTATION_GUIDE.md +41 -4
- package/docs/GETTING_STARTED.md +39 -8
- package/docs/RECIPES.md +66 -7
- package/docs/TROUBLESHOOTING.md +279 -6
- package/package.json +1 -1
- package/schemas/policy-baseline.schema.json +26 -0
- package/schemas/policy-discovery.schema.json +45 -0
- package/schemas/policy-lock.schema.json +16 -0
- package/schemas/policy-rules.schema.json +48 -0
- package/schemas/policy-snapshot.schema.json +16 -0
- package/src/cli.js +102 -1
- package/src/commands/baseline.js +120 -0
- package/src/commands/init.js +304 -6
- package/src/commands/next.js +15 -1
- package/src/commands/policy-diff.js +51 -0
- package/src/commands/policy-discover.js +42 -0
- package/src/commands/policy-status.js +33 -0
- package/src/commands/profile-interview.js +50 -0
- package/src/commands/progress.js +51 -0
- package/src/commands/reconcile-closure.js +49 -0
- package/src/commands/record-decision-criterion.js +34 -0
- package/src/commands/record-diagnosis.js +49 -0
- package/src/commands/rule-verify.js +36 -0
- package/src/commands/validate-receipt.js +38 -3
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +24 -0
- package/src/core/cli-command-definitions.js +163 -7
- package/src/core/cli-metadata.js +1 -1
- package/src/core/completion-artifacts.js +29 -3
- package/src/core/completion.js +101 -10
- package/src/core/diagnosis-model.js +214 -0
- package/src/core/diagnosis.js +171 -0
- package/src/core/error-codes.js +292 -0
- package/src/core/events.js +47 -1
- package/src/core/execution-prerequisites.js +38 -20
- package/src/core/execution.js +20 -3
- package/src/core/native-adapters.js +14 -4
- package/src/core/next-action-model.js +40 -5
- package/src/core/next-action.js +234 -91
- package/src/core/phase.js +29 -0
- package/src/core/policy-adapters.js +276 -0
- package/src/core/policy-baseline.js +144 -0
- package/src/core/policy-diff.js +133 -0
- package/src/core/policy-discovery.js +225 -0
- package/src/core/policy-engine.js +533 -0
- package/src/core/policy-mutation.js +139 -0
- package/src/core/preflight-consistency.js +23 -15
- package/src/core/preflight-model.js +10 -2
- package/src/core/preflight.js +65 -1
- package/src/core/progress.js +143 -0
- package/src/core/protocol.js +8 -0
- package/src/core/reconcile-closure.js +173 -0
- package/src/core/schema-validation.js +6 -0
- package/src/core/settlement-model.js +85 -0
- package/src/core/settlement.js +78 -0
- package/src/core/task-context.js +11 -0
- package/src/core/task-discovery.js +67 -1
- package/src/core/task-paths.js +9 -0
- package/src/core/templates.js +5 -0
package/src/commands/next.js
CHANGED
|
@@ -14,7 +14,21 @@ export function formatNextActionResult(result) {
|
|
|
14
14
|
];
|
|
15
15
|
if (result.reasons.length > 0) {
|
|
16
16
|
lines.push("REASONS:");
|
|
17
|
-
|
|
17
|
+
for (const reason of result.reasons) {
|
|
18
|
+
lines.push(`- ${reason.code}: ${reason.message}`);
|
|
19
|
+
if (reason.resolution?.kind === "SETTLEMENT_CRITERION" && reason.resolution.settledBy) {
|
|
20
|
+
lines.push(` SETTLED BY: ${reason.resolution.settledBy}`);
|
|
21
|
+
} else if (reason.resolution?.kind === "SETTLEMENT_CRITERIA" && Array.isArray(reason.resolution.items)) {
|
|
22
|
+
lines.push(" SETTLEMENT CRITERIA:");
|
|
23
|
+
for (const item of reason.resolution.items) {
|
|
24
|
+
lines.push(` - ${item.decision}`);
|
|
25
|
+
lines.push(` SETTLED BY: ${item.settledBy}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (result.progress) {
|
|
31
|
+
lines.push(`PROGRESS: ${result.progress.status}`);
|
|
18
32
|
}
|
|
19
33
|
if (result.commands.length > 0) {
|
|
20
34
|
lines.push("COMMANDS (SAFE SYNOPSIS ONLY):");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { diffPolicies } from "../core/policy-diff.js";
|
|
4
|
+
import {
|
|
5
|
+
loadEffectiveRules,
|
|
6
|
+
readBaseline,
|
|
7
|
+
readTaskPolicySnapshot,
|
|
8
|
+
} from "../core/policy-engine.js";
|
|
9
|
+
|
|
10
|
+
export async function runPolicyDiff({
|
|
11
|
+
target = process.cwd(),
|
|
12
|
+
packageRoot,
|
|
13
|
+
taskId = null,
|
|
14
|
+
before = null,
|
|
15
|
+
after = null,
|
|
16
|
+
} = {}) {
|
|
17
|
+
let beforePolicy;
|
|
18
|
+
let afterPolicy;
|
|
19
|
+
|
|
20
|
+
if (before) {
|
|
21
|
+
const rawBefore = await readFile(path.resolve(target, before), "utf8");
|
|
22
|
+
beforePolicy = JSON.parse(rawBefore);
|
|
23
|
+
} else if (taskId) {
|
|
24
|
+
const snapshot = await readTaskPolicySnapshot(target, taskId, packageRoot);
|
|
25
|
+
beforePolicy = snapshot ? { rules: snapshot.rules, baseline: { entries: [] } } : { rules: [], baseline: { entries: [] } };
|
|
26
|
+
} else {
|
|
27
|
+
beforePolicy = { rules: [], baseline: { entries: [] } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (after) {
|
|
31
|
+
const rawAfter = await readFile(path.resolve(target, after), "utf8");
|
|
32
|
+
afterPolicy = JSON.parse(rawAfter);
|
|
33
|
+
} else {
|
|
34
|
+
const rules = await loadEffectiveRules(target, packageRoot);
|
|
35
|
+
const baseline = await readBaseline(target, packageRoot);
|
|
36
|
+
afterPolicy = { rules, baseline };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diffPolicies(beforePolicy, afterPolicy);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatPolicyDiffResult(result) {
|
|
43
|
+
const lines = [
|
|
44
|
+
`FORGELOOP POLICY DIFF: ${result.classification}`,
|
|
45
|
+
`Changes: ${result.changes?.length ?? 0}`,
|
|
46
|
+
];
|
|
47
|
+
for (const c of result.changes ?? []) {
|
|
48
|
+
lines.push(` - [${c.type}] ${c.path}: ${c.description}`);
|
|
49
|
+
}
|
|
50
|
+
return `${lines.join("\n")}\n`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { discoverPolicy } from "../core/policy-discovery.js";
|
|
2
|
+
import {
|
|
3
|
+
computePolicyLockData,
|
|
4
|
+
loadEffectiveRules,
|
|
5
|
+
readBaseline,
|
|
6
|
+
writeDiscoveryReport,
|
|
7
|
+
writePolicyLock,
|
|
8
|
+
} from "../core/policy-engine.js";
|
|
9
|
+
|
|
10
|
+
export async function runPolicyDiscover({ target = process.cwd(), packageRoot, write = false } = {}) {
|
|
11
|
+
const discovery = await discoverPolicy({ target });
|
|
12
|
+
let lock = null;
|
|
13
|
+
|
|
14
|
+
if (write) {
|
|
15
|
+
await writeDiscoveryReport(target, discovery, packageRoot);
|
|
16
|
+
const baseline = await readBaseline(target, packageRoot);
|
|
17
|
+
const rules = await loadEffectiveRules(target, packageRoot);
|
|
18
|
+
lock = computePolicyLockData(rules, baseline);
|
|
19
|
+
await writePolicyLock(target, lock, packageRoot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...discovery,
|
|
24
|
+
written: Boolean(write),
|
|
25
|
+
lock: lock?.digest ?? null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatPolicyDiscoverResult(result) {
|
|
30
|
+
const lines = [
|
|
31
|
+
"FORGELOOP POLICY DISCOVERY:",
|
|
32
|
+
`Languages: ${result.languages?.join(", ") || "none detected"}`,
|
|
33
|
+
`Testing: ${result.testing?.detected ? `detected (${result.testing.framework || result.testing.command?.join(" ")}) [${result.testing.confidence}]` : "none"}`,
|
|
34
|
+
`Linting: ${result.linting?.detected ? `detected (${result.linting.tool || result.linting.command?.join(" ")}) [${result.linting.confidence}]` : "none"}`,
|
|
35
|
+
`Architecture: ${result.architecture?.value ? `${result.architecture.value} [${result.architecture.confidence}]` : "none [UNKNOWN]"}`,
|
|
36
|
+
`Discovered Rules: ${result.discoveredRules?.length ?? 0}`,
|
|
37
|
+
];
|
|
38
|
+
for (const rule of result.discoveredRules ?? []) {
|
|
39
|
+
lines.push(` - ${rule.id} (${rule.severity}${rule.blocking ? ", BLOCKING" : ", ADVISORY"}): ${rule.why}`);
|
|
40
|
+
}
|
|
41
|
+
return `${lines.join("\n")}\n`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { evaluateTargetPolicy } from "../core/policy-engine.js";
|
|
2
|
+
|
|
3
|
+
export async function runPolicyStatus({ target = process.cwd(), packageRoot, taskId = null } = {}) {
|
|
4
|
+
return evaluateTargetPolicy({ target, packageRoot, taskId });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatPolicyStatusResult(result) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`FORGELOOP POLICY STATUS: ${result.status}`,
|
|
10
|
+
`Lock: ${result.lock?.digest ?? "none"}`,
|
|
11
|
+
`Rules: ${result.rules?.length ?? 0} total (Proven: ${result.provenRules}, Inert: ${result.inertRules}, Unsupported: ${result.unsupportedRules})`,
|
|
12
|
+
`Baseline Violations: ${result.baselineViolations}`,
|
|
13
|
+
`New Violations: ${result.newViolations?.length ?? 0}`,
|
|
14
|
+
`Drift: ${result.drift?.detected ? `DETECTED (${result.drift.classification})` : "none"}`,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
if (result.errors?.length > 0) {
|
|
18
|
+
lines.push("Errors:");
|
|
19
|
+
for (const err of result.errors) {
|
|
20
|
+
lines.push(` - ${err.code}: ${err.why || err.message || err.ruleId}`);
|
|
21
|
+
if (err.fix) lines.push(` Fix: ${err.fix}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (result.warnings?.length > 0) {
|
|
26
|
+
lines.push("Warnings:");
|
|
27
|
+
for (const warn of result.warnings) {
|
|
28
|
+
lines.push(` - ${warn.code}: ${warn.message || warn.why}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `${lines.join("\n")}\n`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { discoverPolicy } from "../core/policy-discovery.js";
|
|
2
|
+
|
|
3
|
+
export async function runProfileInterview({
|
|
4
|
+
target = process.cwd(),
|
|
5
|
+
packageRoot,
|
|
6
|
+
dryRun = false,
|
|
7
|
+
} = {}) {
|
|
8
|
+
const discovery = await discoverPolicy({ target });
|
|
9
|
+
return {
|
|
10
|
+
status: "COMPLETE",
|
|
11
|
+
mode: "OPTIONAL_INTERVIEW",
|
|
12
|
+
dryRun,
|
|
13
|
+
questions: [
|
|
14
|
+
{
|
|
15
|
+
topic: "languages",
|
|
16
|
+
detected: discovery.languages,
|
|
17
|
+
recommendation: discovery.languages.join(", ") || "none",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
topic: "testing",
|
|
21
|
+
detected: discovery.testing.detected,
|
|
22
|
+
framework: discovery.testing.framework,
|
|
23
|
+
confidence: discovery.testing.confidence,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
topic: "linting",
|
|
27
|
+
detected: discovery.linting.detected,
|
|
28
|
+
tool: discovery.linting.tool,
|
|
29
|
+
confidence: discovery.linting.confidence,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
topic: "architecture",
|
|
33
|
+
detected: discovery.architecture.value,
|
|
34
|
+
confidence: discovery.architecture.confidence,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
discovery,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatProfileInterviewResult(result) {
|
|
42
|
+
const lines = [
|
|
43
|
+
"FORGELOOP PROFILE INTERVIEW (OPTIONAL):",
|
|
44
|
+
`Languages: ${result.discovery?.languages?.join(", ") || "none detected"}`,
|
|
45
|
+
`Testing: ${result.discovery?.testing?.detected ? `${result.discovery.testing.framework} [${result.discovery.testing.confidence}]` : "none"}`,
|
|
46
|
+
`Linting: ${result.discovery?.linting?.detected ? `${result.discovery.linting.tool} [${result.discovery.linting.confidence}]` : "none"}`,
|
|
47
|
+
`Architecture: ${result.discovery?.architecture?.value ? `${result.discovery.architecture.value} [${result.discovery.architecture.confidence}]` : "unknown"}`,
|
|
48
|
+
];
|
|
49
|
+
return `${lines.join("\n")}\n`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { evaluateProgress, PROGRESS_STATUS } from "../core/progress.js";
|
|
2
|
+
import { readEvents } from "../core/events.js";
|
|
3
|
+
import { readWorkState } from "../core/work-state.js";
|
|
4
|
+
import { resolveTaskContext } from "../core/task-context.js";
|
|
5
|
+
|
|
6
|
+
export { evaluateProgress };
|
|
7
|
+
|
|
8
|
+
export async function runProgress({ target, packageRoot, taskId, task }) {
|
|
9
|
+
const resolved = await resolveTaskContext(target, { packageRoot, explicitTaskId: taskId ?? task });
|
|
10
|
+
const activeTaskId = resolved.taskId;
|
|
11
|
+
|
|
12
|
+
const state = await readWorkState(target, { packageRoot, taskId: activeTaskId });
|
|
13
|
+
const events = await readEvents(target, packageRoot, { taskId: activeTaskId });
|
|
14
|
+
|
|
15
|
+
const progress = evaluateProgress({ state, events });
|
|
16
|
+
return {
|
|
17
|
+
taskId: activeTaskId ?? state?.taskId ?? "unknown",
|
|
18
|
+
phase: state?.phase ?? "UNKNOWN",
|
|
19
|
+
verificationCycle: state?.verificationCycle ?? 1,
|
|
20
|
+
...progress,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatProgressResult(result) {
|
|
25
|
+
const lines = [
|
|
26
|
+
`FORGELOOP PROGRESS: ${result.status}`,
|
|
27
|
+
`PHASE: ${result.phase}`,
|
|
28
|
+
`CYCLE: ${result.verificationCycle}`,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (result.signals && result.signals.length > 0) {
|
|
32
|
+
lines.push("SIGNALS:");
|
|
33
|
+
for (const signal of result.signals) {
|
|
34
|
+
lines.push(`- ${signal.code}: ${signal.message}`);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
lines.push("SIGNALS: none");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let recommended = "NONE";
|
|
41
|
+
if (result.status === PROGRESS_STATUS.STALLED) {
|
|
42
|
+
recommended = "CHANGE_STRATEGY";
|
|
43
|
+
} else if (result.status === PROGRESS_STATUS.WATCH) {
|
|
44
|
+
recommended = "REVIEW_CHECKS";
|
|
45
|
+
} else {
|
|
46
|
+
recommended = "ADVANCE";
|
|
47
|
+
}
|
|
48
|
+
lines.push(`RECOMMENDED: ${recommended}`);
|
|
49
|
+
|
|
50
|
+
return lines.join("\n") + "\n";
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { runReconcileClosure } from "../core/reconcile-closure.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export async function reconcileClosure({
|
|
5
|
+
target,
|
|
6
|
+
packageRoot,
|
|
7
|
+
taskId,
|
|
8
|
+
task,
|
|
9
|
+
checkId,
|
|
10
|
+
checkRequirement,
|
|
11
|
+
checkDetails,
|
|
12
|
+
commandArgv,
|
|
13
|
+
authorityContext,
|
|
14
|
+
runtimeContext,
|
|
15
|
+
}) {
|
|
16
|
+
return withTaskMutation(
|
|
17
|
+
target,
|
|
18
|
+
{ taskId: taskId ?? task, packageRoot },
|
|
19
|
+
"reconcile-closure",
|
|
20
|
+
async (ctx) => {
|
|
21
|
+
return runReconcileClosure({
|
|
22
|
+
target,
|
|
23
|
+
packageRoot,
|
|
24
|
+
taskId: ctx?.taskId ?? null,
|
|
25
|
+
checkId,
|
|
26
|
+
requirement: checkRequirement,
|
|
27
|
+
argv: commandArgv,
|
|
28
|
+
details: checkDetails,
|
|
29
|
+
authorityContext,
|
|
30
|
+
runtimeContext,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
{ explicitRequired: true },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatReconcileClosureResult(result) {
|
|
38
|
+
const previous = result.previousRepositoryFingerprint;
|
|
39
|
+
return [
|
|
40
|
+
"FORGELOOP CHECKPOINT RECONCILED",
|
|
41
|
+
`task: ${result.taskId}`,
|
|
42
|
+
`check: ${result.checkId} (passed)`,
|
|
43
|
+
`execution: ${result.executionId}`,
|
|
44
|
+
`previous: ${previous?.branch ?? "unknown"} @ ${previous?.head ?? "unknown"}`,
|
|
45
|
+
`current: ${result.repositoryFingerprint?.branch ?? "unknown"} @ ${result.repositoryFingerprint?.head ?? "unknown"}`,
|
|
46
|
+
`event: ${result.event}`,
|
|
47
|
+
"",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { recordDecisionCriterion } from "../core/settlement.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export { recordDecisionCriterion };
|
|
5
|
+
|
|
6
|
+
export async function runRecordDecisionCriterion({
|
|
7
|
+
target,
|
|
8
|
+
packageRoot,
|
|
9
|
+
decision,
|
|
10
|
+
settledBy,
|
|
11
|
+
taskId,
|
|
12
|
+
task,
|
|
13
|
+
}) {
|
|
14
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "record-decision-criterion", async (ctx) => {
|
|
15
|
+
return recordDecisionCriterion({
|
|
16
|
+
target,
|
|
17
|
+
packageRoot,
|
|
18
|
+
decision,
|
|
19
|
+
settledBy,
|
|
20
|
+
taskId: ctx?.taskId ?? null,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatRecordDecisionCriterionResult(result) {
|
|
26
|
+
const c = result.criterion ?? result.event?.details ?? {};
|
|
27
|
+
return [
|
|
28
|
+
`FORGELOOP DECISION CRITERION RECORDED`,
|
|
29
|
+
`DECISION: ${c.decision}`,
|
|
30
|
+
`DECISION ID: ${c.decisionId}`,
|
|
31
|
+
`SETTLED BY: ${c.settledBy}`,
|
|
32
|
+
`CONTRACT FINGERPRINT: ${c.contractFingerprint}`,
|
|
33
|
+
].join("\n") + "\n";
|
|
34
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { recordDiagnosis } from "../core/diagnosis.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export { recordDiagnosis };
|
|
5
|
+
|
|
6
|
+
export async function runRecordDiagnosis({
|
|
7
|
+
target,
|
|
8
|
+
packageRoot,
|
|
9
|
+
hypothesis,
|
|
10
|
+
failureClass,
|
|
11
|
+
evidenceRefs = [],
|
|
12
|
+
evidenceRef = null,
|
|
13
|
+
settledBy,
|
|
14
|
+
nextSafeAction,
|
|
15
|
+
taskId,
|
|
16
|
+
task,
|
|
17
|
+
}) {
|
|
18
|
+
const refs = Array.isArray(evidenceRefs) && evidenceRefs.length > 0
|
|
19
|
+
? evidenceRefs
|
|
20
|
+
: (evidenceRef ? [evidenceRef] : []);
|
|
21
|
+
|
|
22
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "record-diagnosis", async (ctx) => {
|
|
23
|
+
return recordDiagnosis({
|
|
24
|
+
target,
|
|
25
|
+
packageRoot,
|
|
26
|
+
hypothesis,
|
|
27
|
+
failureClass,
|
|
28
|
+
evidenceRefs: refs,
|
|
29
|
+
settledBy,
|
|
30
|
+
nextSafeAction,
|
|
31
|
+
taskId: ctx?.taskId ?? null,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatRecordDiagnosisResult(result) {
|
|
37
|
+
const d = result.diagnosis ?? result.event?.details ?? {};
|
|
38
|
+
return [
|
|
39
|
+
`FORGELOOP DIAGNOSIS RECORDED`,
|
|
40
|
+
`CYCLE: ${d.verificationCycle}`,
|
|
41
|
+
`FAILURE CLASS: ${d.failureClass}`,
|
|
42
|
+
`HYPOTHESIS: ${d.hypothesis}`,
|
|
43
|
+
`INFORMATION GAIN: ${d.informationGain}`,
|
|
44
|
+
`EVIDENCE: ${(d.evidenceRefs ?? []).join(", ")}`,
|
|
45
|
+
`SETTLED BY: ${d.settledBy}`,
|
|
46
|
+
`NEXT SAFE ACTION: ${d.nextSafeAction}`,
|
|
47
|
+
`FINGERPRINT: ${d.diagnosisFingerprint}`,
|
|
48
|
+
].join("\n") + "\n";
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadEffectiveRules } from "../core/policy-engine.js";
|
|
2
|
+
import { verifyRuleMutation } from "../core/policy-mutation.js";
|
|
3
|
+
|
|
4
|
+
export async function runRuleVerify({
|
|
5
|
+
target = process.cwd(),
|
|
6
|
+
packageRoot,
|
|
7
|
+
rule = null,
|
|
8
|
+
} = {}) {
|
|
9
|
+
const rules = await loadEffectiveRules(target, packageRoot);
|
|
10
|
+
const targetRules = rule ? rules.filter((r) => r.id === rule) : rules;
|
|
11
|
+
|
|
12
|
+
const verifications = [];
|
|
13
|
+
for (const r of targetRules) {
|
|
14
|
+
const res = await verifyRuleMutation({ target, rule: r });
|
|
15
|
+
verifications.push(res);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const allProven = verifications.every((v) => v.status === "PROVEN" || v.status === "UNSUPPORTED");
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
status: allProven ? "VALID" : "UNPROVEN",
|
|
22
|
+
verifications,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatRuleVerifyResult(result) {
|
|
27
|
+
const lines = [
|
|
28
|
+
`FORGELOOP RULE VERIFICATION: ${result.status}`,
|
|
29
|
+
];
|
|
30
|
+
for (const v of result.verifications ?? []) {
|
|
31
|
+
lines.push(` - ${v.ruleId}: ${v.status} (Mutation: ${v.mutation ?? "none"}, Expected: ${v.expected ?? "n/a"}, Observed: ${v.observed ?? "n/a"})`);
|
|
32
|
+
if (v.why) lines.push(` Why: ${v.why}`);
|
|
33
|
+
if (v.fix && v.status !== "PROVEN") lines.push(` Fix: ${v.fix}`);
|
|
34
|
+
}
|
|
35
|
+
return `${lines.join("\n")}\n`;
|
|
36
|
+
}
|
|
@@ -2,9 +2,10 @@ import { assertSafePath, ensureWithin, readBytes } from "../core/filesystem.js";
|
|
|
2
2
|
import { validateReceipt } from "../core/receipt.js";
|
|
3
3
|
import { assertJsonBytes, assertJsonLimits } from "../core/json-safety.js";
|
|
4
4
|
import { ARTIFACT_PATHS } from "../core/artifacts.js";
|
|
5
|
+
import { taskArtifactPath } from "../core/task-paths.js";
|
|
6
|
+
import { withResolvedTask } from "../core/task-command.js";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const relativeFile = file ?? ARTIFACT_PATHS.receipt;
|
|
8
|
+
async function validateReceiptFile(target, packageRoot, relativeFile) {
|
|
8
9
|
await assertSafePath(target, relativeFile);
|
|
9
10
|
const receiptPath = ensureWithin(target, relativeFile);
|
|
10
11
|
let receipt;
|
|
@@ -16,5 +17,39 @@ export async function runValidateReceipt({ target, packageRoot, file }) {
|
|
|
16
17
|
} catch (error) {
|
|
17
18
|
throw new Error(`Unable to parse receipt ${relativeFile}: ${error.message}`);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
try {
|
|
21
|
+
return await validateReceipt(receipt, packageRoot);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Invalid receipt ${relativeFile}: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates an execution receipt with deterministic resolution precedence:
|
|
29
|
+
* 1. explicit `--file` validates exactly that relative file;
|
|
30
|
+
* 2. explicit or context-resolved `--task` validates that task's namespaced
|
|
31
|
+
* `.forgeloop/task-state/<taskKey>/execution-receipt.json`;
|
|
32
|
+
* 3. a single active task is resolved automatically through the shared
|
|
33
|
+
* task-command resolver;
|
|
34
|
+
* 4. when no task descriptors exist, the legacy singleton
|
|
35
|
+
* `.forgeloop/execution-receipt.json` compatibility path is preserved.
|
|
36
|
+
* Multiple active tasks without `--task`/`--file` fail with E_TASK_AMBIGUOUS
|
|
37
|
+
* through the shared resolver instead of silently falling back to the legacy
|
|
38
|
+
* singleton.
|
|
39
|
+
*/
|
|
40
|
+
export async function runValidateReceipt({
|
|
41
|
+
target,
|
|
42
|
+
packageRoot,
|
|
43
|
+
file = null,
|
|
44
|
+
taskId = null,
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (file) {
|
|
47
|
+
return validateReceiptFile(target, packageRoot, file);
|
|
48
|
+
}
|
|
49
|
+
return withResolvedTask(target, { taskId, packageRoot }, async (ctx) => {
|
|
50
|
+
const relativeFile = ctx
|
|
51
|
+
? taskArtifactPath(ctx.taskId, "receipt")
|
|
52
|
+
: ARTIFACT_PATHS.receipt;
|
|
53
|
+
return validateReceiptFile(target, packageRoot, relativeFile);
|
|
54
|
+
});
|
|
20
55
|
}
|
|
@@ -163,4 +163,64 @@ export const ARTIFACT_REGISTRY = Object.freeze({
|
|
|
163
163
|
isPersisted: true,
|
|
164
164
|
description: "Attested command execution provenance recording argv, cwd, resolution, status, and exit code.",
|
|
165
165
|
}),
|
|
166
|
+
policyRules: Object.freeze({
|
|
167
|
+
key: "policyRules",
|
|
168
|
+
scope: "PROJECT",
|
|
169
|
+
path: PROJECT_ARTIFACT_PATHS.policyRules,
|
|
170
|
+
schema: "policy-rules",
|
|
171
|
+
owner: "OPERATOR_OR_AGENT",
|
|
172
|
+
mutability: "MUTABLE_CONFIGURATION",
|
|
173
|
+
trustRole: "POLICY_SPECIFICATION",
|
|
174
|
+
isPublic: true,
|
|
175
|
+
isPersisted: true,
|
|
176
|
+
description: "Executable policy rule definitions declaring severities, checks, why, and fix guidance.",
|
|
177
|
+
}),
|
|
178
|
+
policyDiscovery: Object.freeze({
|
|
179
|
+
key: "policyDiscovery",
|
|
180
|
+
scope: "PROJECT",
|
|
181
|
+
path: PROJECT_ARTIFACT_PATHS.policyDiscovery,
|
|
182
|
+
schema: "policy-discovery",
|
|
183
|
+
owner: "PROTOCOL_GENERATED",
|
|
184
|
+
mutability: "MUTABLE_ON_DISCOVERY",
|
|
185
|
+
trustRole: "DISCOVERED_POLICY_SPECIFICATION",
|
|
186
|
+
isPublic: true,
|
|
187
|
+
isPersisted: true,
|
|
188
|
+
description: "Deterministic repository policy discovery report with confidence scoring.",
|
|
189
|
+
}),
|
|
190
|
+
policyBaseline: Object.freeze({
|
|
191
|
+
key: "policyBaseline",
|
|
192
|
+
scope: "PROJECT",
|
|
193
|
+
path: PROJECT_ARTIFACT_PATHS.policyBaseline,
|
|
194
|
+
schema: "policy-baseline",
|
|
195
|
+
owner: "PROTOCOL_GENERATED_OR_OPERATOR",
|
|
196
|
+
mutability: "MONOTONIC_RATCHET_DOWN",
|
|
197
|
+
trustRole: "BROWNFIELD_BASELINE",
|
|
198
|
+
isPublic: true,
|
|
199
|
+
isPersisted: true,
|
|
200
|
+
description: "Brownfield policy baseline recording existing debt with sha256 violation fingerprints.",
|
|
201
|
+
}),
|
|
202
|
+
policyLock: Object.freeze({
|
|
203
|
+
key: "policyLock",
|
|
204
|
+
scope: "PROJECT",
|
|
205
|
+
path: PROJECT_ARTIFACT_PATHS.policyLock,
|
|
206
|
+
schema: "policy-lock",
|
|
207
|
+
owner: "PROTOCOL_GENERATED",
|
|
208
|
+
mutability: "ATOMIC_DIGEST_COMPILATION",
|
|
209
|
+
trustRole: "POLICY_INTEGRITY_LOCK",
|
|
210
|
+
isPublic: true,
|
|
211
|
+
isPersisted: true,
|
|
212
|
+
description: "Canonical policy lockfile holding cryptographic digest of effective rules and baseline.",
|
|
213
|
+
}),
|
|
214
|
+
policySnapshot: Object.freeze({
|
|
215
|
+
key: "policySnapshot",
|
|
216
|
+
scope: "TASK",
|
|
217
|
+
path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.policySnapshot}`,
|
|
218
|
+
schema: "policy-snapshot",
|
|
219
|
+
owner: "PROTOCOL_GENERATED",
|
|
220
|
+
mutability: "MUTABLE_BEFORE_EXECUTION",
|
|
221
|
+
trustRole: "TASK_POLICY_ATTESTATION",
|
|
222
|
+
isPublic: true,
|
|
223
|
+
isPersisted: true,
|
|
224
|
+
description: "Task-scoped policy snapshot binding task activation to effective policy digest.",
|
|
225
|
+
}),
|
|
166
226
|
});
|
package/src/core/audit.js
CHANGED
|
@@ -131,6 +131,29 @@ export async function evaluateAudit({
|
|
|
131
131
|
: blocked
|
|
132
132
|
? "INCOMPLETE"
|
|
133
133
|
: "INVALID";
|
|
134
|
+
let policyStatus = null;
|
|
135
|
+
const { detectPolicyCapability, evaluateTargetPolicy } = await import("./policy-engine.js");
|
|
136
|
+
const policyCapability = await detectPolicyCapability(target, packageRoot);
|
|
137
|
+
if (policyCapability === "AVAILABLE") {
|
|
138
|
+
try {
|
|
139
|
+
const policyEval = await evaluateTargetPolicy({ target, packageRoot, taskId });
|
|
140
|
+
policyStatus = {
|
|
141
|
+
status: policyEval.status,
|
|
142
|
+
provenRules: policyEval.provenRules,
|
|
143
|
+
inertRules: policyEval.inertRules,
|
|
144
|
+
unsupportedRules: policyEval.unsupportedRules,
|
|
145
|
+
baselineViolations: policyEval.baselineViolations,
|
|
146
|
+
drift: policyEval.drift?.detected ?? false,
|
|
147
|
+
};
|
|
148
|
+
} catch {
|
|
149
|
+
policyStatus = { status: "INVALID", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
|
|
150
|
+
}
|
|
151
|
+
} else if (policyCapability === "INVALID") {
|
|
152
|
+
policyStatus = { status: "INVALID", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
|
|
153
|
+
} else {
|
|
154
|
+
policyStatus = { status: "NOT_APPLICABLE", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
return {
|
|
135
158
|
schemaVersion: 1,
|
|
136
159
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -142,6 +165,7 @@ export async function evaluateAudit({
|
|
|
142
165
|
status: manifest ? "ready" : manifestError ? "invalid" : "missing",
|
|
143
166
|
manifest: Boolean(manifest),
|
|
144
167
|
},
|
|
168
|
+
policy: policyStatus,
|
|
145
169
|
completion,
|
|
146
170
|
changedPaths,
|
|
147
171
|
publicationStatus: completion.publicationStatus,
|