@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readContract } from "./contract.js";
|
|
2
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
3
|
+
import {
|
|
4
|
+
assertDecisionCriterionDetails,
|
|
5
|
+
criterionForDecision,
|
|
6
|
+
decisionCriterionEvents,
|
|
7
|
+
decisionId,
|
|
8
|
+
normalizeDecisionCriterionInput,
|
|
9
|
+
normalizeDecisionText,
|
|
10
|
+
} from "./settlement-model.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
assertDecisionCriterionDetails,
|
|
14
|
+
criterionForDecision,
|
|
15
|
+
decisionCriterionEvents,
|
|
16
|
+
decisionId,
|
|
17
|
+
normalizeDecisionCriterionInput,
|
|
18
|
+
normalizeDecisionText,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function recordDecisionCriterion({
|
|
22
|
+
target,
|
|
23
|
+
packageRoot,
|
|
24
|
+
decision,
|
|
25
|
+
settledBy,
|
|
26
|
+
taskId = null,
|
|
27
|
+
contractPath = null,
|
|
28
|
+
eventsPath = null,
|
|
29
|
+
}) {
|
|
30
|
+
const normalized = normalizeDecisionCriterionInput({ decision, settledBy });
|
|
31
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
32
|
+
if (!contract || !contract.value) {
|
|
33
|
+
const error = new Error("Current contract not found");
|
|
34
|
+
error.code = "E_CONTRACT_MISSING";
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const unresolved = contract.value.unresolvedDecisions ?? [];
|
|
39
|
+
if (!unresolved.includes(normalized.decision)) {
|
|
40
|
+
const error = new Error(`Decision "${normalized.decision}" is not present in current unresolvedDecisions`);
|
|
41
|
+
error.code = "E_DECISION_NOT_UNRESOLVED";
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: taskId ?? null, eventsPath });
|
|
46
|
+
if (!ledger.valid) {
|
|
47
|
+
const first = ledger.errors[0];
|
|
48
|
+
const error = new Error(first.message);
|
|
49
|
+
error.code = first.code;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const decId = decisionId(normalized.decision);
|
|
54
|
+
const details = {
|
|
55
|
+
decision: normalized.decision,
|
|
56
|
+
decisionId: decId,
|
|
57
|
+
settledBy: normalized.settledBy,
|
|
58
|
+
contractFingerprint: contract.fingerprint,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
assertDecisionCriterionDetails(details);
|
|
62
|
+
|
|
63
|
+
const event = await appendProtocolEvent(
|
|
64
|
+
target,
|
|
65
|
+
{
|
|
66
|
+
taskId: contract.value.taskId,
|
|
67
|
+
event: "DECISION_CRITERION_RECORDED",
|
|
68
|
+
details,
|
|
69
|
+
},
|
|
70
|
+
packageRoot,
|
|
71
|
+
{ taskId: taskId ?? null, eventsPath },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
event,
|
|
76
|
+
criterion: details,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/core/task-context.js
CHANGED
|
@@ -71,6 +71,17 @@ export async function resolveTaskContext(target, {
|
|
|
71
71
|
const tasks = await discoverTasks(target, packageRoot);
|
|
72
72
|
const healthyTasks = tasks.filter((t) => t.healthy !== false);
|
|
73
73
|
|
|
74
|
+
// Fail closed: modern task namespaces exist but are all corrupt/unhealthy.
|
|
75
|
+
// Never fall back to legacy singleton state over corrupt modern state.
|
|
76
|
+
if (tasks.length > 0 && healthyTasks.length === 0) {
|
|
77
|
+
const firstInvalid = tasks.find((t) => t.healthy === false);
|
|
78
|
+
const error = new Error(
|
|
79
|
+
firstInvalid?.error?.message ?? "Task namespaces exist but none are valid",
|
|
80
|
+
);
|
|
81
|
+
error.code = firstInvalid?.error?.code ?? "E_TASK_DESCRIPTOR_INVALID";
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
if (healthyTasks.length === 1) {
|
|
75
86
|
const single = healthyTasks[0];
|
|
76
87
|
return createTaskContext({
|
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { ensureWithin, fileExists } from "./filesystem.js";
|
|
3
3
|
import { getPackageRoot } from "./templates.js";
|
|
4
|
-
import { TASK_STATE_ROOT, taskArtifactPath } from "./task-paths.js";
|
|
4
|
+
import { TASK_STATE_ROOT, TASK_ARTIFACT_FILES, taskArtifactPath } from "./task-paths.js";
|
|
5
5
|
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
6
6
|
import { readJsonArtifact } from "./artifacts.js";
|
|
7
7
|
import { readLockInfo } from "./task-lock.js";
|
|
8
8
|
import { taskStorageKey } from "./task-identity.js";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Explicitly recognized legacy-incidental artifacts that may legitimately
|
|
12
|
+
* exist inside a 64-hex task-state directory WITHOUT a task.json descriptor.
|
|
13
|
+
* A legacy preflight writes a task-scoped policy snapshot for a task that has
|
|
14
|
+
* no modern namespace; that directory is not a task namespace. Any other
|
|
15
|
+
* content makes the directory a corrupt modern task namespace that must fail
|
|
16
|
+
* closed.
|
|
17
|
+
*/
|
|
18
|
+
const LEGACY_INCIDENTAL_ARTIFACTS = new Set([
|
|
19
|
+
TASK_ARTIFACT_FILES.policySnapshot,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Classifies a 64-hex task-state directory whose task.json is missing.
|
|
24
|
+
*
|
|
25
|
+
* - directory contains only explicitly recognized legacy-incidental
|
|
26
|
+
* artifacts (policy-snapshot.json) -> LEGACY_INCIDENTAL (ignored)
|
|
27
|
+
* - anything else, including an empty directory -> CORRUPT_TASK_NAMESPACE
|
|
28
|
+
* (no positive evidence of legitimate legacy spillover)
|
|
29
|
+
*/
|
|
30
|
+
async function classifyDescriptorlessTaskDirectory(target, taskKey) {
|
|
31
|
+
const directory = ensureWithin(target, `${TASK_STATE_ROOT}/${taskKey}`);
|
|
32
|
+
let entries = [];
|
|
33
|
+
try {
|
|
34
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
35
|
+
} catch {
|
|
36
|
+
return {
|
|
37
|
+
kind: "CORRUPT_TASK_NAMESPACE",
|
|
38
|
+
error: {
|
|
39
|
+
code: "E_TASK_DESCRIPTOR_INVALID",
|
|
40
|
+
message: `Task namespace ${taskKey} is missing task.json`,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
45
|
+
if (names.size > 0 && [...names].every((name) => LEGACY_INCIDENTAL_ARTIFACTS.has(name))) {
|
|
46
|
+
return { kind: "LEGACY_INCIDENTAL" };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: "CORRUPT_TASK_NAMESPACE",
|
|
50
|
+
error: {
|
|
51
|
+
code: "E_TASK_DESCRIPTOR_INVALID",
|
|
52
|
+
message: `Task namespace ${taskKey} contains task artifacts but task.json is missing`,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
10
57
|
export async function discoverTasks(target, packageRoot = getPackageRoot()) {
|
|
11
58
|
const rootPath = ensureWithin(target, TASK_STATE_ROOT);
|
|
12
59
|
if (!(await fileExists(rootPath))) {
|
|
@@ -92,6 +139,25 @@ export async function discoverTasks(target, packageRoot = getPackageRoot()) {
|
|
|
92
139
|
directory: `${TASK_STATE_ROOT}/${descriptor.taskKey}`,
|
|
93
140
|
});
|
|
94
141
|
} catch (err) {
|
|
142
|
+
// A directory without a task.json descriptor is not automatically a
|
|
143
|
+
// task namespace: classify by contents so explicitly recognized legacy
|
|
144
|
+
// spillover (policy-snapshot.json) stays compatible while any modern
|
|
145
|
+
// task artifact without a descriptor fails closed instead of silently
|
|
146
|
+
// reopening the legacy singleton fallback.
|
|
147
|
+
if (err.code === "E_TASK_NOT_FOUND" || err.code === "ARTIFACT_MISSING") {
|
|
148
|
+
const classification = await classifyDescriptorlessTaskDirectory(target, entry.name);
|
|
149
|
+
if (classification.kind === "LEGACY_INCIDENTAL") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
tasks.push({
|
|
153
|
+
taskId: null,
|
|
154
|
+
taskKey: entry.name,
|
|
155
|
+
directory: `${TASK_STATE_ROOT}/${entry.name}`,
|
|
156
|
+
healthy: false,
|
|
157
|
+
error: classification.error,
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
95
161
|
// P1-2: Surface corrupt task namespaces instead of silently hiding them
|
|
96
162
|
tasks.push({
|
|
97
163
|
taskId: null,
|
package/src/core/task-paths.js
CHANGED
|
@@ -15,14 +15,22 @@ export const TASK_ARTIFACT_FILES = Object.freeze({
|
|
|
15
15
|
gates: "gates",
|
|
16
16
|
executions: "executions",
|
|
17
17
|
lock: ".lock",
|
|
18
|
+
policySnapshot: "policy-snapshot.json",
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
export const POLICY_ROOT = ".forgeloop/policy";
|
|
22
|
+
|
|
20
23
|
export const PROJECT_ARTIFACT_PATHS = Object.freeze({
|
|
21
24
|
config: ".forgeloop/config.json",
|
|
22
25
|
sources: ".forgeloop/sources.json",
|
|
23
26
|
manifest: ".forgeloop/.manifest.json",
|
|
24
27
|
kit: ".forgeloop/kit",
|
|
25
28
|
gitignore: ".forgeloop/.gitignore",
|
|
29
|
+
policyDir: ".forgeloop/policy",
|
|
30
|
+
policyRules: ".forgeloop/policy/rules.json",
|
|
31
|
+
policyBaseline: ".forgeloop/policy/baseline.json",
|
|
32
|
+
policyLock: ".forgeloop/policy/policy.lock",
|
|
33
|
+
policyDiscovery: ".forgeloop/policy/discovery.json",
|
|
26
34
|
});
|
|
27
35
|
|
|
28
36
|
export const LEGACY_TASK_ARTIFACT_PATHS = Object.freeze({
|
|
@@ -92,5 +100,6 @@ export function buildTaskArtifactPaths(taskId) {
|
|
|
92
100
|
gates: `${dir}/${TASK_ARTIFACT_FILES.gates}`,
|
|
93
101
|
executions: `${dir}/${TASK_ARTIFACT_FILES.executions}`,
|
|
94
102
|
lock: `${dir}/${TASK_ARTIFACT_FILES.lock}`,
|
|
103
|
+
policySnapshot: `${dir}/${TASK_ARTIFACT_FILES.policySnapshot}`,
|
|
95
104
|
});
|
|
96
105
|
}
|
package/src/core/templates.js
CHANGED
|
@@ -58,6 +58,11 @@ export const TEMPLATE_PATHS = [
|
|
|
58
58
|
"schemas/event.schema.json",
|
|
59
59
|
"schemas/activation.schema.json",
|
|
60
60
|
"schemas/policy.schema.json",
|
|
61
|
+
"schemas/policy-rules.schema.json",
|
|
62
|
+
"schemas/policy-discovery.schema.json",
|
|
63
|
+
"schemas/policy-baseline.schema.json",
|
|
64
|
+
"schemas/policy-lock.schema.json",
|
|
65
|
+
"schemas/policy-snapshot.schema.json",
|
|
61
66
|
"schemas/task-bundle.schema.json",
|
|
62
67
|
"schemas/authority.schema.json",
|
|
63
68
|
"schemas/task-descriptor.schema.json",
|