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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installing and updating the workflow files.
|
|
3
|
+
*
|
|
4
|
+
* The rule that shapes all of this: files the project owns are seeded once and never
|
|
5
|
+
* touched again, while files the workflow owns are managed and can be updated. Getting
|
|
6
|
+
* that backwards destroys someone's plans on an update, so the two sets are kept explicit
|
|
7
|
+
* rather than inferred from paths at the point of use.
|
|
8
|
+
*/
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { exists, STATE_DIR } from "./paths.js";
|
|
13
|
+
export const ADAPTERS = {
|
|
14
|
+
claude: { label: "Claude Code", trees: [".claude/skills", ".claude/hooks"], entry: ["CLAUDE.md"] },
|
|
15
|
+
codex: { label: "Codex", trees: [".agents/skills"], entry: ["AGENTS.md"] },
|
|
16
|
+
copilot: { label: "GitHub Copilot", trees: [".agents/skills"], entry: ["AGENTS.md"] },
|
|
17
|
+
opencode: { label: "OpenCode", trees: [".agents/skills"], entry: ["AGENTS.md"] }
|
|
18
|
+
};
|
|
19
|
+
const MANIFEST = path.join(STATE_DIR, ".state", "manifest.json");
|
|
20
|
+
export function hash(contents) {
|
|
21
|
+
return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
/** Managed files are the workflow's own; everything under the state directory is the project's. */
|
|
24
|
+
export function isManaged(relative) {
|
|
25
|
+
return !relative.startsWith(`${STATE_DIR}/`);
|
|
26
|
+
}
|
|
27
|
+
export async function planInstall(templateRoot, target, adapters, previous) {
|
|
28
|
+
const wanted = wantedPaths(adapters);
|
|
29
|
+
const plan = [];
|
|
30
|
+
for (const relative of await walk(templateRoot)) {
|
|
31
|
+
if (!wanted(relative))
|
|
32
|
+
continue;
|
|
33
|
+
const destination = path.join(target, relative);
|
|
34
|
+
const source = await fs.readFile(path.join(templateRoot, relative));
|
|
35
|
+
if (!(await exists(destination))) {
|
|
36
|
+
plan.push({ relative, action: "create" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// State files hold the project's real work. Once present they are never rewritten.
|
|
40
|
+
if (!isManaged(relative)) {
|
|
41
|
+
plan.push({ relative, action: "seed-skip" });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const current = await fs.readFile(destination);
|
|
45
|
+
if (hash(current) === hash(source)) {
|
|
46
|
+
plan.push({ relative, action: "unchanged" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// A managed file that differs is only safe to replace when it still matches what was
|
|
50
|
+
// installed. Anything else is a local edit, and overwriting it silently is the failure
|
|
51
|
+
// this manifest exists to prevent.
|
|
52
|
+
const recorded = previous?.managed[relative];
|
|
53
|
+
plan.push({ relative, action: recorded && recorded === hash(current) ? "update" : "conflict" });
|
|
54
|
+
}
|
|
55
|
+
return plan;
|
|
56
|
+
}
|
|
57
|
+
export async function applyInstall(templateRoot, target, plan, options) {
|
|
58
|
+
const written = [];
|
|
59
|
+
const conflicts = [];
|
|
60
|
+
const backups = [];
|
|
61
|
+
for (const entry of plan) {
|
|
62
|
+
if (entry.action === "unchanged" || entry.action === "seed-skip")
|
|
63
|
+
continue;
|
|
64
|
+
if (entry.action === "conflict" && !options.force) {
|
|
65
|
+
conflicts.push(entry.relative);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const destination = path.join(target, entry.relative);
|
|
69
|
+
if (entry.action === "conflict") {
|
|
70
|
+
const backup = path.join(target, STATE_DIR, ".state", "backups", entry.relative);
|
|
71
|
+
await fs.mkdir(path.dirname(backup), { recursive: true });
|
|
72
|
+
await fs.copyFile(destination, backup);
|
|
73
|
+
backups.push(entry.relative);
|
|
74
|
+
}
|
|
75
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
76
|
+
await fs.copyFile(path.join(templateRoot, entry.relative), destination);
|
|
77
|
+
written.push(entry.relative);
|
|
78
|
+
}
|
|
79
|
+
return { written, conflicts, backups };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Records what was *installed*, not what is currently on disk.
|
|
83
|
+
*
|
|
84
|
+
* This distinction is the whole mechanism. The manifest answers "is this file still the one
|
|
85
|
+
* we shipped, or did someone edit it", so it must hold the template's hash. Hashing the
|
|
86
|
+
* working copy instead records a local edit as though it were the installed version, and
|
|
87
|
+
* the next update then treats that edit as stale and overwrites it. The protection would
|
|
88
|
+
* work exactly once.
|
|
89
|
+
*
|
|
90
|
+
* A file left alone as a conflict keeps its previous entry, since that is still the last
|
|
91
|
+
* version actually installed there.
|
|
92
|
+
*/
|
|
93
|
+
export async function writeManifest(target, version, adapters, templateRoot, previous, skipped) {
|
|
94
|
+
const managed = {};
|
|
95
|
+
const untouched = new Set(skipped);
|
|
96
|
+
for (const relative of await walk(templateRoot, wantedPaths(adapters))) {
|
|
97
|
+
if (!isManaged(relative))
|
|
98
|
+
continue;
|
|
99
|
+
if (untouched.has(relative)) {
|
|
100
|
+
const recorded = previous?.managed[relative];
|
|
101
|
+
if (recorded)
|
|
102
|
+
managed[relative] = recorded;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
managed[relative] = hash(await fs.readFile(path.join(templateRoot, relative)));
|
|
106
|
+
}
|
|
107
|
+
const manifest = { schemaVersion: 1, version, adapters: [...adapters], managed };
|
|
108
|
+
const file = path.join(target, MANIFEST);
|
|
109
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
110
|
+
await fs.writeFile(file, JSON.stringify(manifest, null, 2) + "\n");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Wires the hooks into Claude Code's settings.
|
|
114
|
+
*
|
|
115
|
+
* Only when there are no settings yet. A project's existing settings are its own, and
|
|
116
|
+
* merging into them blind is how a tool destroys configuration it did not write. When they
|
|
117
|
+
* exist, the caller is told to wire it manually and shown where the template is.
|
|
118
|
+
*/
|
|
119
|
+
export async function wireHooks(target) {
|
|
120
|
+
const template = path.join(target, STATE_DIR, ".state", "settings-template.json");
|
|
121
|
+
const settings = path.join(target, ".claude", "settings.json");
|
|
122
|
+
if (!(await exists(template)))
|
|
123
|
+
return "no-template";
|
|
124
|
+
if (await exists(settings))
|
|
125
|
+
return "exists";
|
|
126
|
+
await fs.mkdir(path.dirname(settings), { recursive: true });
|
|
127
|
+
await fs.copyFile(template, settings);
|
|
128
|
+
return "written";
|
|
129
|
+
}
|
|
130
|
+
export async function readManifest(target) {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(await fs.readFile(path.join(target, MANIFEST), "utf8"));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function wantedPaths(adapters) {
|
|
139
|
+
const prefixes = new Set();
|
|
140
|
+
for (const adapter of adapters) {
|
|
141
|
+
for (const tree of ADAPTERS[adapter].trees)
|
|
142
|
+
prefixes.add(tree);
|
|
143
|
+
for (const entry of ADAPTERS[adapter].entry)
|
|
144
|
+
prefixes.add(entry);
|
|
145
|
+
}
|
|
146
|
+
prefixes.add(STATE_DIR);
|
|
147
|
+
return (relative) => [...prefixes].some((p) => relative === p || relative.startsWith(`${p}/`));
|
|
148
|
+
}
|
|
149
|
+
async function walk(dir, filter, prefix = "") {
|
|
150
|
+
let entries;
|
|
151
|
+
try {
|
|
152
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const files = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
160
|
+
continue;
|
|
161
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
files.push(...(await walk(path.join(dir, entry.name), filter, relative)));
|
|
164
|
+
}
|
|
165
|
+
else if (!filter || filter(relative)) {
|
|
166
|
+
files.push(relative);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return files.sort();
|
|
170
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const STATE_DIR = "religion";
|
|
4
|
+
/**
|
|
5
|
+
* Walks up from `start` to the nearest directory holding the state directory.
|
|
6
|
+
*
|
|
7
|
+
* Commands are routinely run from a subdirectory, and a tool that only works from the
|
|
8
|
+
* repository root is a tool people stop using.
|
|
9
|
+
*/
|
|
10
|
+
export async function findProjectRoot(start) {
|
|
11
|
+
let current = path.resolve(start);
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (await exists(path.join(current, STATE_DIR, "config.json")))
|
|
14
|
+
return current;
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current)
|
|
17
|
+
return null;
|
|
18
|
+
current = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function statePath(root, ...parts) {
|
|
22
|
+
return path.join(root, STATE_DIR, ...parts);
|
|
23
|
+
}
|
|
24
|
+
export async function exists(target) {
|
|
25
|
+
try {
|
|
26
|
+
await fs.stat(target);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function readIfPresent(file) {
|
|
34
|
+
try {
|
|
35
|
+
return await fs.readFile(file, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for the state files.
|
|
3
|
+
*
|
|
4
|
+
* Each file has one parser and one shape. These are deliberately tolerant: a half-written
|
|
5
|
+
* plan or a hand-edited ledger should degrade into partial information, never an exception,
|
|
6
|
+
* because the commands that use this are how someone finds out what is wrong.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import { readIfPresent, statePath } from "./paths.js";
|
|
10
|
+
const CHECKBOX = /^(\s*)- \[( |x|X)\]\s*(.*)$/;
|
|
11
|
+
export function parsePlan(source) {
|
|
12
|
+
if (!source)
|
|
13
|
+
return [];
|
|
14
|
+
// The shipped template explains the format with worked examples, which are checkboxes
|
|
15
|
+
// too. Reading the whole file counts those as real work. When a `## Plan` heading exists,
|
|
16
|
+
// only what follows it is the plan; the guidance above it is documentation.
|
|
17
|
+
const planSection = /^##\s+Plan\s*$/im.exec(source);
|
|
18
|
+
const body = planSection ? source.slice(planSection.index + planSection[0].length) : source;
|
|
19
|
+
const items = [];
|
|
20
|
+
for (const line of body.split(/\r?\n/)) {
|
|
21
|
+
const match = CHECKBOX.exec(line);
|
|
22
|
+
if (!match)
|
|
23
|
+
continue;
|
|
24
|
+
const body = (match[3] ?? "").trim();
|
|
25
|
+
// The template ships two placeholder items so the shape is visible. They are not work.
|
|
26
|
+
if (/^\d+\.\s+\*\*(Feature one|Feature two)\*\*/.test(body))
|
|
27
|
+
continue;
|
|
28
|
+
items.push({
|
|
29
|
+
number: /^([\dA-Za-z]+)\./.exec(body)?.[1] ?? null,
|
|
30
|
+
title: body.replace(/^[\dA-Za-z]+\.\s*/, "").replace(/\*\*/g, ""),
|
|
31
|
+
done: (match[2] ?? " ").toLowerCase() === "x",
|
|
32
|
+
depth: Math.floor((match[1] ?? "").length / 2)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return items;
|
|
36
|
+
}
|
|
37
|
+
export function parseWork(source) {
|
|
38
|
+
const empty = {
|
|
39
|
+
active: false,
|
|
40
|
+
title: null,
|
|
41
|
+
type: null,
|
|
42
|
+
status: null,
|
|
43
|
+
stepsDone: 0,
|
|
44
|
+
stepsTotal: 0,
|
|
45
|
+
nextStep: null
|
|
46
|
+
};
|
|
47
|
+
if (!source || /_Nothing in progress\./.test(source))
|
|
48
|
+
return empty;
|
|
49
|
+
const steps = [...source.matchAll(/^\s*- \[( |x|X)\]\s*(?:\*\*)?(.+?)(?:\*\*)?(?:\s+-\s|$)/gim)];
|
|
50
|
+
const done = steps.filter((s) => (s[1] ?? " ").toLowerCase() === "x").length;
|
|
51
|
+
return {
|
|
52
|
+
active: true,
|
|
53
|
+
title: /^#\s+(.+)$/m.exec(source)?.[1]?.trim() ?? null,
|
|
54
|
+
type: /^\*\*Type:\*\*\s*(.+)$/m.exec(source)?.[1]?.trim() ?? null,
|
|
55
|
+
status: /^\*\*Status:\*\*\s*(.+)$/m.exec(source)?.[1]?.trim() ?? null,
|
|
56
|
+
stepsDone: done,
|
|
57
|
+
stepsTotal: steps.length,
|
|
58
|
+
nextStep: steps.find((s) => (s[1] ?? " ").toLowerCase() !== "x")?.[2]?.trim() ?? null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function parseFindings(source) {
|
|
62
|
+
if (!source)
|
|
63
|
+
return [];
|
|
64
|
+
return [...source.matchAll(/^###\s+(F-\d+)\s+\[(P[0-3])\]\s+(\w+)\s+-\s+(.+)$/gim)].map((m) => ({
|
|
65
|
+
id: m[1],
|
|
66
|
+
severity: m[2],
|
|
67
|
+
status: m[3],
|
|
68
|
+
title: (m[4] ?? "").trim()
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
export function parseOpenQuestions(overview) {
|
|
72
|
+
if (!overview)
|
|
73
|
+
return [];
|
|
74
|
+
const section = /##\s+Open questions\s*\n([\s\S]*?)(?:\n##\s|$)/i.exec(overview);
|
|
75
|
+
if (!section?.[1])
|
|
76
|
+
return [];
|
|
77
|
+
return [...section[1].matchAll(/^-\s+\*\*(.+?)\*\*/gim)].map((m) => (m[1] ?? "").trim());
|
|
78
|
+
}
|
|
79
|
+
/** The stamp the overview carries, and the hash the current plans produce. */
|
|
80
|
+
export function overviewHash(projectPlan, buildPlan) {
|
|
81
|
+
return crypto.createHash("sha256").update(projectPlan).update(buildPlan).digest("hex").slice(0, 16);
|
|
82
|
+
}
|
|
83
|
+
export async function readProjectState(root) {
|
|
84
|
+
const [configRaw, projectPlan, buildPlan, work, findings, overview] = await Promise.all([
|
|
85
|
+
readIfPresent(statePath(root, "config.json")),
|
|
86
|
+
readIfPresent(statePath(root, "project-plan.md")),
|
|
87
|
+
readIfPresent(statePath(root, "build-plan.md")),
|
|
88
|
+
readIfPresent(statePath(root, "context", "current-work.md")),
|
|
89
|
+
readIfPresent(statePath(root, "context", "findings.md")),
|
|
90
|
+
readIfPresent(statePath(root, "context", "project-overview.md"))
|
|
91
|
+
]);
|
|
92
|
+
let config = null;
|
|
93
|
+
let configValid = true;
|
|
94
|
+
if (configRaw !== null) {
|
|
95
|
+
try {
|
|
96
|
+
config = JSON.parse(configRaw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
configValid = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const stamped = overview ? /source-hash\s+([0-9a-f]{16})/.exec(overview)?.[1] ?? null : null;
|
|
103
|
+
const generated = overview !== null && !/_Not generated yet\./.test(overview);
|
|
104
|
+
return {
|
|
105
|
+
root,
|
|
106
|
+
config,
|
|
107
|
+
configValid,
|
|
108
|
+
plan: parsePlan(buildPlan),
|
|
109
|
+
work: parseWork(work),
|
|
110
|
+
findings: parseFindings(findings),
|
|
111
|
+
overviewPresent: generated,
|
|
112
|
+
overviewFresh: !generated || stamped === null
|
|
113
|
+
? null
|
|
114
|
+
: stamped === overviewHash(projectPlan ?? "", buildPlan ?? ""),
|
|
115
|
+
openQuestions: parseOpenQuestions(overview)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the project's state means, and the one action that follows from it.
|
|
3
|
+
*
|
|
4
|
+
* The next action is the point. A report that lists five things someone could do has not
|
|
5
|
+
* answered the question they asked, which is almost always "what now".
|
|
6
|
+
*/
|
|
7
|
+
export function computeStatus(state) {
|
|
8
|
+
const leaves = state.plan.filter((item, index) => state.plan[index + 1] === undefined || state.plan[index + 1].depth <= item.depth);
|
|
9
|
+
const done = leaves.filter((i) => i.done).length;
|
|
10
|
+
const nextItem = leaves.find((i) => !i.done) ?? null;
|
|
11
|
+
const byStatus = {};
|
|
12
|
+
for (const finding of state.findings) {
|
|
13
|
+
byStatus[finding.status] = (byStatus[finding.status] ?? 0) + 1;
|
|
14
|
+
}
|
|
15
|
+
const blocking = state.findings
|
|
16
|
+
.filter((f) => (f.severity === "P0" || f.severity === "P1") && (f.status === "open" || f.status === "fixed"))
|
|
17
|
+
.map((f) => `${f.id} [${f.severity}] ${f.status} - ${f.title}`);
|
|
18
|
+
const warnings = [];
|
|
19
|
+
if (!state.configValid)
|
|
20
|
+
warnings.push("config.json does not parse; workflow commands will stop rather than guess");
|
|
21
|
+
if (state.overviewFresh === false)
|
|
22
|
+
warnings.push("the plans changed after the overview was generated");
|
|
23
|
+
if (state.openQuestions.length > 0) {
|
|
24
|
+
warnings.push(`open question(s) in the overview: ${state.openQuestions.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
if (state.work.active && state.work.stepsTotal > 0 && state.work.stepsDone === state.work.stepsTotal &&
|
|
27
|
+
state.work.status !== "verified") {
|
|
28
|
+
warnings.push("every step is ticked but the work is not verified");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
next: nextAction(state, nextItem?.title ?? null, blocking.length > 0),
|
|
33
|
+
plan: { done, total: leaves.length, nextItem: nextItem?.title ?? null },
|
|
34
|
+
work: state.work,
|
|
35
|
+
findings: { total: state.findings.length, blocking, byStatus },
|
|
36
|
+
overview: { present: state.overviewPresent, fresh: state.overviewFresh },
|
|
37
|
+
warnings
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function nextAction(state, nextItem, blocked) {
|
|
41
|
+
if (!state.configValid)
|
|
42
|
+
return { command: "doctor", because: "configuration cannot be parsed" };
|
|
43
|
+
if (!state.overviewPresent)
|
|
44
|
+
return { command: "overview", because: "no source of truth has been generated" };
|
|
45
|
+
if (state.overviewFresh === false)
|
|
46
|
+
return { command: "overview", because: "the plans have changed since it was generated" };
|
|
47
|
+
if (state.work.active) {
|
|
48
|
+
if (blocked)
|
|
49
|
+
return { command: "audit", because: "a blocking finding must be re-reviewed before completion" };
|
|
50
|
+
if (state.work.stepsDone < state.work.stepsTotal) {
|
|
51
|
+
return { command: "implement", because: `step ${state.work.stepsDone + 1} of ${state.work.stepsTotal} is next` };
|
|
52
|
+
}
|
|
53
|
+
if (state.work.status !== "verified")
|
|
54
|
+
return { command: "check", because: "every step is built but nothing has proved it" };
|
|
55
|
+
return { command: "complete", because: "the work is built and verified" };
|
|
56
|
+
}
|
|
57
|
+
if (nextItem)
|
|
58
|
+
return { command: "feature", because: `next in the plan: ${nextItem}` };
|
|
59
|
+
if (state.plan.length === 0)
|
|
60
|
+
return { command: "overview", because: "the build plan has no items yet" };
|
|
61
|
+
return { command: "release", because: "the build plan is complete" };
|
|
62
|
+
}
|
|
63
|
+
export function renderStatus(status, cmd = "/") {
|
|
64
|
+
const out = [];
|
|
65
|
+
const pct = status.plan.total > 0 ? Math.round((status.plan.done / status.plan.total) * 100) : 0;
|
|
66
|
+
out.push(`Next: ${cmd}${status.next.command} (${status.next.because})`, "");
|
|
67
|
+
out.push(status.work.active
|
|
68
|
+
? `Active: ${status.work.title ?? "untitled"} - step ${status.work.stepsDone}/${status.work.stepsTotal}` +
|
|
69
|
+
(status.work.status ? ` (${status.work.status})` : "")
|
|
70
|
+
: "Active: nothing in progress");
|
|
71
|
+
out.push(`Plan: ${status.plan.done}/${status.plan.total} complete (${pct}%)`);
|
|
72
|
+
if (status.findings.total === 0) {
|
|
73
|
+
out.push("Findings: none");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const summary = Object.entries(status.findings.byStatus).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
77
|
+
out.push(`Findings: ${summary}`);
|
|
78
|
+
for (const item of status.findings.blocking)
|
|
79
|
+
out.push(` BLOCKING ${item}`);
|
|
80
|
+
}
|
|
81
|
+
out.push(`Overview: ${!status.overview.present ? "not generated" : status.overview.fresh === false ? "stale" : "current"}`);
|
|
82
|
+
if (status.warnings.length > 0) {
|
|
83
|
+
out.push("", "Warnings:");
|
|
84
|
+
for (const warning of status.warnings)
|
|
85
|
+
out.push(` - ${warning}`);
|
|
86
|
+
}
|
|
87
|
+
return out.join("\n");
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-religion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A file-backed, spec-driven AI development workflow. Spec before code, small reviewed diffs, and project state in markdown so a cleared context costs nothing.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Nate",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"homepage": "https://github.com/foo-stack/religion#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/foo-stack/religion.git",
|
|
12
|
+
"directory": "packages/create-religion"
|
|
13
|
+
},
|
|
14
|
+
"bugs": { "url": "https://github.com/foo-stack/religion/issues" },
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai",
|
|
17
|
+
"ai-coding",
|
|
18
|
+
"agent",
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"codex",
|
|
22
|
+
"copilot",
|
|
23
|
+
"opencode",
|
|
24
|
+
"spec-driven-development",
|
|
25
|
+
"context-engineering",
|
|
26
|
+
"workflow",
|
|
27
|
+
"scaffold"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"create-religion": "dist/bin/religion.js",
|
|
31
|
+
"religion": "dist/bin/religion.js"
|
|
32
|
+
},
|
|
33
|
+
"files": ["dist/", "template/", "README.md", "LICENSE"],
|
|
34
|
+
"engines": { "node": ">=20" },
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"prepack": "npm --prefix ../.. run build && npm run build"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": { "access": "public" }
|
|
40
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit
|
|
3
|
+
summary: review the code itself across four lenses, recording findings in the ledger
|
|
4
|
+
description: "Read-only code review across four lenses (quality, security, performance, tests) and four scopes (current work, changed files, a path, or the whole project), fanning the lenses out to subagents that read and return findings while this skill performs the single write. Records each finding in religion/context/findings.md with a durable identifier, a severity from P0 to P3, and a status, where an open or fixed P0 or P1 blocks completion. Use when the user runs $audit, asks for a code, quality, security, performance, or test review, wants dead code or duplication found, or wants a repair re-reviewed before it closes."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# audit - review the code, and record what you find
|
|
8
|
+
|
|
9
|
+
$check proves the work behaves as promised. This skill reviews the code itself.
|
|
10
|
+
|
|
11
|
+
It changes nothing except the findings ledger. It never edits source, installs anything,
|
|
12
|
+
commits, merges, or starts repair work.
|
|
13
|
+
|
|
14
|
+
## Input
|
|
15
|
+
|
|
16
|
+
Scope and lens are separate, and may appear in either order: `$audit security current`,
|
|
17
|
+
`$audit src/auth tests`.
|
|
18
|
+
|
|
19
|
+
**Scope**, defaulting to `current` when work is active, else `changed` when the tree is
|
|
20
|
+
dirty, else `full`:
|
|
21
|
+
|
|
22
|
+
- `current` - the active spec, everything the work item has changed so far, and the code
|
|
23
|
+
around it that the change affects
|
|
24
|
+
- `changed` - staged, unstaged, and untracked source, plus nearby code
|
|
25
|
+
- `full` - all project-owned source, tests, and configuration, excluding dependencies,
|
|
26
|
+
generated files, build output, caches, vendored code, and minified assets
|
|
27
|
+
- a path - that area, plus the tests and callers needed to understand it
|
|
28
|
+
|
|
29
|
+
**Lens**, defaulting to all four:
|
|
30
|
+
|
|
31
|
+
- `quality` - duplication, dead code, unreachable paths, oversized modules, abstractions
|
|
32
|
+
that do not pay for themselves, inconsistency, drift from the project's standards
|
|
33
|
+
- `security` - missing authorization, client-controlled ownership, injection, unsafe
|
|
34
|
+
parsing, data exposure, secret handling, insecure defaults, trust-boundary mistakes
|
|
35
|
+
- `performance` - repeated queries and network calls, unnecessary rendering, blocking work
|
|
36
|
+
on hot paths, unbounded loops and collections, memory growth, oversized payloads,
|
|
37
|
+
missing pagination, unsafe concurrency
|
|
38
|
+
- `tests` - important logic with no coverage, weak assertions, tests that only mirror the
|
|
39
|
+
implementation, excessive mocking, shared state, order or time dependence, skipped or
|
|
40
|
+
focused tests, swallowed failures
|
|
41
|
+
|
|
42
|
+
State the scope and lens you chose before reviewing.
|
|
43
|
+
|
|
44
|
+
## Step 1 - gather context
|
|
45
|
+
|
|
46
|
+
Read the project overview, the coding standards, the active spec, the existing findings
|
|
47
|
+
ledger (for identifiers and statuses), the configuration, and the relevant source.
|
|
48
|
+
|
|
49
|
+
For `current`, resolve the comparison base without touching the network: a base named by
|
|
50
|
+
the spec or project instructions, else the recorded default branch, else a local `main`
|
|
51
|
+
then `master`. Find the merge base and inspect the committed range through `HEAD`, then add
|
|
52
|
+
staged, unstaged, and untracked work. If no reliable base exists, say so and review the
|
|
53
|
+
spec plus local changes, without claiming committed work was covered.
|
|
54
|
+
|
|
55
|
+
For `full`, state the excluded paths before reviewing, so generated or third-party code
|
|
56
|
+
does not consume the review.
|
|
57
|
+
|
|
58
|
+
## Step 2 - run the signals that exist
|
|
59
|
+
|
|
60
|
+
Use only commands the project already has. Do not install tools, and do not fetch.
|
|
61
|
+
|
|
62
|
+
Lint and typecheck when relevant; the test command for the tests lens or to validate a
|
|
63
|
+
suspected risk; the build when the lens needs compilation evidence. A useful command that
|
|
64
|
+
does not exist is a gap worth reporting, not a reason to add one.
|
|
65
|
+
|
|
66
|
+
## Step 3 - review, one subagent per lens
|
|
67
|
+
|
|
68
|
+
Run each selected lens as its own subagent over the resolved scope. They read code and
|
|
69
|
+
**return** findings; they never write. This skill merges what comes back, assigns
|
|
70
|
+
identifiers, and performs the single write in Step 4. Parallel reviewers cannot race a
|
|
71
|
+
file that only one of them writes.
|
|
72
|
+
|
|
73
|
+
Give each subagent the scope, the file list, the standards, and its own lens only. A lens
|
|
74
|
+
that wanders into another lens produces duplicates that then have to be reconciled.
|
|
75
|
+
|
|
76
|
+
**The ledger never scopes the review.** Review the code fresh, then record what was found.
|
|
77
|
+
Working from the open findings as a checklist and verifying only those is the exact failure
|
|
78
|
+
the ledger exists to prevent: a repair can introduce a defect no existing entry points at.
|
|
79
|
+
|
|
80
|
+
Ground every finding in reachable code. Prefer a short list of real findings to a long list
|
|
81
|
+
of guesses, and do not report style differences that are merely different.
|
|
82
|
+
|
|
83
|
+
Do not broaden a focused pass because another lens looks interesting. If an obvious P0
|
|
84
|
+
appears outside the selected lens, record it as an out-of-lens critical risk, but do not
|
|
85
|
+
start searching that lens.
|
|
86
|
+
|
|
87
|
+
If a possible secret appears, never quote its value, paste the matching line, or include
|
|
88
|
+
raw output containing it. Report the redacted category, file, line, risk, and remediation.
|
|
89
|
+
|
|
90
|
+
## Step 4 - write the ledger
|
|
91
|
+
|
|
92
|
+
`religion/context/findings.md` is the durable record. Chat does not survive a cleared
|
|
93
|
+
context; this does. Create it with a `# Findings` heading if it is missing.
|
|
94
|
+
|
|
95
|
+
One block per finding. The header line is the machine-readable part and keeps this exact
|
|
96
|
+
shape; the prose below it is for people.
|
|
97
|
+
|
|
98
|
+
### F-03 [P0] open - Retained auth volumes carry the run label
|
|
99
|
+
|
|
100
|
+
**File:** ops/compose.yaml:86
|
|
101
|
+
**Found:** 2026-09-04 by audit (scope: current; lens: security)
|
|
102
|
+
**Why it matters:** ...
|
|
103
|
+
**Suggested fix:** ...
|
|
104
|
+
**Resolution:**
|
|
105
|
+
|
|
106
|
+
Identifiers are sequential within the ledger, one past the highest present, never reused
|
|
107
|
+
and never renumbered while their entries live there. Completion archives resolved entries
|
|
108
|
+
under a work-item prefix, and that prefixed form is the permanent reference.
|
|
109
|
+
|
|
110
|
+
| Status | Meaning | Blocks completion at P0/P1 |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `unverified` | Suspected, no confirming evidence yet | No |
|
|
113
|
+
| `open` | Confirmed, not yet repaired | Yes |
|
|
114
|
+
| `fixed` | Repaired, not yet re-reviewed | Yes |
|
|
115
|
+
| `closed` | Repaired and re-reviewed against the new code | No |
|
|
116
|
+
| `accepted` | Not fixing, by the user's explicit decision, reason recorded | No |
|
|
117
|
+
| `invalid` | Re-examination proved it wrong, evidence recorded | No |
|
|
118
|
+
|
|
119
|
+
After reviewing:
|
|
120
|
+
|
|
121
|
+
- Append each new confirmed finding as `open`.
|
|
122
|
+
- Record a lead worth tracking as `unverified`. It never gates anything.
|
|
123
|
+
- Update entries this pass re-examined, noting the evidence in **Resolution**.
|
|
124
|
+
- Move `fixed` to `closed` only when all three hold: this pass covered the finding's file,
|
|
125
|
+
re-examining the repaired code confirmed the defect is gone and nothing worse replaced
|
|
126
|
+
it, and the report names it as closed. An unrelated new problem in the same file gets its
|
|
127
|
+
own entry. Never close implicitly.
|
|
128
|
+
- Set `accepted` only on the user's explicit decision, with their reason. Never on their
|
|
129
|
+
behalf.
|
|
130
|
+
- Set `invalid` only when re-examination shows the finding was wrong, with that evidence.
|
|
131
|
+
It is a review verdict, never a shortcut past a gate.
|
|
132
|
+
|
|
133
|
+
`fixed` blocking completion is deliberate: a repair is done when a review has looked at the
|
|
134
|
+
result, not when the code changed.
|
|
135
|
+
|
|
136
|
+
## Step 5 - report
|
|
137
|
+
|
|
138
|
+
Findings first, ordered by severity, using the identifiers just assigned.
|
|
139
|
+
|
|
140
|
+
Severity: **P0** data loss, a security break, or code that cannot ship. **P1** a likely bug,
|
|
141
|
+
a broken contract, a missing guard. **P2** a maintainability problem worth fixing before
|
|
142
|
+
the work closes. **P3** a small cleanup or follow-up.
|
|
143
|
+
|
|
144
|
+
P0 and P1 require a concrete code path, a violated contract or boundary, a failing command,
|
|
145
|
+
or reproducible behavior. Incomplete evidence goes under **Unverified risks** with what is
|
|
146
|
+
missing, never presented as a confirmed high-severity finding.
|
|
147
|
+
|
|
148
|
+
Then report: ledger changes by identifier; commands run and their results; the scope and
|
|
149
|
+
lens chosen; the commit range for `current`; the paths reviewed and excluded; checks that
|
|
150
|
+
could not run; and a suggested repair order.
|
|
151
|
+
|
|
152
|
+
A focused lens is not a broad review. Say what was not reviewed, and never imply the
|
|
153
|
+
omitted lenses passed. For `full`, say whether coverage was complete or partial.
|
|
154
|
+
|
|
155
|
+
## Rules
|
|
156
|
+
|
|
157
|
+
- The ledger is the only file this skill writes.
|
|
158
|
+
- The ledger reports status; it never defines what the review looks at.
|
|
159
|
+
- Never reproduce a secret or sensitive value.
|
|
160
|
+
- Ground every finding in a file and line where possible.
|
|
161
|
+
- Recommend the smallest fix that removes the risk, not a rewrite.
|
|
162
|
+
- Respect the project's existing patterns over generic advice.
|
|
163
|
+
- The goal is code that is understandable, consistent, tested where it matters, and safe to
|
|
164
|
+
keep building on. Not perfection.
|
|
165
|
+
|
|
166
|
+
## Formatting
|
|
167
|
+
|
|
168
|
+
Match the conventions in `religion/context/ai-interaction.md`: concise, scannable
|
|
169
|
+
markdown, lists for enumerations and tables for matrices rather than dense paragraphs.
|