@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.
- 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 +11 -9
- 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 +33 -11
- 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/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 +23 -10
- package/src/index.ts +1 -1
- package/src/prompts/claude-md.ts +4 -2
- package/src/resources/project-claude-md.md +33 -11
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: bundling a large data file into the app (the 180MB-JSON failure).
|
|
3
|
+
*
|
|
4
|
+
* Data belongs in PostgreSQL, served through the API — not shipped as a static
|
|
5
|
+
* asset or imported blob. This catches the file *as it lands*: a Write of a data
|
|
6
|
+
* file whose content exceeds the budget. The authoritative gate-to-prod check is
|
|
7
|
+
* `bundle:audit`'s weight dimension; this is the cheap authoring-time nudge.
|
|
8
|
+
*
|
|
9
|
+
* Pure: size is measured from the proposed content, no filesystem read.
|
|
10
|
+
*/
|
|
11
|
+
import type { CheatGate } from '../types.js';
|
|
12
|
+
export declare const embeddedDataBundle: CheatGate;
|
|
13
|
+
//# sourceMappingURL=embedded-data-bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedded-data-bundle.d.ts","sourceRoot":"","sources":["../../../src/gates/detectors/embedded-data-bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AA4B9D,eAAO,MAAM,kBAAkB,EAAE,SAiBhC,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: bundling a large data file into the app (the 180MB-JSON failure).
|
|
3
|
+
*
|
|
4
|
+
* Data belongs in PostgreSQL, served through the API — not shipped as a static
|
|
5
|
+
* asset or imported blob. This catches the file *as it lands*: a Write of a data
|
|
6
|
+
* file whose content exceeds the budget. The authoritative gate-to-prod check is
|
|
7
|
+
* `bundle:audit`'s weight dimension; this is the cheap authoring-time nudge.
|
|
8
|
+
*
|
|
9
|
+
* Pure: size is measured from the proposed content, no filesystem read.
|
|
10
|
+
*/
|
|
11
|
+
import { basename, relative } from 'path';
|
|
12
|
+
/** Files over this many bytes of bundled data are almost certainly DB-shaped data. */
|
|
13
|
+
const DATA_BYTE_BUDGET = 512 * 1024; // 0.5 MB
|
|
14
|
+
const DATA_EXT = /\.(json|csv|tsv|geojson|ndjson|xml)$/i;
|
|
15
|
+
/** Config/lockfiles that are legitimately data-shaped — never flagged. */
|
|
16
|
+
const EXEMPT = /^(package(-lock)?\.json|tsconfig.*\.json|app\.json|app\.config\.json|eas\.json|.*\.config\.json|manifest\.json|tsconfig\.json)$/i;
|
|
17
|
+
function formatBytes(n) {
|
|
18
|
+
if (n < 1024)
|
|
19
|
+
return `${n} B`;
|
|
20
|
+
const units = ['KB', 'MB', 'GB'];
|
|
21
|
+
let i = -1;
|
|
22
|
+
do {
|
|
23
|
+
n /= 1024;
|
|
24
|
+
i++;
|
|
25
|
+
} while (n >= 1024 && i < units.length - 1);
|
|
26
|
+
return `${n.toFixed(1)} ${units[i]}`;
|
|
27
|
+
}
|
|
28
|
+
function rel(ctx, p) {
|
|
29
|
+
try {
|
|
30
|
+
return relative(ctx.cwd, p) || p;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const embeddedDataBundle = {
|
|
37
|
+
id: 'embedded-data-bundle',
|
|
38
|
+
tier: 'framework',
|
|
39
|
+
severity: 'deny',
|
|
40
|
+
guide: 'Data belongs in PostgreSQL, served through the API — model it and load it (or render an empty state if it does not exist yet). Never bundle large data into the app.',
|
|
41
|
+
conform: 'define a Model + everystack db:generate + an API hook (or the ingest pipeline)',
|
|
42
|
+
verify: 'everystack bundle:audit stays under the weight budget',
|
|
43
|
+
detect(ctx) {
|
|
44
|
+
if (ctx.tool !== 'Write')
|
|
45
|
+
return null; // Write lands the whole file; Edit content is partial
|
|
46
|
+
const p = ctx.filePath;
|
|
47
|
+
if (!p || !DATA_EXT.test(p))
|
|
48
|
+
return null;
|
|
49
|
+
if (EXEMPT.test(basename(p)))
|
|
50
|
+
return null;
|
|
51
|
+
const bytes = Buffer.byteLength(ctx.content ?? '', 'utf8');
|
|
52
|
+
if (bytes < DATA_BYTE_BUDGET)
|
|
53
|
+
return null;
|
|
54
|
+
return `${rel(ctx, p)} is ${formatBytes(bytes)} of data bundled into the app`;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: hand-writing a migration or editing the generated schema.
|
|
3
|
+
*
|
|
4
|
+
* Under the migration-authority workflow nobody authors migrations at all:
|
|
5
|
+
* the Models declare the state, `db:sync` moves a dev database to it,
|
|
6
|
+
* protected stages take fingerprint-verified plans (`db:plan` → `db:apply`),
|
|
7
|
+
* and CI (`db:check`) refuses generated artifacts that don't match
|
|
8
|
+
* regeneration. A hand-written `.sql` migration or a hand-edit to
|
|
9
|
+
* `db/schema.generated.ts` drifts the database from the Models. Pure path
|
|
10
|
+
* check; no IO.
|
|
11
|
+
*
|
|
12
|
+
* The homes that ARE legitimately authored — this gate deliberately matches
|
|
13
|
+
* none of them:
|
|
14
|
+
* - `db/models/` — the Models themselves (the source of truth).
|
|
15
|
+
* - `db/sql/` — the DERIVED layer (functions/views/matviews), deployed
|
|
16
|
+
* via `db:reconcile`/`db:sync`.
|
|
17
|
+
* - `db/backfills/` — one-shot DATA jobs, run via `db:backfill` (content-
|
|
18
|
+
* addressed, own record, never a schema side effect).
|
|
19
|
+
* - `db/schema.ts` — the thin hand-maintained barrel that re-exports the
|
|
20
|
+
* generated schema plus package-owned schemas.
|
|
21
|
+
*/
|
|
22
|
+
import type { CheatGate } from '../types.js';
|
|
23
|
+
export declare const handWrittenMigration: CheatGate;
|
|
24
|
+
//# sourceMappingURL=hand-written-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hand-written-migration.d.ts","sourceRoot":"","sources":["../../../src/gates/detectors/hand-written-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AAe9D,eAAO,MAAM,oBAAoB,EAAE,SAgBlC,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: hand-writing a migration or editing the generated schema.
|
|
3
|
+
*
|
|
4
|
+
* Under the migration-authority workflow nobody authors migrations at all:
|
|
5
|
+
* the Models declare the state, `db:sync` moves a dev database to it,
|
|
6
|
+
* protected stages take fingerprint-verified plans (`db:plan` → `db:apply`),
|
|
7
|
+
* and CI (`db:check`) refuses generated artifacts that don't match
|
|
8
|
+
* regeneration. A hand-written `.sql` migration or a hand-edit to
|
|
9
|
+
* `db/schema.generated.ts` drifts the database from the Models. Pure path
|
|
10
|
+
* check; no IO.
|
|
11
|
+
*
|
|
12
|
+
* The homes that ARE legitimately authored — this gate deliberately matches
|
|
13
|
+
* none of them:
|
|
14
|
+
* - `db/models/` — the Models themselves (the source of truth).
|
|
15
|
+
* - `db/sql/` — the DERIVED layer (functions/views/matviews), deployed
|
|
16
|
+
* via `db:reconcile`/`db:sync`.
|
|
17
|
+
* - `db/backfills/` — one-shot DATA jobs, run via `db:backfill` (content-
|
|
18
|
+
* addressed, own record, never a schema side effect).
|
|
19
|
+
* - `db/schema.ts` — the thin hand-maintained barrel that re-exports the
|
|
20
|
+
* generated schema plus package-owned schemas.
|
|
21
|
+
*/
|
|
22
|
+
import { relative } from 'path';
|
|
23
|
+
/** A `.sql` file under a migrations/drizzle directory — these are generated, never authored. */
|
|
24
|
+
const MIGRATION_SQL = /(?:^|\/)(?:drizzle|migrations)\/[^/]+\.sql$/i;
|
|
25
|
+
/** The generated drizzle schema artifact (any location — monorepos relocate it via --schema-out). */
|
|
26
|
+
const GENERATED_SCHEMA = /schema\.generated\.tsx?$/;
|
|
27
|
+
function rel(ctx, p) {
|
|
28
|
+
try {
|
|
29
|
+
return relative(ctx.cwd, p) || p;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const handWrittenMigration = {
|
|
36
|
+
id: 'hand-written-migration',
|
|
37
|
+
tier: 'framework',
|
|
38
|
+
severity: 'deny',
|
|
39
|
+
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 — protected stages take `db:plan` → `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 — and generated artifacts (schema.generated.ts) are never edited.',
|
|
40
|
+
conform: 'edit db/models/ then `everystack db:sync` (dev) or `db:plan` → `db:apply` (protected) · db/sql/ for derived objects · db/backfills/ for data moves',
|
|
41
|
+
verify: 'everystack db:check passes (declared state composes; generated artifacts match regeneration) AND everystack db:fingerprint reports MATCH',
|
|
42
|
+
detect(ctx) {
|
|
43
|
+
if (ctx.tool !== 'Write' && ctx.tool !== 'Edit')
|
|
44
|
+
return null;
|
|
45
|
+
const p = ctx.filePath;
|
|
46
|
+
if (!p)
|
|
47
|
+
return null;
|
|
48
|
+
if (MIGRATION_SQL.test(p))
|
|
49
|
+
return `${rel(ctx, p)} is a SQL migration being written by hand`;
|
|
50
|
+
if (GENERATED_SCHEMA.test(p))
|
|
51
|
+
return `${rel(ctx, p)} is a generated artifact (compiled from your Models — db:check refuses hand edits)`;
|
|
52
|
+
return null;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: a secret value behind an EXPO_PUBLIC_* name.
|
|
3
|
+
*
|
|
4
|
+
* Metro compiles EXPO_PUBLIC_* env vars into the client bundle — public, forever.
|
|
5
|
+
* A DB URL / API key / token behind one of those names is leaked the moment it
|
|
6
|
+
* ships. Reuses the canonical secret-shape patterns from `@everystack/cli/audit`
|
|
7
|
+
* (no duplicated, drift-prone pattern list), loaded lazily behind a cheap
|
|
8
|
+
* pre-filter so the common tool call never pulls the audit graph.
|
|
9
|
+
*/
|
|
10
|
+
import type { CheatGate } from '../types.js';
|
|
11
|
+
export declare const secretInPublicEnv: CheatGate;
|
|
12
|
+
//# sourceMappingURL=secret-in-public-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-in-public-env.d.ts","sourceRoot":"","sources":["../../../src/gates/detectors/secret-in-public-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AAI9D,eAAO,MAAM,iBAAiB,EAAE,SA0B/B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat: a secret value behind an EXPO_PUBLIC_* name.
|
|
3
|
+
*
|
|
4
|
+
* Metro compiles EXPO_PUBLIC_* env vars into the client bundle — public, forever.
|
|
5
|
+
* A DB URL / API key / token behind one of those names is leaked the moment it
|
|
6
|
+
* ships. Reuses the canonical secret-shape patterns from `@everystack/cli/audit`
|
|
7
|
+
* (no duplicated, drift-prone pattern list), loaded lazily behind a cheap
|
|
8
|
+
* pre-filter so the common tool call never pulls the audit graph.
|
|
9
|
+
*/
|
|
10
|
+
const PUBLIC_KEY = /\b(EXPO_PUBLIC_[A-Z0-9_]+)\b/;
|
|
11
|
+
export const secretInPublicEnv = {
|
|
12
|
+
id: 'secret-in-public-env',
|
|
13
|
+
tier: 'framework',
|
|
14
|
+
severity: 'deny',
|
|
15
|
+
guide: 'EXPO_PUBLIC_* is compiled into the client bundle forever — never put a secret behind it. Keep secrets server-side and reference them only on the server.',
|
|
16
|
+
conform: 'everystack secrets set (SSM) / an SST secret, referenced server-side only',
|
|
17
|
+
verify: 'everystack bundle:audit (leak scan) finds no leaked secret',
|
|
18
|
+
async detect(ctx) {
|
|
19
|
+
if (ctx.tool !== 'Write' && ctx.tool !== 'Edit')
|
|
20
|
+
return null;
|
|
21
|
+
const text = ctx.content;
|
|
22
|
+
// Cheap sync pre-filter: only env-ish writes pay for the canonical scanner.
|
|
23
|
+
if (!text || !text.includes('EXPO_PUBLIC_'))
|
|
24
|
+
return null;
|
|
25
|
+
const { SECRET_PATTERNS, isPublicEnvKey } = await import('@everystack/cli/audit');
|
|
26
|
+
for (const line of text.split('\n')) {
|
|
27
|
+
const m = line.match(PUBLIC_KEY);
|
|
28
|
+
if (!m || !isPublicEnvKey(m[1]))
|
|
29
|
+
continue;
|
|
30
|
+
for (const { name, re } of SECRET_PATTERNS) {
|
|
31
|
+
if (re.test(line)) {
|
|
32
|
+
return `${m[1]} carries a ${name} — a secret value compiled into the client bundle`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine — run a set of cheat gates against a tool-call context.
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation (no IO): `evaluateGates` returns findings; `decide` collapses
|
|
5
|
+
* them to an allow/deny decision (deny wins if any `deny`-tier gate hit). The
|
|
6
|
+
* caller (governance/cli.ts) records telemetry and renders the hook payload —
|
|
7
|
+
* keeping detection pure and side effects at the boundary.
|
|
8
|
+
*
|
|
9
|
+
* Fail-OPEN: a detector that throws is skipped, never propagated — a governance
|
|
10
|
+
* bug must not brick tool use.
|
|
11
|
+
*/
|
|
12
|
+
import type { CheatGate, GateFinding, ToolCallContext } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Run every gate's detector against the context; collect findings. Detectors that
|
|
15
|
+
* throw are skipped (fail-open). `detect` may be sync or async — async lets a gate
|
|
16
|
+
* lazy-load a heavier scanner only after a cheap sync pre-filter matches, so the
|
|
17
|
+
* common tool call pays nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function evaluateGates(ctx: ToolCallContext, gates: CheatGate[]): Promise<GateFinding[]>;
|
|
20
|
+
export interface GateDecision {
|
|
21
|
+
decision: 'allow' | 'deny';
|
|
22
|
+
findings: GateFinding[];
|
|
23
|
+
}
|
|
24
|
+
/** Collapse findings to a decision: deny if any deny-tier finding, else allow. */
|
|
25
|
+
export declare function decide(findings: GateFinding[]): GateDecision;
|
|
26
|
+
/** The PreToolUse deny/warn reason: each finding as Guide → Conform → Verify. */
|
|
27
|
+
export declare function findingsReason(findings: GateFinding[]): string;
|
|
28
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/gates/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAsBpG;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,YAAY,CAG5D;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,CAc9D"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine — run a set of cheat gates against a tool-call context.
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation (no IO): `evaluateGates` returns findings; `decide` collapses
|
|
5
|
+
* them to an allow/deny decision (deny wins if any `deny`-tier gate hit). The
|
|
6
|
+
* caller (governance/cli.ts) records telemetry and renders the hook payload —
|
|
7
|
+
* keeping detection pure and side effects at the boundary.
|
|
8
|
+
*
|
|
9
|
+
* Fail-OPEN: a detector that throws is skipped, never propagated — a governance
|
|
10
|
+
* bug must not brick tool use.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Run every gate's detector against the context; collect findings. Detectors that
|
|
14
|
+
* throw are skipped (fail-open). `detect` may be sync or async — async lets a gate
|
|
15
|
+
* lazy-load a heavier scanner only after a cheap sync pre-filter matches, so the
|
|
16
|
+
* common tool call pays nothing.
|
|
17
|
+
*/
|
|
18
|
+
export async function evaluateGates(ctx, gates) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const g of gates) {
|
|
21
|
+
let detail = null;
|
|
22
|
+
try {
|
|
23
|
+
detail = await g.detect(ctx);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
detail = null; // fail open on a buggy detector
|
|
27
|
+
}
|
|
28
|
+
if (detail == null)
|
|
29
|
+
continue;
|
|
30
|
+
findings.push({
|
|
31
|
+
id: g.id,
|
|
32
|
+
tier: g.tier,
|
|
33
|
+
severity: g.severity,
|
|
34
|
+
guide: g.guide,
|
|
35
|
+
conform: g.conform,
|
|
36
|
+
verify: g.verify,
|
|
37
|
+
detail,
|
|
38
|
+
file: ctx.filePath,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return findings;
|
|
42
|
+
}
|
|
43
|
+
/** Collapse findings to a decision: deny if any deny-tier finding, else allow. */
|
|
44
|
+
export function decide(findings) {
|
|
45
|
+
const decision = findings.some((f) => f.severity === 'deny') ? 'deny' : 'allow';
|
|
46
|
+
return { decision, findings };
|
|
47
|
+
}
|
|
48
|
+
/** The PreToolUse deny/warn reason: each finding as Guide → Conform → Verify. */
|
|
49
|
+
export function findingsReason(findings) {
|
|
50
|
+
const lines = [];
|
|
51
|
+
const denies = findings.filter((f) => f.severity === 'deny');
|
|
52
|
+
const warns = findings.filter((f) => f.severity === 'warn');
|
|
53
|
+
if (denies.length) {
|
|
54
|
+
lines.push('OFF-SCRIPT — this cheats the everystack architecture:');
|
|
55
|
+
for (const f of denies)
|
|
56
|
+
lines.push(...renderFinding(f));
|
|
57
|
+
}
|
|
58
|
+
if (warns.length) {
|
|
59
|
+
if (denies.length)
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push('Heads up (advisory):');
|
|
62
|
+
for (const f of warns)
|
|
63
|
+
lines.push(...renderFinding(f));
|
|
64
|
+
}
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
function renderFinding(f) {
|
|
68
|
+
return [
|
|
69
|
+
` • [${f.id}] ${f.detail}`,
|
|
70
|
+
` do this: ${f.guide}`,
|
|
71
|
+
` with: ${f.conform}`,
|
|
72
|
+
` verify: ${f.verify}`,
|
|
73
|
+
];
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cheat-gate registry.
|
|
3
|
+
*
|
|
4
|
+
* Framework tier: on by default for any everystack project — the gates whose
|
|
5
|
+
* Detect is local and whose Verify already ships (db:generate, bundle:audit).
|
|
6
|
+
* Project tier (declared per project via a recipe manifest) lands in a later
|
|
7
|
+
* brick; `gatesFor` is the seam where that composition will happen.
|
|
8
|
+
*/
|
|
9
|
+
import type { CheatGate } from './types.js';
|
|
10
|
+
/** Framework-tier gates, on by default. */
|
|
11
|
+
export declare const FRAMEWORK_GATES: CheatGate[];
|
|
12
|
+
/** The gates that apply to a working directory. Project-tier composition is added later. */
|
|
13
|
+
export declare function gatesFor(_cwd: string): CheatGate[];
|
|
14
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/gates/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,2CAA2C;AAC3C,eAAO,MAAM,eAAe,EAAE,SAAS,EAItC,CAAC;AAEF,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,CAElD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cheat-gate registry.
|
|
3
|
+
*
|
|
4
|
+
* Framework tier: on by default for any everystack project — the gates whose
|
|
5
|
+
* Detect is local and whose Verify already ships (db:generate, bundle:audit).
|
|
6
|
+
* Project tier (declared per project via a recipe manifest) lands in a later
|
|
7
|
+
* brick; `gatesFor` is the seam where that composition will happen.
|
|
8
|
+
*/
|
|
9
|
+
import { handWrittenMigration } from './detectors/hand-written-migration.js';
|
|
10
|
+
import { embeddedDataBundle } from './detectors/embedded-data-bundle.js';
|
|
11
|
+
import { secretInPublicEnv } from './detectors/secret-in-public-env.js';
|
|
12
|
+
/** Framework-tier gates, on by default. */
|
|
13
|
+
export const FRAMEWORK_GATES = [
|
|
14
|
+
handWrittenMigration,
|
|
15
|
+
embeddedDataBundle,
|
|
16
|
+
secretInPublicEnv,
|
|
17
|
+
];
|
|
18
|
+
/** The gates that apply to a working directory. Project-tier composition is added later. */
|
|
19
|
+
export function gatesFor(_cwd) {
|
|
20
|
+
return FRAMEWORK_GATES;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry — the gate is also the sensor.
|
|
3
|
+
*
|
|
4
|
+
* Every cheat finding is appended to a per-session JSONL under
|
|
5
|
+
* ~/.everystack/governance/<session>.jsonl. Pointed at real testers, this file is
|
|
6
|
+
* the off-rails dataset: which cheats the agent attempts, how often, what gets
|
|
7
|
+
* denied vs warned. `report` summarises it (the empirical loop that ranks which
|
|
8
|
+
* gates and generators matter next).
|
|
9
|
+
*
|
|
10
|
+
* Privacy: record rule + path + severity only — never file contents.
|
|
11
|
+
* Fail-OPEN: a telemetry write must never throw into the hook path.
|
|
12
|
+
*/
|
|
13
|
+
import type { GateFinding } from './types.js';
|
|
14
|
+
/** The telemetry directory: ~/.everystack/governance. */
|
|
15
|
+
export declare function telemetryDir(): string;
|
|
16
|
+
export interface TelemetryEvent {
|
|
17
|
+
ts: string;
|
|
18
|
+
session: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
rule: string;
|
|
21
|
+
tier: string;
|
|
22
|
+
severity: string;
|
|
23
|
+
tool: string;
|
|
24
|
+
file?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Append one telemetry line per finding. Decision (deny vs warn) is implicit in
|
|
28
|
+
* `severity`. Never throws.
|
|
29
|
+
*/
|
|
30
|
+
export declare function recordFindings(sessionId: string, ctx: {
|
|
31
|
+
cwd: string;
|
|
32
|
+
tool: string;
|
|
33
|
+
}, findings: GateFinding[]): void;
|
|
34
|
+
/** Read all events for one session, or every session when sessionId is omitted. */
|
|
35
|
+
export declare function readEvents(sessionId?: string): TelemetryEvent[];
|
|
36
|
+
export interface TelemetrySummary {
|
|
37
|
+
total: number;
|
|
38
|
+
denies: number;
|
|
39
|
+
warns: number;
|
|
40
|
+
byRule: Record<string, number>;
|
|
41
|
+
sessions: number;
|
|
42
|
+
}
|
|
43
|
+
/** Tally events into a summary (the `report` payload). */
|
|
44
|
+
export declare function summarize(sessionId?: string): TelemetrySummary;
|
|
45
|
+
/** Human-readable rendering of the summary (the `report` stdout). */
|
|
46
|
+
export declare function renderSummary(s: TelemetrySummary): string;
|
|
47
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/gates/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,yDAAyD;AACzD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAMD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAClC,QAAQ,EAAE,WAAW,EAAE,GACtB,IAAI,CAyBN;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CA2B/D;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAa9D;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAUzD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry — the gate is also the sensor.
|
|
3
|
+
*
|
|
4
|
+
* Every cheat finding is appended to a per-session JSONL under
|
|
5
|
+
* ~/.everystack/governance/<session>.jsonl. Pointed at real testers, this file is
|
|
6
|
+
* the off-rails dataset: which cheats the agent attempts, how often, what gets
|
|
7
|
+
* denied vs warned. `report` summarises it (the empirical loop that ranks which
|
|
8
|
+
* gates and generators matter next).
|
|
9
|
+
*
|
|
10
|
+
* Privacy: record rule + path + severity only — never file contents.
|
|
11
|
+
* Fail-OPEN: a telemetry write must never throw into the hook path.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, appendFileSync, readdirSync, readFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
function home() {
|
|
17
|
+
return process.env.HOME || homedir();
|
|
18
|
+
}
|
|
19
|
+
/** The telemetry directory: ~/.everystack/governance. */
|
|
20
|
+
export function telemetryDir() {
|
|
21
|
+
return join(home(), '.everystack', 'governance');
|
|
22
|
+
}
|
|
23
|
+
function safeId(sessionId) {
|
|
24
|
+
return (sessionId || 'nosession').replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Append one telemetry line per finding. Decision (deny vs warn) is implicit in
|
|
28
|
+
* `severity`. Never throws.
|
|
29
|
+
*/
|
|
30
|
+
export function recordFindings(sessionId, ctx, findings) {
|
|
31
|
+
if (findings.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(telemetryDir(), { recursive: true });
|
|
35
|
+
const path = join(telemetryDir(), `${safeId(sessionId)}.jsonl`);
|
|
36
|
+
const ts = new Date().toISOString();
|
|
37
|
+
const lines = findings
|
|
38
|
+
.map((f) => {
|
|
39
|
+
const ev = {
|
|
40
|
+
ts,
|
|
41
|
+
session: safeId(sessionId),
|
|
42
|
+
cwd: ctx.cwd,
|
|
43
|
+
rule: f.id,
|
|
44
|
+
tier: f.tier,
|
|
45
|
+
severity: f.severity,
|
|
46
|
+
tool: ctx.tool,
|
|
47
|
+
file: f.file,
|
|
48
|
+
};
|
|
49
|
+
return JSON.stringify(ev);
|
|
50
|
+
})
|
|
51
|
+
.join('\n');
|
|
52
|
+
appendFileSync(path, lines + '\n');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* fail open: telemetry never bricks the hook */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Read all events for one session, or every session when sessionId is omitted. */
|
|
59
|
+
export function readEvents(sessionId) {
|
|
60
|
+
const events = [];
|
|
61
|
+
let files;
|
|
62
|
+
try {
|
|
63
|
+
files = readdirSync(telemetryDir()).filter((f) => f.endsWith('.jsonl'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return events;
|
|
67
|
+
}
|
|
68
|
+
const wanted = sessionId ? `${safeId(sessionId)}.jsonl` : null;
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
if (wanted && f !== wanted)
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
const text = readFileSync(join(telemetryDir(), f), 'utf-8');
|
|
74
|
+
for (const line of text.split('\n')) {
|
|
75
|
+
const t = line.trim();
|
|
76
|
+
if (!t)
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
events.push(JSON.parse(t));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* skip a malformed line, keep the rest */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* skip an unreadable file */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return events;
|
|
91
|
+
}
|
|
92
|
+
/** Tally events into a summary (the `report` payload). */
|
|
93
|
+
export function summarize(sessionId) {
|
|
94
|
+
const events = readEvents(sessionId);
|
|
95
|
+
const byRule = {};
|
|
96
|
+
const sessions = new Set();
|
|
97
|
+
let denies = 0;
|
|
98
|
+
let warns = 0;
|
|
99
|
+
for (const e of events) {
|
|
100
|
+
byRule[e.rule] = (byRule[e.rule] ?? 0) + 1;
|
|
101
|
+
sessions.add(e.session);
|
|
102
|
+
if (e.severity === 'deny')
|
|
103
|
+
denies++;
|
|
104
|
+
else if (e.severity === 'warn')
|
|
105
|
+
warns++;
|
|
106
|
+
}
|
|
107
|
+
return { total: events.length, denies, warns, byRule, sessions: sessions.size };
|
|
108
|
+
}
|
|
109
|
+
/** Human-readable rendering of the summary (the `report` stdout). */
|
|
110
|
+
export function renderSummary(s) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`governance telemetry — ${s.total} findings across ${s.sessions} session(s)`);
|
|
113
|
+
lines.push(` ${s.denies} deny · ${s.warns} warn`);
|
|
114
|
+
const ranked = Object.entries(s.byRule).sort((a, b) => b[1] - a[1]);
|
|
115
|
+
if (ranked.length) {
|
|
116
|
+
lines.push(' by rule (most-tripped first):');
|
|
117
|
+
for (const [rule, n] of ranked)
|
|
118
|
+
lines.push(` ${n.toString().padStart(4)} ${rule}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat-gate types — the atomic unit of governance.
|
|
3
|
+
*
|
|
4
|
+
* A cheat gate catches the agent *cheating the architecture*: taking the
|
|
5
|
+
* training-consensus shortcut (bundle the JSON, hand-write the migration, leak a
|
|
6
|
+
* secret) that the maintainer would veto on sight. Anatomy, fixed:
|
|
7
|
+
*
|
|
8
|
+
* Detect → Block → Guide → Conform → Verify
|
|
9
|
+
*
|
|
10
|
+
* Governing rule (see docs/plans/everystack-cheat-gates.md): no gate ships without
|
|
11
|
+
* Guide + Conform + Verify. If you can't name what to do instead, the tool that
|
|
12
|
+
* does it, and the check that proves it, the rule is grounding — not a gate.
|
|
13
|
+
*/
|
|
14
|
+
export type GateSeverity = 'deny' | 'warn';
|
|
15
|
+
export type GateTier = 'framework' | 'project';
|
|
16
|
+
/**
|
|
17
|
+
* What a detector inspects at the tool-call boundary, normalised from hook JSON
|
|
18
|
+
* (PreToolUse/PostToolUse) or synthesised for a tree scan. Pure data.
|
|
19
|
+
*/
|
|
20
|
+
export interface ToolCallContext {
|
|
21
|
+
/** The tool about to run / that ran: 'Write' | 'Edit' | 'Bash' | 'Read' | … */
|
|
22
|
+
tool: string;
|
|
23
|
+
/** Target path for Write/Edit/Read. */
|
|
24
|
+
filePath?: string;
|
|
25
|
+
/** Proposed file content (Write) or the new text (Edit) — what is about to land. */
|
|
26
|
+
content?: string;
|
|
27
|
+
/** Command line for Bash. */
|
|
28
|
+
command?: string;
|
|
29
|
+
/** Working directory of the session. */
|
|
30
|
+
cwd: string;
|
|
31
|
+
}
|
|
32
|
+
/** A single cheat detected — carries its own remediation and proof. */
|
|
33
|
+
export interface GateFinding {
|
|
34
|
+
id: string;
|
|
35
|
+
tier: GateTier;
|
|
36
|
+
severity: GateSeverity;
|
|
37
|
+
/** One imperative sentence: what to do instead. */
|
|
38
|
+
guide: string;
|
|
39
|
+
/** The sanctioned tool/command/generator that does it right. */
|
|
40
|
+
conform: string;
|
|
41
|
+
/** The runnable check that proves the result is clean. */
|
|
42
|
+
verify: string;
|
|
43
|
+
/** Specifics of this hit (e.g. "assets/data.json is 184.0 MB"). */
|
|
44
|
+
detail: string;
|
|
45
|
+
/** The file the cheat lives in, when applicable. */
|
|
46
|
+
file?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A cheat gate. `detect` is pure + synchronous: it returns a detail string when
|
|
50
|
+
* the cheat is present in the given context, or null when the context is clean.
|
|
51
|
+
* The Guide/Conform/Verify fields are static metadata copied onto every finding.
|
|
52
|
+
*/
|
|
53
|
+
export interface CheatGate {
|
|
54
|
+
id: string;
|
|
55
|
+
tier: GateTier;
|
|
56
|
+
severity: GateSeverity;
|
|
57
|
+
guide: string;
|
|
58
|
+
conform: string;
|
|
59
|
+
verify: string;
|
|
60
|
+
/**
|
|
61
|
+
* Returns a detail string when the cheat is present, else null. May be sync or
|
|
62
|
+
* async — keep the common path sync and cheap; use async only to lazy-load a
|
|
63
|
+
* heavier scanner *after* a cheap pre-filter matches.
|
|
64
|
+
*/
|
|
65
|
+
detect(ctx: ToolCallContext): string | null | Promise<string | null>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/gates/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3C,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACtE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheat-gate types — the atomic unit of governance.
|
|
3
|
+
*
|
|
4
|
+
* A cheat gate catches the agent *cheating the architecture*: taking the
|
|
5
|
+
* training-consensus shortcut (bundle the JSON, hand-write the migration, leak a
|
|
6
|
+
* secret) that the maintainer would veto on sight. Anatomy, fixed:
|
|
7
|
+
*
|
|
8
|
+
* Detect → Block → Guide → Conform → Verify
|
|
9
|
+
*
|
|
10
|
+
* Governing rule (see docs/plans/everystack-cheat-gates.md): no gate ships without
|
|
11
|
+
* Guide + Conform + Verify. If you can't name what to do instead, the tool that
|
|
12
|
+
* does it, and the check that proves it, the rule is grounding — not a gate.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|