@theagilemonkeys/facility 0.3.1
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 +201 -0
- package/README.md +68 -0
- package/bin/facility.mjs +10 -0
- package/modules/README.md +35 -0
- package/modules/ai-queryability/agents/queryability-reviewer.md +35 -0
- package/modules/ai-queryability/module.json +9 -0
- package/modules/ai-queryability/standard-section.md +22 -0
- package/modules/analytics/agents/analytics-reviewer.md +32 -0
- package/modules/analytics/commands/add-telemetry.md +23 -0
- package/modules/analytics/module.json +10 -0
- package/modules/analytics/standard-section.md +23 -0
- package/modules/database/agents/data-security-reviewer.md +38 -0
- package/modules/database/commands/new-migration.md +24 -0
- package/modules/database/guards/migration-versions.mjs +41 -0
- package/modules/database/guards/migrations-immutable.mjs +57 -0
- package/modules/database/hooks/protect-migrations.fragment.mjs +10 -0
- package/modules/database/module.json +25 -0
- package/modules/database/standard-section.md +20 -0
- package/modules/design-system/agents/design-reviewer.md +37 -0
- package/modules/design-system/module.json +9 -0
- package/modules/design-system/standard-section.md +15 -0
- package/package.json +42 -0
- package/src/add.mjs +77 -0
- package/src/cli.mjs +352 -0
- package/src/detect.mjs +127 -0
- package/src/doctor.mjs +582 -0
- package/src/init.mjs +572 -0
- package/src/instance.mjs +114 -0
- package/src/platform-admin.mjs +1542 -0
- package/src/platform-config.mjs +39 -0
- package/src/platform.mjs +1759 -0
- package/src/prompts.mjs +64 -0
- package/src/render.mjs +66 -0
- package/src/ui.mjs +30 -0
- package/templates/claude/agents/security-reviewer.md +41 -0
- package/templates/claude/agents/standards-reviewer.md +31 -0
- package/templates/claude/commands/open-pr.md +21 -0
- package/templates/claude/commands/verify.md +16 -0
- package/templates/claude/hooks/protect-branch.mjs +58 -0
- package/templates/claude/hooks/protect-files.mjs +35 -0
- package/templates/claude/settings.json +71 -0
- package/templates/claude/skills/maintainable-software/SKILL.md +67 -0
- package/templates/claude/skills/reviewing-to-standard/SKILL.md +49 -0
- package/templates/claude/skills/working-to-standard/SKILL.md +45 -0
- package/templates/delivery/verify.mjs +157 -0
- package/templates/doctor/resolve.mjs +144 -0
- package/templates/guards/README.md +30 -0
- package/templates/guards/_kit.mjs +81 -0
- package/templates/guards/actions-pinned.mjs +38 -0
- package/templates/guards/run.mjs +111 -0
- package/templates/guards/watchtower-locked.mjs +66 -0
- package/templates/prompts/address-review.md +14 -0
- package/templates/prompts/architect.md +62 -0
- package/templates/prompts/builder.md +71 -0
- package/templates/prompts/doctor.md +64 -0
- package/templates/prompts/review.md +14 -0
- package/templates/prompts/sweep.md +75 -0
- package/templates/receipts/collect.mjs +289 -0
- package/templates/review/finalize.mjs +38 -0
- package/templates/scripts/move-board-status.sh +155 -0
- package/templates/security/sync-findings.mjs +226 -0
- package/templates/standard/STANDARD.md +141 -0
- package/templates/standard/agents-block.md +25 -0
- package/templates/watchtower/budgets.json +12 -0
- package/templates/watchtower/canary.mjs +216 -0
- package/templates/watchtower/health.mjs +148 -0
- package/templates/watchtower/outcomes.mjs +188 -0
- package/templates/workflows/facility-address-review.yml +153 -0
- package/templates/workflows/facility-canary.yml +61 -0
- package/templates/workflows/facility-codex.yml +326 -0
- package/templates/workflows/facility-crew.yml +350 -0
- package/templates/workflows/facility-doctor.yml +155 -0
- package/templates/workflows/facility-review.yml +134 -0
- package/templates/workflows/facility-security-sweep.yml +204 -0
- package/templates/workflows/facility-watchtower.yml +87 -0
package/src/prompts.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Interactive prompts on node:readline. Zero dependencies.
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { bold, dim } from "./ui.mjs";
|
|
4
|
+
|
|
5
|
+
let rl = null;
|
|
6
|
+
|
|
7
|
+
function iface() {
|
|
8
|
+
if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
return rl;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function closePrompts() {
|
|
13
|
+
if (rl) {
|
|
14
|
+
rl.close();
|
|
15
|
+
rl = null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Free-text question with a default. Empty answer returns the default. */
|
|
20
|
+
export async function ask(question, defaultValue = "") {
|
|
21
|
+
const suffix = defaultValue ? ` ${dim(`(${defaultValue})`)}` : "";
|
|
22
|
+
const answer = (await questionOrEof(` ${bold(question)}${suffix} `)).trim();
|
|
23
|
+
return answer || defaultValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Yes/no question. */
|
|
27
|
+
export async function confirm(question, defaultYes = true) {
|
|
28
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
29
|
+
const answer = (await questionOrEof(` ${bold(question)} ${dim(`[${hint}]`)} `))
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
if (!answer) return defaultYes;
|
|
33
|
+
return answer.startsWith("y");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function questionOrEof(question) {
|
|
37
|
+
const readline = iface();
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
const onClose = () => {
|
|
41
|
+
if (!settled) {
|
|
42
|
+
settled = true;
|
|
43
|
+
const error = new Error("Input ended before the prompt was answered.");
|
|
44
|
+
error.code = "prompt_eof";
|
|
45
|
+
reject(error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
readline.once("close", onClose);
|
|
49
|
+
readline.question(question).then(
|
|
50
|
+
(answer) => {
|
|
51
|
+
if (settled) return;
|
|
52
|
+
settled = true;
|
|
53
|
+
readline.removeListener("close", onClose);
|
|
54
|
+
resolve(answer);
|
|
55
|
+
},
|
|
56
|
+
(error) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
readline.removeListener("close", onClose);
|
|
60
|
+
reject(error);
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/render.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Template rendering and managed-content insertion.
|
|
2
|
+
//
|
|
3
|
+
// Placeholders are {{UPPER_SNAKE}} with no spaces, so GitHub Actions
|
|
4
|
+
// expressions (`${{ secrets.X }}` — spaces, lowercase, dots) never match and
|
|
5
|
+
// pass through untouched.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Substitute {{VARS}} in a template. A placeholder alone on its line is a
|
|
9
|
+
* block variable: an empty value removes the whole line, a multi-line value
|
|
10
|
+
* lands verbatim. Unknown placeholders are left as-is.
|
|
11
|
+
*/
|
|
12
|
+
export function render(template, vars) {
|
|
13
|
+
const withBlocks = template.replace(
|
|
14
|
+
/^[ \t]*\{\{([A-Z0-9_]+)\}\}[ \t]*\r?\n/gm,
|
|
15
|
+
(match, name) => {
|
|
16
|
+
const value = vars[name];
|
|
17
|
+
if (value === undefined) return match;
|
|
18
|
+
if (value === "") return "";
|
|
19
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
return withBlocks.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, name) => vars[name] ?? match);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** True when `content` already contains the facility managed-block marker. */
|
|
26
|
+
export function hasManagedBlock(content) {
|
|
27
|
+
return content.includes("<!-- facility:start");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Append a managed block to existing file content (with a blank line). */
|
|
31
|
+
export function appendManagedBlock(content, block) {
|
|
32
|
+
const base = content.replace(/\s*$/, "");
|
|
33
|
+
return base ? `${base}\n\n${block}` : block;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MODULES_START = "<!-- facility:modules:start -->";
|
|
37
|
+
const MODULES_END = "<!-- facility:modules:end -->";
|
|
38
|
+
|
|
39
|
+
/** Insert a module's standard section between the modules markers. */
|
|
40
|
+
export function insertModuleSection(standard, section, moduleTitle) {
|
|
41
|
+
if (standard.includes(`(facility module)`) && standard.includes(`### ${moduleTitle} (facility module)`)) {
|
|
42
|
+
return { content: standard, inserted: false };
|
|
43
|
+
}
|
|
44
|
+
const end = standard.indexOf(MODULES_END);
|
|
45
|
+
if (end === -1) {
|
|
46
|
+
// No markers (user removed them): append under a Modules heading instead.
|
|
47
|
+
const block = `\n## Modules\n\n${section.trim()}\n`;
|
|
48
|
+
return { content: `${standard.replace(/\s*$/, "")}\n${block}`, inserted: true };
|
|
49
|
+
}
|
|
50
|
+
const before = standard.slice(0, end).replace(/[ \t]*$/, "");
|
|
51
|
+
const after = standard.slice(end);
|
|
52
|
+
return { content: `${before}${section.trim()}\n\n${after}`, inserted: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HOOK_MARKER = "/* facility:module-rules */";
|
|
56
|
+
|
|
57
|
+
/** Splice a module's hook rules at the marker in protect-files.mjs. */
|
|
58
|
+
export function insertHookRules(hookSource, fragment, moduleName) {
|
|
59
|
+
const sentinel = `facility module: ${moduleName}`;
|
|
60
|
+
if (hookSource.includes(sentinel)) return { content: hookSource, inserted: false };
|
|
61
|
+
if (!hookSource.includes(HOOK_MARKER)) return { content: hookSource, inserted: false };
|
|
62
|
+
return {
|
|
63
|
+
content: hookSource.replace(HOOK_MARKER, `${fragment.trim()}\n\n${HOOK_MARKER}`),
|
|
64
|
+
inserted: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Terminal output helpers. Zero dependencies; ANSI only when it's a TTY and
|
|
2
|
+
// NO_COLOR is unset. Restrained on purpose — the CLI should read like a
|
|
3
|
+
// well-run factory floor, not a slot machine.
|
|
4
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
5
|
+
const ESC = "\u001b";
|
|
6
|
+
|
|
7
|
+
const wrap = (code) => (text) => (useColor ? `${ESC}[${code}m${text}${ESC}[0m` : String(text));
|
|
8
|
+
|
|
9
|
+
export const bold = wrap("1");
|
|
10
|
+
export const dim = wrap("2");
|
|
11
|
+
export const accent = wrap("38;5;220"); // facility yellow — reserved for agent work
|
|
12
|
+
export const green = wrap("32");
|
|
13
|
+
export const red = wrap("31");
|
|
14
|
+
export const yellow = wrap("33");
|
|
15
|
+
|
|
16
|
+
export function banner(version) {
|
|
17
|
+
console.log("");
|
|
18
|
+
console.log(` ${bold("facility")} ${dim(`v${version}`)} ${dim("— the AI software factory for your repo")}`);
|
|
19
|
+
console.log("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function heading(text) {
|
|
23
|
+
console.log(`\n${bold(text)}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ok = (text) => console.log(` ${green("✓")} ${text}`);
|
|
27
|
+
export const skip = (text) => console.log(` ${dim("—")} ${dim(text)}`);
|
|
28
|
+
export const warn = (text) => console.log(` ${yellow("!")} ${text}`);
|
|
29
|
+
export const fail = (text) => console.log(` ${red("✗")} ${text}`);
|
|
30
|
+
export const item = (text) => console.log(` ${text}`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-reviewer
|
|
3
|
+
description: Adversarial reviewer for auth boundaries, secret handling, injection paths, and privilege escalation. Use proactively before merging any change that touches auth, privileged access, external input parsing, CI workflows, or agent-facing surfaces.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a security reviewer. You review a diff in a fresh context, so you are
|
|
8
|
+
not biased toward the code under review. Assume authorization gaps **fail
|
|
9
|
+
silently** — verify the negative case, never trust a prose claim that a
|
|
10
|
+
boundary is enforced.
|
|
11
|
+
|
|
12
|
+
## What to check
|
|
13
|
+
1. **Secrets**: no credentials, tokens, or signed URLs in code, fixtures,
|
|
14
|
+
logs, or test snapshots. `.env` handling respects the repo's hooks.
|
|
15
|
+
2. **Input trust**: external input (HTTP, webhooks, issue/PR text consumed by
|
|
16
|
+
agents, file uploads) is validated at the boundary and never interpolated
|
|
17
|
+
into shell, SQL, or query builders.
|
|
18
|
+
3. **Authorization**: every new read/write path re-checks permission at the
|
|
19
|
+
boundary that owns the data; no privileged client or service account leaks
|
|
20
|
+
into user-facing or agent-facing read paths.
|
|
21
|
+
4. **CI surface**: workflow changes keep actions pinned to full commit SHAs,
|
|
22
|
+
never echo secrets, never widen `permissions:`, and never expose secrets to
|
|
23
|
+
fork-originated code.
|
|
24
|
+
5. **Agent surface**: prompts and tool contracts keep untrusted text framed as
|
|
25
|
+
DATA; no new path lets repository content instruct an agent to exfiltrate
|
|
26
|
+
or escalate.
|
|
27
|
+
6. **Failure modes**: errors don't leak internals; empty results are
|
|
28
|
+
permission-safe (they don't hint that hidden data exists).
|
|
29
|
+
|
|
30
|
+
## How to verify
|
|
31
|
+
- `node guards/run.mjs` — deterministic repo invariants, including the
|
|
32
|
+
workflow-pinning guard.
|
|
33
|
+
- Targeted greps for the diff's trust boundaries; run the repo's auth/security
|
|
34
|
+
tests when the diff justifies it.
|
|
35
|
+
|
|
36
|
+
## Output contract
|
|
37
|
+
Return findings ordered by severity (Blocker / High / Medium), each with
|
|
38
|
+
file:line, the exact risk, and the smallest fix. State which checks you ran
|
|
39
|
+
and their result. Report **only** security/privacy/correctness gaps — not
|
|
40
|
+
style. If you find nothing, say "No security gaps found" and list the checks
|
|
41
|
+
that prove it.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standards-reviewer
|
|
3
|
+
description: Lead quality reviewer that runs this repo's STANDARD.md completion checklist end to end. Use before finalizing or opening a PR for any change with product behavior.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the lead reviewer enforcing `STANDARD.md` as the development standard
|
|
8
|
+
for the whole team, not only as agent guidance. You review the diff in a fresh
|
|
9
|
+
context and lead with correctness, security, maintainability, and
|
|
10
|
+
product-quality before any polish. Be the adversarial second opinion: report
|
|
11
|
+
only real correctness/security/requirements/quality gaps — do not invent style
|
|
12
|
+
nits or impossible-case concerns.
|
|
13
|
+
|
|
14
|
+
## Method
|
|
15
|
+
1. Establish scope: read the diff and identify user-facing behavior, affected
|
|
16
|
+
domains, and risk level.
|
|
17
|
+
2. Walk the **Pull request review standard** and **Completion checklist** from
|
|
18
|
+
`STANDARD.md`, including any module sections between the
|
|
19
|
+
`facility:modules` markers.
|
|
20
|
+
3. For any domain that carries real risk, recommend (or dispatch, when doing a
|
|
21
|
+
deep review) the matching specialist subagent from `.claude/agents/`.
|
|
22
|
+
4. Run the lightest useful verification yourself and escalate based on risk.
|
|
23
|
+
`node guards/run.mjs` is always cheap and always relevant.
|
|
24
|
+
|
|
25
|
+
## Output contract
|
|
26
|
+
Produce a verdict: **Ready** / **Ready with follow-ups** / **Not ready**. Then
|
|
27
|
+
findings grouped by severity (Blocker / High / Medium), each with file:line,
|
|
28
|
+
the exact risk, the missed standard, and the smallest fix. Avoid vague asks
|
|
29
|
+
like "clean this up" or "add tests" without naming the missing case. Finish
|
|
30
|
+
with the exact checks you ran and their results. Keep it concise and
|
|
31
|
+
team-lead-ready.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Open a concise, team-lead-ready PR following STANDARD.md
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Open a pull request for the current branch following STANDARD.md's PR rules.
|
|
6
|
+
|
|
7
|
+
1. Preconditions: branch name is semantic (`feature/...`, `fix/...`, ... —
|
|
8
|
+
no agent/tool prefixes); commits follow Conventional Commits; the
|
|
9
|
+
relevant checks from /verify have run in this session — if not, run them
|
|
10
|
+
first.
|
|
11
|
+
2. Push the branch and create the PR with `gh pr create` targeting
|
|
12
|
+
{{DEFAULT_BRANCH}}, with:
|
|
13
|
+
- A Conventional-Commits title describing the product/domain intent.
|
|
14
|
+
- The body structure from STANDARD.md (Summary / Context / Verification /
|
|
15
|
+
Linked issues) — tight, no implementation diary, no filler.
|
|
16
|
+
- The full issue URL when one exists in the branch name, commits, or
|
|
17
|
+
conversation, as a `Closes #<n>` closing keyword. Never invent issue
|
|
18
|
+
links.
|
|
19
|
+
3. Mention what was intentionally NOT touched (migrations, analytics,
|
|
20
|
+
security surface) when a reviewer would otherwise expect it.
|
|
21
|
+
4. Print the PR URL.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the right checks for the current change and report honestly
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Verify the current working-tree change per STANDARD.md's verification ladder.
|
|
6
|
+
|
|
7
|
+
1. Look at the diff (`git status`, `git diff`) and classify what it touches:
|
|
8
|
+
pure code, data/migrations, security surface, UI, analytics, agent
|
|
9
|
+
surface.
|
|
10
|
+
2. Run the lightest relevant checks first, escalating by risk:
|
|
11
|
+
|
|
12
|
+
{{CHECKS_LIST}}
|
|
13
|
+
|
|
14
|
+
3. Report a short table: check → result. Name every relevant check you did
|
|
15
|
+
NOT run and why. Never summarize a failure away — paste the meaningful
|
|
16
|
+
tail of the output and say what you'd try next.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generated by facility — https://github.com/theam/facility
|
|
3
|
+
//
|
|
4
|
+
// PreToolUse hook (Bash). Blocks a small set of unambiguously dangerous
|
|
5
|
+
// commands before they run (exit 2 = deny, stderr is shown to the agent).
|
|
6
|
+
// Deliberately narrow: it guards destructive/irreversible actions, not normal
|
|
7
|
+
// development.
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
const PROTECTED = /^(origin\/)?({{DEFAULT_BRANCH}}|main|master)$/;
|
|
11
|
+
|
|
12
|
+
function readPayload() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const command = readPayload()?.tool_input?.command ?? "";
|
|
21
|
+
if (!command) process.exit(0);
|
|
22
|
+
|
|
23
|
+
const tokens = command.split(/\s+/);
|
|
24
|
+
const has = (t) => tokens.includes(t);
|
|
25
|
+
const targetsProtected = (t) => PROTECTED.test(t) || /:({{DEFAULT_BRANCH}}|main|master)$/.test(t);
|
|
26
|
+
const targetsProtectedRef = tokens.some(targetsProtected);
|
|
27
|
+
const isForce = has("--force") || has("-f") || has("--force-with-lease");
|
|
28
|
+
|
|
29
|
+
function block(reason) {
|
|
30
|
+
console.error(`⛔ Blocked by .claude/hooks/protect-branch.mjs\n${reason}`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Recursive force-remove rooted at filesystem root or $HOME.
|
|
35
|
+
if (
|
|
36
|
+
/\brm\b/.test(command) &&
|
|
37
|
+
/-[a-z]*r/i.test(command) &&
|
|
38
|
+
/-[a-z]*f/i.test(command) &&
|
|
39
|
+
/\s(\/|~|\$HOME)(\s|\/|$)/.test(command)
|
|
40
|
+
) {
|
|
41
|
+
block("Recursive force-remove of a root or home path is not allowed.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (has("git") && has("push") && targetsProtectedRef && isForce) {
|
|
45
|
+
block("Force-pushing the default branch is not allowed. Use a feature branch + PR.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (has("git") && has("push") && targetsProtectedRef) {
|
|
49
|
+
block("Direct pushes to the default branch are not allowed — open a PR (see STANDARD.md).");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (/>>?\s*\.?\/?\.env(\.[\w.]+)?\b/.test(command) && !/\.env\.example\b/.test(command)) {
|
|
53
|
+
block(
|
|
54
|
+
"Refusing to redirect output into a real .env file (secrets). Edit .env.example for placeholders.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generated by facility — https://github.com/theam/facility
|
|
3
|
+
//
|
|
4
|
+
// PreToolUse hook (Edit|Write|MultiEdit). Protects files that must not be
|
|
5
|
+
// mutated casually (exit 2 = deny, stderr is shown to the agent).
|
|
6
|
+
//
|
|
7
|
+
// Real .env files hold live secrets — placeholders belong in .env.example.
|
|
8
|
+
// Facility modules may extend this file with extra protections (for example,
|
|
9
|
+
// the database module adds migration immutability).
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
function readPayload() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const payload = readPayload();
|
|
21
|
+
const filePath = payload?.tool_input?.file_path ?? "";
|
|
22
|
+
if (!filePath) process.exit(0);
|
|
23
|
+
|
|
24
|
+
function block(reason) {
|
|
25
|
+
console.error(`⛔ Blocked by .claude/hooks/protect-files.mjs\n${reason}`);
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (/(^|\/)\.env(\.[\w.]+)?$/.test(filePath) && !/\.env\.example$/.test(filePath)) {
|
|
30
|
+
block("Refusing to write a real .env file (secrets). Use .env.example for placeholders.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* facility:module-rules */
|
|
34
|
+
|
|
35
|
+
process.exit(0);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"includeCoAuthoredBy": false,
|
|
4
|
+
"permissions": {
|
|
5
|
+
"defaultMode": "default",
|
|
6
|
+
"allow": [
|
|
7
|
+
{{ALLOW_CHECKS_JSON}}
|
|
8
|
+
"Bash(node guards/run.mjs)",
|
|
9
|
+
"Bash(git status:*)",
|
|
10
|
+
"Bash(git diff:*)",
|
|
11
|
+
"Bash(git log:*)",
|
|
12
|
+
"Bash(git show:*)",
|
|
13
|
+
"Bash(git add:*)",
|
|
14
|
+
"Bash(git restore:*)",
|
|
15
|
+
"Bash(git branch:*)",
|
|
16
|
+
"Bash(git switch:*)",
|
|
17
|
+
"Bash(git checkout:*)",
|
|
18
|
+
"Bash(git commit:*)",
|
|
19
|
+
"Bash(git fetch:*)",
|
|
20
|
+
"Bash(git rev-parse:*)",
|
|
21
|
+
"Bash(gh pr view:*)",
|
|
22
|
+
"Bash(gh pr list:*)",
|
|
23
|
+
"Bash(gh pr diff:*)",
|
|
24
|
+
"Bash(gh pr checks:*)",
|
|
25
|
+
"Bash(gh issue view:*)",
|
|
26
|
+
"Bash(gh issue list:*)",
|
|
27
|
+
"Bash(gh run list:*)",
|
|
28
|
+
"Bash(gh run view:*)"
|
|
29
|
+
],
|
|
30
|
+
"ask": [
|
|
31
|
+
"Bash(git push:*)",
|
|
32
|
+
"Bash(gh pr create:*)",
|
|
33
|
+
"Bash(gh pr merge:*)",
|
|
34
|
+
"Bash(gh issue create:*)"
|
|
35
|
+
],
|
|
36
|
+
"deny": [
|
|
37
|
+
"Read(.env)",
|
|
38
|
+
"Read(.env.local)",
|
|
39
|
+
"Read(.env.*.local)",
|
|
40
|
+
"Bash(sudo:*)",
|
|
41
|
+
"Bash(rm -rf /)",
|
|
42
|
+
"Bash(rm -rf ~)",
|
|
43
|
+
"Bash(git push --force:*)",
|
|
44
|
+
"Bash(git push -f:*)"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"hooks": {
|
|
48
|
+
"PreToolUse": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "Bash",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-branch.mjs\"",
|
|
55
|
+
"timeout": 30
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.mjs\"",
|
|
65
|
+
"timeout": 30
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: maintainable-software
|
|
3
|
+
description: Engineering judgment for writing code that stays cheap to change — simplicity, naming, boundaries, duplication, tests. Apply to any feature work, refactor, API design, or architecture decision, in any language.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Maintainable software
|
|
7
|
+
|
|
8
|
+
The cost of software is the cost of changing it later. Every rule here
|
|
9
|
+
optimizes for the next reader and the next change, not for the author.
|
|
10
|
+
|
|
11
|
+
## Simplicity
|
|
12
|
+
|
|
13
|
+
- Solve the problem in front of you. Speculative generality — the parameter
|
|
14
|
+
nobody passes, the interface with one implementation — is debt with no
|
|
15
|
+
loan.
|
|
16
|
+
- Prefer boring constructs. Cleverness needs a comment; boring code doesn't.
|
|
17
|
+
- One concept per unit: a function that needs "and" in its description is two
|
|
18
|
+
functions.
|
|
19
|
+
|
|
20
|
+
## Naming
|
|
21
|
+
|
|
22
|
+
- Names come from the domain, not the implementation: `settleInvoice`, not
|
|
23
|
+
`processData2`.
|
|
24
|
+
- A name that needs a comment to be understood is the wrong name. Rename
|
|
25
|
+
first, comment only for what code cannot say (constraints, why-not-the-
|
|
26
|
+
obvious-way).
|
|
27
|
+
- Inconsistent vocabulary is a bug factory: one concept, one word, repo-wide.
|
|
28
|
+
|
|
29
|
+
## Boundaries and interfaces
|
|
30
|
+
|
|
31
|
+
- Narrow interfaces, deep modules: expose the smallest surface that serves
|
|
32
|
+
the caller; keep the complexity behind it.
|
|
33
|
+
- Dependencies point one way. A module that reaches into its consumers — or
|
|
34
|
+
into globals — can't be tested or replaced.
|
|
35
|
+
- Side effects live at the edges; the core is data in, data out. If a
|
|
36
|
+
function both computes and writes, split it.
|
|
37
|
+
|
|
38
|
+
## Duplication
|
|
39
|
+
|
|
40
|
+
- Duplicate **shape** is fine; duplicate **concept** is not. Two functions
|
|
41
|
+
that look alike but change for different reasons should stay apart; one
|
|
42
|
+
business rule written twice will diverge and one copy will be wrong.
|
|
43
|
+
- Extract on the second real occurrence of the same concept, with a domain
|
|
44
|
+
name. Extracting on resemblance creates the worst abstraction: the shared
|
|
45
|
+
helper full of flags.
|
|
46
|
+
|
|
47
|
+
## Errors
|
|
48
|
+
|
|
49
|
+
- Make failure explicit at the boundary: validate input where it enters,
|
|
50
|
+
fail with a message the caller can act on, never swallow.
|
|
51
|
+
- Impossible states should be unrepresentable (types, schemas, constraints)
|
|
52
|
+
rather than checked everywhere.
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
|
|
56
|
+
- Tests protect behavior, not implementation. A test that breaks on a
|
|
57
|
+
rename is friction; a behavior change that breaks no test is a hole.
|
|
58
|
+
- Test the boundary you'd be afraid to change. If a bug got through, the
|
|
59
|
+
first fix is the test that would have caught it.
|
|
60
|
+
|
|
61
|
+
## Changes
|
|
62
|
+
|
|
63
|
+
- The best diff is the smallest one that fully solves the problem. Refactors
|
|
64
|
+
travel separately from behavior changes — a reviewer can hold one of them
|
|
65
|
+
in their head, not both.
|
|
66
|
+
- Leave the campsite slightly cleaner, never reorganized: opportunistic
|
|
67
|
+
renames yes, opportunistic redesigns no.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewing-to-standard
|
|
3
|
+
description: Review code changes against this repo's STANDARD.md. Use when reviewing a PR, a diff, or another agent's work — enforces the review order, the comment quality bar, and severity discipline.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reviewing to standard
|
|
7
|
+
|
|
8
|
+
A review exists to change the implementer's next action. Everything else is
|
|
9
|
+
noise.
|
|
10
|
+
|
|
11
|
+
## Review order (stop-the-line first)
|
|
12
|
+
|
|
13
|
+
1. **Correctness and fit** — the change does what was asked, no more. Scope
|
|
14
|
+
creep is a finding, not a bonus.
|
|
15
|
+
2. **Security and privacy** — auth regressions, data exposure, secret
|
|
16
|
+
handling, injection paths, new attack surface. Verify the negative case;
|
|
17
|
+
never accept a prose claim that a boundary holds.
|
|
18
|
+
3. **Maintainability** — same concept reused not duplicated, clear ownership
|
|
19
|
+
boundaries, domain names, side effects visible, easy for the next caller
|
|
20
|
+
and the next test.
|
|
21
|
+
4. **Standard compliance** — `STANDARD.md` and its module sections were
|
|
22
|
+
followed. A missed module requirement (seeds, analytics, evidence,
|
|
23
|
+
exposure) is a product-quality gap, not optional polish.
|
|
24
|
+
5. **Verification evidence** — the right checks ran for the risk taken, and
|
|
25
|
+
skipped checks are named. `node guards/run.mjs` should be green.
|
|
26
|
+
|
|
27
|
+
## The comment contract
|
|
28
|
+
|
|
29
|
+
Every finding carries four things: `file:line`, the exact risk, the smallest
|
|
30
|
+
practical fix, and the standard it missed. Severity is explicit — Blocker /
|
|
31
|
+
High / Medium — and separated from optional suggestions. Never post:
|
|
32
|
+
|
|
33
|
+
- Style-only nits, unless they hide a real bug or future maintenance cost.
|
|
34
|
+
- Vague asks ("clean this up", "add tests") — name the missing case, command,
|
|
35
|
+
or assertion instead.
|
|
36
|
+
- Requests for broad rewrites when a narrow change closes the risk.
|
|
37
|
+
|
|
38
|
+
## Leverage
|
|
39
|
+
|
|
40
|
+
For domains with real risk, dispatch the matching reviewer subagent from
|
|
41
|
+
`.claude/agents/` and consolidate its findings rather than re-deriving them.
|
|
42
|
+
If the same problem has now appeared twice in reviews, the review is the
|
|
43
|
+
wrong tool: propose the guard (`guards/`) that makes the third occurrence
|
|
44
|
+
impossible, and say so in the review.
|
|
45
|
+
|
|
46
|
+
## Verdict
|
|
47
|
+
|
|
48
|
+
End with **Ready / Ready with follow-ups / Not ready**, the checks you ran,
|
|
49
|
+
and nothing else. You never approve or merge — that signature is human.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: working-to-standard
|
|
3
|
+
description: Apply this repo's STANDARD.md while implementing, refactoring, or fixing. Use for any change that affects product behavior — before editing, while verifying, and when reporting done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working to standard
|
|
7
|
+
|
|
8
|
+
`STANDARD.md` is the contract; this skill is how it's applied while the work
|
|
9
|
+
is happening, not checked after the fact.
|
|
10
|
+
|
|
11
|
+
## Before editing
|
|
12
|
+
|
|
13
|
+
1. Read `STANDARD.md`, including the module sections between the
|
|
14
|
+
`facility:modules` markers — they carry domain rules (data, analytics,
|
|
15
|
+
design, AI exposure) that change what "done" means.
|
|
16
|
+
2. Name the user-facing outcome, the affected boundaries, the invariants that
|
|
17
|
+
must hold, and the rollback surface. If you can't, the task isn't clear
|
|
18
|
+
enough to edit yet — ask the one question that unblocks it.
|
|
19
|
+
3. Find the local pattern first. The repo's existing shape beats the general
|
|
20
|
+
best practice; new abstractions need to remove real complexity.
|
|
21
|
+
|
|
22
|
+
## While editing
|
|
23
|
+
|
|
24
|
+
- Deliver the WHOLE request as the smallest coherent change — no drive-by
|
|
25
|
+
refactors, no "phase 1" unless phasing was requested.
|
|
26
|
+
- Keep side effects at the edges and visible: network, database, time,
|
|
27
|
+
randomness, file system, model calls.
|
|
28
|
+
- Validate input at trust boundaries with the existing schema/parsing
|
|
29
|
+
patterns; make invalid states hard to represent.
|
|
30
|
+
- When you touch a domain with a reviewer subagent in `.claude/agents/`,
|
|
31
|
+
apply its checklist as you go — cheaper than failing its review later.
|
|
32
|
+
|
|
33
|
+
## Verifying
|
|
34
|
+
|
|
35
|
+
Run the lightest useful checks first, escalate by risk, per the verification
|
|
36
|
+
ladder in `STANDARD.md`. `node guards/run.mjs` is always cheap and always
|
|
37
|
+
relevant. A check you cannot run is reported by name with the reason — never
|
|
38
|
+
claimed.
|
|
39
|
+
|
|
40
|
+
## Reporting done
|
|
41
|
+
|
|
42
|
+
One concise summary: what changed and why, the checks you ran with their
|
|
43
|
+
results, and any genuinely out-of-scope follow-ups. Walk the completion
|
|
44
|
+
checklist in `STANDARD.md` before saying done; an unmet item is either fixed
|
|
45
|
+
or explicitly reported, never silent.
|