create-religion 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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/bin/religion.js +209 -0
  4. package/dist/lib/dashboard.js +123 -0
  5. package/dist/lib/doctor.js +164 -0
  6. package/dist/lib/install.js +170 -0
  7. package/dist/lib/paths.js +40 -0
  8. package/dist/lib/state.js +117 -0
  9. package/dist/lib/status.js +88 -0
  10. package/package.json +40 -0
  11. package/template/.agents/skills/audit/SKILL.md +169 -0
  12. package/template/.agents/skills/auto/SKILL.md +110 -0
  13. package/template/.agents/skills/browser-tests/SKILL.md +65 -0
  14. package/template/.agents/skills/check/SKILL.md +86 -0
  15. package/template/.agents/skills/ci/SKILL.md +79 -0
  16. package/template/.agents/skills/complete/SKILL.md +184 -0
  17. package/template/.agents/skills/debug/SKILL.md +72 -0
  18. package/template/.agents/skills/discovery/SKILL.md +83 -0
  19. package/template/.agents/skills/distill/SKILL.md +95 -0
  20. package/template/.agents/skills/doctor/SKILL.md +65 -0
  21. package/template/.agents/skills/extend/SKILL.md +102 -0
  22. package/template/.agents/skills/feature/SKILL.md +172 -0
  23. package/template/.agents/skills/feature/reference/spec-template.md +67 -0
  24. package/template/.agents/skills/fix/SKILL.md +76 -0
  25. package/template/.agents/skills/implement/SKILL.md +174 -0
  26. package/template/.agents/skills/overview/SKILL.md +150 -0
  27. package/template/.agents/skills/overview/reference/project-overview-template.md +101 -0
  28. package/template/.agents/skills/prototype/SKILL.md +90 -0
  29. package/template/.agents/skills/release/SKILL.md +88 -0
  30. package/template/.agents/skills/rollback/SKILL.md +114 -0
  31. package/template/.agents/skills/setup/SKILL.md +105 -0
  32. package/template/.agents/skills/status/SKILL.md +63 -0
  33. package/template/.agents/skills/tests/SKILL.md +90 -0
  34. package/template/.agents/skills/try/SKILL.md +59 -0
  35. package/template/.claude/hooks/guard-rendered-output.mjs +49 -0
  36. package/template/.claude/hooks/guard-tier-one.mjs +63 -0
  37. package/template/.claude/hooks/write-handoff.mjs +83 -0
  38. package/template/.claude/skills/audit/SKILL.md +169 -0
  39. package/template/.claude/skills/auto/SKILL.md +110 -0
  40. package/template/.claude/skills/browser-tests/SKILL.md +65 -0
  41. package/template/.claude/skills/check/SKILL.md +86 -0
  42. package/template/.claude/skills/ci/SKILL.md +79 -0
  43. package/template/.claude/skills/complete/SKILL.md +184 -0
  44. package/template/.claude/skills/debug/SKILL.md +72 -0
  45. package/template/.claude/skills/discovery/SKILL.md +83 -0
  46. package/template/.claude/skills/distill/SKILL.md +95 -0
  47. package/template/.claude/skills/doctor/SKILL.md +65 -0
  48. package/template/.claude/skills/extend/SKILL.md +102 -0
  49. package/template/.claude/skills/feature/SKILL.md +172 -0
  50. package/template/.claude/skills/feature/reference/spec-template.md +67 -0
  51. package/template/.claude/skills/fix/SKILL.md +76 -0
  52. package/template/.claude/skills/implement/SKILL.md +174 -0
  53. package/template/.claude/skills/overview/SKILL.md +150 -0
  54. package/template/.claude/skills/overview/reference/project-overview-template.md +101 -0
  55. package/template/.claude/skills/prototype/SKILL.md +90 -0
  56. package/template/.claude/skills/release/SKILL.md +88 -0
  57. package/template/.claude/skills/rollback/SKILL.md +114 -0
  58. package/template/.claude/skills/setup/SKILL.md +105 -0
  59. package/template/.claude/skills/status/SKILL.md +63 -0
  60. package/template/.claude/skills/tests/SKILL.md +90 -0
  61. package/template/.claude/skills/try/SKILL.md +59 -0
  62. package/template/AGENTS.md +206 -0
  63. package/template/CLAUDE.md +175 -0
  64. package/template/religion/.state/README.md +10 -0
  65. package/template/religion/.state/settings-template.json +19 -0
  66. package/template/religion/build-plan.md +43 -0
  67. package/template/religion/config.json +26 -0
  68. package/template/religion/context/ai-interaction.md +84 -0
  69. package/template/religion/context/coding-standards.md +77 -0
  70. package/template/religion/context/current-work.md +8 -0
  71. package/template/religion/context/findings.md +9 -0
  72. package/template/religion/context/handoff.md +8 -0
  73. package/template/religion/context/project-overview.md +8 -0
  74. package/template/religion/history/features/README.md +8 -0
  75. package/template/religion/history/fixes/README.md +6 -0
  76. package/template/religion/history/rollbacks/README.md +7 -0
  77. package/template/religion/learning/journal.md +7 -0
  78. package/template/religion/learning/lessons.md +8 -0
  79. package/template/religion/project-plan.md +49 -0
  80. package/template/religion/reference/README.md +7 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # create-religion
