@cassiomc1/forgeloop 0.1.0

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 (63) hide show
  1. package/.cursor/rules/project-loop.mdc +18 -0
  2. package/.forgeloop/.gitignore +2 -0
  3. package/.github/copilot-instructions.md +16 -0
  4. package/AGENTS.md +16 -0
  5. package/AGENT_COMPATIBILITY.md +147 -0
  6. package/CLAUDE.md +14 -0
  7. package/CONTRACT_COVERAGE.md +27 -0
  8. package/DELEGATION_PROTOCOL.md +91 -0
  9. package/ENG/accessibility-eng.md +155 -0
  10. package/ENG/clean-code-eng.md +223 -0
  11. package/ENG/design-code-eng.md +511 -0
  12. package/ENG/games-code-design-web-eng.md +751 -0
  13. package/ENG/perf-code-eng.md +441 -0
  14. package/ENG/premium-sites-studio-eng.md +320 -0
  15. package/ENG/sec-code-eng.md +706 -0
  16. package/ENG/test-code-eng.md +257 -0
  17. package/EXECUTION_STATE.md +107 -0
  18. package/GUIDE_ROUTER.md +274 -0
  19. package/LICENSE +21 -0
  20. package/LICENSE-DOCS.md +13 -0
  21. package/LOOP_ENGINEERING.md +551 -0
  22. package/LOOP_SYSTEM_DESIGN.md +394 -0
  23. package/ORCHESTRATOR_INTEGRATION.md +106 -0
  24. package/PROJECT_PROFILE.md +124 -0
  25. package/QUALITY_SCORECARD.md +54 -0
  26. package/README.md +492 -0
  27. package/TERMINOLOGY.md +21 -0
  28. package/THIRD_PARTY_NOTICES.md +129 -0
  29. package/THREAT_MODEL.md +35 -0
  30. package/package.json +51 -0
  31. package/schemas/delegated-result.schema.json +33 -0
  32. package/schemas/evidence.schema.json +15 -0
  33. package/schemas/execution-receipt.schema.json +46 -0
  34. package/schemas/routing-input.schema.json +17 -0
  35. package/schemas/routing-result.schema.json +17 -0
  36. package/schemas/task-brief.schema.json +24 -0
  37. package/schemas/work-state.schema.json +46 -0
  38. package/src/cli.js +341 -0
  39. package/src/commands/clear-state.js +11 -0
  40. package/src/commands/doctor.js +165 -0
  41. package/src/commands/init.js +42 -0
  42. package/src/commands/inspect.js +17 -0
  43. package/src/commands/route.js +32 -0
  44. package/src/commands/status.js +29 -0
  45. package/src/commands/update.js +109 -0
  46. package/src/commands/validate-protocol.js +133 -0
  47. package/src/commands/validate-receipt.js +19 -0
  48. package/src/commands/validate-state.js +30 -0
  49. package/src/core/agent-support.js +89 -0
  50. package/src/core/conformance.js +133 -0
  51. package/src/core/delegation.js +283 -0
  52. package/src/core/evidence.js +56 -0
  53. package/src/core/filesystem.js +122 -0
  54. package/src/core/inspect.js +115 -0
  55. package/src/core/json-safety.js +54 -0
  56. package/src/core/manifest.js +75 -0
  57. package/src/core/protocol.js +81 -0
  58. package/src/core/receipt.js +129 -0
  59. package/src/core/repository.js +19 -0
  60. package/src/core/router.js +296 -0
  61. package/src/core/schema-validation.js +179 -0
  62. package/src/core/templates.js +56 -0
  63. package/src/core/work-state.js +471 -0
