kataster 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.claude/agents/acceptance-tester.md +53 -0
  2. package/.claude/agents/code-reviewer.md +56 -0
  3. package/.claude/agents/domain-architect.md +40 -0
  4. package/.claude/agents/lead-dev.md +50 -0
  5. package/.claude/agents/product-owner.md +39 -0
  6. package/.claude/agents/repo-analyst.md +78 -0
  7. package/.claude/agents/requirements-engineer.md +44 -0
  8. package/.claude/agents/ux-designer.md +64 -0
  9. package/.claude/hooks/bash-guard.ts +92 -0
  10. package/.claude/hooks/code-check.ts +37 -0
  11. package/.claude/hooks/path-guard.ts +34 -0
  12. package/.claude/hooks/path-rules.ts +66 -0
  13. package/.claude/hooks/project-check.ts +108 -0
  14. package/.claude/hooks/protect-files.ts +55 -0
  15. package/.claude/hooks/session-start.ts +213 -0
  16. package/.claude/hooks/stop-gate.ts +56 -0
  17. package/.claude/hooks/validate-docs.ts +58 -0
  18. package/.claude/lib/architecture.ts +736 -0
  19. package/.claude/lib/backlog-github.ts +454 -0
  20. package/.claude/lib/backlog-jira.ts +347 -0
  21. package/.claude/lib/backlog-sync.ts +42 -0
  22. package/.claude/lib/backlog.ts +396 -0
  23. package/.claude/lib/bazel.ts +212 -0
  24. package/.claude/lib/config.ts +122 -0
  25. package/.claude/lib/discovery.ts +821 -0
  26. package/.claude/lib/engineering.ts +406 -0
  27. package/.claude/lib/nx.ts +129 -0
  28. package/.claude/lib/repo.ts +1194 -0
  29. package/.claude/lib/toolgraph.ts +74 -0
  30. package/.claude/lib/turbo.ts +166 -0
  31. package/.claude/package-lock.json +427 -0
  32. package/.claude/package.json +21 -0
  33. package/.claude/settings.json +82 -0
  34. package/.claude/skills/adopt/SKILL.md +283 -0
  35. package/.claude/skills/adopt/scripts/boundary-check.ts +156 -0
  36. package/.claude/skills/adopt/scripts/package-doc.ts +71 -0
  37. package/.claude/skills/adopt/scripts/repo-map.ts +123 -0
  38. package/.claude/skills/adopt/scripts/rule-doc.ts +80 -0
  39. package/.claude/skills/architect/ADR-FORMAT.md +97 -0
  40. package/.claude/skills/architect/SKILL.md +64 -0
  41. package/.claude/skills/architect/TECH-STACK.md +21 -0
  42. package/.claude/skills/backlog/SKILL.md +198 -0
  43. package/.claude/skills/backlog/scripts/backlog-pull.ts +45 -0
  44. package/.claude/skills/backlog/scripts/backlog-push.ts +38 -0
  45. package/.claude/skills/backlog/scripts/backlog-status.ts +32 -0
  46. package/.claude/skills/backlog/scripts/jira-import.ts +40 -0
  47. package/.claude/skills/backlog/scripts/jira-plan.ts +26 -0
  48. package/.claude/skills/backlog/scripts/jira-record.ts +41 -0
  49. package/.claude/skills/bootstrap/SKILL.md +119 -0
  50. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  51. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/.claude/skills/codebase-design/SKILL.md +114 -0
  53. package/.claude/skills/domain-model/CONTEXT-FORMAT.md +47 -0
  54. package/.claude/skills/domain-model/SKILL.md +64 -0
  55. package/.claude/skills/domain-model/templates/data-model.md +29 -0
  56. package/.claude/skills/engineering-conventions/SKILL.md +31 -0
  57. package/.claude/skills/event-storming/SKILL.md +44 -0
  58. package/.claude/skills/event-storming/schema/events.schema.json +124 -0
  59. package/.claude/skills/event-storming/scripts/render-event-storming.ts +11 -0
  60. package/.claude/skills/event-storming/scripts/validate-events.ts +11 -0
  61. package/.claude/skills/event-storming/templates/events.example.yaml +91 -0
  62. package/.claude/skills/feature/SKILL.md +86 -0
  63. package/.claude/skills/feature/scripts/intake.ts +356 -0
  64. package/.claude/skills/grill-me/SKILL.md +7 -0
  65. package/.claude/skills/grill-with-docs/SKILL.md +7 -0
  66. package/.claude/skills/grilling/SKILL.md +30 -0
  67. package/.claude/skills/groom-backlog/SKILL.md +93 -0
  68. package/.claude/skills/groom-backlog/scripts/backlog-health.ts +238 -0
  69. package/.claude/skills/implement/SKILL.md +129 -0
  70. package/.claude/skills/implement/scripts/affected.ts +245 -0
  71. package/.claude/skills/implement/scripts/check-commands.ts +37 -0
  72. package/.claude/skills/implement/scripts/check-language.ts +15 -0
  73. package/.claude/skills/implement/scripts/check-scenarios.ts +59 -0
  74. package/.claude/skills/implement/scripts/next-story.ts +23 -0
  75. package/.claude/skills/implement/scripts/story-context.ts +135 -0
  76. package/.claude/skills/implement/scripts/story-status.ts +47 -0
  77. package/.claude/skills/implement/scripts/verify.ts +92 -0
  78. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  79. package/.claude/skills/improve-codebase-architecture/SKILL.md +71 -0
  80. package/.claude/skills/retro/SKILL.md +53 -0
  81. package/.claude/skills/review-stories/SKILL.md +31 -0
  82. package/.claude/skills/shape/SKILL.md +193 -0
  83. package/.claude/skills/tdd/SKILL.md +38 -0
  84. package/.claude/skills/tdd/mocking.md +59 -0
  85. package/.claude/skills/tdd/tests.md +77 -0
  86. package/.claude/skills/user-stories/SKILL.md +138 -0
  87. package/.claude/skills/user-stories/schema/story.schema.json +39 -0
  88. package/.claude/skills/user-stories/scripts/render-backlog.ts +4 -0
  89. package/.claude/skills/user-stories/scripts/validate-stories.ts +13 -0
  90. package/.claude/skills/user-stories/templates/story-template.md +38 -0
  91. package/.claude/skills/ux-design/SKILL.md +107 -0
  92. package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  93. package/.claude/skills/writing-for-agents/SKILL.md +81 -0
  94. package/.claude/tsconfig.json +15 -0
  95. package/.opencode/agents/acceptance-tester.md +64 -0
  96. package/.opencode/agents/code-reviewer.md +69 -0
  97. package/.opencode/agents/domain-architect.md +59 -0
  98. package/.opencode/agents/lead-dev.md +67 -0
  99. package/.opencode/agents/product-owner.md +59 -0
  100. package/.opencode/agents/repo-analyst.md +86 -0
  101. package/.opencode/agents/requirements-engineer.md +58 -0
  102. package/.opencode/agents/ux-designer.md +83 -0
  103. package/.opencode/commands/adopt.md +7 -0
  104. package/.opencode/commands/backlog-sync.md +7 -0
  105. package/.opencode/commands/bootstrap.md +7 -0
  106. package/.opencode/commands/event-storming.md +7 -0
  107. package/.opencode/commands/feature.md +7 -0
  108. package/.opencode/commands/grill-me.md +7 -0
  109. package/.opencode/commands/grill-with-docs.md +7 -0
  110. package/.opencode/commands/groom-backlog.md +7 -0
  111. package/.opencode/commands/implement.md +7 -0
  112. package/.opencode/commands/improve-codebase-architecture.md +7 -0
  113. package/.opencode/commands/retro.md +7 -0
  114. package/.opencode/commands/review-stories.md +7 -0
  115. package/.opencode/commands/shape.md +7 -0
  116. package/.opencode/opencode.jsonc +15 -0
  117. package/.opencode/plugins/kataster/index.ts +283 -0
  118. package/AGENTS.md +21 -0
  119. package/CHANGELOG.md +82 -0
  120. package/LICENSE +21 -0
  121. package/README.md +324 -2
  122. package/bin/kataster.mjs +7 -0
  123. package/extras/skills/shadcn/SKILL.md +277 -0
  124. package/extras/skills/shadcn/agents/openai.yml +5 -0
  125. package/extras/skills/shadcn/assets/shadcn-small.png +0 -0
  126. package/extras/skills/shadcn/assets/shadcn.png +0 -0
  127. package/extras/skills/shadcn/cli.md +290 -0
  128. package/extras/skills/shadcn/customization.md +209 -0
  129. package/extras/skills/shadcn/evals/evals.json +77 -0
  130. package/extras/skills/shadcn/mcp.md +105 -0
  131. package/extras/skills/shadcn/registry.md +277 -0
  132. package/extras/skills/shadcn/rules/base-vs-radix.md +306 -0
  133. package/extras/skills/shadcn/rules/chat.md +250 -0
  134. package/extras/skills/shadcn/rules/composition.md +213 -0
  135. package/extras/skills/shadcn/rules/forms.md +192 -0
  136. package/extras/skills/shadcn/rules/icons.md +101 -0
  137. package/extras/skills/shadcn/rules/styling.md +185 -0
  138. package/guide/README.md +9 -0
  139. package/guide/brownfield.md +242 -0
  140. package/guide/concepts.md +109 -0
  141. package/guide/greenfield.md +84 -0
  142. package/guide/install.md +144 -0
  143. package/guide/release.md +62 -0
  144. package/package.json +56 -4
  145. package/src/cli.mjs +302 -0
  146. package/src/detect.mjs +237 -0
  147. package/src/install.mjs +427 -0
  148. package/src/repo.mjs +129 -0
  149. package/src/util.mjs +110 -0
  150. package/templates/CONTEXT.md +21 -0
  151. package/templates/conventions.md +63 -0
  152. package/templates/docs/adr/README.md +7 -0
  153. package/templates/docs/architecture/data-model.md +31 -0
  154. package/templates/docs/domain/context-map.md +21 -0
  155. package/templates/docs/domain/events.yaml +23 -0
  156. package/templates/docs/product/initiatives/README.md +41 -0
  157. package/templates/docs/product/ux-guidelines.md +27 -0
  158. package/templates/docs/product/vision.md +36 -0
  159. package/templates/docs/reviews/.gitkeep +0 -0
  160. package/templates/docs/stories/OPEN_QUESTIONS.md +8 -0
  161. package/templates/docs/stories/README.md +26 -0
  162. package/templates/docs/workflow.md +135 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Path-scoped rules (`.claude/rules/*.md`) for harnesses without native support.
