@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.
- package/dist/adding-database.md +36 -23
- package/dist/cli.md +44 -3
- package/dist/core.md +19 -1
- package/dist/database-operations.md +236 -0
- package/dist/derived-objects.md +225 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
- package/dist/gates/detectors/embedded-data-bundle.js +56 -0
- package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
- package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
- package/dist/gates/detectors/hand-written-migration.js +54 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
- package/dist/gates/detectors/secret-in-public-env.js +38 -0
- package/dist/gates/engine.d.ts +28 -0
- package/dist/gates/engine.d.ts.map +1 -0
- package/dist/gates/engine.js +74 -0
- package/dist/gates/registry.d.ts +14 -0
- package/dist/gates/registry.d.ts.map +1 -0
- package/dist/gates/registry.js +21 -0
- package/dist/gates/telemetry.d.ts +47 -0
- package/dist/gates/telemetry.d.ts.map +1 -0
- package/dist/gates/telemetry.js +121 -0
- package/dist/gates/types.d.ts +67 -0
- package/dist/gates/types.d.ts.map +1 -0
- package/dist/gates/types.js +14 -0
- package/dist/governance/cli.d.ts +57 -0
- package/dist/governance/cli.d.ts.map +1 -0
- package/dist/governance/cli.js +169 -0
- package/dist/governance/grounding.d.ts +78 -0
- package/dist/governance/grounding.d.ts.map +1 -0
- package/dist/governance/grounding.js +299 -0
- package/dist/index.cjs +119 -107
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/project-claude-md.md +35 -18
- package/dist/prompts/add-feature.d.ts +3 -0
- package/dist/prompts/add-feature.d.ts.map +1 -0
- package/dist/prompts/add-feature.js +154 -0
- package/dist/prompts/claude-md.d.ts +12 -0
- package/dist/prompts/claude-md.d.ts.map +1 -0
- package/dist/prompts/claude-md.js +87 -0
- package/dist/prompts/debug.d.ts +3 -0
- package/dist/prompts/debug.d.ts.map +1 -0
- package/dist/prompts/debug.js +129 -0
- package/dist/prompts/deploy.d.ts +3 -0
- package/dist/prompts/deploy.d.ts.map +1 -0
- package/dist/prompts/deploy.js +118 -0
- package/dist/prompts/design-schema.d.ts +3 -0
- package/dist/prompts/design-schema.d.ts.map +1 -0
- package/dist/prompts/design-schema.js +97 -0
- package/dist/prompts/governance-setup.d.ts +9 -0
- package/dist/prompts/governance-setup.d.ts.map +1 -0
- package/dist/prompts/governance-setup.js +76 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +20 -0
- package/dist/prompts/new-app.d.ts +3 -0
- package/dist/prompts/new-app.d.ts.map +1 -0
- package/dist/prompts/new-app.js +203 -0
- package/dist/prompts/runbook.d.ts +12 -0
- package/dist/prompts/runbook.d.ts.map +1 -0
- package/dist/prompts/runbook.js +70 -0
- package/dist/prompts/secure.d.ts +3 -0
- package/dist/prompts/secure.d.ts.map +1 -0
- package/dist/prompts/secure.js +219 -0
- package/dist/resources/index.d.ts +12 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +182 -0
- package/dist/schema-patterns.md +92 -106
- package/dist/tools/check-environment.d.ts +25 -0
- package/dist/tools/check-environment.d.ts.map +1 -0
- package/dist/tools/check-environment.js +281 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/project-status.d.ts +28 -0
- package/dist/tools/project-status.d.ts.map +1 -0
- package/dist/tools/project-status.js +138 -0
- package/dist/tools/project-validate.d.ts +19 -0
- package/dist/tools/project-validate.d.ts.map +1 -0
- package/dist/tools/project-validate.js +323 -0
- package/dist/tools/schema-analyze.d.ts +46 -0
- package/dist/tools/schema-analyze.d.ts.map +1 -0
- package/dist/tools/schema-analyze.js +336 -0
- package/package.json +3 -3
- package/src/gates/detectors/hand-written-migration.ts +29 -13
- package/src/index.ts +1 -1
- package/src/prompts/add-feature.ts +4 -4
- package/src/prompts/claude-md.ts +4 -2
- package/src/prompts/debug.ts +3 -4
- package/src/prompts/deploy.ts +17 -8
- package/src/prompts/design-schema.ts +45 -59
- package/src/prompts/new-app.ts +21 -19
- package/src/prompts/secure.ts +3 -3
- package/src/resources/adding-database.md +36 -23
- package/src/resources/cli.md +44 -3
- package/src/resources/core.md +19 -1
- package/src/resources/database-operations.md +236 -0
- package/src/resources/derived-objects.md +225 -0
- package/src/resources/index.ts +15 -1
- package/src/resources/project-claude-md.md +35 -18
- 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
|
+
}
|