@everystack/mcp 0.3.1 → 0.3.3

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 (85) hide show
  1. package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
  2. package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
  3. package/dist/gates/detectors/embedded-data-bundle.js +56 -0
  4. package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
  5. package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
  6. package/dist/gates/detectors/hand-written-migration.js +54 -0
  7. package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
  8. package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
  9. package/dist/gates/detectors/secret-in-public-env.js +38 -0
  10. package/dist/gates/engine.d.ts +28 -0
  11. package/dist/gates/engine.d.ts.map +1 -0
  12. package/dist/gates/engine.js +74 -0
  13. package/dist/gates/registry.d.ts +14 -0
  14. package/dist/gates/registry.d.ts.map +1 -0
  15. package/dist/gates/registry.js +21 -0
  16. package/dist/gates/telemetry.d.ts +47 -0
  17. package/dist/gates/telemetry.d.ts.map +1 -0
  18. package/dist/gates/telemetry.js +121 -0
  19. package/dist/gates/types.d.ts +67 -0
  20. package/dist/gates/types.d.ts.map +1 -0
  21. package/dist/gates/types.js +14 -0
  22. package/dist/governance/cli.d.ts +57 -0
  23. package/dist/governance/cli.d.ts.map +1 -0
  24. package/dist/governance/cli.js +169 -0
  25. package/dist/governance/grounding.d.ts +78 -0
  26. package/dist/governance/grounding.d.ts.map +1 -0
  27. package/dist/governance/grounding.js +299 -0
  28. package/dist/index.cjs +11 -9
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +94 -0
  32. package/dist/project-claude-md.md +33 -11
  33. package/dist/prompts/add-feature.d.ts +3 -0
  34. package/dist/prompts/add-feature.d.ts.map +1 -0
  35. package/dist/prompts/add-feature.js +154 -0
  36. package/dist/prompts/claude-md.d.ts +12 -0
  37. package/dist/prompts/claude-md.d.ts.map +1 -0
  38. package/dist/prompts/claude-md.js +87 -0
  39. package/dist/prompts/debug.d.ts +3 -0
  40. package/dist/prompts/debug.d.ts.map +1 -0
  41. package/dist/prompts/debug.js +129 -0
  42. package/dist/prompts/deploy.d.ts +3 -0
  43. package/dist/prompts/deploy.d.ts.map +1 -0
  44. package/dist/prompts/deploy.js +118 -0
  45. package/dist/prompts/design-schema.d.ts +3 -0
  46. package/dist/prompts/design-schema.d.ts.map +1 -0
  47. package/dist/prompts/design-schema.js +97 -0
  48. package/dist/prompts/governance-setup.d.ts +9 -0
  49. package/dist/prompts/governance-setup.d.ts.map +1 -0
  50. package/dist/prompts/governance-setup.js +76 -0
  51. package/dist/prompts/index.d.ts +3 -0
  52. package/dist/prompts/index.d.ts.map +1 -0
  53. package/dist/prompts/index.js +20 -0
  54. package/dist/prompts/new-app.d.ts +3 -0
  55. package/dist/prompts/new-app.d.ts.map +1 -0
  56. package/dist/prompts/new-app.js +203 -0
  57. package/dist/prompts/runbook.d.ts +12 -0
  58. package/dist/prompts/runbook.d.ts.map +1 -0
  59. package/dist/prompts/runbook.js +70 -0
  60. package/dist/prompts/secure.d.ts +3 -0
  61. package/dist/prompts/secure.d.ts.map +1 -0
  62. package/dist/prompts/secure.js +219 -0
  63. package/dist/resources/index.d.ts +12 -0
  64. package/dist/resources/index.d.ts.map +1 -0
  65. package/dist/resources/index.js +182 -0
  66. package/dist/tools/check-environment.d.ts +25 -0
  67. package/dist/tools/check-environment.d.ts.map +1 -0
  68. package/dist/tools/check-environment.js +281 -0
  69. package/dist/tools/index.d.ts +3 -0
  70. package/dist/tools/index.d.ts.map +1 -0
  71. package/dist/tools/index.js +18 -0
  72. package/dist/tools/project-status.d.ts +28 -0
  73. package/dist/tools/project-status.d.ts.map +1 -0
  74. package/dist/tools/project-status.js +138 -0
  75. package/dist/tools/project-validate.d.ts +19 -0
  76. package/dist/tools/project-validate.d.ts.map +1 -0
  77. package/dist/tools/project-validate.js +323 -0
  78. package/dist/tools/schema-analyze.d.ts +46 -0
  79. package/dist/tools/schema-analyze.d.ts.map +1 -0
  80. package/dist/tools/schema-analyze.js +336 -0
  81. package/package.json +3 -3
  82. package/src/gates/detectors/hand-written-migration.ts +23 -10
  83. package/src/index.ts +1 -1
  84. package/src/prompts/claude-md.ts +4 -2
  85. package/src/resources/project-claude-md.md +33 -11