@@ -0,0 +1,165 @@
1
+ import { assertSafePath, fileExists, ensureWithin, readBytes } from "../core/filesystem.js";
2
+ import { readManifest, sha256, writeManifest } from "../core/manifest.js";
3
+ import { readTemplateEntries } from "../core/templates.js";
4
+ import { createEvidence } from "../core/evidence.js";
5
+
6
+ function finding(code, severity, relativePath, message, remediation = null, evidence = null) {
7
+ const evidenceRecord = evidence && typeof evidence === "object"
8
+ ? evidence
9
+ : createEvidence({
10
+ kind: "OBSERVED",
11
+ source: `ForgeLoop doctor:${relativePath}`,
12
+ result: evidence ?? message,
13
+ });
14
+ return {
15
+ code,
16
+ severity,
17
+ path: relativePath,
18
+ message,
19
+ remediation: remediation ?? (severity === "info" ? "No action required." : "Review the target path and apply the suggested correction."),
20
+ evidence: evidenceRecord,
21
+ };
22
+ }
23
+
24
+ function readProfileMode(bytes) {
25
+ const text = bytes.toString("utf8");
26
+ return text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
27
+ }
28
+
29
+ const ADAPTER_PATHS = new Set([
30
+ "AGENTS.md",
31
+ "CLAUDE.md",
32
+ ".cursor/rules/project-loop.mdc",
33
+ ".github/copilot-instructions.md",
34
+ ]);
35
+
36
+ async function adoptAdapters({ target, manifest, adoptPaths, findings }) {
37
+ if (!manifest || adoptPaths.length === 0) return manifest;
38
+
39
+ const nextManifest = structuredClone(manifest);
40
+ let changed = false;
41
+
42
+ for (const relativePath of adoptPaths) {
43
+ if (!ADAPTER_PATHS.has(relativePath)) {
44
+ findings.push(
45
+ finding(
46
+ "adopt-invalid-path",
47
+ "error",
48
+ relativePath,
49
+ "Only supported adapter paths can be adopted.",
50
+ ),
51
+ );
52
+ continue;
53
+ }
54
+
55
+ const destination = ensureWithin(target, relativePath);
56
+ try {
57
+ await assertSafePath(target, relativePath);
58
+ } catch (error) {
59
+ findings.push(finding("unsafe-path", "error", relativePath, error.message));
60
+ continue;
61
+ }
62
+ if (!(await fileExists(destination))) {
63
+ findings.push(finding("adopt-file-missing", "error", relativePath, "Adapter file is missing."));
64
+ continue;
65
+ }
66
+
67
+ nextManifest.files[relativePath] = {
68
+ sha256: sha256(await readBytes(destination)),
69
+ preserve: true,
70
+ };
71
+ findings.push(
72
+ finding(
73
+ "file-adopted",
74
+ "info",
75
+ relativePath,
76
+ "Existing adapter is now recorded as preserved by ForgeLoop.",
77
+ ),
78
+ );
79
+ changed = true;
80
+ }
81
+
82
+ if (changed) await writeManifest(target, nextManifest);
83
+ return nextManifest;
84
+ }
85
+
86
+ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false }) {
87
+ const findings = [];
88
+ let manifest = null;
89
+ try {
90
+ manifest = await readManifest(target);
91
+ if (!manifest) {
92
+ findings.push(finding("manifest-missing", "error", ".forgeloop/manifest.json", "Run forgeloop init first."));
93
+ }
94
+ } catch (error) {
95
+ findings.push(finding("manifest-invalid", "error", ".forgeloop/manifest.json", error.message));
96
+ }
97
+
98
+ manifest = await adoptAdapters({ target, manifest, adoptPaths, findings });
99
+ const entries = await readTemplateEntries(packageRoot);
100
+ for (const entry of entries) {
101
+ const destination = ensureWithin(target, entry.relativePath);
102
+ try {
103
+ await assertSafePath(target, entry.relativePath);
104
+ } catch (error) {
105
+ findings.push(finding("unsafe-path", "error", entry.relativePath, error.message));
106
+ continue;
107
+ }
108
+ if (!(await fileExists(destination))) {
109
+ findings.push(finding("file-missing", "error", entry.relativePath, "Managed file is missing."));
110
+ continue;
111
+ }
112
+
113
+ if (entry.relativePath === "PROJECT_PROFILE.md") {
114
+ const mode = readProfileMode(await readBytes(destination));
115
+ if (mode === "template") {
116
+ findings.push(finding("profile-template", "info", entry.relativePath, "Initialize profile-mode as project after confirming real project facts."));
117
+ }
118
+ }
119
+
120
+ const record = manifest?.files?.[entry.relativePath];
121
+ if (!record) {
122
+ if (ADAPTER_PATHS.has(entry.relativePath)) {
123
+ findings.push(
124
+ finding(
125
+ "unmanaged-file",
126
+ "error",
127
+ entry.relativePath,
128
+ "Existing adapter is not managed by ForgeLoop; merge the loop reference and rerun doctor.",
129
+ ),
130
+ );
131
+ }
132
+ continue;
133
+ }
134
+ const actualHash = sha256(await readBytes(destination));
135
+ if (actualHash !== record.sha256 && !record.preserve) {
136
+ findings.push(finding("file-drift", "warning", entry.relativePath, "File differs from the last managed version; update will preserve it."));
137
+ }
138
+ }
139
+
140
+ const shippedPaths = new Set(entries.map((entry) => entry.relativePath));
141
+ for (const relativePath of Object.keys(manifest?.files ?? {})) {
142
+ if (!shippedPaths.has(relativePath)) {
143
+ findings.push(
144
+ finding(
145
+ "manifest-orphan",
146
+ "warning",
147
+ relativePath,
148
+ "Manifest entry no longer corresponds to a shipped template.",
149
+ ),
150
+ );
151
+ }
152
+ }
153
+
154
+ const ok = findings.every((item) => item.severity !== "error")
155
+ && (!strict || findings.every((item) => item.severity !== "warning"));
156
+ return {
157
+ ok,
158
+ findings,
159
+ evidence: [createEvidence({
160
+ kind: "OBSERVED",
161
+ source: "ForgeLoop doctor",
162
+ result: `${findings.length} findings; ${ok ? "healthy" : "needs attention"}`,
163
+ })],
164
+ };
165
+ }
@@ -0,0 +1,42 @@
1
+ import { assertSafePath, fileExists, ensureWithin, writeFileAtomic } from "../core/filesystem.js";
2
+ import {
3
+ createManifest,
4
+ readManifest,
5
+ sha256,
6
+ writeManifest,
7
+ } from "../core/manifest.js";
8
+ import { readTemplateEntries } from "../core/templates.js";
9
+
10
+ const PROFILE_PATH = "PROJECT_PROFILE.md";
11
+
12
+ export async function runInit({ target, dryRun, packageRoot, packageVersion }) {
13
+ const entries = await readTemplateEntries(packageRoot);
14
+ const existingManifest = await readManifest(target);
15
+ if (existingManifest) {
16
+ throw new Error("Target is already initialized; run forgeloop update instead.");
17
+ }
18
+ const manifest = createManifest(packageVersion);
19
+ const actions = [];
20
+
21
+ for (const entry of entries) {
22
+ const destination = ensureWithin(target, entry.relativePath);
23
+ await assertSafePath(target, entry.relativePath);
24
+ if (await fileExists(destination)) {
25
+ actions.push({ action: "skip", path: entry.relativePath, reason: "exists" });
26
+ continue;
27
+ }
28
+
29
+ actions.push({
30
+ action: dryRun ? "would-create" : "created",
31
+ path: entry.relativePath,
32
+ });
33
+ await writeFileAtomic(destination, entry.bytes, { dryRun });
34
+ manifest.files[entry.relativePath] = {
35
+ sha256: sha256(entry.bytes),
36
+ preserve: entry.relativePath === PROFILE_PATH,
37
+ };
38
+ }
39
+
40
+ await writeManifest(target, manifest, { dryRun });
41
+ return { actions, manifest };
42
+ }
@@ -0,0 +1,17 @@
1
+ import { inspectTarget } from "../core/inspect.js";
2
+
3
+ export { inspectTarget };
4
+
5
+ export function formatInspectResult(report) {
6
+ const lines = [
7
+ `Target: ${report.target.path}`,
8
+ `Manifest: ${report.manifest.status}`,
9
+ `Profile: ${report.profile.mode ?? "unknown"}/${report.profile.status ?? "unknown"}`,
10
+ `Protocol: v${report.protocol.version}`,
11
+ `State: ${report.state.status}`,
12
+ `Adapters: ${report.adapters.detected.length} detected`,
13
+ `Findings: ${report.findings.length}`,
14
+ report.ok ? "healthy: ForgeLoop target is ready" : "unhealthy: ForgeLoop target needs attention",
15
+ ];
16
+ return `${lines.join("\n")}\n`;
17
+ }
@@ -0,0 +1,32 @@
1
+ import { evaluateRoute } from "../core/router.js";
2
+
3
+ export function runRoute({ workType, surfaces, risks, platforms, behaviorChange, executableChange }) {
4
+ return evaluateRoute({
5
+ workType,
6
+ surfaces,
7
+ risks,
8
+ platforms,
9
+ behaviorChange,
10
+ executableChange,
11
+ });
12
+ }
13
+
14
+ export function formatRouteResult(result) {
15
+ const lines = ["Selected:"];
16
+ if (result.guides.length === 0) {
17
+ lines.push("- none (use the relevant domain guide for this documentation task)");
18
+ } else {
19
+ for (const guide of result.guides) {
20
+ lines.push(`- ${guide}: ${result.reasons[guide].join(", ")}`);
21
+ }
22
+ }
23
+
24
+ const excludedEntries = Object.entries(result.excluded);
25
+ if (excludedEntries.length > 0) {
26
+ lines.push("Excluded:");
27
+ for (const [guide, reasons] of excludedEntries) {
28
+ lines.push(`- ${guide}: ${reasons.join(", ")}`);
29
+ }
30
+ }
31
+ return `${lines.join("\n")}\n`;
32
+ }
@@ -0,0 +1,29 @@
1
+ import { readAndClassifyWorkState } from "../core/work-state.js";
2
+ import { inspectSchemaHealth } from "../core/schema-validation.js";
3
+
4
+ export async function runStatus({ target, packageRoot, contractFile = null }) {
5
+ const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
6
+ const protocol = await inspectSchemaHealth(target);
7
+ return {
8
+ ...state,
9
+ protocol,
10
+ evidence: [...(state.evidence ?? []), ...(protocol.evidence ?? [])],
11
+ };
12
+ }
13
+
14
+ export function formatStatusResult(result) {
15
+ const lines = [
16
+ `State: ${result.path}`,
17
+ `Status: ${result.status}`,
18
+ `Phase: ${result.phase ?? "none"}`,
19
+ `Completed: ${result.completed.join(", ") || "none"}`,
20
+ `Pending: ${result.pending.join(", ") || "none"}`,
21
+ ];
22
+ if (result.reasons.length > 0) lines.push(`Reasons: ${result.reasons.join(", ")}`);
23
+ if (result.warnings?.length > 0) lines.push(`Warnings: ${result.warnings.join(", ")}`);
24
+ if (result.contractComparison) lines.push(`Contract: ${result.contractComparison}`);
25
+ if (result.artifactComparison) lines.push(`Artifacts: ${result.artifactComparison}`);
26
+ if (result.protocol) lines.push(`Schemas: ${result.protocol.status}`);
27
+ if (result.error) lines.push(`Error: ${result.error}`);
28
+ return `${lines.join("\n")}\n`;
29
+ }
@@ -0,0 +1,109 @@
1
+ import { assertSafePath, fileExists, ensureWithin, readBytes, writeFileAtomic } from "../core/filesystem.js";
2
+ import {
3
+ PACKAGE_NAME,
4
+ readManifest,
5
+ sha256,
6
+ writeManifest,
7
+ } from "../core/manifest.js";
8
+ import { readTemplateEntries } from "../core/templates.js";
9
+
10
+ const PROFILE_PATH = "PROJECT_PROFILE.md";
11
+
12
+ export async function runUpdate({ target, dryRun, packageRoot, packageVersion }) {
13
+ const currentManifest = await readManifest(target);
14
+ if (!currentManifest) {
15
+ throw new Error("No .forgeloop/manifest.json found; run forgeloop init first.");
16
+ }
17
+
18
+ const entries = await readTemplateEntries(packageRoot);
19
+ const nextManifest = structuredClone(currentManifest);
20
+ const actions = [];
21
+ const conflicts = [];
22
+ const plans = [];
23
+ const pruneActions = [];
24
+ const shippedPaths = new Set(entries.map((entry) => entry.relativePath));
25
+
26
+ for (const relativePath of Object.keys(nextManifest.files)) {
27
+ if (!shippedPaths.has(relativePath)) {
28
+ pruneActions.push({
29
+ action: dryRun ? "would-prune" : "pruned",
30
+ path: relativePath,
31
+ reason: "template-removed",
32
+ });
33
+ }
34
+ }
35
+
36
+ for (const entry of entries) {
37
+ const destination = ensureWithin(target, entry.relativePath);
38
+ await assertSafePath(target, entry.relativePath);
39
+ const sourceHash = sha256(entry.bytes);
40
+ const record = currentManifest.files[entry.relativePath];
41
+ const exists = await fileExists(destination);
42
+
43
+ if (!exists) {
44
+ plans.push({
45
+ action: dryRun ? "would-create" : "created",
46
+ destination,
47
+ entry,
48
+ record: {
49
+ sha256: sourceHash,
50
+ preserve: entry.relativePath === PROFILE_PATH,
51
+ },
52
+ });
53
+ continue;
54
+ }
55
+
56
+ if (!record) {
57
+ actions.push({ action: "skip", path: entry.relativePath, reason: "unmanaged" });
58
+ continue;
59
+ }
60
+
61
+ if (record.preserve || entry.relativePath === PROFILE_PATH) {
62
+ actions.push({ action: "skip", path: entry.relativePath, reason: "preserved" });
63
+ continue;
64
+ }
65
+
66
+ const currentBytes = await readBytes(destination);
67
+ const currentHash = sha256(currentBytes);
68
+ if (currentHash !== record.sha256) {
69
+ conflicts.push({
70
+ path: entry.relativePath,
71
+ message: "Local changes detected; file was not overwritten.",
72
+ });
73
+ actions.push({ action: "conflict", path: entry.relativePath });
74
+ continue;
75
+ }
76
+
77
+ if (currentHash === sourceHash) {
78
+ actions.push({ action: "skip", path: entry.relativePath, reason: "current" });
79
+ continue;
80
+ }
81
+
82
+ plans.push({
83
+ action: dryRun ? "would-update" : "updated",
84
+ destination,
85
+ entry,
86
+ record: { ...record, sha256: sourceHash },
87
+ });
88
+ }
89
+
90
+ if (conflicts.length > 0) {
91
+ return { actions, conflicts, manifest: currentManifest };
92
+ }
93
+
94
+ for (const relativePath of Object.keys(nextManifest.files)) {
95
+ if (!shippedPaths.has(relativePath)) delete nextManifest.files[relativePath];
96
+ }
97
+ actions.push(...pruneActions);
98
+
99
+ for (const plan of plans) {
100
+ actions.push({ action: plan.action, path: plan.entry.relativePath });
101
+ await writeFileAtomic(plan.destination, plan.entry.bytes, { dryRun });
102
+ nextManifest.files[plan.entry.relativePath] = plan.record;
103
+ }
104
+
105
+ nextManifest.packageName = PACKAGE_NAME;
106
+ nextManifest.packageVersion = packageVersion;
107
+ await writeManifest(target, nextManifest, { dryRun });
108
+ return { actions, conflicts, manifest: nextManifest };
109
+ }
@@ -0,0 +1,133 @@
1
+ import { assertSafePath, ensureWithin, fileExists, readBytes } from "../core/filesystem.js";
2
+ import { assertJsonBytes, assertJsonLimits } from "../core/json-safety.js";
3
+ import { validateTaskArtifactSet } from "../core/conformance.js";
4
+ import { assertSchema, readSchema } from "../core/schema-validation.js";
5
+ import { assertRouteInvariants } from "../core/router.js";
6
+ import { assertWorkStateSemantics, classifyLoadedWorkState } from "../core/work-state.js";
7
+ import { validateReceipt } from "../core/receipt.js";
8
+ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.js";
9
+
10
+ async function readArtifact(target, relativePath, label) {
11
+ if (!relativePath) return null;
12
+ await assertSafePath(target, relativePath);
13
+ const artifactPath = ensureWithin(target, relativePath);
14
+ if (!(await fileExists(artifactPath))) {
15
+ return {
16
+ error: {
17
+ code: "ARTIFACT_MISSING",
18
+ message: `${label} is missing: ${relativePath}`,
19
+ artifacts: [relativePath],
20
+ },
21
+ };
22
+ }
23
+ const bytes = await readBytes(artifactPath);
24
+ try {
25
+ assertJsonBytes(bytes, label);
26
+ const value = JSON.parse(bytes.toString("utf8"));
27
+ assertJsonLimits(value, label);
28
+ return { value };
29
+ } catch (error) {
30
+ return {
31
+ error: {
32
+ code: error.code === "JSON_LIMIT_EXCEEDED" ? error.code : "ARTIFACT_INVALID_JSON",
33
+ message: `${label} is invalid: ${error.message}`,
34
+ artifacts: [relativePath],
35
+ },
36
+ };
37
+ }
38
+ }
39
+
40
+ export async function runValidateProtocol({
41
+ target,
42
+ packageRoot,
43
+ routeFile = null,
44
+ stateFile = null,
45
+ receiptFile = null,
46
+ contractFile = null,
47
+ taskBriefFiles = [],
48
+ delegatedResultFiles = [],
49
+ }) {
50
+ const descriptors = [
51
+ ["route", routeFile],
52
+ ["state", stateFile],
53
+ ["receipt", receiptFile],
54
+ ...taskBriefFiles.map((file) => [`task brief:${file}`, file]),
55
+ ...delegatedResultFiles.map((file) => [`delegated result:${file}`, file]),
56
+ ];
57
+ const loaded = [];
58
+ for (const [label, relativePath] of descriptors) {
59
+ const artifact = await readArtifact(target, relativePath, label);
60
+ loaded.push({ label, relativePath, ...artifact });
61
+ }
62
+
63
+ const readErrors = loaded.filter((item) => item.error).map((item) => item.error);
64
+ const route = loaded.find((item) => item.label === "route")?.value ?? null;
65
+ const state = loaded.find((item) => item.label === "state")?.value ?? null;
66
+ const receipt = loaded.find((item) => item.label === "receipt")?.value ?? null;
67
+ const taskBriefs = loaded.filter((item) => item.label.startsWith("task brief:") && item.value).map((item) => item.value);
68
+ const delegatedResults = loaded.filter((item) => item.label.startsWith("delegated result:") && item.value).map((item) => item.value);
69
+ const schemaErrors = [];
70
+ const validateLoaded = async (item, schemaName, semanticValidator = null) => {
71
+ if (!item.value) return;
72
+ try {
73
+ const schema = await readSchema(schemaName, packageRoot);
74
+ assertSchema(item.value, schema, item.label);
75
+ if (semanticValidator) await semanticValidator(item.value);
76
+ } catch (error) {
77
+ schemaErrors.push({
78
+ code: "ARTIFACT_SCHEMA_INVALID",
79
+ message: `${item.label} failed schema or semantic validation: ${error.message}`,
80
+ artifacts: [item.relativePath],
81
+ });
82
+ }
83
+ };
84
+ await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
85
+ await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
86
+ await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
87
+ for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
88
+ await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
89
+ }
90
+ for (const item of loaded.filter((candidate) => candidate.label.startsWith("delegated result:"))) {
91
+ await validateLoaded(item, "delegated-result", async (value) => validateDelegatedResult(value, packageRoot));
92
+ }
93
+ const stateValidationError = schemaErrors.some((error) => error.artifacts.includes(stateFile));
94
+ const stateClassification = state && readErrors.length === 0 && !stateValidationError
95
+ ? await classifyLoadedWorkState({ target, state, contractFile })
96
+ : null;
97
+ const result = validateTaskArtifactSet({
98
+ route,
99
+ state,
100
+ stateClassification,
101
+ receipt,
102
+ taskBriefs,
103
+ delegatedResults,
104
+ });
105
+ if (readErrors.length > 0 || schemaErrors.length > 0) {
106
+ return {
107
+ ...result,
108
+ status: "INVALID",
109
+ errors: [...result.errors, ...readErrors, ...schemaErrors].sort((left, right) => left.code.localeCompare(right.code) || left.message.localeCompare(right.message)),
110
+ };
111
+ }
112
+ return result;
113
+ }
114
+
115
+ export function formatValidateProtocolResult(result) {
116
+ const lines = [`Protocol: ${result.status}`];
117
+ if (result.stale) {
118
+ lines.push(`Repository: ${result.stale.repositoryComparison}`);
119
+ lines.push(`Contract: ${result.stale.contractComparison}`);
120
+ lines.push(`Required artifacts: ${result.stale.artifactComparison}`);
121
+ if (result.stale.reasons.length > 0) {
122
+ lines.push("Reasons:");
123
+ for (const reason of result.stale.reasons) lines.push(`- ${reason}`);
124
+ }
125
+ if (result.stale.warnings.length > 0) {
126
+ lines.push("Warnings:");
127
+ for (const warning of result.stale.warnings) lines.push(`- ${warning}`);
128
+ }
129
+ }
130
+ for (const item of result.errors) lines.push(`- ${item.code}: ${item.message}`);
131
+ for (const item of result.incomplete) lines.push(`- INCOMPLETE: ${item}`);
132
+ return `${lines.join("\n")}\n`;
133
+ }
@@ -0,0 +1,19 @@
1
+ import { assertSafePath, ensureWithin, readBytes } from "../core/filesystem.js";
2
+ import { validateReceipt } from "../core/receipt.js";
3
+ import { assertJsonBytes, assertJsonLimits } from "../core/json-safety.js";
4
+
5
+ export async function runValidateReceipt({ target, packageRoot, file }) {
6
+ if (!file) throw new Error("--file is required for validate-receipt");
7
+ await assertSafePath(target, file);
8
+ const receiptPath = ensureWithin(target, file);
9
+ let receipt;
10
+ try {
11
+ const bytes = await readBytes(receiptPath);
12
+ assertJsonBytes(bytes, file);
13
+ receipt = JSON.parse(bytes.toString("utf8"));
14
+ assertJsonLimits(receipt, file);
15
+ } catch (error) {
16
+ throw new Error(`Unable to parse receipt ${file}: ${error.message}`);
17
+ }
18
+ return validateReceipt(receipt, packageRoot);
19
+ }
@@ -0,0 +1,30 @@
1
+ import { readWorkState, WORK_STATE_PATH } from "../core/work-state.js";
2
+
3
+ export async function runValidateState({ target, packageRoot }) {
4
+ try {
5
+ const state = await readWorkState(target, packageRoot);
6
+ if (!state) {
7
+ return {
8
+ ok: true,
9
+ path: WORK_STATE_PATH,
10
+ state: null,
11
+ errors: [],
12
+ warnings: ["No work-state checkpoint is present."],
13
+ };
14
+ }
15
+ return { ok: true, path: WORK_STATE_PATH, state, errors: [], warnings: [] };
16
+ } catch (error) {
17
+ return {
18
+ ok: false,
19
+ path: WORK_STATE_PATH,
20
+ state: null,
21
+ errors: [error.message],
22
+ warnings: [],
23
+ };
24
+ }
25
+ }
26
+
27
+ export function formatValidateStateResult(result) {
28
+ if (result.ok) return `valid: ${result.path}\n`;
29
+ return `invalid: ${result.path}\n${result.errors.map((error) => `- ${error}`).join("\n")}\n`;
30
+ }
@@ -0,0 +1,89 @@
1
+ function agent(record) {
2
+ return Object.freeze({
3
+ ...record,
4
+ instructionFiles: Object.freeze([...record.instructionFiles]),
5
+ });
6
+ }
7
+
8
+ export const AGENT_SUPPORT = Object.freeze([
9
+ agent({
10
+ id: "codex",
11
+ name: "Codex",
12
+ support: "direct",
13
+ instructionFiles: ["AGENTS.md"],
14
+ officialDocs: "https://developers.openai.com/codex/guides/agents-md",
15
+ notes: "Reads AGENTS.md files from the global scope down to the working directory.",
16
+ }),
17
+ agent({
18
+ id: "claude-code",
19
+ name: "Claude Code",
20
+ support: "direct",
21
+ instructionFiles: ["CLAUDE.md"],
22
+ officialDocs: "https://code.claude.com/docs/en/memory",
23
+ notes: "Reads CLAUDE.md; it does not read AGENTS.md unless CLAUDE.md imports it.",
24
+ }),
25
+ agent({
26
+ id: "cursor",
27
+ name: "Cursor",
28
+ support: "direct",
29
+ instructionFiles: ["AGENTS.md", ".cursor/rules/project-loop.mdc"],
30
+ officialDocs: "https://cursor.com/docs/rules",
31
+ notes: "Uses the always-applicable MDC rule and also supports a root AGENTS.md.",
32
+ }),
33
+ agent({
34
+ id: "github-copilot",
35
+ name: "GitHub Copilot",
36
+ support: "direct",
37
+ instructionFiles: [".github/copilot-instructions.md"],
38
+ officialDocs: "https://docs.github.com/en/copilot/how-tos/configure-custom-instructions-in-your-ide/add-repository-instructions-in-your-ide",
39
+ notes: "Uses repository-wide Copilot instructions; supported agent instruction files vary by Copilot surface.",
40
+ }),
41
+ agent({
42
+ id: "antigravity",
43
+ name: "Antigravity",
44
+ support: "agents-md",
45
+ instructionFiles: ["AGENTS.md"],
46
+ officialDocs: "https://antigravity.google/docs/cli/best-practices",
47
+ notes: "Parses a workspace-root AGENTS.md; workspace rules can additionally live in .agents/rules/.",
48
+ }),
49
+ agent({
50
+ id: "opencode",
51
+ name: "OpenCode",
52
+ support: "agents-md",
53
+ instructionFiles: ["AGENTS.md"],
54
+ officialDocs: "https://opencode.ai/docs/rules/",
55
+ notes: "Uses project AGENTS.md; opencode.json can add other instruction files when needed.",
56
+ }),
57
+ agent({
58
+ id: "hermes",
59
+ name: "Hermes",
60
+ support: "agents-md",
61
+ instructionFiles: ["AGENTS.md"],
62
+ officialDocs: "https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/context-files.md",
63
+ notes: "AGENTS.md is supported, but .hermes.md or HERMES.md has higher project priority.",
64
+ }),
65
+ agent({
66
+ id: "pi",
67
+ name: "Pi",
68
+ support: "agents-md",
69
+ instructionFiles: ["AGENTS.md"],
70
+ officialDocs: "https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/sdk.md",
71
+ notes: "Discovers AGENTS.md from the project root through the current working directory.",
72
+ }),
73
+ agent({
74
+ id: "command-code",
75
+ name: "Command Code",
76
+ support: "agents-md",
77
+ instructionFiles: ["AGENTS.md"],
78
+ officialDocs: "https://commandcode.ai/docs/core-concepts/memory",
79
+ notes: "Reads project AGENTS.md; .commandcode/AGENTS.md is an optional project-specific location.",
80
+ }),
81
+ agent({
82
+ id: "freebuff",
83
+ name: "Freebuff",
84
+ support: "agents-md",
85
+ instructionFiles: ["AGENTS.md"],
86
+ officialDocs: "https://github.com/CodebuffAI/freebuff/blob/main/common/src/constants/knowledge.ts",
87
+ notes: "Recognizes AGENTS.md as a project knowledge file alongside knowledge.md and CLAUDE.md.",
88
+ }),
89
+ ]);