@cassiomc1/forgeloop 0.1.0 → 0.1.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 +7 -1
- package/.github/copilot-instructions.md +6 -0
- package/AGENTS.md +9 -1
- package/AGENT_COMPATIBILITY.md +14 -1
- package/CLAUDE.md +7 -1
- package/ENG/accessibility-eng.md +5 -0
- package/ENG/clean-code-eng.md +1 -0
- package/ENG/design-code-eng.md +5 -0
- package/ENG/games-code-design-web-eng.md +6 -0
- package/ENG/perf-code-eng.md +3 -0
- package/ENG/premium-sites-studio-eng.md +9 -0
- package/ENG/sec-code-eng.md +5 -0
- package/ENG/test-code-eng.md +3 -0
- package/GUIDE_ROUTER.md +16 -0
- package/LOOP_ENGINEERING.md +139 -4
- package/ORCHESTRATOR_INTEGRATION.md +15 -0
- package/QUALITY_SCORECARD.md +4 -0
- package/README.md +43 -1
- package/THREAT_MODEL.md +6 -0
- package/conformance/README.md +42 -0
- package/conformance/backend-auth/EXPECTED_ROUTE.json +7 -0
- package/conformance/backend-auth/REQUEST.md +4 -0
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/backend-auth/REQUIRED_GATES.json +3 -0
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/blind-premium-website/REQUEST.md +6 -0
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +14 -0
- package/conformance/blind-premium-website/REQUIRED_GATES.json +3 -0
- package/conformance/complete-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/complete-website/REQUEST.md +6 -0
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/complete-website/REQUIRED_GATES.json +3 -0
- package/conformance/docs-only/EXPECTED_ROUTE.json +7 -0
- package/conformance/docs-only/REQUEST.md +4 -0
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/docs-only/REQUIRED_GATES.json +3 -0
- package/conformance/runs/2026-08-11-codex-first-live.md +98 -0
- package/conformance/runs/2026-08-11-codex-second-live.md +87 -0
- package/conformance/simple-bug/EXPECTED_ROUTE.json +7 -0
- package/conformance/simple-bug/REQUEST.md +4 -0
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/simple-bug/REQUIRED_GATES.json +3 -0
- package/package.json +5 -4
- package/schemas/activation.schema.json +15 -0
- package/schemas/check.schema.json +23 -0
- package/schemas/config.schema.json +16 -0
- package/schemas/current-contract.schema.json +50 -0
- package/schemas/event.schema.json +20 -0
- package/schemas/evidence-coverage.schema.json +17 -0
- package/schemas/execution-receipt.schema.json +10 -0
- package/schemas/gate.schema.json +32 -0
- package/schemas/policy.schema.json +19 -0
- package/schemas/preflight.schema.json +23 -0
- package/schemas/routing-result.schema.json +1 -0
- package/schemas/source-registry.schema.json +29 -0
- package/schemas/task-bundle.schema.json +14 -0
- package/schemas/work-state.schema.json +7 -1
- package/src/cli.js +230 -9
- package/src/commands/activate.js +11 -0
- package/src/commands/advance.js +12 -0
- package/src/commands/audit.js +12 -0
- package/src/commands/bundle.js +10 -0
- package/src/commands/complete.js +18 -0
- package/src/commands/policy.js +19 -0
- package/src/commands/preflight.js +12 -0
- package/src/commands/prepare-completion.js +18 -0
- package/src/commands/record-check.js +20 -0
- package/src/commands/report.js +10 -0
- package/src/commands/route.js +14 -2
- package/src/commands/validate-receipt.js +7 -6
- package/src/core/activation.js +16 -0
- package/src/core/artifacts.js +132 -0
- package/src/core/audit.js +88 -0
- package/src/core/bundles.js +120 -0
- package/src/core/checks.js +115 -0
- package/src/core/completion-artifacts.js +281 -0
- package/src/core/completion.js +295 -0
- package/src/core/config.js +35 -0
- package/src/core/conformance.js +7 -0
- package/src/core/contract.js +141 -0
- package/src/core/coverage.js +89 -0
- package/src/core/events.js +124 -0
- package/src/core/gate-artifact.js +54 -0
- package/src/core/gates.js +55 -0
- package/src/core/guide-metadata.js +62 -0
- package/src/core/phase.js +88 -0
- package/src/core/policies.js +69 -0
- package/src/core/preflight.js +246 -0
- package/src/core/profile.js +48 -0
- package/src/core/protocol.js +34 -0
- package/src/core/receipt.js +40 -1
- package/src/core/report.js +49 -0
- package/src/core/repository.js +18 -0
- package/src/core/route-artifact.js +33 -0
- package/src/core/router.js +16 -8
- package/src/core/schema-validation.js +11 -0
- package/src/core/sources.js +86 -0
- package/src/core/templates.js +22 -4
- package/src/core/work-state.js +41 -13
- /package/.forgeloop/{.gitignore → forgeloop.gitignore} +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
|
|
3
|
+
export const SOURCE_REGISTRY_SCHEMA_VERSION = 1;
|
|
4
|
+
export const SOURCE_KINDS = Object.freeze([
|
|
5
|
+
"user-request",
|
|
6
|
+
"repository-fact",
|
|
7
|
+
"observation",
|
|
8
|
+
"command",
|
|
9
|
+
"agent-decision",
|
|
10
|
+
"inference",
|
|
11
|
+
"unknown",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function createSourceRegistry(sources = {}) {
|
|
15
|
+
if (!sources || typeof sources !== "object" || Array.isArray(sources)) {
|
|
16
|
+
throw new Error("Source registry must be an object");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: SOURCE_REGISTRY_SCHEMA_VERSION,
|
|
20
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
21
|
+
sources: structuredClone(sources),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertSourceKind(kind, label = "source.kind") {
|
|
26
|
+
if (!SOURCE_KINDS.includes(kind)) throw new Error(`${label} must be one of ${SOURCE_KINDS.join(", ")}`);
|
|
27
|
+
return kind;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertSourceRefs(registry, refs = []) {
|
|
31
|
+
const ids = new Set(Object.keys(registry?.sources ?? {}));
|
|
32
|
+
for (const ref of refs) {
|
|
33
|
+
if (!ids.has(ref)) {
|
|
34
|
+
const error = new Error(`Unknown source ID: ${ref}`);
|
|
35
|
+
error.code = "E_PROFILE_SOURCE_UNKNOWN";
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assertSourceRegistry(registry) {
|
|
43
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
|
|
44
|
+
const error = new Error("Source registry must be an object");
|
|
45
|
+
error.code = "E_PROFILE_SOURCE_INVALID";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
if (registry.schemaVersion !== SOURCE_REGISTRY_SCHEMA_VERSION || registry.protocolVersion !== PROTOCOL_VERSION) {
|
|
49
|
+
const error = new Error("Source registry has an unsupported protocol version");
|
|
50
|
+
error.code = "E_PROFILE_SOURCE_INVALID";
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
for (const [id, source] of Object.entries(registry.sources ?? {})) {
|
|
54
|
+
if (!id || !source || typeof source !== "object" || Array.isArray(source)) {
|
|
55
|
+
const error = new Error(`Invalid source entry: ${id}`);
|
|
56
|
+
error.code = "E_PROFILE_SOURCE_INVALID";
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
assertSourceKind(source.kind, `sources.${id}.kind`);
|
|
60
|
+
if (typeof source.summary !== "string" || source.summary.trim() === "") {
|
|
61
|
+
const error = new Error(`Source summary is required: ${id}`);
|
|
62
|
+
error.code = "E_PROFILE_SOURCE_INVALID";
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function assertSourceProvenance(registry, refs = [], { expectedKind } = {}) {
|
|
70
|
+
assertSourceRegistry(registry);
|
|
71
|
+
assertSourceRefs(registry, refs);
|
|
72
|
+
const expectedKinds = expectedKind === undefined
|
|
73
|
+
? null
|
|
74
|
+
: new Set(Array.isArray(expectedKind) ? expectedKind : [expectedKind]);
|
|
75
|
+
if (expectedKinds) {
|
|
76
|
+
for (const ref of refs) {
|
|
77
|
+
const source = registry.sources[ref];
|
|
78
|
+
if (!expectedKinds.has(source.kind)) {
|
|
79
|
+
const error = new Error(`Source ${ref} has kind ${source.kind}, expected ${[...expectedKinds].join(", ")}`);
|
|
80
|
+
error.code = "E_PROFILE_SOURCE_MISCLASSIFIED";
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
package/src/core/templates.js
CHANGED
|
@@ -4,6 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
|
|
5
5
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
6
6
|
|
|
7
|
+
const TEMPLATE_SOURCE_PATHS = Object.freeze({
|
|
8
|
+
".forgeloop/.gitignore": ".forgeloop/forgeloop.gitignore",
|
|
9
|
+
});
|
|
10
|
+
|
|
7
11
|
export const TEMPLATE_PATHS = [
|
|
8
12
|
".forgeloop/.gitignore",
|
|
9
13
|
"AGENTS.md",
|
|
@@ -40,6 +44,17 @@ export const TEMPLATE_PATHS = [
|
|
|
40
44
|
"schemas/task-brief.schema.json",
|
|
41
45
|
"schemas/delegated-result.schema.json",
|
|
42
46
|
"schemas/evidence.schema.json",
|
|
47
|
+
"schemas/current-contract.schema.json",
|
|
48
|
+
"schemas/gate.schema.json",
|
|
49
|
+
"schemas/source-registry.schema.json",
|
|
50
|
+
"schemas/config.schema.json",
|
|
51
|
+
"schemas/preflight.schema.json",
|
|
52
|
+
"schemas/check.schema.json",
|
|
53
|
+
"schemas/evidence-coverage.schema.json",
|
|
54
|
+
"schemas/event.schema.json",
|
|
55
|
+
"schemas/activation.schema.json",
|
|
56
|
+
"schemas/policy.schema.json",
|
|
57
|
+
"schemas/task-bundle.schema.json",
|
|
43
58
|
];
|
|
44
59
|
|
|
45
60
|
export function getPackageRoot() {
|
|
@@ -48,9 +63,12 @@ export function getPackageRoot() {
|
|
|
48
63
|
|
|
49
64
|
export async function readTemplateEntries(packageRoot = PACKAGE_ROOT) {
|
|
50
65
|
return Promise.all(
|
|
51
|
-
TEMPLATE_PATHS.map(async (relativePath) =>
|
|
52
|
-
relativePath
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
TEMPLATE_PATHS.map(async (relativePath) => {
|
|
67
|
+
const sourcePath = TEMPLATE_SOURCE_PATHS[relativePath] ?? relativePath;
|
|
68
|
+
return {
|
|
69
|
+
relativePath,
|
|
70
|
+
bytes: await readFile(path.join(packageRoot, sourcePath)),
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
55
73
|
);
|
|
56
74
|
}
|
package/src/core/work-state.js
CHANGED
|
@@ -16,6 +16,8 @@ import { getPackageRoot } from "./templates.js";
|
|
|
16
16
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
17
17
|
import { createEvidence } from "./evidence.js";
|
|
18
18
|
import { assertJsonBytes } from "./json-safety.js";
|
|
19
|
+
import { assertCoverageList } from "./coverage.js";
|
|
20
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
19
21
|
|
|
20
22
|
export const WORK_STATE_PATH = ".forgeloop/work-state.json";
|
|
21
23
|
|
|
@@ -27,20 +29,8 @@ export class WorkStateError extends Error {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function canonicalize(value) {
|
|
31
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
32
|
-
if (value && typeof value === "object") {
|
|
33
|
-
return Object.fromEntries(
|
|
34
|
-
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
export function contractFingerprint(contract) {
|
|
41
|
-
return
|
|
42
|
-
.update(JSON.stringify(canonicalize(contract)))
|
|
43
|
-
.digest("hex");
|
|
33
|
+
return canonicalFingerprint(contract);
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
function assertFingerprint(value, label) {
|
|
@@ -101,6 +91,7 @@ export function assertWorkStateSemantics(state) {
|
|
|
101
91
|
throw new WorkStateError("Work state taskId is required");
|
|
102
92
|
}
|
|
103
93
|
assertFingerprint(state.contractFingerprint, "contractFingerprint");
|
|
94
|
+
if (state.routeFingerprint !== undefined) assertFingerprint(state.routeFingerprint, "routeFingerprint");
|
|
104
95
|
if (!state.repositoryFingerprint || typeof state.repositoryFingerprint !== "object" || Array.isArray(state.repositoryFingerprint)) {
|
|
105
96
|
throw new WorkStateError("repositoryFingerprint is required");
|
|
106
97
|
}
|
|
@@ -117,6 +108,34 @@ export function assertWorkStateSemantics(state) {
|
|
|
117
108
|
if (new Set(state.selectedGuides).size !== state.selectedGuides.length) {
|
|
118
109
|
throw new WorkStateError("selectedGuides must not contain duplicates");
|
|
119
110
|
}
|
|
111
|
+
for (const key of ["requiredGates", "satisfiedGates"]) {
|
|
112
|
+
if (state[key] !== undefined) {
|
|
113
|
+
assertStringArray(state[key], key);
|
|
114
|
+
if (new Set(state[key]).size !== state[key].length) {
|
|
115
|
+
throw new WorkStateError(`${key} must not contain duplicates`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (state.satisfiedGates?.some((gate) => !state.requiredGates?.includes(gate))) {
|
|
120
|
+
throw new WorkStateError("satisfiedGates must be a subset of requiredGates");
|
|
121
|
+
}
|
|
122
|
+
if (state.complianceMode !== undefined && !["advisory", "standard", "strict"].includes(state.complianceMode)) {
|
|
123
|
+
throw new WorkStateError(`Unknown compliance mode: ${state.complianceMode}`);
|
|
124
|
+
}
|
|
125
|
+
if (state.publicationStatus !== undefined
|
|
126
|
+
&& !["not-published", "local-only", "committed", "pushed", "published", "deployed"].includes(state.publicationStatus)) {
|
|
127
|
+
throw new WorkStateError(`Unknown publication status: ${state.publicationStatus}`);
|
|
128
|
+
}
|
|
129
|
+
if (state.evidenceCoverage !== undefined && !Array.isArray(state.evidenceCoverage)) {
|
|
130
|
+
throw new WorkStateError("evidenceCoverage must be an array");
|
|
131
|
+
}
|
|
132
|
+
if (state.evidenceCoverage !== undefined) {
|
|
133
|
+
try {
|
|
134
|
+
assertCoverageList(state.evidenceCoverage);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new WorkStateError(error.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
120
139
|
assertStringArray(state.completedSteps, "completedSteps");
|
|
121
140
|
assertStringArray(state.pendingSteps, "pendingSteps");
|
|
122
141
|
normalizeRequiredArtifacts(state.requiredArtifacts);
|
|
@@ -126,6 +145,9 @@ export function assertWorkStateSemantics(state) {
|
|
|
126
145
|
if (state.phase === "COMPLETE" && state.verificationEvidence.length === 0) {
|
|
127
146
|
throw new WorkStateError("COMPLETE requires verification evidence");
|
|
128
147
|
}
|
|
148
|
+
if (state.phase === "COMPLETE" && state.evidenceCoverage?.some((item) => item.status !== "COVERED")) {
|
|
149
|
+
throw new WorkStateError("COMPLETE requires covered evidence for every recorded criterion");
|
|
150
|
+
}
|
|
129
151
|
if (state.phase === "BLOCKED" && state.blockers.length === 0) {
|
|
130
152
|
throw new WorkStateError("BLOCKED requires a blocker");
|
|
131
153
|
}
|
|
@@ -151,9 +173,15 @@ export function createWorkState(input) {
|
|
|
151
173
|
protocolVersion: PROTOCOL_VERSION,
|
|
152
174
|
taskId: input.taskId,
|
|
153
175
|
contractFingerprint: input.contractFingerprint,
|
|
176
|
+
...(input.routeFingerprint !== undefined ? { routeFingerprint: input.routeFingerprint } : {}),
|
|
154
177
|
repositoryFingerprint: input.repositoryFingerprint ?? { branch: null, head: null },
|
|
155
178
|
phase: input.phase,
|
|
156
179
|
selectedGuides: [...(input.selectedGuides ?? [])],
|
|
180
|
+
...(input.requiredGates !== undefined ? { requiredGates: [...input.requiredGates] } : {}),
|
|
181
|
+
...(input.satisfiedGates !== undefined ? { satisfiedGates: [...input.satisfiedGates] } : {}),
|
|
182
|
+
...(input.complianceMode !== undefined ? { complianceMode: input.complianceMode } : {}),
|
|
183
|
+
...(input.evidenceCoverage !== undefined ? { evidenceCoverage: [...input.evidenceCoverage] } : {}),
|
|
184
|
+
...(input.publicationStatus !== undefined ? { publicationStatus: input.publicationStatus } : {}),
|
|
157
185
|
completedSteps: [...(input.completedSteps ?? [])],
|
|
158
186
|
pendingSteps: [...(input.pendingSteps ?? [])],
|
|
159
187
|
requiredArtifacts: normalizeRequiredArtifacts(input.requiredArtifacts),
|
|
File without changes
|