3
+ *
4
+ * Claude Code reads `.claude/rules/` itself and scopes each rule by its `paths:` frontmatter. OpenCode has no
5
+ * path-scoped rules, so `.opencode/plugins/kataster` calls this script whenever a file is read or written and
6
+ * attaches the matching rules to the session as keyed instruction entries. One source of truth, two harnesses.
7
+ *
8
+ * echo '{"paths":["libs/shared/src/index.ts"],"cwd":"/repo"}' | node .claude/hooks/path-rules.ts
9
+ *
10
+ * Output: `{"rules":[{"key":"<slug>","value":"<rule body>"}]}` – empty when nothing matches. This hook never
11
+ * blocks; a broken rule must not stop a tool call.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { isAbsolute, relative, sep } from "node:path";
15
+ import { globToRegExp, validateRules } from "../lib/architecture.ts";
16
+ import { ROOT } from "../lib/repo.ts";
17
+
18
+ interface Entry {
19
+ key: string;
20
+ value: string;
21
+ }
22
+
23
+ let data: Record<string, unknown>;
24
+ try {
25
+ data = JSON.parse(readFileSync(0, "utf8"));
26
+ } catch {
27
+ process.exit(0);
28
+ }
29
+
30
+ const root = typeof data.cwd === "string" && data.cwd ? data.cwd : ROOT;
31
+ const raw = Array.isArray(data.paths)
32
+ ? data.paths
33
+ : data.tool_input && typeof data.tool_input === "object" && typeof (data.tool_input as Record<string, unknown>).file_path === "string"
34
+ ? [(data.tool_input as Record<string, unknown>).file_path]
35
+ : [];
36
+
37
+ const files = raw
38
+ .filter((p): p is string => typeof p === "string" && p.length > 0)
39
+ .map((p) => toRepoPath(root, p))
40
+ .filter((p): p is string => p !== undefined);
41
+ if (!files.length) fail();
42
+
43
+ const { rules } = validateRules(root);
44
+ const matched: Entry[] = [];
45
+ for (const rule of rules) {
46
+ if (!rule.paths.some((glob) => files.some((file) => globToRegExp(glob).test(file)))) continue;
47
+ const key = rule.path.replace(/^\.claude\/rules\//, "").replace(/\.md$/, "").replaceAll("/", "-");
48
+ const scope = `Applies to ${rule.paths.map((p) => `\`${p}\``).join(", ")}.`;
49
+ matched.push({ key, value: `${scope}\n\n${rule.body}` });
50
+ }
51
+
52
+ process.stdout.write(JSON.stringify({ rules: matched }));
53
+ process.exit(0);
54
+
55
+ /** Repository-relative POSIX path, or undefined when the file is outside the repository. */
56
+ function toRepoPath(root: string, path: string): string | undefined {
57
+ const rel = isAbsolute(path) ? relative(root, path) : path;
58
+ const posix = rel.split(sep).join("/").replace(/^\.\//, "");
59
+ if (!posix || posix.startsWith("../") || posix === ".." || isAbsolute(posix)) return undefined;
60
+ return posix;
61
+ }
62
+
63
+ function fail(): never {
64
+ process.stdout.write(JSON.stringify({ rules: [] }));
65
+ process.exit(0);
66
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Project-check dispatcher – one hook, project-owned checks.
3
+ *
4
+ * docs/engineering/checks.yaml lists checks with a `phase` (edit | stop | ci), optional `paths` globs,
5
+ * a timeout and an optional `advisory` flag. This script runs the checks for the phase:
6
+ *
7
+ * edit – after Write/Edit (PostToolUse); only checks whose `paths` match the changed file
8
+ * stop – full set; called by the stop gate and by `verify.ts`
9
+ *
10
+ * It is wired into both harnesses once (Claude settings, OpenCode plugin). Adding a check never touches the
11
+ * hook wiring; adding an arbitrary hook per package is deliberately not supported.
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+ import { readFileSync } from "node:fs";
15
+ import { isAbsolute, join } from "node:path";
16
+ import { loadChecks, matchesAny, relPath, type Phase } from "../lib/architecture.ts";
17
+ import { ROOT } from "../lib/repo.ts";
18
+
19
+ const args = process.argv.slice(2);
20
+ const phaseArg = args[args.indexOf("--phase") + 1];
21
+ const phase: Phase = phaseArg === "stop" || phaseArg === "ci" ? phaseArg : "edit";
22
+ const changedIndex = args.indexOf("--changed");
23
+ const changedArg = changedIndex === -1 ? undefined : args[changedIndex + 1];
24
+
25
+ function shellQuote(value: string): string {
26
+ return `'${value.replaceAll("'", "'\\''")}'`;
27
+ }
28
+
29
+ function tail(text: string, lines = 40): string {
30
+ return text.trim().split("\n").slice(-lines).join("\n");
31
+ }
32
+
33
+ let changed = changedArg && !changedArg.startsWith("--") ? changedArg : process.env.CHANGED_FILE;
34
+
35
+ if (phase === "edit" && !changed && !process.stdin.isTTY) {
36
+ // The hook payload carries the changed file; a stop/CI run has none.
37
+ try {
38
+ const raw = readFileSync(0, "utf8").trim();
39
+ if (raw) {
40
+ const data = JSON.parse(raw) as { cwd?: string; tool_input?: { file_path?: unknown } };
41
+ const fp = data.tool_input?.file_path;
42
+ if (typeof fp === "string" && fp) changed = isAbsolute(fp) ? fp : join(data.cwd ?? ROOT, fp);
43
+ }
44
+ } catch {
45
+ // no stdin or malformed payload – there is nothing to scope an edit check to
46
+ }
47
+ }
48
+
49
+ if (changed) changed = relPath(ROOT, changed);
50
+
51
+ try {
52
+ const config = loadChecks(ROOT);
53
+ const checks = config?.checks ?? [];
54
+ if (!checks.length) process.exit(0);
55
+ if (phase === "edit" && !changed) process.exit(0);
56
+
57
+ const failures: string[] = [];
58
+ const notes: string[] = [];
59
+
60
+ for (const check of checks) {
61
+ if (check.phase !== phase) continue;
62
+ // `paths` scopes the edit-time check. Stop/CI runs the full set.
63
+ if (phase === "edit" && check.paths.length) {
64
+ if (!changed) continue;
65
+ if (!matchesAny(changed, check.paths)) continue;
66
+ }
67
+
68
+ const command = check.command.replaceAll("{changed}", changed ? shellQuote(changed) : "");
69
+ const result = spawnSync(command, {
70
+ cwd: ROOT,
71
+ shell: true,
72
+ encoding: "utf8",
73
+ timeout: check.timeout * 1000,
74
+ env: { ...process.env, CHANGED_FILE: changed ?? "", KATASTER_PHASE: phase },
75
+ });
76
+ const ok = result.status === 0;
77
+ if (ok) continue;
78
+
79
+ const timedOut = result.error && "code" in result.error && result.error.code === "ETIMEDOUT";
80
+ const detail = timedOut
81
+ ? `timed out after ${check.timeout}s`
82
+ : tail(`${result.stdout ?? ""}${result.stderr ?? ""}`) || `exit ${result.status ?? "?"}`;
83
+ const block = `── ${check.name} ──\n${detail}`;
84
+ if (check.advisory) notes.push(block);
85
+ else failures.push(block);
86
+ }
87
+
88
+ if (failures.length) {
89
+ process.stderr.write(
90
+ `Project checks failed – fix before continuing:\n\n${failures.join("\n\n")}\n\n` +
91
+ "A brownfield violation that already existed belongs in the baseline (boundary-check --update-baseline), not in a red hook.\n",
92
+ );
93
+ process.exit(2);
94
+ }
95
+
96
+ if (notes.length) {
97
+ console.log(JSON.stringify({
98
+ hookSpecificOutput: {
99
+ hookEventName: phase === "edit" ? "PostToolUse" : "Stop",
100
+ additionalContext: `Project checks (advisory):\n\n${notes.join("\n\n")}`,
101
+ },
102
+ }));
103
+ }
104
+ process.exit(0);
105
+ } catch (error) {
106
+ process.stderr.write(`Project check configuration is broken:\n${error instanceof Error ? error.message : String(error)}\n`);
107
+ process.exit(2);
108
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * PreToolUse hook (Write|Edit|MultiEdit): protects files that must not be edited by hand.
3
+ *
4
+ * - Generated files (BACKLOG.md, event-storming.md) → blocked; they are rendered by hooks and scripts.
5
+ * - Accepted ADRs are never edited, only superseded: the only change allowed is
6
+ * `status: accepted` → `status: superseded by ADR-NNNN`.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { isAbsolute, join } from "node:path";
10
+ import { rel, ROOT } from "../lib/discovery.ts";
11
+
12
+ const GENERATED = new Set(["docs/stories/BACKLOG.md", "docs/domain/event-storming.md"]);
13
+
14
+ let data: Record<string, any>;
15
+ try {
16
+ data = JSON.parse(readFileSync(0, "utf8"));
17
+ } catch {
18
+ process.exit(0);
19
+ }
20
+ const ti = data.tool_input ?? {};
21
+ const fp: unknown = ti.file_path;
22
+ if (typeof fp !== "string" || !fp) process.exit(0);
23
+ const path = isAbsolute(fp) ? fp : join(data.cwd || ROOT, fp);
24
+ const r = rel(path);
25
+
26
+ function block(msg: string): never {
27
+ process.stderr.write(`Blocked: ${msg}\n`);
28
+ process.exit(2);
29
+ }
30
+
31
+ if (GENERATED.has(r)) block(`'${r}' is generated – change its sources (story files, events.yaml) instead.`);
32
+
33
+ if (/^docs\/adr\/\d{4}-.*\.md$/.test(r) && existsSync(path)) {
34
+ const old = readFileSync(path, "utf8");
35
+ if (!/^status:\s*accepted\s*$/m.test(old)) process.exit(0);
36
+
37
+ let next: string;
38
+ if (data.tool_name === "Write") {
39
+ next = String(ti.content ?? "");
40
+ } else {
41
+ const edits: { old_string: string; new_string: string; replace_all?: boolean }[] =
42
+ data.tool_name === "MultiEdit" ? ti.edits ?? [] : [ti];
43
+ next = old;
44
+ for (const e of edits) next = e.replace_all ? next.split(e.old_string).join(e.new_string) : next.replace(e.old_string, e.new_string);
45
+ }
46
+ const withoutStatus = (t: string) => t.replace(/^status:.*$/m, "");
47
+ const newStatus = /^status:\s*(.*?)\s*$/m.exec(next)?.[1] ?? "";
48
+ if (withoutStatus(next) !== withoutStatus(old) || !/^superseded by ADR-\d{4}$/.test(newStatus)) {
49
+ block(
50
+ `'${r}' is an accepted ADR. Accepted ADRs are never edited – write a new ADR (status: proposed) and, ` +
51
+ "once the user accepts it, only change this one's status to 'superseded by ADR-NNNN'.",
52
+ );
53
+ }
54
+ }
55
+ process.exit(0);
@@ -0,0 +1,213 @@
1
+ /**
2
+ * SessionStart hook: what a fresh checkout is still missing (only what is missing),
3
+ * and where the work stands – branch, stories in progress, the next story, the last verify result.
4
+ *
5
+ * Everything project-specific comes from `.claude/project.json`; this file contains no project knowledge.
6
+ * The setup checks run before anything is imported from .claude/lib, so they still work when the
7
+ * tooling dependencies are not installed yet.
8
+ */
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync, readFileSync, statSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
15
+
16
+ type Profile = {
17
+ name?: string;
18
+ kind?: "greenfield" | "brownfield";
19
+ languages?: { artifacts?: string; ui?: string };
20
+ commands?: { install?: string };
21
+ review?: { preview?: string; pullRequest?: string };
22
+ setup?: { envFile?: string; envHint?: string };
23
+ backlog?: {
24
+ provider?: "local" | "github" | "jira";
25
+ jira?: { project?: string; mcpServer?: string };
26
+ github?: { repo?: string };
27
+ };
28
+ };
29
+
30
+ function profile(): Profile {
31
+ try {
32
+ return JSON.parse(readFileSync(join(root, ".claude", "project.json"), "utf8")) as Profile;
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ /** Runs a command with stderr folded into stdout; `ok` is the exit status. */
39
+ function sh(command: string): { ok: boolean; out: string } {
40
+ try {
41
+ return { ok: true, out: execSync(`${command} 2>&1`, { cwd: root, encoding: "utf8", timeout: 5000 }).trim() };
42
+ } catch (e) {
43
+ const err = e as { stdout?: string };
44
+ return { ok: false, out: (err.stdout ?? "").trim() };
45
+ }
46
+ }
47
+
48
+ type McpApproval = {
49
+ mcpServers?: Record<string, unknown>;
50
+ enableAllProjectMcpServers?: boolean;
51
+ enabledMcpjsonServers?: string[];
52
+ disabledMcpjsonServers?: string[];
53
+ };
54
+
55
+ /** A JSON settings file, or nothing when it is missing, unreadable or too large to bother. */
56
+ function readJson(path: string): Record<string, unknown> | undefined {
57
+ try {
58
+ if (statSync(path).size > 50_000_000) return undefined;
59
+ return JSON.parse(readFileSync(path, "utf8"));
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Is the preview MCP server from .mcp.json approved for this project? The approval can live in the project's
67
+ * entry of ~/.claude.json or in any settings file (user, project, local – the "approve" dialog writes
68
+ * .claude/settings.local.json). undefined = can't tell.
69
+ */
70
+ function mcpApproved(server: string): boolean | undefined {
71
+ const global = readJson(join(homedir(), ".claude.json"));
72
+ const sources: McpApproval[] = [
73
+ (global?.projects as Record<string, McpApproval> | undefined)?.[root],
74
+ readJson(join(homedir(), ".claude/settings.json")),
75
+ readJson(join(root, ".claude", "settings.json")),
76
+ readJson(join(root, ".claude", "settings.local.json")),
77
+ ].filter((source): source is McpApproval => source !== undefined);
78
+ if (sources.length === 0) return undefined;
79
+ if (sources.some((source) => source.disabledMcpjsonServers?.includes(server))) return false;
80
+ return sources.some(
81
+ (source) =>
82
+ Boolean(source.mcpServers?.[server]) ||
83
+ source.enableAllProjectMcpServers === true ||
84
+ source.enabledMcpjsonServers?.includes(server),
85
+ );
86
+ }
87
+
88
+ /** Is an MCP server with this name configured anywhere the harness reads? */
89
+ function mcpConfigured(server: string): boolean {
90
+ for (const file of [".mcp.json", ".claude/settings.json", ".claude/settings.local.json", ".opencode/opencode.jsonc", ".opencode/opencode.json"]) {
91
+ const path = join(root, file);
92
+ try {
93
+ if (existsSync(path) && readFileSync(path, "utf8").includes(server)) return true;
94
+ } catch {
95
+ // an unreadable config never blocks a session start
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+
101
+ function setupHints(p: Profile): string[] {
102
+ const hints: string[] = [];
103
+
104
+ if (!existsSync(join(root, ".claude/node_modules/yaml")))
105
+ hints.push("- Tooling dependencies are missing – the validators and scripts won't run: `npm install --prefix .claude`");
106
+
107
+ const install = p.commands?.install;
108
+ if (install && existsSync(join(root, "package.json")) && !existsSync(join(root, "node_modules")))
109
+ hints.push(`- The application's dependencies are missing: \`${install}\``);
110
+
111
+ if (p.review?.pullRequest === "gh") {
112
+ if (!sh("command -v gh").ok) {
113
+ hints.push("- GitHub CLI (`gh`) not installed – /implement can't open the pull request itself, it falls back to the compare URL from `git push`: https://cli.github.com");
114
+ } else {
115
+ const auth = sh("gh auth status");
116
+ if (!auth.ok) hints.push("- `gh` is not authenticated – no pull requests from here: `gh auth login` (needs the `repo` scope)");
117
+ else if (!/Token scopes:.*'repo'/.test(auth.out)) hints.push("- The `gh` token has no `repo` scope – creating pull requests will fail: `gh auth refresh -s repo`");
118
+ }
119
+ }
120
+
121
+ const backlog = p.backlog?.provider ?? "local";
122
+ if (backlog === "github" && p.review?.pullRequest !== "gh") {
123
+ if (!sh("command -v gh").ok)
124
+ hints.push("- Backlog provider is `github`, but the GitHub CLI (`gh`) is missing – the mirror cannot sync: https://cli.github.com");
125
+ else if (!sh("gh auth status").ok)
126
+ hints.push("- Backlog provider is `github`, but `gh` is not authenticated – `gh auth login` (needs the `repo` scope).");
127
+ }
128
+ if (backlog === "jira") {
129
+ const project = p.backlog?.jira?.project ?? "";
130
+ const server = p.backlog?.jira?.mcpServer ?? "mcp-atlassian";
131
+ if (!project) hints.push("- Backlog provider is `jira`, but `backlog.jira.project` is empty in `.claude/project.json`.");
132
+ if (!mcpConfigured(server))
133
+ hints.push(
134
+ `- Backlog provider is \`jira\`, but no \`${server}\` MCP server is configured in this project. ` +
135
+ "Add it in your harness (see the `backlog` skill), otherwise no story can be read from or written to Jira.",
136
+ );
137
+ }
138
+
139
+ if (p.review?.preview === "vercel" && existsSync(join(root, ".mcp.json")) && mcpApproved("vercel") === false)
140
+ hints.push(
141
+ "- Vercel MCP (`vercel` in .mcp.json) is not enabled yet: approve the project server, then authenticate it. " +
142
+ "It gives preview deployment status and build/runtime logs for the acceptance step; without it, ask the user for the preview URL.",
143
+ );
144
+
145
+ if (p.setup?.envFile && !existsSync(join(root, p.setup.envFile)))
146
+ hints.push(`- No ${p.setup.envFile} – ${p.setup.envHint ?? "the app may not start"}`);
147
+
148
+ if (!existsSync(join(root, "CONTEXT.md")))
149
+ hints.push(
150
+ `- Discovery has not started: no CONTEXT.md. Run \`${p.kind === "brownfield" ? "/adopt" : "/bootstrap"}\` to build the domain model with the user.`,
151
+ );
152
+
153
+ if (p.kind === "brownfield" && !existsSync(join(root, "docs", "architecture", "repo-map.json")))
154
+ hints.push(
155
+ "- No repository map yet: `/adopt` maps the repository first, then adopts one slice at a time (one domain plus at most one shared library).",
156
+ );
157
+
158
+ return hints;
159
+ }
160
+
161
+ const p = profile();
162
+ const name = p.name ?? "this project";
163
+ const lines: string[] = [];
164
+
165
+ try {
166
+ const hints = setupHints(p);
167
+ if (hints.length > 0) lines.push(`${name} – setup (tell the user, don't fix it silently):`, ...hints, "");
168
+ } catch {
169
+ // a broken check never blocks a session start
170
+ }
171
+
172
+ try {
173
+ const { rel, storyId, loadStories } = await import("../lib/discovery.ts");
174
+ const { git, nextStory, readVerifyState } = await import("../lib/engineering.ts");
175
+
176
+ lines.push(`${name} – engineering status:`);
177
+ const branch = git("rev-parse", "--abbrev-ref", "HEAD");
178
+ const dirty = (git("status", "--porcelain") ?? "") !== "";
179
+ lines.push(`- Branch: ${branch ?? "?"}${dirty ? " (uncommitted changes)" : ""}`);
180
+
181
+ const provider = p.backlog?.provider ?? "local";
182
+ if (provider !== "local") {
183
+ const stories = loadStories().parsed;
184
+ const unlinked = stories.filter((story) => {
185
+ const ext = story.meta.external as { provider?: unknown; key?: unknown } | undefined;
186
+ return !ext || ext.provider !== provider || typeof ext.key !== "string" || !ext.key;
187
+ }).length;
188
+ const target = provider === "jira" ? `Jira (${p.backlog?.jira?.project || "project not configured"})` : "GitHub Issues";
189
+ lines.push(
190
+ `- Backlog: ${target} – ${stories.length - unlinked} of ${stories.length} stories linked` +
191
+ (unlinked ? `; ${unlinked} need /backlog-sync` : ""),
192
+ );
193
+ }
194
+
195
+ const { inProgress, next } = nextStory();
196
+ for (const s of inProgress) lines.push(`- In progress: ${storyId(s)} – ${s.meta.title} (${rel(s.path)})`);
197
+ if (next) lines.push(`- Next ready story: ${storyId(next)} – ${next.meta.title} (start with /implement)`);
198
+ if (!inProgress.length && !next && !existsSync(join(root, "docs", "stories")))
199
+ lines.push("- No backlog yet – discovery comes first (/bootstrap or /adopt).");
200
+
201
+ const v = readVerifyState();
202
+ if (v) {
203
+ const head = git("rev-parse", "--short", "HEAD");
204
+ const stale = v.commit !== head || v.dirty ? " – older than the current code" : "";
205
+ const failed = v.steps.filter((s) => !s.ok).map((s) => s.name);
206
+ lines.push(`- Last verify: ${v.ok ? "green" : `RED (${failed.join(", ")})`} at ${v.at} on ${v.branch}@${v.commit}${stale}`);
207
+ }
208
+ } catch {
209
+ // never block a session start
210
+ }
211
+
212
+ if (lines.length > 0) console.log(lines.join("\n"));
213
+ process.exit(0);
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Stop hook: quick gate before Claude finishes a turn in which application code changed.
3
+ *
4
+ * Runs the type check and the tests related to the changed files (Vitest). If they fail, Claude is sent back
5
+ * once to fix them (exit 2); `stop_hook_active` prevents a loop – the second time it only reports.
6
+ * The full gate is `node .claude/skills/implement/scripts/verify.ts`.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { ROOT } from "../lib/discovery.ts";
12
+ import { appBin, git, isCode } from "../lib/engineering.ts";
13
+
14
+ let data: Record<string, any>;
15
+ try {
16
+ data = JSON.parse(readFileSync(0, "utf8"));
17
+ } catch {
18
+ process.exit(0);
19
+ }
20
+ if (data.stop_hook_active) process.exit(0);
21
+
22
+ const changed = (git("status", "--porcelain", "--untracked-files=all") ?? "")
23
+ .split("\n")
24
+ .map((l) => l.slice(3).trim().replace(/^.* -> /, ""))
25
+ .filter((f) => f && !f.startsWith(".claude/") && !f.startsWith("docs/") && isCode(f) && existsSync(join(ROOT, f)));
26
+ if (!changed.length) process.exit(0);
27
+
28
+ const failures: string[] = [];
29
+ const tail = (s: string) => s.trim().split("\n").slice(-40).join("\n");
30
+
31
+ const tsc = appBin("tsc");
32
+ if (tsc && existsSync(join(ROOT, "tsconfig.json"))) {
33
+ const r = spawnSync(tsc, ["--noEmit", "-p", "."], { cwd: ROOT, encoding: "utf8", timeout: 240_000 });
34
+ if (r.status !== 0) failures.push(`Type check failed:\n${tail(r.stdout + r.stderr)}`);
35
+ }
36
+ const vitest = appBin("vitest");
37
+ if (vitest) {
38
+ const r = spawnSync(vitest, ["related", "--run", "--passWithNoTests", ...changed], { cwd: ROOT, encoding: "utf8", timeout: 300_000 });
39
+ if (r.status !== 0) failures.push(`Tests related to the changed files failed:\n${tail(r.stdout + r.stderr)}`);
40
+ }
41
+
42
+ // Project-owned checks from docs/engineering/checks.yaml (boundaries, architecture tests, …).
43
+ const project = spawnSync(process.execPath, [join(ROOT, ".claude", "hooks", "project-check.ts"), "--phase", "stop"], {
44
+ cwd: ROOT,
45
+ encoding: "utf8",
46
+ timeout: 120_000,
47
+ });
48
+ if (project.status !== 0) {
49
+ failures.push(`Project checks failed:\n${tail(`${project.stdout ?? ""}${project.stderr ?? ""}`)}`);
50
+ }
51
+
52
+ if (failures.length) {
53
+ process.stderr.write(`Quick gate is red – fix before finishing (or say explicitly why it is red on purpose, e.g. a failing test in the red step):\n\n${failures.join("\n\n")}\n`);
54
+ process.exit(2);
55
+ }
56
+ process.exit(0);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * PostToolUse hook: validates discovery artifacts right after every write.
3
+ *
4
+ * - docs/domain/events.yaml → schema + references; on success event-storming.md and BACKLOG.md are re-rendered
5
+ * - docs/stories/*.md → validation of the changed story (lenient on dependencies) and of its status change
6
+ * since the last commit (engineering workflow); on success BACKLOG.md is re-rendered
7
+ * Exit 2 + stderr = Claude gets the errors back and has to fix them.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { isAbsolute, join } from "node:path";
11
+ import * as d from "../lib/discovery.ts";
12
+ import * as e from "../lib/engineering.ts";
13
+
14
+ let data: d.Obj;
15
+ try {
16
+ data = JSON.parse(readFileSync(0, "utf8"));
17
+ } catch {
18
+ process.exit(0);
19
+ }
20
+
21
+ const fp: unknown = data.tool_input?.file_path;
22
+ if (typeof fp !== "string" || !fp) process.exit(0);
23
+ const path = isAbsolute(fp) ? fp : join(data.cwd || d.ROOT, fp);
24
+ const rel = d.rel(path);
25
+
26
+ const report = new d.Report();
27
+ if (rel === "docs/domain/events.yaml") {
28
+ const model = d.validateEvents(report);
29
+ if (report.ok && model !== null) {
30
+ d.writeEventStormingMd(model);
31
+ d.writeBacklogMd();
32
+ }
33
+ } else if (d.storyFiles().some((f) => d.realPath(f) === d.realPath(path))) {
34
+ const stories = d.validateStories(report, path, true);
35
+ // Status workflow: compared with the last commit, the new status must be reachable and its preconditions met
36
+ const story = [...stories.values()].find((s) => d.realPath(s.path) === d.realPath(path));
37
+ const from = story ? e.headStatus(story) : null;
38
+ if (story && from !== null) {
39
+ for (const msg of e.transitionErrors(story, from, String(story.meta.status), stories)) report.error(rel, msg);
40
+ }
41
+ if (report.ok) d.writeBacklogMd();
42
+ } else {
43
+ process.exit(0);
44
+ }
45
+
46
+ if (!report.ok) {
47
+ process.stderr.write(`Validation of ${rel} failed – please fix:\n${report.lines().join("\n")}\n`);
48
+ process.exit(2);
49
+ }
50
+ if (report.warnings.length) {
51
+ // Return warnings as context without blocking
52
+ console.log(JSON.stringify({
53
+ hookSpecificOutput: {
54
+ hookEventName: "PostToolUse",
55
+ additionalContext: "Validation OK, notes:\n" + report.warnings.join("\n"),
56
+ },
57
+ }));
58
+ }