@cassiomc1/forgeloop 0.1.1 → 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.
Files changed (99) hide show
  1. package/.cursor/rules/project-loop.mdc +7 -1
  2. package/.github/copilot-instructions.md +6 -0
  3. package/AGENTS.md +9 -1
  4. package/AGENT_COMPATIBILITY.md +14 -1
  5. package/CLAUDE.md +7 -1
  6. package/ENG/accessibility-eng.md +5 -0
  7. package/ENG/clean-code-eng.md +1 -0
  8. package/ENG/design-code-eng.md +5 -0
  9. package/ENG/games-code-design-web-eng.md +6 -0
  10. package/ENG/perf-code-eng.md +3 -0
  11. package/ENG/premium-sites-studio-eng.md +9 -0
  12. package/ENG/sec-code-eng.md +5 -0
  13. package/ENG/test-code-eng.md +3 -0
  14. package/GUIDE_ROUTER.md +16 -0
  15. package/LOOP_ENGINEERING.md +139 -4
  16. package/ORCHESTRATOR_INTEGRATION.md +15 -0
  17. package/QUALITY_SCORECARD.md +4 -0
  18. package/README.md +43 -1
  19. package/THREAT_MODEL.md +6 -0
  20. package/conformance/README.md +42 -0
  21. package/conformance/backend-auth/EXPECTED_ROUTE.json +7 -0
  22. package/conformance/backend-auth/REQUEST.md +4 -0
  23. package/conformance/backend-auth/REQUIRED_EVIDENCE.json +3 -0
  24. package/conformance/backend-auth/REQUIRED_GATES.json +3 -0
  25. package/conformance/blind-premium-website/EXPECTED_ROUTE.json +7 -0
  26. package/conformance/blind-premium-website/REQUEST.md +6 -0
  27. package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +14 -0
  28. package/conformance/blind-premium-website/REQUIRED_GATES.json +3 -0
  29. package/conformance/complete-website/EXPECTED_ROUTE.json +7 -0
  30. package/conformance/complete-website/REQUEST.md +6 -0
  31. package/conformance/complete-website/REQUIRED_EVIDENCE.json +3 -0
  32. package/conformance/complete-website/REQUIRED_GATES.json +3 -0
  33. package/conformance/docs-only/EXPECTED_ROUTE.json +7 -0
  34. package/conformance/docs-only/REQUEST.md +4 -0
  35. package/conformance/docs-only/REQUIRED_EVIDENCE.json +3 -0
  36. package/conformance/docs-only/REQUIRED_GATES.json +3 -0
  37. package/conformance/runs/2026-08-11-codex-first-live.md +98 -0
  38. package/conformance/runs/2026-08-11-codex-second-live.md +87 -0
  39. package/conformance/simple-bug/EXPECTED_ROUTE.json +7 -0
  40. package/conformance/simple-bug/REQUEST.md +4 -0
  41. package/conformance/simple-bug/REQUIRED_EVIDENCE.json +3 -0
  42. package/conformance/simple-bug/REQUIRED_GATES.json +3 -0
  43. package/package.json +4 -3
  44. package/schemas/activation.schema.json +15 -0
  45. package/schemas/check.schema.json +23 -0
  46. package/schemas/config.schema.json +16 -0
  47. package/schemas/current-contract.schema.json +50 -0
  48. package/schemas/event.schema.json +20 -0
  49. package/schemas/evidence-coverage.schema.json +17 -0
  50. package/schemas/execution-receipt.schema.json +10 -0
  51. package/schemas/gate.schema.json +32 -0
  52. package/schemas/policy.schema.json +19 -0
  53. package/schemas/preflight.schema.json +23 -0
  54. package/schemas/routing-result.schema.json +1 -0
  55. package/schemas/source-registry.schema.json +29 -0
  56. package/schemas/task-bundle.schema.json +14 -0
  57. package/schemas/work-state.schema.json +7 -1
  58. package/src/cli.js +230 -9
  59. package/src/commands/activate.js +11 -0
  60. package/src/commands/advance.js +12 -0
  61. package/src/commands/audit.js +12 -0
  62. package/src/commands/bundle.js +10 -0
  63. package/src/commands/complete.js +18 -0
  64. package/src/commands/policy.js +19 -0
  65. package/src/commands/preflight.js +12 -0
  66. package/src/commands/prepare-completion.js +18 -0
  67. package/src/commands/record-check.js +20 -0
  68. package/src/commands/report.js +10 -0
  69. package/src/commands/route.js +14 -2
  70. package/src/commands/validate-receipt.js +7 -6
  71. package/src/core/activation.js +16 -0
  72. package/src/core/artifacts.js +132 -0
  73. package/src/core/audit.js +88 -0
  74. package/src/core/bundles.js +120 -0
  75. package/src/core/checks.js +115 -0
  76. package/src/core/completion-artifacts.js +281 -0
  77. package/src/core/completion.js +295 -0
  78. package/src/core/config.js +35 -0
  79. package/src/core/conformance.js +7 -0
  80. package/src/core/contract.js +141 -0
  81. package/src/core/coverage.js +89 -0
  82. package/src/core/events.js +124 -0
  83. package/src/core/gate-artifact.js +54 -0
  84. package/src/core/gates.js +55 -0
  85. package/src/core/guide-metadata.js +62 -0
  86. package/src/core/phase.js +88 -0
  87. package/src/core/policies.js +69 -0
  88. package/src/core/preflight.js +246 -0
  89. package/src/core/profile.js +48 -0
  90. package/src/core/protocol.js +34 -0
  91. package/src/core/receipt.js +40 -1
  92. package/src/core/report.js +49 -0
  93. package/src/core/repository.js +18 -0
  94. package/src/core/route-artifact.js +33 -0
  95. package/src/core/router.js +16 -8
  96. package/src/core/schema-validation.js +11 -0
  97. package/src/core/sources.js +86 -0
  98. package/src/core/templates.js +11 -0
  99. package/src/core/work-state.js +41 -13