2
+
3
+ A file-backed, spec-driven AI development workflow.
4
+
5
+ ```bash
6
+ npx create-religion@latest
7
+ ```
8
+
9
+ You write two planning documents. The workflow turns them into project context, work-item
10
+ specs, and small reviewable build steps. You review every spec before code exists, and
11
+ every diff before it lands.
12
+
13
+ ## What it installs
14
+
15
+ - **22 skills** your AI tool can run: `feature`, `implement`, `check`, `audit`, `complete`,
16
+ and the rest of the loop around them.
17
+ - **A state directory** holding your plans, the active spec, the findings ledger, and the
18
+ archive of everything finished.
19
+ - **Enforcement hooks** for Claude Code, backing rules that apply to every supported tool.
20
+
21
+ Works with Claude Code, Codex, GitHub Copilot, and OpenCode. Each tool's files are
22
+ generated separately, so they carry that tool's own invocation syntax.
23
+
24
+ ## After installing
25
+
26
+ 1. Run the `setup` skill. On an existing codebase it surveys what is there and drafts your
27
+ plans from it.
28
+ 2. Fill in `religion/project-plan.md` and `religion/build-plan.md`.
29
+ 3. Run `overview`, then the loop: `feature`, `implement`, `check`, `complete`.
30
+
31
+ ## Commands
32
+
33
+ ```bash
34
+ religion status # where the work stands, and the one thing to do next
35
+ religion doctor # is the setup healthy
36
+ religion dashboard # a local read-only view
37
+ religion update # update the workflow files, preserving yours
38
+ ```
39
+
40
+ `update` will not overwrite a file you have edited. It tells you what conflicts and backs
41
+ up the original if you choose to replace it.
42
+
43
+ ## Documentation
44
+
45
+ [github.com/foo-stack/religion](https://github.com/foo-stack/religion)
46
+
47
+ MIT.
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The Religion command-line tool.
4
+ *
5
+ * Install and update the workflow files, and read project state. It never runs a workflow
6
+ * command: deciding what to build is the agent's job, and reporting what is true is this
7
+ * one's.
8
+ */
9
+ import fs from "node:fs/promises";
10
+ import fsSync from "node:fs";
11
+ import path from "node:path";
12
+ import readline from "node:readline/promises";
13
+ import { fileURLToPath } from "node:url";
14
+ import { startDashboard } from "../lib/dashboard.js";
15
+ import { renderDoctor, runDoctor } from "../lib/doctor.js";
16
+ import { ADAPTERS, applyInstall, planInstall, readManifest, wireHooks, writeManifest } from "../lib/install.js";
17
+ import { findProjectRoot, STATE_DIR } from "../lib/paths.js";
18
+ import { readProjectState } from "../lib/state.js";
19
+ import { computeStatus, renderStatus } from "../lib/status.js";
20
+ const here = path.dirname(fileURLToPath(import.meta.url));
21
+ /**
22
+ * Finds the installed package root by walking up for the template.
23
+ *
24
+ * The depth differs between running from source and running from the build: in the
25
+ * repository this file sits at `bin/`, one level below the package, while a published
26
+ * install puts it at `dist/bin/`, two levels below. A fixed `..` is correct in exactly one
27
+ * of those, and gets the other silently wrong: the template resolves to a directory that
28
+ * does not exist, the walk returns nothing, and the install reports success having written
29
+ * no files.
30
+ */
31
+ function findPackageRoot(from) {
32
+ let current = from;
33
+ for (let depth = 0; depth < 5; depth += 1) {
34
+ if (fsSync.existsSync(path.join(current, "template")))
35
+ return current;
36
+ const parent = path.dirname(current);
37
+ if (parent === current)
38
+ break;
39
+ current = parent;
40
+ }
41
+ throw new Error("Could not find the template directory. This install of create-religion is incomplete; reinstall it.");
42
+ }
43
+ const packageRoot = findPackageRoot(here);
44
+ const templateRoot = path.join(packageRoot, "template");
45
+ async function main(argv) {
46
+ const options = parse(argv);
47
+ if (options.command === "help")
48
+ return printHelp();
49
+ if (options.command === "install" || options.command === "update")
50
+ return runInstall(options);
51
+ const root = await findProjectRoot(options.target);
52
+ if (!root) {
53
+ console.error("No Religion project found here or in any parent directory.");
54
+ console.error("Run `religion install` from the project root to add one.");
55
+ process.exit(1);
56
+ }
57
+ if (options.command === "status") {
58
+ const status = computeStatus(await readProjectState(root));
59
+ console.log(options.json ? JSON.stringify(status, null, 2) : renderStatus(status));
60
+ return;
61
+ }
62
+ if (options.command === "doctor") {
63
+ const results = await runDoctor(root);
64
+ console.log(options.json ? JSON.stringify(results, null, 2) : renderDoctor(results));
65
+ if (results.some((r) => !r.ok))
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ const dashboard = await startDashboard(root);
70
+ console.log(`Religion dashboard: ${dashboard.url}`);
71
+ console.log("Read-only. Press Ctrl+C to stop.");
72
+ process.on("SIGINT", () => {
73
+ void dashboard.close().then(() => process.exit(0));
74
+ });
75
+ }
76
+ async function runInstall(options) {
77
+ const target = path.resolve(options.target);
78
+ const updating = options.command === "update";
79
+ const previous = await readManifest(target);
80
+ if (updating && !previous) {
81
+ console.log("No manifest found. Files matching the template will be adopted; others are conflicts.");
82
+ }
83
+ const adapters = options.adapters ?? previous?.adapters ?? (await chooseAdapters(options.yes));
84
+ const plan = await planInstall(templateRoot, target, adapters, previous);
85
+ const counts = {
86
+ create: plan.filter((p) => p.action === "create").length,
87
+ update: plan.filter((p) => p.action === "update").length,
88
+ conflict: plan.filter((p) => p.action === "conflict").length,
89
+ kept: plan.filter((p) => p.action === "seed-skip").length
90
+ };
91
+ console.log(`\nAdapters: ${adapters.map((a) => ADAPTERS[a].label).join(", ")}`);
92
+ console.log(` create ${counts.create}`);
93
+ console.log(` update ${counts.update}`);
94
+ console.log(` keep ${counts.kept} (your files, never overwritten)`);
95
+ console.log(` conflict ${counts.conflict}`);
96
+ for (const entry of plan.filter((p) => p.action === "conflict")) {
97
+ console.log(` ${entry.relative}`);
98
+ }
99
+ if (options.dryRun) {
100
+ console.log("\nDry run. Nothing written.");
101
+ return;
102
+ }
103
+ if (plan.length === 0) {
104
+ throw new Error(`No template files found under ${templateRoot}. Nothing would be installed, which is never correct.`);
105
+ }
106
+ const result = await applyInstall(templateRoot, target, plan, { force: options.force });
107
+ await writeManifest(target, await version(), adapters, templateRoot, previous, result.conflicts);
108
+ console.log(`\nWrote ${result.written.length} file(s).`);
109
+ if (result.backups.length > 0)
110
+ console.log(`Backed up ${result.backups.length} conflicting file(s).`);
111
+ if (result.conflicts.length > 0) {
112
+ console.log(`\n${result.conflicts.length} file(s) were changed locally and left alone.` +
113
+ `\nReview them, then re-run with --force to replace them (originals are backed up).`);
114
+ }
115
+ if (adapters.includes("claude")) {
116
+ const wired = await wireHooks(target);
117
+ if (wired === "written") {
118
+ console.log("Wired the enforcement hooks into .claude/settings.json.");
119
+ }
120
+ else if (wired === "exists") {
121
+ console.log("\n.claude/settings.json already exists and was left alone." +
122
+ `\nTo enable the hooks, merge ${path.join(STATE_DIR, ".state", "settings-template.json")} into it.`);
123
+ }
124
+ }
125
+ if (!updating) {
126
+ console.log("\nNext: run the setup skill in your AI tool, then fill in the two plans.");
127
+ }
128
+ }
129
+ async function chooseAdapters(assumeYes) {
130
+ const all = Object.keys(ADAPTERS);
131
+ if (assumeYes || !process.stdin.isTTY)
132
+ return all;
133
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
134
+ console.log("\nWhich AI tools should this install for?");
135
+ all.forEach((a, i) => console.log(` ${i + 1}. ${ADAPTERS[a].label}`));
136
+ const answer = (await rl.question("\nNumbers separated by commas, or blank for all: ")).trim();
137
+ rl.close();
138
+ if (!answer)
139
+ return all;
140
+ const chosen = answer
141
+ .split(",")
142
+ .map((part) => all[Number(part.trim()) - 1])
143
+ .filter((a) => a !== undefined);
144
+ return chosen.length > 0 ? chosen : all;
145
+ }
146
+ async function version() {
147
+ try {
148
+ const manifest = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
149
+ return manifest.version ?? "0.0.0";
150
+ }
151
+ catch {
152
+ return "0.0.0";
153
+ }
154
+ }
155
+ function parse(argv) {
156
+ const options = {
157
+ command: "install",
158
+ adapters: null,
159
+ json: false,
160
+ dryRun: false,
161
+ force: false,
162
+ yes: false,
163
+ target: process.cwd()
164
+ };
165
+ const commands = new Set(["install", "update", "status", "doctor", "dashboard", "help"]);
166
+ const adapters = [];
167
+ for (const arg of argv) {
168
+ if (commands.has(arg))
169
+ options.command = arg;
170
+ else if (arg === "--json")
171
+ options.json = true;
172
+ else if (arg === "--dry-run")
173
+ options.dryRun = true;
174
+ else if (arg === "--force")
175
+ options.force = true;
176
+ else if (arg === "--yes" || arg === "-y")
177
+ options.yes = true;
178
+ else if (arg === "--help" || arg === "-h")
179
+ options.command = "help";
180
+ else if (arg.startsWith("--") && arg.slice(2) in ADAPTERS)
181
+ adapters.push(arg.slice(2));
182
+ else if (!arg.startsWith("-"))
183
+ options.target = arg;
184
+ }
185
+ if (adapters.length > 0)
186
+ options.adapters = adapters;
187
+ return options;
188
+ }
189
+ function printHelp() {
190
+ console.log(`religion - a file-backed, spec-driven AI development workflow
191
+
192
+ religion install [dir] add the workflow to a project
193
+ religion update [dir] update the workflow files, preserving your own
194
+ religion status where the work stands, and what to do next
195
+ religion doctor check the setup is healthy
196
+ religion dashboard a local read-only view
197
+
198
+ Options
199
+ --claude --codex --copilot --opencode pick adapters (default: all)
200
+ --dry-run show what would change, write nothing
201
+ --force replace locally-changed managed files, backing them up first
202
+ --json machine-readable output for status and doctor
203
+ --yes no prompts
204
+ `);
205
+ }
206
+ main(process.argv.slice(2)).catch((error) => {
207
+ console.error(error instanceof Error ? error.message : error);
208
+ process.exit(1);
209
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * A local, read-only view of project state.
3
+ *
4
+ * Binds to the loopback interface only. It renders what the state files say and never
5
+ * writes, runs a workflow command, or starts the project.
6
+ */
7
+ import http from "node:http";
8
+ import fs from "node:fs/promises";
9
+ import { readIfPresent, statePath } from "./paths.js";
10
+ import { readProjectState } from "./state.js";
11
+ import { computeStatus } from "./status.js";
12
+ export async function startDashboard(root) {
13
+ const server = http.createServer((request, response) => {
14
+ void handle(root, request, response);
15
+ });
16
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
17
+ const address = server.address();
18
+ const port = typeof address === "object" && address ? address.port : 0;
19
+ return {
20
+ url: `http://127.0.0.1:${port}`,
21
+ close: () => new Promise((resolve) => server.close(() => resolve()))
22
+ };
23
+ }
24
+ async function handle(root, request, response) {
25
+ if (request.url === "/state.json") {
26
+ const state = await readProjectState(root);
27
+ const activity = await readActivity(root);
28
+ response.writeHead(200, { "content-type": "application/json" });
29
+ response.end(JSON.stringify({ status: computeStatus(state), plan: state.plan, findings: state.findings, activity }));
30
+ return;
31
+ }
32
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
33
+ response.end(PAGE);
34
+ }
35
+ async function readActivity(root) {
36
+ const raw = await readIfPresent(statePath(root, ".state", "run.json"));
37
+ if (!raw)
38
+ return null;
39
+ try {
40
+ return JSON.parse(raw);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ export async function historyCount(root) {
47
+ let total = 0;
48
+ for (const kind of ["features", "fixes", "rollbacks"]) {
49
+ try {
50
+ const entries = await fs.readdir(statePath(root, "history", kind));
51
+ total += entries.filter((e) => e.endsWith(".md") && e !== "README.md").length;
52
+ }
53
+ catch {
54
+ // absent directory contributes nothing
55
+ }
56
+ }
57
+ return total;
58
+ }
59
+ const PAGE = `<!doctype html>
60
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
61
+ <title>Religion</title>
62
+ <style>
63
+ :root { color-scheme: light dark; --bg:#fbfbfa; --fg:#1a1a19; --dim:#6b6b68; --line:#e5e5e2; --card:#fff; --warn:#b45309; --block:#b91c1c; }
64
+ @media (prefers-color-scheme: dark) { :root { --bg:#16161a; --fg:#e8e8e6; --dim:#9a9a96; --line:#2a2a30; --card:#1e1e24; } }
65
+ * { box-sizing: border-box; }
66
+ body { margin:0; padding:2rem 1.25rem; background:var(--bg); color:var(--fg);
67
+ font:14px/1.6 ui-sans-serif,system-ui,-apple-system,sans-serif; }
68
+ main { max-width:56rem; margin:0 auto; }
69
+ h1 { font-size:1.1rem; margin:0 0 1.5rem; letter-spacing:-.01em; }
70
+ h1 span { color:var(--dim); font-weight:400; }
71
+ .next { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1rem 1.25rem; margin-bottom:1.25rem; }
72
+ .next b { font-size:1.15rem; font-family:ui-monospace,monospace; }
73
+ .next p { margin:.35rem 0 0; color:var(--dim); }
74
+ .grid { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); }
75
+ .card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1rem 1.25rem; }
76
+ .card h2 { font-size:.7rem; text-transform:uppercase; letter-spacing:.08em; color:var(--dim); margin:0 0 .6rem; }
77
+ .bar { height:5px; background:var(--line); border-radius:3px; overflow:hidden; margin:.5rem 0; }
78
+ .bar i { display:block; height:100%; background:var(--fg); }
79
+ ul { margin:.4rem 0 0; padding-left:1.1rem; }
80
+ li { margin:.2rem 0; }
81
+ .warn { color:var(--warn); } .block { color:var(--block); font-weight:600; }
82
+ .dim { color:var(--dim); }
83
+ code { font-family:ui-monospace,monospace; }
84
+ </style>
85
+ <main>
86
+ <h1>Religion <span id="root"></span></h1>
87
+ <div class="next"><b id="next">...</b><p id="why"></p></div>
88
+ <div class="grid">
89
+ <div class="card"><h2>Active work</h2><div id="work"></div></div>
90
+ <div class="card"><h2>Plan</h2><div id="plan"></div></div>
91
+ <div class="card"><h2>Findings</h2><div id="findings"></div></div>
92
+ <div class="card"><h2>Warnings</h2><div id="warnings"></div></div>
93
+ </div>
94
+ </main>
95
+ <script>
96
+ const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
97
+ async function load() {
98
+ let d; try { d = await (await fetch("/state.json")).json(); } catch { return; }
99
+ const s = d.status;
100
+ next.textContent = "/" + s.next.command;
101
+ why.textContent = s.next.because;
102
+ work.innerHTML = s.work.active
103
+ ? "<b>" + esc(s.work.title || "untitled") + "</b><div class=bar><i style='width:" +
104
+ (s.work.stepsTotal ? Math.round(s.work.stepsDone / s.work.stepsTotal * 100) : 0) +
105
+ "%'></i></div><span class=dim>step " + s.work.stepsDone + " of " + s.work.stepsTotal +
106
+ (s.work.status ? " &middot; " + esc(s.work.status) : "") + "</span>"
107
+ : "<span class=dim>nothing in progress</span>";
108
+ plan.innerHTML = "<div class=bar><i style='width:" +
109
+ (s.plan.total ? Math.round(s.plan.done / s.plan.total * 100) : 0) + "%'></i></div><span class=dim>" +
110
+ s.plan.done + " of " + s.plan.total + " complete</span>" +
111
+ (s.plan.nextItem ? "<ul><li>" + esc(s.plan.nextItem) + "</li></ul>" : "");
112
+ findings.innerHTML = s.findings.total === 0
113
+ ? "<span class=dim>none</span>"
114
+ : (s.findings.blocking.length
115
+ ? "<ul>" + s.findings.blocking.map((f) => "<li class=block>" + esc(f) + "</li>").join("") + "</ul>"
116
+ : "") + "<span class=dim>" +
117
+ Object.entries(s.findings.byStatus).map(([k, v]) => v + " " + k).join(", ") + "</span>";
118
+ warnings.innerHTML = s.warnings.length
119
+ ? "<ul>" + s.warnings.map((w) => "<li class=warn>" + esc(w) + "</li>").join("") + "</ul>"
120
+ : "<span class=dim>none</span>";
121
+ }
122
+ load(); setInterval(load, 3000);
123
+ </script>`;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Deterministic health checks.
3
+ *
4
+ * Everything here is decidable by reading files, which is why it belongs in a program
5
+ * rather than in a skill. The skill's job is explaining what a failure means.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { exists, readIfPresent, STATE_DIR, statePath } from "./paths.js";
10
+ import { readProjectState } from "./state.js";
11
+ const REQUIRED = [
12
+ "config.json",
13
+ "project-plan.md",
14
+ "build-plan.md",
15
+ "context/project-overview.md",
16
+ "context/coding-standards.md",
17
+ "context/ai-interaction.md",
18
+ "context/current-work.md",
19
+ "context/findings.md",
20
+ "history/features",
21
+ "history/fixes",
22
+ "history/rollbacks",
23
+ "reference"
24
+ ];
25
+ const ALLOWED = {
26
+ "workflow.stepReview": ["every", "item"],
27
+ "git.mode": ["trunk", "branch-per-item"],
28
+ "git.checkpoints": ["none", "every-step", "squash"],
29
+ "verification.logicTests": ["when-configured", "required"],
30
+ "verification.uiEvidence": ["when-available", "required"],
31
+ "qualityGates.audit": ["manual", "when-sensitive", "always"],
32
+ "qualityGates.check": ["manual", "when-behavioral", "always"]
33
+ };
34
+ export async function runDoctor(root) {
35
+ const results = [];
36
+ const state = await readProjectState(root);
37
+ const missing = [];
38
+ for (const relative of REQUIRED) {
39
+ if (!(await exists(statePath(root, relative))))
40
+ missing.push(relative);
41
+ }
42
+ results.push({
43
+ name: "required files",
44
+ ok: missing.length === 0,
45
+ detail: missing.length === 0 ? `all ${REQUIRED.length} present` : `missing: ${missing.join(", ")}`,
46
+ blocks: missing.some((m) => m.startsWith("history/")) ? "completion" : "work"
47
+ });
48
+ if (!state.configValid) {
49
+ results.push({ name: "configuration", ok: false, detail: "config.json does not parse", blocks: "work" });
50
+ }
51
+ else {
52
+ const bad = invalidSettings(state.config);
53
+ results.push({
54
+ name: "configuration",
55
+ ok: bad.length === 0,
56
+ detail: bad.length === 0 ? "valid" : bad.join("; "),
57
+ blocks: "work"
58
+ });
59
+ }
60
+ const trees = [".claude/skills", ".agents/skills"];
61
+ const present = {};
62
+ for (const tree of trees) {
63
+ const dir = path.join(root, tree);
64
+ if (!(await exists(dir)))
65
+ continue;
66
+ present[tree] = (await fs.readdir(dir, { withFileTypes: true }))
67
+ .filter((e) => e.isDirectory())
68
+ .map((e) => e.name)
69
+ .sort();
70
+ }
71
+ const installed = Object.keys(present);
72
+ if (installed.length === 0) {
73
+ results.push({ name: "adapters", ok: false, detail: "no skill tree installed", blocks: "work" });
74
+ }
75
+ else if (installed.length === 1) {
76
+ results.push({
77
+ name: "adapters",
78
+ ok: true,
79
+ detail: `${installed[0]} with ${present[installed[0]]?.length ?? 0} skills`,
80
+ blocks: "nothing"
81
+ });
82
+ }
83
+ else {
84
+ const [a, b] = installed;
85
+ const same = JSON.stringify(present[a]) === JSON.stringify(present[b]);
86
+ results.push({
87
+ name: "adapters",
88
+ ok: same,
89
+ detail: same ? `${installed.length} trees, same skills` : `${a} and ${b} hold different skills`,
90
+ blocks: "nothing"
91
+ });
92
+ }
93
+ const entry = (await readIfPresent(path.join(root, "AGENTS.md"))) ??
94
+ (await readIfPresent(path.join(root, "CLAUDE.md")));
95
+ const placeholder = entry !== null && entry.includes(`${STATE_DIR}:setup-required`);
96
+ results.push({
97
+ name: "entry file commands",
98
+ ok: entry !== null && !placeholder,
99
+ detail: entry === null
100
+ ? "no entry file found"
101
+ : placeholder
102
+ ? "still the shipped placeholder; setup has not run"
103
+ : "filled in",
104
+ blocks: "work"
105
+ });
106
+ results.push({
107
+ name: "overview freshness",
108
+ ok: state.overviewPresent && state.overviewFresh !== false,
109
+ detail: !state.overviewPresent
110
+ ? "not generated yet"
111
+ : state.overviewFresh === false
112
+ ? "the plans changed after it was generated"
113
+ : "current",
114
+ blocks: "work"
115
+ });
116
+ const gitignore = (await readIfPresent(path.join(root, ".gitignore"))) ?? "";
117
+ const ignored = new RegExp(`^\\s*/?${STATE_DIR}/?\\s*$`, "m").test(gitignore);
118
+ results.push({
119
+ name: "visibility",
120
+ ok: true,
121
+ detail: ignored ? "state is local only" : "state is committed",
122
+ blocks: "nothing"
123
+ });
124
+ results.push({
125
+ name: "git",
126
+ ok: await exists(path.join(root, ".git")),
127
+ detail: (await exists(path.join(root, ".git"))) ? "repository present" : "not a git repository",
128
+ blocks: "completion"
129
+ });
130
+ return results;
131
+ }
132
+ function invalidSettings(config) {
133
+ const problems = [];
134
+ if (config === null || typeof config !== "object")
135
+ return ["not an object"];
136
+ for (const [key, allowed] of Object.entries(ALLOWED)) {
137
+ const value = key.split(".").reduce((node, part) => (node && typeof node === "object" ? node[part] : undefined), config);
138
+ if (value === undefined)
139
+ continue;
140
+ if (!allowed.includes(String(value))) {
141
+ problems.push(`${key} is "${String(value)}", expected one of ${allowed.join(", ")}`);
142
+ }
143
+ }
144
+ return problems;
145
+ }
146
+ export function renderDoctor(results) {
147
+ const failures = results.filter((r) => !r.ok);
148
+ const order = { work: 0, completion: 1, nothing: 2 };
149
+ const sorted = [...failures].sort((a, b) => order[a.blocks] - order[b.blocks]);
150
+ const out = [
151
+ failures.length === 0 ? "Healthy." : `${failures.length} problem(s).`,
152
+ ""
153
+ ];
154
+ for (const result of sorted) {
155
+ const what = result.blocks === "nothing" ? "cosmetic" : `blocks ${result.blocks}`;
156
+ out.push(` FAIL ${result.name} (${what})`, ` ${result.detail}`);
157
+ }
158
+ if (failures.length > 0)
159
+ out.push("");
160
+ for (const result of results.filter((r) => r.ok)) {
161
+ out.push(` ok ${result.name}: ${result.detail}`);
162
+ }
163
+ return out.join("\n");
164
+ }