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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/bin/religion.js +209 -0
- package/dist/lib/dashboard.js +123 -0
- package/dist/lib/doctor.js +164 -0
- package/dist/lib/install.js +170 -0
- package/dist/lib/paths.js +40 -0
- package/dist/lib/state.js +117 -0
- package/dist/lib/status.js +88 -0
- package/package.json +40 -0
- package/template/.agents/skills/audit/SKILL.md +169 -0
- package/template/.agents/skills/auto/SKILL.md +110 -0
- package/template/.agents/skills/browser-tests/SKILL.md +65 -0
- package/template/.agents/skills/check/SKILL.md +86 -0
- package/template/.agents/skills/ci/SKILL.md +79 -0
- package/template/.agents/skills/complete/SKILL.md +184 -0
- package/template/.agents/skills/debug/SKILL.md +72 -0
- package/template/.agents/skills/discovery/SKILL.md +83 -0
- package/template/.agents/skills/distill/SKILL.md +95 -0
- package/template/.agents/skills/doctor/SKILL.md +65 -0
- package/template/.agents/skills/extend/SKILL.md +102 -0
- package/template/.agents/skills/feature/SKILL.md +172 -0
- package/template/.agents/skills/feature/reference/spec-template.md +67 -0
- package/template/.agents/skills/fix/SKILL.md +76 -0
- package/template/.agents/skills/implement/SKILL.md +174 -0
- package/template/.agents/skills/overview/SKILL.md +150 -0
- package/template/.agents/skills/overview/reference/project-overview-template.md +101 -0
- package/template/.agents/skills/prototype/SKILL.md +90 -0
- package/template/.agents/skills/release/SKILL.md +88 -0
- package/template/.agents/skills/rollback/SKILL.md +114 -0
- package/template/.agents/skills/setup/SKILL.md +105 -0
- package/template/.agents/skills/status/SKILL.md +63 -0
- package/template/.agents/skills/tests/SKILL.md +90 -0
- package/template/.agents/skills/try/SKILL.md +59 -0
- package/template/.claude/hooks/guard-rendered-output.mjs +49 -0
- package/template/.claude/hooks/guard-tier-one.mjs +63 -0
- package/template/.claude/hooks/write-handoff.mjs +83 -0
- package/template/.claude/skills/audit/SKILL.md +169 -0
- package/template/.claude/skills/auto/SKILL.md +110 -0
- package/template/.claude/skills/browser-tests/SKILL.md +65 -0
- package/template/.claude/skills/check/SKILL.md +86 -0
- package/template/.claude/skills/ci/SKILL.md +79 -0
- package/template/.claude/skills/complete/SKILL.md +184 -0
- package/template/.claude/skills/debug/SKILL.md +72 -0
- package/template/.claude/skills/discovery/SKILL.md +83 -0
- package/template/.claude/skills/distill/SKILL.md +95 -0
- package/template/.claude/skills/doctor/SKILL.md +65 -0
- package/template/.claude/skills/extend/SKILL.md +102 -0
- package/template/.claude/skills/feature/SKILL.md +172 -0
- package/template/.claude/skills/feature/reference/spec-template.md +67 -0
- package/template/.claude/skills/fix/SKILL.md +76 -0
- package/template/.claude/skills/implement/SKILL.md +174 -0
- package/template/.claude/skills/overview/SKILL.md +150 -0
- package/template/.claude/skills/overview/reference/project-overview-template.md +101 -0
- package/template/.claude/skills/prototype/SKILL.md +90 -0
- package/template/.claude/skills/release/SKILL.md +88 -0
- package/template/.claude/skills/rollback/SKILL.md +114 -0
- package/template/.claude/skills/setup/SKILL.md +105 -0
- package/template/.claude/skills/status/SKILL.md +63 -0
- package/template/.claude/skills/tests/SKILL.md +90 -0
- package/template/.claude/skills/try/SKILL.md +59 -0
- package/template/AGENTS.md +206 -0
- package/template/CLAUDE.md +175 -0
- package/template/religion/.state/README.md +10 -0
- package/template/religion/.state/settings-template.json +19 -0
- package/template/religion/build-plan.md +43 -0
- package/template/religion/config.json +26 -0
- package/template/religion/context/ai-interaction.md +84 -0
- package/template/religion/context/coding-standards.md +77 -0
- package/template/religion/context/current-work.md +8 -0
- package/template/religion/context/findings.md +9 -0
- package/template/religion/context/handoff.md +8 -0
- package/template/religion/context/project-overview.md +8 -0
- package/template/religion/history/features/README.md +8 -0
- package/template/religion/history/fixes/README.md +6 -0
- package/template/religion/history/rollbacks/README.md +7 -0
- package/template/religion/learning/journal.md +7 -0
- package/template/religion/learning/lessons.md +8 -0
- package/template/religion/project-plan.md +49 -0
- 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) => ({ "&":"&","<":"<",">":">" }[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 ? " · " + 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
|
+
}
|