@everystack/mcp 0.3.2 → 0.4.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 (104) hide show
  1. package/dist/adding-database.md +36 -23
  2. package/dist/cli.md +44 -3
  3. package/dist/core.md +19 -1
  4. package/dist/database-operations.md +236 -0
  5. package/dist/derived-objects.md +225 -0
  6. package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
  7. package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
  8. package/dist/gates/detectors/embedded-data-bundle.js +56 -0
  9. package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
  10. package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
  11. package/dist/gates/detectors/hand-written-migration.js +54 -0
  12. package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
  13. package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
  14. package/dist/gates/detectors/secret-in-public-env.js +38 -0
  15. package/dist/gates/engine.d.ts +28 -0
  16. package/dist/gates/engine.d.ts.map +1 -0
  17. package/dist/gates/engine.js +74 -0
  18. package/dist/gates/registry.d.ts +14 -0
  19. package/dist/gates/registry.d.ts.map +1 -0
  20. package/dist/gates/registry.js +21 -0
  21. package/dist/gates/telemetry.d.ts +47 -0
  22. package/dist/gates/telemetry.d.ts.map +1 -0
  23. package/dist/gates/telemetry.js +121 -0
  24. package/dist/gates/types.d.ts +67 -0
  25. package/dist/gates/types.d.ts.map +1 -0
  26. package/dist/gates/types.js +14 -0
  27. package/dist/governance/cli.d.ts +57 -0
  28. package/dist/governance/cli.d.ts.map +1 -0
  29. package/dist/governance/cli.js +169 -0
  30. package/dist/governance/grounding.d.ts +78 -0
  31. package/dist/governance/grounding.d.ts.map +1 -0
  32. package/dist/governance/grounding.js +299 -0
  33. package/dist/index.cjs +119 -107
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +94 -0
  37. package/dist/project-claude-md.md +35 -18
  38. package/dist/prompts/add-feature.d.ts +3 -0
  39. package/dist/prompts/add-feature.d.ts.map +1 -0
  40. package/dist/prompts/add-feature.js +154 -0
  41. package/dist/prompts/claude-md.d.ts +12 -0
  42. package/dist/prompts/claude-md.d.ts.map +1 -0
  43. package/dist/prompts/claude-md.js +87 -0
  44. package/dist/prompts/debug.d.ts +3 -0
  45. package/dist/prompts/debug.d.ts.map +1 -0
  46. package/dist/prompts/debug.js +129 -0
  47. package/dist/prompts/deploy.d.ts +3 -0
  48. package/dist/prompts/deploy.d.ts.map +1 -0
  49. package/dist/prompts/deploy.js +118 -0
  50. package/dist/prompts/design-schema.d.ts +3 -0
  51. package/dist/prompts/design-schema.d.ts.map +1 -0
  52. package/dist/prompts/design-schema.js +97 -0
  53. package/dist/prompts/governance-setup.d.ts +9 -0
  54. package/dist/prompts/governance-setup.d.ts.map +1 -0
  55. package/dist/prompts/governance-setup.js +76 -0
  56. package/dist/prompts/index.d.ts +3 -0
  57. package/dist/prompts/index.d.ts.map +1 -0
  58. package/dist/prompts/index.js +20 -0
  59. package/dist/prompts/new-app.d.ts +3 -0
  60. package/dist/prompts/new-app.d.ts.map +1 -0
  61. package/dist/prompts/new-app.js +203 -0
  62. package/dist/prompts/runbook.d.ts +12 -0
  63. package/dist/prompts/runbook.d.ts.map +1 -0
  64. package/dist/prompts/runbook.js +70 -0
  65. package/dist/prompts/secure.d.ts +3 -0
  66. package/dist/prompts/secure.d.ts.map +1 -0
  67. package/dist/prompts/secure.js +219 -0
  68. package/dist/resources/index.d.ts +12 -0
  69. package/dist/resources/index.d.ts.map +1 -0
  70. package/dist/resources/index.js +182 -0
  71. package/dist/schema-patterns.md +92 -106
  72. package/dist/tools/check-environment.d.ts +25 -0
  73. package/dist/tools/check-environment.d.ts.map +1 -0
  74. package/dist/tools/check-environment.js +281 -0
  75. package/dist/tools/index.d.ts +3 -0
  76. package/dist/tools/index.d.ts.map +1 -0
  77. package/dist/tools/index.js +18 -0
  78. package/dist/tools/project-status.d.ts +28 -0
  79. package/dist/tools/project-status.d.ts.map +1 -0
  80. package/dist/tools/project-status.js +138 -0
  81. package/dist/tools/project-validate.d.ts +19 -0
  82. package/dist/tools/project-validate.d.ts.map +1 -0
  83. package/dist/tools/project-validate.js +323 -0
  84. package/dist/tools/schema-analyze.d.ts +46 -0
  85. package/dist/tools/schema-analyze.d.ts.map +1 -0
  86. package/dist/tools/schema-analyze.js +336 -0
  87. package/package.json +3 -3
  88. package/src/gates/detectors/hand-written-migration.ts +29 -13
  89. package/src/index.ts +1 -1
  90. package/src/prompts/add-feature.ts +4 -4
  91. package/src/prompts/claude-md.ts +4 -2
  92. package/src/prompts/debug.ts +3 -4
  93. package/src/prompts/deploy.ts +17 -8
  94. package/src/prompts/design-schema.ts +45 -59
  95. package/src/prompts/new-app.ts +21 -19
  96. package/src/prompts/secure.ts +3 -3
  97. package/src/resources/adding-database.md +36 -23
  98. package/src/resources/cli.md +44 -3
  99. package/src/resources/core.md +19 -1
  100. package/src/resources/database-operations.md +236 -0
  101. package/src/resources/derived-objects.md +225 -0
  102. package/src/resources/index.ts +15 -1
  103. package/src/resources/project-claude-md.md +35 -18
  104. package/src/resources/schema-patterns.md +92 -106
@@ -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
+ }