@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.
Files changed (100) 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 +5 -4
  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 +22 -4
  99. package/src/core/work-state.js +41 -13
  100. /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
+ }
@@ -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
- bytes: await readFile(path.join(packageRoot, relativePath)),
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
  }
@@ -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 createHash("sha256")
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