@@ -0,0 +1,57 @@
1
+ /**
2
+ * governance/cli — the hook-shim entry for `everystack-mcp <subcommand>`.
3
+ *
4
+ * Claude Code hooks pipe their JSON event on stdin to one of these subcommands;
5
+ * the hook config itself carries no logic (the local-agent grounding-gate shape).
6
+ * The handlers are exported so the same logic is unit-testable and so the operator
7
+ * CLI can self-gate on it later — no drift between what the hook blocks and what
8
+ * the gate-to-prod refuses.
9
+ *
10
+ * This module stays LIGHT to import: it pulls only grounding + the pure engine +
11
+ * telemetry. The one heavy dependency (the canonical secret scanner) is lazy-loaded
12
+ * inside its detector behind a cheap pre-filter, so the common tool call is fast.
13
+ *
14
+ * Fail-OPEN on any internal error or unknown input — a governance bug must never
15
+ * brick tool use globally. Only the explicit "ungrounded" / "off-script deny"
16
+ * conditions fail closed.
17
+ */
18
+ export declare const GOVERNANCE_COMMANDS: readonly ["context", "gate", "mark", "validate", "report"];
19
+ export type GovernanceCommand = (typeof GOVERNANCE_COMMANDS)[number];
20
+ interface HookInput {
21
+ session_id?: string;
22
+ cwd?: string;
23
+ source?: string;
24
+ tool_name?: string;
25
+ tool_input?: {
26
+ file_path?: string;
27
+ content?: string;
28
+ new_string?: string;
29
+ old_string?: string;
30
+ command?: string;
31
+ offset?: number;
32
+ limit?: number;
33
+ };
34
+ }
35
+ /** SessionStart: the grounding contract text. */
36
+ export declare function handleContext(input: HookInput): string;
37
+ /**
38
+ * PreToolUse: grounding first (the contract must be read), then cheat gates. Returns
39
+ * a deny payload object to emit, or null to allow. Telemetry is recorded for any
40
+ * cheat findings at this boundary (the authoritative "attempt" event).
41
+ */
42
+ export declare function handleGate(input: HookInput): Promise<unknown | null>;
43
+ /**
44
+ * PostToolUse(Write|Edit): re-scan after the write and surface anything (a backstop
45
+ * for what a pre-write check couldn't see). Non-blocking. Does NOT re-record — the
46
+ * attempt was already counted at the gate — it only surfaces guidance.
47
+ */
48
+ export declare function handleValidate(input: HookInput): Promise<unknown | null>;
49
+ /** PostToolUse(Read): mark a required-file read toward grounding. */
50
+ export declare function handleMark(input: HookInput): void;
51
+ /**
52
+ * Dispatch a governance subcommand from hook JSON on stdin. `report` reads
53
+ * telemetry instead of stdin and prints a summary. Always fails open.
54
+ */
55
+ export declare function runGovernanceCli(argv: string[]): Promise<void>;
56
+ export {};
57
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/governance/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,eAAO,MAAM,mBAAmB,4DAMtB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QACX,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAuCD,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAItD;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAe1E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAM9E;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAOjD;AAoBD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BpE"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * governance/cli — the hook-shim entry for `everystack-mcp <subcommand>`.
3
+ *
4
+ * Claude Code hooks pipe their JSON event on stdin to one of these subcommands;
5
+ * the hook config itself carries no logic (the local-agent grounding-gate shape).
6
+ * The handlers are exported so the same logic is unit-testable and so the operator
7
+ * CLI can self-gate on it later — no drift between what the hook blocks and what
8
+ * the gate-to-prod refuses.
9
+ *
10
+ * This module stays LIGHT to import: it pulls only grounding + the pure engine +
11
+ * telemetry. The one heavy dependency (the canonical secret scanner) is lazy-loaded
12
+ * inside its detector behind a cheap pre-filter, so the common tool call is fast.
13
+ *
14
+ * Fail-OPEN on any internal error or unknown input — a governance bug must never
15
+ * brick tool use globally. Only the explicit "ungrounded" / "off-script deny"
16
+ * conditions fail closed.
17
+ */
18
+ import { context, gate, mark, denyReason } from './grounding.js';
19
+ import { evaluateGates, decide, findingsReason } from '../gates/engine.js';
20
+ import { gatesFor } from '../gates/registry.js';
21
+ import { recordFindings, summarize, renderSummary } from '../gates/telemetry.js';
22
+ export const GOVERNANCE_COMMANDS = [
23
+ 'context', // SessionStart: inject the grounding contract
24
+ 'gate', // PreToolUse: allow/deny (grounding + cheat gates)
25
+ 'mark', // PostToolUse(Read): record a required-file read
26
+ 'validate', // PostToolUse(Write|Edit): surface any cheat the write landed
27
+ 'report', // summarize the per-session telemetry (the sensor)
28
+ ];
29
+ /** Deny payload for PreToolUse. */
30
+ function denyPayload(reason) {
31
+ return {
32
+ hookSpecificOutput: {
33
+ hookEventName: 'PreToolUse',
34
+ permissionDecision: 'deny',
35
+ permissionDecisionReason: reason,
36
+ },
37
+ };
38
+ }
39
+ /** Non-blocking advisory payload for PostToolUse. */
40
+ function contextPayload(text) {
41
+ return {
42
+ hookSpecificOutput: {
43
+ hookEventName: 'PostToolUse',
44
+ additionalContext: text,
45
+ },
46
+ };
47
+ }
48
+ function toToolContext(input) {
49
+ const ti = input.tool_input ?? {};
50
+ return {
51
+ tool: input.tool_name ?? '',
52
+ filePath: ti.file_path,
53
+ // Write carries `content`; Edit carries `new_string` (what is about to land).
54
+ content: ti.content ?? ti.new_string,
55
+ command: ti.command,
56
+ cwd: input.cwd ?? process.cwd(),
57
+ };
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Handlers (exported for tests; pure-ish — telemetry is the only side effect)
61
+ // ---------------------------------------------------------------------------
62
+ /** SessionStart: the grounding contract text. */
63
+ export function handleContext(input) {
64
+ const sid = input.session_id ?? 'nosession';
65
+ const cwd = input.cwd ?? process.cwd();
66
+ return context(sid, cwd, input.source ?? 'startup');
67
+ }
68
+ /**
69
+ * PreToolUse: grounding first (the contract must be read), then cheat gates. Returns
70
+ * a deny payload object to emit, or null to allow. Telemetry is recorded for any
71
+ * cheat findings at this boundary (the authoritative "attempt" event).
72
+ */
73
+ export async function handleGate(input) {
74
+ const sid = input.session_id ?? 'nosession';
75
+ const cwd = input.cwd ?? process.cwd();
76
+ const tool = input.tool_name ?? '';
77
+ // 1) Grounding: until the contract is read, every non-Read tool is denied.
78
+ const g = gate(sid, cwd, tool);
79
+ if (g.decision === 'deny')
80
+ return denyPayload(denyReason(g.unread));
81
+ // 2) Cheat gates.
82
+ const ctx = toToolContext(input);
83
+ const findings = await evaluateGates(ctx, gatesFor(cwd));
84
+ if (findings.length)
85
+ recordFindings(sid, { cwd, tool }, findings);
86
+ if (decide(findings).decision === 'deny')
87
+ return denyPayload(findingsReason(findings));
88
+ return null; // allow (any warn-tier findings are recorded, not blocked)
89
+ }
90
+ /**
91
+ * PostToolUse(Write|Edit): re-scan after the write and surface anything (a backstop
92
+ * for what a pre-write check couldn't see). Non-blocking. Does NOT re-record — the
93
+ * attempt was already counted at the gate — it only surfaces guidance.
94
+ */
95
+ export async function handleValidate(input) {
96
+ const cwd = input.cwd ?? process.cwd();
97
+ const ctx = toToolContext(input);
98
+ const findings = await evaluateGates(ctx, gatesFor(cwd));
99
+ if (!findings.length)
100
+ return null;
101
+ return contextPayload(findingsReason(findings));
102
+ }
103
+ /** PostToolUse(Read): mark a required-file read toward grounding. */
104
+ export function handleMark(input) {
105
+ const sid = input.session_id ?? 'nosession';
106
+ const cwd = input.cwd ?? process.cwd();
107
+ const ti = input.tool_input ?? {};
108
+ if (!ti.file_path)
109
+ return;
110
+ const partial = ti.offset !== undefined || ti.limit !== undefined;
111
+ mark(sid, cwd, ti.file_path, partial);
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // stdin / dispatch
115
+ // ---------------------------------------------------------------------------
116
+ function readStdin() {
117
+ return new Promise((res) => {
118
+ let data = '';
119
+ if (process.stdin.isTTY) {
120
+ res('');
121
+ return;
122
+ }
123
+ process.stdin.setEncoding('utf-8');
124
+ process.stdin.on('data', (c) => (data += c));
125
+ process.stdin.on('end', () => res(data));
126
+ process.stdin.on('error', () => res(data));
127
+ });
128
+ }
129
+ /**
130
+ * Dispatch a governance subcommand from hook JSON on stdin. `report` reads
131
+ * telemetry instead of stdin and prints a summary. Always fails open.
132
+ */
133
+ export async function runGovernanceCli(argv) {
134
+ const mode = argv[0];
135
+ if (!mode || !GOVERNANCE_COMMANDS.includes(mode))
136
+ return;
137
+ try {
138
+ if (mode === 'report') {
139
+ process.stdout.write(renderSummary(summarize(argv[1])) + '\n');
140
+ return;
141
+ }
142
+ let input = {};
143
+ try {
144
+ input = JSON.parse((await readStdin()) || '{}');
145
+ }
146
+ catch {
147
+ return; // fail open on malformed input
148
+ }
149
+ if (mode === 'context') {
150
+ process.stdout.write(handleContext(input) + '\n');
151
+ }
152
+ else if (mode === 'gate') {
153
+ const payload = await handleGate(input);
154
+ if (payload)
155
+ process.stdout.write(JSON.stringify(payload));
156
+ }
157
+ else if (mode === 'mark') {
158
+ handleMark(input);
159
+ }
160
+ else if (mode === 'validate') {
161
+ const payload = await handleValidate(input);
162
+ if (payload)
163
+ process.stdout.write(JSON.stringify(payload));
164
+ }
165
+ }
166
+ catch {
167
+ /* fail open: stay silent, exit 0 */
168
+ }
169
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * grounding — enforce reading the project contract before any non-Read tool.
3
+ *
4
+ * Ported from local-agent's grounding gate (the proven implementation; see
5
+ * ~/Sites/local-agent/docs/grounding-gate.md). The contract: the CLAUDE.md files
6
+ * (global + project) and their declared `REQUIRED-READS:` companions define how
7
+ * the build is meant to go — for everystack that includes the Model/Module-first
8
+ * resources a project names. A model does not get to glide past them as scenery.
9
+ * Until the required files are actually Read this session, every tool EXCEPT Read
10
+ * is denied.
11
+ *
12
+ * everystack adaptation: state lives under ~/.everystack/governance/grounding so
13
+ * it never collides with local-agent's own gate (~/.agent/grounding). The pure
14
+ * functions (requiredFiles / gate / mark / context / groundStatus) are exported
15
+ * for governance/cli.ts to drive from hook JSON; the CLI shim lives there.
16
+ *
17
+ * Honest ceiling: this forces the *read* (deliberate, in-context, links followed).
18
+ * It cannot force comprehension. It kills the one specific failure — skipping the
19
+ * files — dead. Nothing more is claimed.
20
+ *
21
+ * Design decisions (from the bash → TS rewrite that proved this out):
22
+ * - Required set is DECLARED, not inferred: the global and project CLAUDE.md are
23
+ * always required, and each names its mandatory companions with an explicit
24
+ * `REQUIRED-READS:` line. (Scraping every Markdown link over-captures incidental
25
+ * prose links, so the contract declares its own reads instead.)
26
+ * - Paths are canonicalised with realpath on BOTH sides, so a symlinked root
27
+ * (/tmp -> /private/tmp) can't silently fail to match.
28
+ * - Partial reads (Read with offset/limit) do NOT count as grounding.
29
+ * - State is one JSON file per session; resets only on startup/clear, never on
30
+ * resume/compact — and `context` re-injects the contract on compact so it
31
+ * re-enters context after summarisation.
32
+ * - Fail-OPEN on any internal error or empty required set (never brick tool use);
33
+ * fail-CLOSED only on the explicit "not yet grounded" condition.
34
+ */
35
+ export interface RequiredSet {
36
+ /** Canonical paths of required files that exist (the gate can be satisfied by these). */
37
+ present: string[];
38
+ /** Required paths referenced but missing on disk (logged/warned, never block on them). */
39
+ missing: string[];
40
+ }
41
+ /**
42
+ * Resolve the required-reads set for a working directory:
43
+ * global CLAUDE.md + its REQUIRED-READS entries
44
+ * + nearest project CLAUDE.md + its REQUIRED-READS entries.
45
+ * Returns canonical, de-duplicated paths partitioned into present / missing.
46
+ */
47
+ export declare function requiredFiles(cwd: string): RequiredSet;
48
+ export interface GateResult {
49
+ decision: 'allow' | 'deny';
50
+ unread: string[];
51
+ }
52
+ /**
53
+ * PreToolUse decision. Always allows `Read`. Allows everything once the session
54
+ * is grounded. Fails open if no required files resolve. Otherwise denies any
55
+ * non-Read tool while required files remain unread.
56
+ */
57
+ export declare function gate(sessionId: string, cwd: string, toolName: string): GateResult;
58
+ export interface MarkResult {
59
+ grounded: boolean;
60
+ }
61
+ /**
62
+ * PostToolUse(Read) record. A full read of a required file marks it grounded.
63
+ * Partial reads (offset/limit present) are ignored — a one-line peek is not
64
+ * grounding. Reads of non-required files are ignored.
65
+ */
66
+ export declare function mark(sessionId: string, cwd: string, filePath: string, partial?: boolean): MarkResult;
67
+ /**
68
+ * SessionStart payload: the required-file list injected into context. Resets
69
+ * per-session state on `startup`/`clear` (fresh grounding required); leaves it
70
+ * intact on `resume`/`compact` but still re-injects the contract so it survives
71
+ * summarisation. Also runs 7-day housekeeping.
72
+ */
73
+ export declare function context(sessionId: string, cwd: string, source: string): string;
74
+ /** Human-readable grounding status for a session/cwd (the MCP introspection path). */
75
+ export declare function groundStatus(cwd: string, sessionId?: string): string;
76
+ /** The PreToolUse deny reason shown when grounding is incomplete. */
77
+ export declare function denyReason(unread: string[]): string;
78
+ //# sourceMappingURL=grounding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounding.d.ts","sourceRoot":"","sources":["../../src/governance/grounding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA+EH,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAkCtD;AAqED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAgBjF;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,UAAQ,GAAG,UAAU,CAYlG;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAuB9E;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAqBpE;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAQnD"}
@@ -0,0 +1,299 @@
1
+ /**
2
+ * grounding — enforce reading the project contract before any non-Read tool.
3
+ *
4
+ * Ported from local-agent's grounding gate (the proven implementation; see
5
+ * ~/Sites/local-agent/docs/grounding-gate.md). The contract: the CLAUDE.md files
6
+ * (global + project) and their declared `REQUIRED-READS:` companions define how
7
+ * the build is meant to go — for everystack that includes the Model/Module-first
8
+ * resources a project names. A model does not get to glide past them as scenery.
9
+ * Until the required files are actually Read this session, every tool EXCEPT Read
10
+ * is denied.
11
+ *
12
+ * everystack adaptation: state lives under ~/.everystack/governance/grounding so
13
+ * it never collides with local-agent's own gate (~/.agent/grounding). The pure
14
+ * functions (requiredFiles / gate / mark / context / groundStatus) are exported
15
+ * for governance/cli.ts to drive from hook JSON; the CLI shim lives there.
16
+ *
17
+ * Honest ceiling: this forces the *read* (deliberate, in-context, links followed).
18
+ * It cannot force comprehension. It kills the one specific failure — skipping the
19
+ * files — dead. Nothing more is claimed.
20
+ *
21
+ * Design decisions (from the bash → TS rewrite that proved this out):
22
+ * - Required set is DECLARED, not inferred: the global and project CLAUDE.md are
23
+ * always required, and each names its mandatory companions with an explicit
24
+ * `REQUIRED-READS:` line. (Scraping every Markdown link over-captures incidental
25
+ * prose links, so the contract declares its own reads instead.)
26
+ * - Paths are canonicalised with realpath on BOTH sides, so a symlinked root
27
+ * (/tmp -> /private/tmp) can't silently fail to match.
28
+ * - Partial reads (Read with offset/limit) do NOT count as grounding.
29
+ * - State is one JSON file per session; resets only on startup/clear, never on
30
+ * resume/compact — and `context` re-injects the contract on compact so it
31
+ * re-enters context after summarisation.
32
+ * - Fail-OPEN on any internal error or empty required set (never brick tool use);
33
+ * fail-CLOSED only on the explicit "not yet grounded" condition.
34
+ */
35
+ import { existsSync, realpathSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, } from 'fs';
36
+ import { join, dirname, resolve, isAbsolute } from 'path';
37
+ import { homedir } from 'os';
38
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
39
+ /** Home dir, overridable via $HOME (hook contexts + tests); falls back to os.homedir(). */
40
+ function home() {
41
+ return process.env.HOME || homedir();
42
+ }
43
+ function globalClaudeMd() {
44
+ return join(home(), '.claude', 'CLAUDE.md');
45
+ }
46
+ function stateDir() {
47
+ return join(home(), '.everystack', 'governance', 'grounding');
48
+ }
49
+ /** Canonical absolute path: resolve symlinks if the file exists, else normalise. */
50
+ function canonical(p) {
51
+ try {
52
+ return realpathSync(p);
53
+ }
54
+ catch {
55
+ return resolve(p);
56
+ }
57
+ }
58
+ /** Expand a leading `~` (or `~/`) to the home directory. */
59
+ function expandTilde(p) {
60
+ if (p === '~')
61
+ return home();
62
+ if (p.startsWith('~/'))
63
+ return join(home(), p.slice(2));
64
+ return p;
65
+ }
66
+ /** Nearest project CLAUDE.md walking up from cwd, stopping at $HOME or filesystem root. */
67
+ function nearestProjectClaudeMd(cwd) {
68
+ let d = resolve(cwd);
69
+ const stop = resolve(home());
70
+ while (d && d !== '/' && d !== stop) {
71
+ const candidate = join(d, 'CLAUDE.md');
72
+ if (existsSync(candidate))
73
+ return candidate;
74
+ const parent = dirname(d);
75
+ if (parent === d)
76
+ break;
77
+ d = parent;
78
+ }
79
+ return null;
80
+ }
81
+ /** Parse an optional `REQUIRED-READS: a.md, b/c.md` line; paths relative to the CLAUDE.md dir. */
82
+ function parseRequiredReads(claudeMdPath) {
83
+ let text;
84
+ try {
85
+ text = readFileSync(claudeMdPath, 'utf-8');
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const m = text.match(/^[ \t]*REQUIRED-READS:[ \t]*(.+)$/im);
91
+ if (!m)
92
+ return [];
93
+ const base = dirname(claudeMdPath);
94
+ return m[1]
95
+ .split(',')
96
+ .map((s) => s.trim())
97
+ .filter(Boolean)
98
+ .map(expandTilde)
99
+ .map((rel) => (isAbsolute(rel) ? rel : resolve(base, rel)));
100
+ }
101
+ /**
102
+ * Resolve the required-reads set for a working directory:
103
+ * global CLAUDE.md + its REQUIRED-READS entries
104
+ * + nearest project CLAUDE.md + its REQUIRED-READS entries.
105
+ * Returns canonical, de-duplicated paths partitioned into present / missing.
106
+ */
107
+ export function requiredFiles(cwd) {
108
+ const candidates = [];
109
+ const gmd = globalClaudeMd();
110
+ if (existsSync(gmd)) {
111
+ candidates.push(gmd);
112
+ candidates.push(...parseRequiredReads(gmd));
113
+ }
114
+ const proj = nearestProjectClaudeMd(cwd);
115
+ if (proj) {
116
+ candidates.push(proj);
117
+ candidates.push(...parseRequiredReads(proj));
118
+ }
119
+ const present = [];
120
+ const missing = [];
121
+ const seen = new Set();
122
+ for (const c of candidates) {
123
+ if (existsSync(c)) {
124
+ const canon = canonical(c);
125
+ if (!seen.has(canon)) {
126
+ seen.add(canon);
127
+ present.push(canon);
128
+ }
129
+ }
130
+ else {
131
+ const norm = resolve(c);
132
+ if (!seen.has(norm)) {
133
+ seen.add(norm);
134
+ missing.push(norm);
135
+ }
136
+ }
137
+ }
138
+ return { present, missing };
139
+ }
140
+ function safeId(sessionId) {
141
+ return (sessionId || 'nosession').replace(/[^A-Za-z0-9_.-]/g, '_');
142
+ }
143
+ function statePath(sessionId) {
144
+ return join(stateDir(), `${safeId(sessionId)}.json`);
145
+ }
146
+ function loadState(sessionId) {
147
+ try {
148
+ const raw = JSON.parse(readFileSync(statePath(sessionId), 'utf-8'));
149
+ return {
150
+ reads: Array.isArray(raw.reads) ? raw.reads : [],
151
+ grounded: !!raw.grounded,
152
+ updated: typeof raw.updated === 'string' ? raw.updated : '',
153
+ };
154
+ }
155
+ catch {
156
+ return { reads: [], grounded: false, updated: '' };
157
+ }
158
+ }
159
+ function saveState(sessionId, state) {
160
+ mkdirSync(stateDir(), { recursive: true });
161
+ writeFileSync(statePath(sessionId), JSON.stringify(state));
162
+ }
163
+ function resetState(sessionId) {
164
+ try {
165
+ unlinkSync(statePath(sessionId));
166
+ }
167
+ catch {
168
+ /* nothing to reset */
169
+ }
170
+ }
171
+ /** Delete session-state files older than 7 days. */
172
+ function prune() {
173
+ let files;
174
+ try {
175
+ files = readdirSync(stateDir());
176
+ }
177
+ catch {
178
+ return;
179
+ }
180
+ const now = Date.now();
181
+ for (const f of files) {
182
+ const p = join(stateDir(), f);
183
+ try {
184
+ if (now - statSync(p).mtimeMs > MAX_AGE_MS)
185
+ unlinkSync(p);
186
+ }
187
+ catch {
188
+ /* skip */
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * PreToolUse decision. Always allows `Read`. Allows everything once the session
194
+ * is grounded. Fails open if no required files resolve. Otherwise denies any
195
+ * non-Read tool while required files remain unread.
196
+ */
197
+ export function gate(sessionId, cwd, toolName) {
198
+ if (toolName === 'Read')
199
+ return { decision: 'allow', unread: [] };
200
+ const { present } = requiredFiles(cwd);
201
+ if (present.length === 0)
202
+ return { decision: 'allow', unread: [] }; // nothing to ground on
203
+ const state = loadState(sessionId);
204
+ if (state.grounded)
205
+ return { decision: 'allow', unread: [] };
206
+ const read = new Set(state.reads);
207
+ const unread = present.filter((p) => !read.has(p));
208
+ if (unread.length === 0) {
209
+ saveState(sessionId, { reads: state.reads, grounded: true, updated: new Date().toISOString() });
210
+ return { decision: 'allow', unread: [] };
211
+ }
212
+ return { decision: 'deny', unread };
213
+ }
214
+ /**
215
+ * PostToolUse(Read) record. A full read of a required file marks it grounded.
216
+ * Partial reads (offset/limit present) are ignored — a one-line peek is not
217
+ * grounding. Reads of non-required files are ignored.
218
+ */
219
+ export function mark(sessionId, cwd, filePath, partial = false) {
220
+ if (partial || !filePath)
221
+ return { grounded: loadState(sessionId).grounded };
222
+ const { present } = requiredFiles(cwd);
223
+ const canon = canonical(filePath);
224
+ if (!present.includes(canon))
225
+ return { grounded: loadState(sessionId).grounded };
226
+ const state = loadState(sessionId);
227
+ const reads = state.reads.includes(canon) ? state.reads : [...state.reads, canon];
228
+ const grounded = present.every((p) => reads.includes(p));
229
+ saveState(sessionId, { reads, grounded, updated: new Date().toISOString() });
230
+ return { grounded };
231
+ }
232
+ /**
233
+ * SessionStart payload: the required-file list injected into context. Resets
234
+ * per-session state on `startup`/`clear` (fresh grounding required); leaves it
235
+ * intact on `resume`/`compact` but still re-injects the contract so it survives
236
+ * summarisation. Also runs 7-day housekeeping.
237
+ */
238
+ export function context(sessionId, cwd, source) {
239
+ prune();
240
+ if (source === 'startup' || source === 'clear')
241
+ resetState(sessionId);
242
+ const { present, missing } = requiredFiles(cwd);
243
+ const lines = [];
244
+ lines.push('GROUNDING GATE ACTIVE — this is the contract for how this project is built.');
245
+ lines.push('Before ANY other tool this session you MUST Read these files (Read is the only tool allowed until you have). Read them, act on them, follow their links:');
246
+ if (present.length === 0) {
247
+ lines.push(' (no CLAUDE.md contract found for this directory — gate is inactive)');
248
+ }
249
+ else {
250
+ for (const f of present)
251
+ lines.push(` - ${f}`);
252
+ }
253
+ if (missing.length > 0) {
254
+ lines.push('');
255
+ lines.push('Note — referenced but missing on disk (skipped, not required):');
256
+ for (const f of missing)
257
+ lines.push(` - ${f}`);
258
+ }
259
+ lines.push('');
260
+ lines.push('You do not get the right to ignore them.');
261
+ return lines.join('\n');
262
+ }
263
+ /** Human-readable grounding status for a session/cwd (the MCP introspection path). */
264
+ export function groundStatus(cwd, sessionId) {
265
+ const { present, missing } = requiredFiles(cwd);
266
+ const lines = [];
267
+ lines.push(`Required reads for ${resolve(cwd)}:`);
268
+ if (present.length === 0)
269
+ lines.push(' (none — no CLAUDE.md contract found; gate inactive)');
270
+ if (sessionId) {
271
+ const state = loadState(sessionId);
272
+ const read = new Set(state.reads);
273
+ for (const f of present)
274
+ lines.push(` [${read.has(f) ? 'x' : ' '}] ${f}`);
275
+ lines.push('');
276
+ lines.push(`Grounded: ${state.grounded ? 'yes' : 'no'} (session ${safeId(sessionId)})`);
277
+ }
278
+ else {
279
+ for (const f of present)
280
+ lines.push(` - ${f}`);
281
+ }
282
+ if (missing.length > 0) {
283
+ lines.push('');
284
+ lines.push('Referenced but missing (skipped):');
285
+ for (const f of missing)
286
+ lines.push(` - ${f}`);
287
+ }
288
+ return lines.join('\n');
289
+ }
290
+ /** The PreToolUse deny reason shown when grounding is incomplete. */
291
+ export function denyReason(unread) {
292
+ return [
293
+ 'GROUNDING INCOMPLETE — you have not read the contract. Read these files first',
294
+ '(Read is allowed; every other tool is blocked until you have):',
295
+ ...unread.map((f) => ` - ${f}`),
296
+ '',
297
+ 'This is how this project is built. You do not get to skip it.',
298
+ ].join('\n');
299
+ }
package/dist/index.cjs CHANGED
@@ -23050,8 +23050,10 @@ function registerClaudeMdPrompt(server) {
23050
23050
  "",
23051
23051
  "Reconcile the existing CLAUDE.md against three things, and propose a concrete diff:",
23052
23052
  "1. **Framework drift** \u2014 does it teach superseded ways (e.g. `drizzle-kit generate`,",
23053
- ' hand-written migrations, "pass your Drizzle schema") instead of the v3 Model flow',
23054
- " (`defineModel` \u2192 `everystack db:generate`)? Suggest the current conventions.",
23053
+ ' hand-written migrations, a migration journal, "pass your Drizzle schema") instead of',
23054
+ " the migration-authority flow (`defineModel` in db/models/ \u2192 `everystack db:sync` on",
23055
+ " dev; `db:plan` \u2192 `db:apply` on protected stages; `db:check` in CI; data moves in",
23056
+ " db/backfills/ via `db:backfill`)? Suggest the current conventions.",
23055
23057
  "2. **Project drift** \u2014 do the Structure / Commands / package list match what is actually",
23056
23058
  " installed now (new packages, a new tier, a new models/ dir)? Suggest updates. Also",
23057
23059
  " check the operations manual: if docs/RUNBOOK.md is missing, or stale per",
@@ -23399,7 +23401,7 @@ function renderFinding(f) {
23399
23401
  // src/gates/detectors/hand-written-migration.ts
23400
23402
  var import_path2 = require("path");
23401
23403
  var MIGRATION_SQL = /(?:^|\/)(?:drizzle|migrations)\/[^/]+\.sql$/i;
23402
- var GENERATED_SCHEMA = /(?:^|\/)(?:db|drizzle)\/schema\.ts$/;
23404
+ var GENERATED_SCHEMA = /schema\.generated\.tsx?$/;
23403
23405
  function rel(ctx, p) {
23404
23406
  try {
23405
23407
  return (0, import_path2.relative)(ctx.cwd, p) || p;
@@ -23411,15 +23413,15 @@ var handWrittenMigration = {
23411
23413
  id: "hand-written-migration",
23412
23414
  tier: "framework",
23413
23415
  severity: "deny",
23414
- guide: "Schema and migrations are generated from your Models \u2014 edit the Model and run db:generate, never hand-write SQL migrations or edit the generated schema.",
23415
- conform: "everystack db:generate",
23416
- verify: "everystack db:generate produces no diff (a clean no-op)",
23416
+ guide: "Nobody authors migrations. Schema changes have three homes: tables/constraints/authz are declared in db/models/ (edit the Model, then `everystack db:sync` on dev \u2014 protected stages take `db:plan` \u2192 `db:apply`); functions/views/matviews are authored in db/sql/ (deployed by db:reconcile/db:sync); one-shot DATA moves are authored in db/backfills/*.sql (run via db:backfill, never as a schema side effect). No migration file, ever \u2014 and generated artifacts (schema.generated.ts) are never edited.",
23417
+ conform: "edit db/models/ then `everystack db:sync` (dev) or `db:plan` \u2192 `db:apply` (protected) \xB7 db/sql/ for derived objects \xB7 db/backfills/ for data moves",
23418
+ verify: "everystack db:check passes (declared state composes; generated artifacts match regeneration) AND everystack db:fingerprint reports MATCH",
23417
23419
  detect(ctx) {
23418
23420
  if (ctx.tool !== "Write" && ctx.tool !== "Edit") return null;
23419
23421
  const p = ctx.filePath;
23420
23422
  if (!p) return null;
23421
23423
  if (MIGRATION_SQL.test(p)) return `${rel(ctx, p)} is a SQL migration being written by hand`;
23422
- if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is generated from your Models`;
23424
+ if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is a generated artifact (compiled from your Models \u2014 db:check refuses hand edits)`;
23423
23425
  return null;
23424
23426
  }
23425
23427
  };
@@ -23705,7 +23707,7 @@ async function runGovernanceCli(argv) {
23705
23707
  }
23706
23708
 
23707
23709
  // src/index.ts
23708
- var version2 = (true ? "0.3.1" : null) ?? "0.3.0-dev";
23710
+ var version2 = (true ? "0.3.3" : null) ?? "0.3.0-dev";
23709
23711
  var INSTRUCTIONS = [
23710
23712
  "You govern how any agent builds everystack \u2014 a self-hosted application stack for Expo apps on AWS.",
23711
23713
  "Your job is not only to advise but to keep the build on-script: the architecture the maintainer",
@@ -23767,7 +23769,7 @@ var INSTRUCTIONS = [
23767
23769
  "1. Read everystack://core for architecture and conventions.",
23768
23770
  "2. Read everystack://security before any deployment or auth guidance.",
23769
23771
  "3. Load detail resources on demand when the user asks about specific features.",
23770
- "4. Data lives in PostgreSQL via Models/Modules and is served through the API \u2014 never bundle large computed data into the app. Schema changes go through generated migrations (`db:generate`), never hand-written DDL. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.",
23772
+ '4. Data lives in PostgreSQL via Models/Modules and is served through the API \u2014 never bundle large computed data into the app. NOBODY AUTHORS MIGRATIONS. Schema work has three homes: tables/constraints/authz are DECLARED in `db/models/` (edit the Model, then `everystack db:sync` moves the dev database to the checkout \u2014 verified by fingerprint); functions/views/matviews are authored in `db/sql/` (deployed by `db:reconcile`/`db:sync`); one-shot DATA moves are authored in `db/backfills/*.sql` (run via `db:backfill`, never as a schema side effect). Protected stages never take a sync: `db:plan` mints a reviewable, fingerprint-pinned edge and `db:apply` verifies at both ends \u2014 the checkout must descend from the commit declaring the target\'s state ("rebase first"), and destructive plans are confirmed, snapshotted, and approver-gated (`db:approvers`). CI runs `everystack db:check` (the merged declared state must compose; generated artifacts must match regeneration byte-for-byte). Per-branch dev databases: `db:template:refresh` + `db:branch`; deployed feature stages fork data with `db:fork`. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
23771
23773
  '5. When the user wants to start a new project, run check_environment (phase "local" for dev, "deploy" for deployment) to verify prerequisites.',
23772
23774
  "6. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI."
23773
23775
  ].join("\n");