@@ -0,0 +1,141 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
+ import { assertSchema, readSchema } from "./schema-validation.js";
4
+ import { assertSecretFree } from "./receipt.js";
5
+
6
+ export const CONTRACT_SCHEMA_VERSION = 1;
7
+
8
+ const CONTRACT_ARRAY_FIELDS = Object.freeze([
9
+ "deliverables",
10
+ "constraints",
11
+ "risks",
12
+ "verification",
13
+ "successCriteria",
14
+ "stopConditions",
15
+ "unresolvedDecisions",
16
+ "sourceRefs",
17
+ ]);
18
+
19
+ function assertStringArray(value, label) {
20
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
21
+ throw new Error(`${label} must be an array of non-empty strings`);
22
+ }
23
+ }
24
+
25
+ const ASSUMPTION_FIELDS = Object.freeze([
26
+ "value",
27
+ "reason",
28
+ "scope",
29
+ "reversible",
30
+ "source",
31
+ ]);
32
+
33
+ function assertAssumptionObject(value, label) {
34
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new Error(`${label} must be a plain object`);
36
+ }
37
+
38
+ const keys = Object.keys(value);
39
+ const unexpected = keys.filter((key) => !ASSUMPTION_FIELDS.includes(key));
40
+ if (unexpected.length > 0) {
41
+ throw new Error(`${label} contains unknown property: ${unexpected[0]}`);
42
+ }
43
+
44
+ for (const field of ASSUMPTION_FIELDS) {
45
+ if (!Object.prototype.hasOwnProperty.call(value, field)) {
46
+ throw new Error(`${label}.${field} is required`);
47
+ }
48
+ }
49
+
50
+ if (typeof value.value !== "string" || value.value.trim() === "") {
51
+ throw new Error(`${label}.value must be a non-empty string`);
52
+ }
53
+ if (typeof value.reason !== "string" || value.reason.trim() === "") {
54
+ throw new Error(`${label}.reason must be a non-empty string`);
55
+ }
56
+ if (typeof value.scope !== "string" || value.scope.trim() === "") {
57
+ throw new Error(`${label}.scope must be a non-empty string`);
58
+ }
59
+ if (value.reversible !== true) {
60
+ throw new Error(`${label}.reversible must be true`);
61
+ }
62
+ if (value.source !== "agent-default") {
63
+ throw new Error(`${label}.source must be agent-default`);
64
+ }
65
+ }
66
+
67
+ export function assertAssumptions(value, label = "Contract assumptions") {
68
+ if (!Array.isArray(value)) {
69
+ throw new Error(`${label} must be an array`);
70
+ }
71
+ value.forEach((item, index) => {
72
+ assertAssumptionObject(item, `${label}[${index}]`);
73
+ });
74
+ return value;
75
+ }
76
+
77
+ export function createContract(input = {}) {
78
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
79
+ throw new Error("Contract must be a JSON object");
80
+ }
81
+ if (typeof input.taskId !== "string" || input.taskId.trim() === "") {
82
+ throw new Error("Contract taskId is required");
83
+ }
84
+ if (typeof input.objective !== "string" || input.objective.trim() === "") {
85
+ throw new Error("Contract objective is required");
86
+ }
87
+
88
+ const contract = {
89
+ schemaVersion: CONTRACT_SCHEMA_VERSION,
90
+ protocolVersion: PROTOCOL_VERSION,
91
+ taskId: input.taskId,
92
+ objective: input.objective,
93
+ assumptions: [],
94
+ };
95
+ assertAssumptions(input.assumptions ?? []);
96
+ for (const field of CONTRACT_ARRAY_FIELDS) {
97
+ const value = input[field] ?? [];
98
+ assertStringArray(value, `Contract ${field}`);
99
+ contract[field] = [...value];
100
+ }
101
+ contract.assumptions = (input.assumptions ?? []).map((assumption) => ({
102
+ value: assumption.value,
103
+ reason: assumption.reason,
104
+ scope: assumption.scope,
105
+ reversible: assumption.reversible,
106
+ source: assumption.source,
107
+ }));
108
+ assertSecretFree(contract);
109
+ return contract;
110
+ }
111
+
112
+ export function contractFingerprint(contract) {
113
+ return canonicalFingerprint(contract);
114
+ }
115
+
116
+ export async function validateContract(contract, packageRoot) {
117
+ assertSecretFree(contract);
118
+ assertAssumptions(contract.assumptions ?? []);
119
+ const schema = await readSchema("current-contract", packageRoot);
120
+ assertSchema(contract, schema, "current contract");
121
+ return contract;
122
+ }
123
+
124
+ export async function readContract(target, packageRoot) {
125
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.contract, "current-contract", packageRoot);
126
+ await validateContract(artifact.value, packageRoot);
127
+ return artifact;
128
+ }
129
+
130
+ export async function writeContract(target, contract, packageRoot, options = {}) {
131
+ assertSecretFree(contract);
132
+ assertAssumptions(contract.assumptions ?? []);
133
+ return writeJsonArtifact(
134
+ target,
135
+ ARTIFACT_PATHS.contract,
136
+ contract,
137
+ "current-contract",
138
+ packageRoot,
139
+ options,
140
+ );
141
+ }
@@ -0,0 +1,89 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+
3
+ export const COVERAGE_SCHEMA_VERSION = 1;
4
+ export const COVERAGE_STATUSES = Object.freeze(["COVERED", "PARTIAL", "NOT_VERIFIED", "BLOCKED"]);
5
+
6
+ function coverageError(code, message) {
7
+ const error = new Error(message);
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function stringArray(value, label) {
13
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
14
+ throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an array of non-empty strings`);
15
+ }
16
+ return [...new Set(value)];
17
+ }
18
+
19
+ export function evaluateCoverage(requiredEvidence, observedEvidence, { blocked = false } = {}) {
20
+ const required = stringArray(requiredEvidence, "requiredEvidence");
21
+ const observed = stringArray(observedEvidence, "observedEvidence");
22
+ if (blocked) return "BLOCKED";
23
+ if (required.length === 0 || required.every((item) => observed.includes(item))) return "COVERED";
24
+ if (required.some((item) => observed.includes(item))) return "PARTIAL";
25
+ return "NOT_VERIFIED";
26
+ }
27
+
28
+ export function createCoverage(input = {}) {
29
+ const requiredEvidence = stringArray(input.requiredEvidence ?? [], "requiredEvidence");
30
+ const observedEvidence = stringArray(input.observedEvidence ?? [], "observedEvidence");
31
+ const status = input.status ?? evaluateCoverage(requiredEvidence, observedEvidence, { blocked: input.blocked === true });
32
+ const value = {
33
+ schemaVersion: COVERAGE_SCHEMA_VERSION,
34
+ protocolVersion: PROTOCOL_VERSION,
35
+ requirement: input.requirement,
36
+ requiredEvidence,
37
+ observedEvidence,
38
+ status,
39
+ ...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
40
+ };
41
+ return assertCoverage(value);
42
+ }
43
+
44
+ export function assertCoverage(value, label = "evidence coverage") {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
46
+ throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an object`);
47
+ }
48
+ if (value.schemaVersion !== COVERAGE_SCHEMA_VERSION || value.protocolVersion !== PROTOCOL_VERSION) {
49
+ throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} has an unsupported protocol version`);
50
+ }
51
+ if (typeof value.requirement !== "string" || value.requirement.trim() === "") {
52
+ throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label}.requirement is required`);
53
+ }
54
+ const requiredEvidence = stringArray(value.requiredEvidence, `${label}.requiredEvidence`);
55
+ const observedEvidence = stringArray(value.observedEvidence, `${label}.observedEvidence`);
56
+ if (!COVERAGE_STATUSES.includes(value.status)) {
57
+ throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label}.status is invalid`);
58
+ }
59
+ const expected = evaluateCoverage(requiredEvidence, observedEvidence, {
60
+ blocked: value.status === "BLOCKED",
61
+ });
62
+ if (value.status !== expected && !(value.status === "BLOCKED" && expected === "BLOCKED")) {
63
+ throw coverageError("E_EVIDENCE_COVERAGE_PARTIAL", `${label}.status does not match its observed evidence`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ export function assertCoverageList(value, label = "evidenceCoverage") {
69
+ if (!Array.isArray(value)) throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an array`);
70
+ value.forEach((item, index) => assertCoverage(item, `${label}[${index}]`));
71
+ return value;
72
+ }
73
+
74
+ export function coverageForRequirements(requirements, checks, { blockedIds = [] } = {}) {
75
+ const normalizedRequirements = stringArray(requirements ?? [], "requirements");
76
+ const normalizedChecks = Array.isArray(checks) ? checks : [];
77
+ const observed = new Set(
78
+ normalizedChecks
79
+ .filter((check) => check?.status === "passed" && check.evidenceKind === "OBSERVED")
80
+ .flatMap((check) => [check.id, check.kind, check.requirement]),
81
+ );
82
+ const blocked = new Set(blockedIds);
83
+ return normalizedRequirements.map((requirement) => createCoverage({
84
+ requirement,
85
+ requiredEvidence: [requirement],
86
+ observedEvidence: observed.has(requirement) ? [requirement] : [],
87
+ blocked: blocked.has(requirement),
88
+ }));
89
+ }
@@ -0,0 +1,124 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
6
+ import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
7
+ import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
8
+ import { assertSchema, readSchema } from "./schema-validation.js";
9
+ import { assertSecretFree } from "./receipt.js";
10
+ import { PROTOCOL_VERSION } from "./protocol.js";
11
+
12
+ const EVENT_SCHEMA_VERSION = 1;
13
+
14
+ function eventHash(event) {
15
+ const { hash, ...body } = event;
16
+ return canonicalFingerprint(body);
17
+ }
18
+
19
+ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
20
+ const error = new Error(message);
21
+ error.code = code;
22
+ error.artifacts = artifacts;
23
+ return error;
24
+ }
25
+
26
+ async function readEvents(target, packageRoot) {
27
+ await assertSafePath(target, ARTIFACT_PATHS.events);
28
+ const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
29
+ if (!(await fileExists(eventsPath))) return [];
30
+ const text = await readFile(eventsPath, "utf8");
31
+ assertJsonBytes(text, ARTIFACT_PATHS.events);
32
+ const schema = await readSchema("event", packageRoot);
33
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
34
+ return lines.map((line, index) => {
35
+ let event;
36
+ try {
37
+ event = JSON.parse(line);
38
+ assertJsonLimits(event, `${ARTIFACT_PATHS.events}[${index}]`);
39
+ assertSchema(event, schema, `${ARTIFACT_PATHS.events}[${index}]`);
40
+ } catch (error) {
41
+ throw protocolError("E_EVENT_INVALID", `${ARTIFACT_PATHS.events} line ${index + 1}: ${error.message}`);
42
+ }
43
+ return event;
44
+ });
45
+ }
46
+
47
+ export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
48
+ if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
49
+ if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
50
+ const events = await readEvents(target, packageRoot);
51
+ const previous = events.at(-1) ?? null;
52
+ const event = {
53
+ seq: events.length + 1,
54
+ schemaVersion: EVENT_SCHEMA_VERSION,
55
+ protocolVersion: PROTOCOL_VERSION,
56
+ taskId: input.taskId,
57
+ event: input.event,
58
+ at: input.at ?? new Date().toISOString(),
59
+ ...(input.fingerprint ? { fingerprint: input.fingerprint } : {}),
60
+ previousHash: previous?.hash ?? null,
61
+ ...(input.details ? { details: structuredClone(input.details) } : {}),
62
+ };
63
+ assertSecretFree(event);
64
+ const schema = await readSchema("event", packageRoot);
65
+ assertSchema(event, schema, ARTIFACT_PATHS.events);
66
+ event.hash = eventHash(event);
67
+ const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
68
+ if (!options.dryRun) {
69
+ await mkdir(path.dirname(eventsPath), { recursive: true });
70
+ await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
71
+ }
72
+ return event;
73
+ }
74
+
75
+ export async function validateEventLedger(target, packageRoot) {
76
+ let events;
77
+ try {
78
+ events = await readEvents(target, packageRoot);
79
+ } catch (error) {
80
+ return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message }] };
81
+ }
82
+ const errors = [];
83
+ let taskId = null;
84
+ const seen = new Set();
85
+ for (const [index, event] of events.entries()) {
86
+ if (event.seq !== index + 1) {
87
+ errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
88
+ }
89
+ if (taskId === null) taskId = event.taskId;
90
+ if (event.taskId !== taskId) errors.push({ code: "E_EVENT_INVALID", message: "event task IDs must remain stable" });
91
+ if (event.previousHash !== (index === 0 ? null : events[index - 1].hash)) {
92
+ errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} previousHash does not match` });
93
+ }
94
+ if (event.hash !== eventHash(event)) {
95
+ errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
96
+ }
97
+ seen.add(event.event);
98
+ if (event.event === "EXECUTION_STARTED" && !seen.has("ROUTE_VALIDATED")) {
99
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
100
+ }
101
+ if (event.event === "EXECUTION_STARTED" && !seen.has("CONTRACT_VALIDATED")) {
102
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
103
+ }
104
+ if (event.event === "EXECUTION_STARTED" && !seen.has("PREFLIGHT_READY")) {
105
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
106
+ }
107
+ if (event.event === "EXECUTION_STARTED") {
108
+ const preflight = events.slice(0, index).findLast((candidate) => candidate.event === "PREFLIGHT_READY");
109
+ const requiredGates = preflight?.details?.requiredGates ?? [];
110
+ const satisfiedGates = new Set(events.slice(0, index)
111
+ .filter((candidate) => candidate.event === "GATE_SATISFIED")
112
+ .map((candidate) => candidate.details?.gate));
113
+ for (const gate of requiredGates) {
114
+ if (!satisfiedGates.has(gate)) {
115
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
116
+ }
117
+ }
118
+ }
119
+ if (event.event === "COMPLETION_VALIDATED" && !seen.has("VERIFICATION_RECORDED")) {
120
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion validated before verification evidence" });
121
+ }
122
+ }
123
+ return { valid: errors.length === 0, events, errors };
124
+ }
@@ -0,0 +1,54 @@
1
+ import path from "node:path";
2
+
3
+ import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
4
+ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
5
+ import { assertSchema, readSchema } from "./schema-validation.js";
6
+ import { sha256 } from "./manifest.js";
7
+
8
+ function gateName(value) {
9
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) {
10
+ const error = new Error(`Invalid gate name: ${value}`);
11
+ error.code = "E_GATE_INVALID";
12
+ throw error;
13
+ }
14
+ return value;
15
+ }
16
+
17
+ export function gatePath(gate) {
18
+ return `${ARTIFACT_PATHS.gates ?? ".forgeloop/gates"}/${gateName(gate)}.json`;
19
+ }
20
+
21
+ export async function persistGate(target, gate, packageRoot, options = {}) {
22
+ const relativePath = gatePath(gate.gate);
23
+ return writeJsonArtifact(target, relativePath, gate, "gate", packageRoot, options);
24
+ }
25
+
26
+ export async function readGate(target, gate, packageRoot) {
27
+ const relativePath = gatePath(gate);
28
+ return readJsonArtifact(target, relativePath, "gate", packageRoot);
29
+ }
30
+
31
+ export async function readGateIfPresent(target, gate, packageRoot) {
32
+ const relativePath = gatePath(gate);
33
+ await assertSafePath(target, relativePath);
34
+ const filePath = ensureWithin(target, relativePath);
35
+ if (!(await fileExists(filePath))) return null;
36
+ return readJsonArtifact(target, relativePath, "gate", packageRoot);
37
+ }
38
+
39
+ export async function validateGateArtifacts(target, gateValue, packageRoot) {
40
+ const schema = await readSchema("gate", packageRoot);
41
+ assertSchema(gateValue, schema, "gate");
42
+ const stale = [];
43
+ for (const artifact of gateValue.artifacts ?? []) {
44
+ await assertSafePath(target, artifact.path);
45
+ const artifactPath = ensureWithin(target, artifact.path);
46
+ if (!(await fileExists(artifactPath))) {
47
+ stale.push({ path: artifact.path, status: "missing" });
48
+ continue;
49
+ }
50
+ const digest = sha256(await readBytes(artifactPath));
51
+ if (digest !== artifact.sha256) stale.push({ path: artifact.path, status: "changed" });
52
+ }
53
+ return stale;
54
+ }
@@ -0,0 +1,55 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import path from "node:path";
3
+
4
+ export const GATE_SCHEMA_VERSION = 1;
5
+ export const GATE_STATUSES = Object.freeze(["satisfied", "unverified", "blocked"]);
6
+
7
+ function stringArray(value, label) {
8
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
9
+ throw new Error(`${label} must be an array of non-empty strings`);
10
+ }
11
+ return [...value];
12
+ }
13
+
14
+ function artifactList(value) {
15
+ if (!Array.isArray(value)) throw new Error("Gate artifacts must be an array");
16
+ return value.map((artifact) => {
17
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
18
+ throw new Error("Gate artifact entries must be objects");
19
+ }
20
+ if (typeof artifact.path !== "string" || !artifact.path) throw new Error("Gate artifact path is required");
21
+ const portable = artifact.path.replaceAll("\\", "/");
22
+ const normalized = path.posix.normalize(portable);
23
+ if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || normalized === ".." || normalized.startsWith("../")) {
24
+ throw new Error(`Gate artifact path escapes the target: ${artifact.path}`);
25
+ }
26
+ if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256)) {
27
+ throw new Error(`Gate artifact hash is invalid: ${artifact.path}`);
28
+ }
29
+ return { path: normalized.replace(/^\.\//, ""), sha256: artifact.sha256 };
30
+ });
31
+ }
32
+
33
+ export function createGate(input = {}) {
34
+ if (typeof input.taskId !== "string" || !input.taskId) throw new Error("Gate taskId is required");
35
+ if (typeof input.gate !== "string" || !input.gate) throw new Error("Gate name is required");
36
+ if (!GATE_STATUSES.includes(input.status)) throw new Error(`Unknown gate status: ${input.status}`);
37
+ const artifacts = artifactList(input.artifacts ?? []);
38
+ return {
39
+ schemaVersion: GATE_SCHEMA_VERSION,
40
+ protocolVersion: PROTOCOL_VERSION,
41
+ taskId: input.taskId,
42
+ gate: input.gate,
43
+ status: input.status,
44
+ requiredBy: stringArray(input.requiredBy ?? [], "Gate requiredBy"),
45
+ artifacts,
46
+ decisions: stringArray(input.decisions ?? [], "Gate decisions"),
47
+ unknowns: stringArray(input.unknowns ?? [], "Gate unknowns"),
48
+ approvedAssumptions: stringArray(input.approvedAssumptions ?? [], "Gate approvedAssumptions"),
49
+ evidence: Array.isArray(input.evidence) ? input.evidence.map((item) => ({ ...item })) : [],
50
+ };
51
+ }
52
+
53
+ export function requiredGatePaths(gates) {
54
+ return [...new Set(gates)].sort().map((gate) => `.forgeloop/gates/${gate}.json`);
55
+ }
@@ -0,0 +1,62 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const GUIDE_FILES = Object.freeze({
5
+ premium: "ENG/premium-sites-studio-eng.md",
6
+ clean: "ENG/clean-code-eng.md",
7
+ test: "ENG/test-code-eng.md",
8
+ security: "ENG/sec-code-eng.md",
9
+ design: "ENG/design-code-eng.md",
10
+ performance: "ENG/perf-code-eng.md",
11
+ accessibility: "ENG/accessibility-eng.md",
12
+ games: "ENG/games-code-design-web-eng.md",
13
+ });
14
+
15
+ function parseList(frontmatter, key) {
16
+ const lines = frontmatter.split(/\r?\n/);
17
+ const start = lines.findIndex((line) => line.trim() === `${key}:`);
18
+ if (start < 0) return [];
19
+ const values = [];
20
+ for (const line of lines.slice(start + 1)) {
21
+ if (!/^\s+-\s+/.test(line)) break;
22
+ values.push(line.replace(/^\s+-\s+/, "").trim());
23
+ }
24
+ return values.filter(Boolean);
25
+ }
26
+
27
+ function parseFrontmatter(text, relativePath) {
28
+ const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
29
+ if (!match) throw new Error(`${relativePath} is missing YAML frontmatter`);
30
+ const frontmatter = match[1];
31
+ const guideId = frontmatter.match(/^guide-id:\s*([^\s]+)\s*$/m)?.[1] ?? null;
32
+ return {
33
+ guideId,
34
+ requiresGates: parseList(frontmatter, "requires-gates"),
35
+ completionEvidence: parseList(frontmatter, "completion-evidence"),
36
+ };
37
+ }
38
+
39
+ export async function readGuideMetadata(packageRoot) {
40
+ const metadata = {};
41
+ for (const [id, relativePath] of Object.entries(GUIDE_FILES)) {
42
+ const text = await readFile(path.join(packageRoot, relativePath), "utf8");
43
+ const parsed = parseFrontmatter(text, relativePath);
44
+ metadata[id] = {
45
+ guideId: parsed.guideId ?? id,
46
+ requiresGates: [...parsed.requiresGates],
47
+ completionEvidence: [...parsed.completionEvidence],
48
+ path: relativePath,
49
+ };
50
+ }
51
+ return metadata;
52
+ }
53
+
54
+ export async function requiredGatesForGuides(guides, packageRoot) {
55
+ const metadata = await readGuideMetadata(packageRoot);
56
+ return [...new Set(guides.flatMap((guide) => metadata[guide]?.requiresGates ?? []))].sort();
57
+ }
58
+
59
+ export async function completionEvidenceForGuides(guides, packageRoot) {
60
+ const metadata = await readGuideMetadata(packageRoot);
61
+ return [...new Set(guides.flatMap((guide) => metadata[guide]?.completionEvidence ?? []))].sort();
62
+ }
@@ -0,0 +1,88 @@
1
+ import { ARTIFACT_PATHS } from "./artifacts.js";
2
+ import { readJsonArtifact } from "./artifacts.js";
3
+ import { readContract } from "./contract.js";
4
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
5
+ import { readPersistedRoute } from "./route-artifact.js";
6
+ import { assertWorkPhase, isValidTransition } from "./protocol.js";
7
+ import { readWorkState, writeWorkState } from "./work-state.js";
8
+ import { evaluateCompletion } from "./completion.js";
9
+
10
+ function phaseError(code, message, artifacts = []) {
11
+ const error = new Error(message);
12
+ error.code = code;
13
+ error.artifacts = artifacts;
14
+ return error;
15
+ }
16
+
17
+ const PHASE_EVENTS = Object.freeze({
18
+ CONTRACT_READY: "CONTRACT_VALIDATED",
19
+ ROUTED: "ROUTE_VALIDATED",
20
+ DESIGNING: "DESIGN_GATE_STARTED",
21
+ PLANNED: "PLAN_RECORDED",
22
+ EXECUTING: "EXECUTION_STARTED",
23
+ VERIFYING: "VERIFICATION_STARTED",
24
+ COMPLETE: "COMPLETION_VALIDATED",
25
+ });
26
+
27
+ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
28
+ if (toPhase === "CONTRACT_READY" || toPhase === "ROUTED" || toPhase === "EXECUTING") {
29
+ try {
30
+ await readContract(target, packageRoot);
31
+ } catch (error) {
32
+ throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${ARTIFACT_PATHS.contract}: ${error.message}`, [ARTIFACT_PATHS.contract]);
33
+ }
34
+ }
35
+ if (toPhase === "ROUTED" || toPhase === "EXECUTING") {
36
+ try {
37
+ await readPersistedRoute(target, packageRoot);
38
+ } catch (error) {
39
+ throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${ARTIFACT_PATHS.route}: ${error.message}`, [ARTIFACT_PATHS.route]);
40
+ }
41
+ }
42
+ if (toPhase === "EXECUTING") {
43
+ let preflight;
44
+ try {
45
+ preflight = await readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot);
46
+ } catch (error) {
47
+ throw phaseError("E_PHASE_PREREQUISITE_MISSING", `EXECUTING requires READY ${ARTIFACT_PATHS.preflight}: ${error.message}`, [ARTIFACT_PATHS.preflight]);
48
+ }
49
+ if (preflight.value.status !== "READY") {
50
+ throw phaseError("E_PREFLIGHT_NOT_READY", "EXECUTING requires a READY preflight", [ARTIFACT_PATHS.preflight]);
51
+ }
52
+ const ledger = await validateEventLedger(target, packageRoot);
53
+ for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED", "PREFLIGHT_READY"]) {
54
+ if (!ledger.events.some((event) => event.event === requiredEvent)) {
55
+ throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `EXECUTING requires a ${requiredEvent} protocol event`, [ARTIFACT_PATHS.events]);
56
+ }
57
+ }
58
+ }
59
+ if (toPhase === "COMPLETE" && state.verificationEvidence.length === 0) {
60
+ throw phaseError("E_PHASE_EVIDENCE_MISSING", "COMPLETE requires verification evidence");
61
+ }
62
+ if (toPhase === "COMPLETE") {
63
+ const completion = await evaluateCompletion({ target, packageRoot, persist: false });
64
+ if (completion.status !== "VALID") {
65
+ throw phaseError("E_COMPLETION_REJECTED", "COMPLETE requires a valid completion audit", completion.errors.flatMap((error) => error.artifacts ?? []));
66
+ }
67
+ }
68
+ }
69
+
70
+ export async function advanceWorkState(target, toPhase, { packageRoot, now = new Date().toISOString() } = {}) {
71
+ assertWorkPhase(toPhase);
72
+ const state = await readWorkState(target, packageRoot);
73
+ if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
74
+ await assertPhasePrerequisites(target, state, toPhase, packageRoot);
75
+ if (!isValidTransition(state.phase, toPhase)) {
76
+ throw phaseError("E_PHASE_TRANSITION_INVALID", `Invalid work-state transition: ${state.phase} -> ${toPhase}`);
77
+ }
78
+ const next = {
79
+ ...state,
80
+ previousPhase: state.phase,
81
+ phase: toPhase,
82
+ lastUpdated: now,
83
+ };
84
+ await writeWorkState(target, next, { packageRoot });
85
+ const eventType = PHASE_EVENTS[toPhase];
86
+ if (eventType) await appendProtocolEvent(target, { taskId: state.taskId, event: eventType, at: now }, packageRoot);
87
+ return next;
88
+ }
@@ -0,0 +1,69 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { assertSchema, readSchema } from "./schema-validation.js";
3
+
4
+ export const POLICY_SCHEMA_VERSION = 1;
5
+
6
+ const POLICIES = Object.freeze({
7
+ "web-premium": {
8
+ name: "web-premium",
9
+ description: "Strict completion for premium website delivery.",
10
+ complianceMode: "strict",
11
+ requiredGates: ["design", "quality"],
12
+ requiredEvidence: ["responsive-validation", "accessibility-validation", "build", "visual-validation"],
13
+ allowedCompletionStates: ["COMPLETE"],
14
+ freshnessRequired: true,
15
+ },
16
+ bugfix: {
17
+ name: "bugfix",
18
+ description: "Standard completion with diagnosis and regression evidence.",
19
+ complianceMode: "standard",
20
+ requiredGates: ["diagnosis", "regression"],
21
+ requiredEvidence: ["reproduction", "regression"],
22
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
23
+ freshnessRequired: true,
24
+ },
25
+ "security-critical": {
26
+ name: "security-critical",
27
+ description: "Strict completion with a declared trust boundary and observed security evidence.",
28
+ complianceMode: "strict",
29
+ requiredGates: ["threat-boundary", "security-verification"],
30
+ requiredEvidence: ["security-validation", "negative-test"],
31
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
32
+ freshnessRequired: true,
33
+ },
34
+ prototype: {
35
+ name: "prototype",
36
+ description: "Advisory local work with explicit non-production semantics.",
37
+ complianceMode: "advisory",
38
+ requiredGates: [],
39
+ requiredEvidence: [],
40
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
41
+ freshnessRequired: false,
42
+ },
43
+ });
44
+
45
+ export function getPolicy(name) {
46
+ const value = POLICIES[name];
47
+ if (!value) {
48
+ const error = new Error(`Unknown policy pack: ${name}`);
49
+ error.code = "E_POLICY_UNKNOWN";
50
+ throw error;
51
+ }
52
+ return {
53
+ schemaVersion: POLICY_SCHEMA_VERSION,
54
+ protocolVersion: PROTOCOL_VERSION,
55
+ ...value,
56
+ requiredGates: [...value.requiredGates],
57
+ requiredEvidence: [...value.requiredEvidence],
58
+ allowedCompletionStates: [...value.allowedCompletionStates],
59
+ };
60
+ }
61
+
62
+ export function listPolicies() {
63
+ return Object.keys(POLICIES).sort().map(getPolicy);
64
+ }
65
+
66
+ export async function validatePolicy(policy, packageRoot) {
67
+ const schema = await readSchema("policy", packageRoot);
68
+ return assertSchema(policy, schema, "policy");
69
+ }