@devrik-tools/claude-gates 0.4.0 → 0.7.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/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +39 -4
- package/README.md +34 -5
- package/cli/config.mjs +126 -124
- package/cli/init.mjs +303 -276
- package/cli/install.mjs +281 -175
- package/cli/materialize.mjs +103 -102
- package/cli/registry.mjs +139 -136
- package/cli/smoke-fixtures.json +65 -0
- package/cli/task.mjs +140 -140
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/ask-adoption.mjs +147 -147
- package/plugins/gates/hooks/doctor.mjs +207 -207
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
- package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
- package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
- package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
- package/plugins/gates/hooks/hooks.json +61 -0
- package/plugins/gates/hooks/lib/config.mjs +179 -172
- package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
- package/plugins/gates/hooks/lib/signals.mjs +172 -127
- package/plugins/gates/hooks/wiring-check.mjs +227 -227
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/hooks.json +26 -26
- package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
- package/plugins/tasks/hooks/register-requests.mjs +145 -145
- package/plugins/tasks/hooks/session-tasks.mjs +108 -108
- package/registry.json +192 -1
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
// forge-flow — the enforcer forge cannot be. A forge MCP directs a pipeline but cannot
|
|
2
|
-
// intercept Edit/Write/Bash, so nothing makes you actually use it. This gate closes that
|
|
3
|
-
// hole: in a project that ADOPTED forge, a code-mutating action is denied unless there is
|
|
4
|
-
// an active forge run for this project — so every change goes through the pipeline and you
|
|
5
|
-
// always know which phase you are in.
|
|
6
|
-
//
|
|
7
|
-
// justification: no existing tool covers this. A forge MCP audit (see memory
|
|
8
|
-
// forge-mcp-auditoria-flujo) confirmed the structural hole: an MCP cannot gate other tools;
|
|
9
|
-
// only a Claude Code PreToolUse hook can. This is that hook.
|
|
10
|
-
//
|
|
11
|
-
// ── When it acts (never surprises you) ──────────────────────────────────────────────
|
|
12
|
-
// Only when the project adopted forge — a marker on disk (.ai/forge.json, or `forge: true`
|
|
13
|
-
// in .ai/config.json). In any other project it stays silent. Off by default in the
|
|
14
|
-
// registry, so it never fires unless a project turns it on.
|
|
15
|
-
//
|
|
16
|
-
// ── How it checks (deterministic, no judgment) ──────────────────────────────────────
|
|
17
|
-
// forge persists its runs in a SQLite DB (global by default). This gate reads that DB with
|
|
18
|
-
// node:sqlite (a Node built-in — no npm, contract intact) and looks for an active run whose
|
|
19
|
-
// cwd matches this project. Absent → deny with an actionable message. The DB unreadable or
|
|
20
|
-
// node:sqlite unavailable → allow (fail open: a broken lookup must not block all work).
|
|
21
|
-
|
|
22
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
|
-
import { dirname, join } from 'node:path';
|
|
25
|
-
import { runGate, deny, warn, toolInGroups } from '../../lib/hook-io.mjs';
|
|
26
|
-
|
|
27
|
-
const GATE_ID = 'forge-flow';
|
|
28
|
-
const CONFIG_KEY = 'requireForgeRunToEdit';
|
|
29
|
-
|
|
30
|
-
// Any code-mutating surface: native write/shell AND their MCP equivalents. `execution`
|
|
31
|
-
// already unions write+shell+mcp__ide__executeCode, and toolInGroups adds the mcp__* signal
|
|
32
|
-
// match — so an MCP filesystem-write or shell-exec tool no longer slips past the enforcer.
|
|
33
|
-
const ACTING_GROUPS = ['execution'];
|
|
34
|
-
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
35
|
-
const DEFAULT_FORGE_DB = join('.forge', 'forge-mcp.db');
|
|
36
|
-
const FORGE_MARKER_FILE = join('.ai', 'forge.json');
|
|
37
|
-
const PROJECT_CONFIG_FILE = join('.ai', 'config.json');
|
|
38
|
-
|
|
39
|
-
function projectRootOf(startDirectory) {
|
|
40
|
-
let current = startDirectory;
|
|
41
|
-
while (true) {
|
|
42
|
-
if (
|
|
43
|
-
PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
|
|
44
|
-
) {
|
|
45
|
-
return current;
|
|
46
|
-
}
|
|
47
|
-
const parent = dirname(current);
|
|
48
|
-
if (parent === current) return null;
|
|
49
|
-
current = parent;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readJson(path) {
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** True when this project opted into forge: a marker file, or forge:true in .ai/config.json. */
|
|
62
|
-
function projectAdoptedForge(root) {
|
|
63
|
-
if (existsSync(join(root, FORGE_MARKER_FILE))) return true;
|
|
64
|
-
const config = readJson(join(root, PROJECT_CONFIG_FILE));
|
|
65
|
-
return config?.forge === true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* True when forge has an active run whose cwd is this project. Reads forge's SQLite DB with
|
|
70
|
-
* node:sqlite (imported dynamically so an older Node without it degrades instead of
|
|
71
|
-
* throwing). Any failure — module missing, DB absent, locked, unreadable, schema drift —
|
|
72
|
-
* returns true: the gate must fail OPEN, since a broken lookup must never block every edit.
|
|
73
|
-
*/
|
|
74
|
-
// Returns one of three verdicts, so a broken lookup no longer masquerades as "run present":
|
|
75
|
-
// { state: 'active' } → a run for this project exists; allow.
|
|
76
|
-
// { state: 'none' } → DB readable, no active run for this project; deny.
|
|
77
|
-
// { state: 'unknown', reason } → DB absent/locked/corrupt/schema-drift, or node:sqlite
|
|
78
|
-
// missing; we cannot tell. Warn (visible) but allow, so a
|
|
79
|
-
// broken DB never blocks all work AND never disables the
|
|
80
|
-
// enforcer silently — the earlier code returned true here,
|
|
81
|
-
// which looked identical to "run present".
|
|
82
|
-
async function forgeRunState(root, forgeDatabasePath) {
|
|
83
|
-
if (!existsSync(forgeDatabasePath)) return { state: 'none' }; // no DB yet → no runs → deny
|
|
84
|
-
try {
|
|
85
|
-
const { DatabaseSync } = await import('node:sqlite');
|
|
86
|
-
const database = new DatabaseSync(forgeDatabasePath, { readOnly: true });
|
|
87
|
-
const rows = database
|
|
88
|
-
.prepare("SELECT cwd FROM runs WHERE status = 'active'")
|
|
89
|
-
.all();
|
|
90
|
-
database.close();
|
|
91
|
-
return rows.some((row) => String(row.cwd) === root)
|
|
92
|
-
? { state: 'active' }
|
|
93
|
-
: { state: 'none' };
|
|
94
|
-
} catch (error) {
|
|
95
|
-
return { state: 'unknown', reason: error?.message ?? String(error) };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const DENY_MESSAGE =
|
|
100
|
-
'This project uses forge, but there is no active forge run for it. Every change should ' +
|
|
101
|
-
'go through the pipeline so the next step is always clear. Start or resume a run ' +
|
|
102
|
-
'(forge_start / forge_next) before editing — that is how forge tells you which phase ' +
|
|
103
|
-
'you are in. To work outside the pipeline, turn this gate off in .ai/config.json.';
|
|
104
|
-
|
|
105
|
-
runGate(
|
|
106
|
-
{
|
|
107
|
-
id: GATE_ID,
|
|
108
|
-
configKey: CONFIG_KEY,
|
|
109
|
-
enabledByDefault: false,
|
|
110
|
-
defaultParams: { forgeDatabasePath: join(homedir(), DEFAULT_FORGE_DB) },
|
|
111
|
-
},
|
|
112
|
-
async ({ toolName, parameters }) => {
|
|
113
|
-
if (!toolInGroups(toolName, ACTING_GROUPS)) return;
|
|
114
|
-
|
|
115
|
-
const root = projectRootOf(process.cwd());
|
|
116
|
-
if (!root) return; // no project
|
|
117
|
-
if (!projectAdoptedForge(root)) return; // project did not opt into forge
|
|
118
|
-
|
|
119
|
-
const forgeDatabasePath =
|
|
120
|
-
parameters.forgeDatabasePath ?? join(homedir(), DEFAULT_FORGE_DB);
|
|
121
|
-
const result = await forgeRunState(root, forgeDatabasePath);
|
|
122
|
-
|
|
123
|
-
if (result.state === 'active') return; // pipeline is running → allow
|
|
124
|
-
if (result.state === 'none') deny(GATE_ID, DENY_MESSAGE); // no run → block
|
|
125
|
-
// state 'unknown': the DB could not be read. Allow so a broken lookup never freezes work,
|
|
126
|
-
// but surface it loudly — a silent allow here would disable the enforcer without a trace.
|
|
127
|
-
warn(
|
|
128
|
-
GATE_ID,
|
|
129
|
-
`forge enforcement is degraded: the forge DB could not be read (${result.reason}). ` +
|
|
130
|
-
'Allowing this action, but the pipeline is NOT being enforced. Check that forge is ' +
|
|
131
|
-
`installed and ${forgeDatabasePath} is readable, or turn this gate off if intended.`,
|
|
132
|
-
);
|
|
133
|
-
},
|
|
134
|
-
);
|
|
1
|
+
// forge-flow — the enforcer forge cannot be. A forge MCP directs a pipeline but cannot
|
|
2
|
+
// intercept Edit/Write/Bash, so nothing makes you actually use it. This gate closes that
|
|
3
|
+
// hole: in a project that ADOPTED forge, a code-mutating action is denied unless there is
|
|
4
|
+
// an active forge run for this project — so every change goes through the pipeline and you
|
|
5
|
+
// always know which phase you are in.
|
|
6
|
+
//
|
|
7
|
+
// justification: no existing tool covers this. A forge MCP audit (see memory
|
|
8
|
+
// forge-mcp-auditoria-flujo) confirmed the structural hole: an MCP cannot gate other tools;
|
|
9
|
+
// only a Claude Code PreToolUse hook can. This is that hook.
|
|
10
|
+
//
|
|
11
|
+
// ── When it acts (never surprises you) ──────────────────────────────────────────────
|
|
12
|
+
// Only when the project adopted forge — a marker on disk (.ai/forge.json, or `forge: true`
|
|
13
|
+
// in .ai/config.json). In any other project it stays silent. Off by default in the
|
|
14
|
+
// registry, so it never fires unless a project turns it on.
|
|
15
|
+
//
|
|
16
|
+
// ── How it checks (deterministic, no judgment) ──────────────────────────────────────
|
|
17
|
+
// forge persists its runs in a SQLite DB (global by default). This gate reads that DB with
|
|
18
|
+
// node:sqlite (a Node built-in — no npm, contract intact) and looks for an active run whose
|
|
19
|
+
// cwd matches this project. Absent → deny with an actionable message. The DB unreadable or
|
|
20
|
+
// node:sqlite unavailable → allow (fail open: a broken lookup must not block all work).
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { runGate, deny, warn, toolInGroups } from '../../lib/hook-io.mjs';
|
|
26
|
+
|
|
27
|
+
const GATE_ID = 'forge-flow';
|
|
28
|
+
const CONFIG_KEY = 'requireForgeRunToEdit';
|
|
29
|
+
|
|
30
|
+
// Any code-mutating surface: native write/shell AND their MCP equivalents. `execution`
|
|
31
|
+
// already unions write+shell+mcp__ide__executeCode, and toolInGroups adds the mcp__* signal
|
|
32
|
+
// match — so an MCP filesystem-write or shell-exec tool no longer slips past the enforcer.
|
|
33
|
+
const ACTING_GROUPS = ['execution'];
|
|
34
|
+
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
35
|
+
const DEFAULT_FORGE_DB = join('.forge', 'forge-mcp.db');
|
|
36
|
+
const FORGE_MARKER_FILE = join('.ai', 'forge.json');
|
|
37
|
+
const PROJECT_CONFIG_FILE = join('.ai', 'config.json');
|
|
38
|
+
|
|
39
|
+
function projectRootOf(startDirectory) {
|
|
40
|
+
let current = startDirectory;
|
|
41
|
+
while (true) {
|
|
42
|
+
if (
|
|
43
|
+
PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
|
|
44
|
+
) {
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
const parent = dirname(current);
|
|
48
|
+
if (parent === current) return null;
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJson(path) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when this project opted into forge: a marker file, or forge:true in .ai/config.json. */
|
|
62
|
+
function projectAdoptedForge(root) {
|
|
63
|
+
if (existsSync(join(root, FORGE_MARKER_FILE))) return true;
|
|
64
|
+
const config = readJson(join(root, PROJECT_CONFIG_FILE));
|
|
65
|
+
return config?.forge === true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True when forge has an active run whose cwd is this project. Reads forge's SQLite DB with
|
|
70
|
+
* node:sqlite (imported dynamically so an older Node without it degrades instead of
|
|
71
|
+
* throwing). Any failure — module missing, DB absent, locked, unreadable, schema drift —
|
|
72
|
+
* returns true: the gate must fail OPEN, since a broken lookup must never block every edit.
|
|
73
|
+
*/
|
|
74
|
+
// Returns one of three verdicts, so a broken lookup no longer masquerades as "run present":
|
|
75
|
+
// { state: 'active' } → a run for this project exists; allow.
|
|
76
|
+
// { state: 'none' } → DB readable, no active run for this project; deny.
|
|
77
|
+
// { state: 'unknown', reason } → DB absent/locked/corrupt/schema-drift, or node:sqlite
|
|
78
|
+
// missing; we cannot tell. Warn (visible) but allow, so a
|
|
79
|
+
// broken DB never blocks all work AND never disables the
|
|
80
|
+
// enforcer silently — the earlier code returned true here,
|
|
81
|
+
// which looked identical to "run present".
|
|
82
|
+
async function forgeRunState(root, forgeDatabasePath) {
|
|
83
|
+
if (!existsSync(forgeDatabasePath)) return { state: 'none' }; // no DB yet → no runs → deny
|
|
84
|
+
try {
|
|
85
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
86
|
+
const database = new DatabaseSync(forgeDatabasePath, { readOnly: true });
|
|
87
|
+
const rows = database
|
|
88
|
+
.prepare("SELECT cwd FROM runs WHERE status = 'active'")
|
|
89
|
+
.all();
|
|
90
|
+
database.close();
|
|
91
|
+
return rows.some((row) => String(row.cwd) === root)
|
|
92
|
+
? { state: 'active' }
|
|
93
|
+
: { state: 'none' };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return { state: 'unknown', reason: error?.message ?? String(error) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const DENY_MESSAGE =
|
|
100
|
+
'This project uses forge, but there is no active forge run for it. Every change should ' +
|
|
101
|
+
'go through the pipeline so the next step is always clear. Start or resume a run ' +
|
|
102
|
+
'(forge_start / forge_next) before editing — that is how forge tells you which phase ' +
|
|
103
|
+
'you are in. To work outside the pipeline, turn this gate off in .ai/config.json.';
|
|
104
|
+
|
|
105
|
+
runGate(
|
|
106
|
+
{
|
|
107
|
+
id: GATE_ID,
|
|
108
|
+
configKey: CONFIG_KEY,
|
|
109
|
+
enabledByDefault: false,
|
|
110
|
+
defaultParams: { forgeDatabasePath: join(homedir(), DEFAULT_FORGE_DB) },
|
|
111
|
+
},
|
|
112
|
+
async ({ toolName, parameters }) => {
|
|
113
|
+
if (!toolInGroups(toolName, ACTING_GROUPS)) return;
|
|
114
|
+
|
|
115
|
+
const root = projectRootOf(process.cwd());
|
|
116
|
+
if (!root) return; // no project
|
|
117
|
+
if (!projectAdoptedForge(root)) return; // project did not opt into forge
|
|
118
|
+
|
|
119
|
+
const forgeDatabasePath =
|
|
120
|
+
parameters.forgeDatabasePath ?? join(homedir(), DEFAULT_FORGE_DB);
|
|
121
|
+
const result = await forgeRunState(root, forgeDatabasePath);
|
|
122
|
+
|
|
123
|
+
if (result.state === 'active') return; // pipeline is running → allow
|
|
124
|
+
if (result.state === 'none') deny(GATE_ID, DENY_MESSAGE); // no run → block
|
|
125
|
+
// state 'unknown': the DB could not be read. Allow so a broken lookup never freezes work,
|
|
126
|
+
// but surface it loudly — a silent allow here would disable the enforcer without a trace.
|
|
127
|
+
warn(
|
|
128
|
+
GATE_ID,
|
|
129
|
+
`forge enforcement is degraded: the forge DB could not be read (${result.reason}). ` +
|
|
130
|
+
'Allowing this action, but the pipeline is NOT being enforced. Check that forge is ' +
|
|
131
|
+
`installed and ${forgeDatabasePath} is readable, or turn this gate off if intended.`,
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
// implementation-pipeline — denies a delegation that sends a builder subagent straight
|
|
2
|
-
// to writing code without the prompt declaring the pipeline stages around it:
|
|
3
|
-
// definition (where the context came from), writing's own verification plan, and a
|
|
4
|
-
// separate reviewer for validation/QA/closure. Migrated from
|
|
5
|
-
// ~/.claude/hooks/guard-pipeline-de-implementacion.mjs.
|
|
6
|
-
//
|
|
7
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
-
// builderSubagents subagent types that count as builders — the ones this gate
|
|
9
|
-
// holds to the pipeline. Replaces the built-in list wholesale.
|
|
10
|
-
// exemptSubagents subagent types exempt outright: read-only pipeline stages that
|
|
11
|
-
// cannot be required to declare a pipeline around themselves.
|
|
12
|
-
// Replaces the built-in list wholesale.
|
|
13
|
-
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
14
|
-
// its override replaces.
|
|
15
|
-
//
|
|
16
|
-
// ── Off by default, and quiet outside its narrow trigger ───────────────────────────
|
|
17
|
-
// This gate only evaluates a delegation whose prompt declares a STANDARD/HIGH-RISK
|
|
18
|
-
// level, uses an implementation verb, targets a builder subagent, and is not about the
|
|
19
|
-
// harness itself. A prompt that only describes work, or that is read-only/exploratory,
|
|
20
|
-
// never reaches the check.
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
runGate,
|
|
24
|
-
deny,
|
|
25
|
-
toolInGroups,
|
|
26
|
-
delegationPromptOf,
|
|
27
|
-
} from '../../lib/hook-io.mjs';
|
|
28
|
-
|
|
29
|
-
const GATE_ID = 'implementation-pipeline';
|
|
30
|
-
const CONFIG_KEY = 'requireImplementationPipeline';
|
|
31
|
-
|
|
32
|
-
const DEFAULT_BUILDER_SUBAGENTS = [
|
|
33
|
-
'frontend',
|
|
34
|
-
'backend',
|
|
35
|
-
'worker',
|
|
36
|
-
'worker-senior',
|
|
37
|
-
'general-purpose',
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const DEFAULT_EXEMPT_SUBAGENTS = [
|
|
41
|
-
'scout',
|
|
42
|
-
'explore',
|
|
43
|
-
'plan',
|
|
44
|
-
'revision',
|
|
45
|
-
'contraste',
|
|
46
|
-
'test-planner',
|
|
47
|
-
'qa',
|
|
48
|
-
'ui',
|
|
49
|
-
'ux',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
function withWordBoundary(alternation) {
|
|
53
|
-
return new RegExp(
|
|
54
|
-
`(?:^|[^\\p{L}\\p{N}_])(?:${alternation})(?:[^\\p{L}\\p{N}_]|$)`,
|
|
55
|
-
'iu',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const IMPLEMENTATION_VERBS = withWordBoundary(
|
|
60
|
-
'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
|
|
61
|
-
'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
|
|
62
|
-
'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
|
|
63
|
-
'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
|
|
64
|
-
'escrib(í|e)|escribir|implement\\w*|writ(?:e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
|
|
65
|
-
'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
/** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
|
|
69
|
-
* the plugin-wide convention (see risk-level.mjs). */
|
|
70
|
-
const DEMANDING_LEVEL_PATTERN =
|
|
71
|
-
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
|
|
72
|
-
const EXEMPT_LEVEL_PATTERN =
|
|
73
|
-
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
|
|
74
|
-
const HIGH_RISK_PATTERN =
|
|
75
|
-
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\bHIGH-RISK\b/iu;
|
|
76
|
-
|
|
77
|
-
const HARNESS_PATTERN =
|
|
78
|
-
/(\.claude[\\/]|hooks[\\/]|plugins[\\/]gates|settings\.json|[\\/]agents[\\/]\w|[\\/]rules[\\/]\w|[\\/]skills[\\/]\w)/i;
|
|
79
|
-
|
|
80
|
-
// Several small patterns instead of one long alternation: a single big regex here
|
|
81
|
-
// tripped the linter's regex-complexity limit. Any one matching counts as declared.
|
|
82
|
-
const REVIEWER_NAME_PATTERN =
|
|
83
|
-
/\b(reviewer|revisor|separate review|revisi[oó]n separada|contraste|auditor)\b/i;
|
|
84
|
-
const REVIEWER_LABEL_PATTERN = /(review|revisi[oó]n)\s*:/i;
|
|
85
|
-
const REVIEWER_ACTION_EN_PATTERN =
|
|
86
|
-
/\breview\s+(?:the\s+)?(?:diff|result|work)\b/i;
|
|
87
|
-
const REVIEWER_ACTION_ES_PATTERN =
|
|
88
|
-
/\brevisa\s+(?:el\s+)?(?:diff|resultado|trabajo)\b/i;
|
|
89
|
-
const REVIEWER_CODE_REVIEW_PATTERN = /code.review/i;
|
|
90
|
-
|
|
91
|
-
function reviewerStageDeclared(prompt) {
|
|
92
|
-
return (
|
|
93
|
-
REVIEWER_NAME_PATTERN.test(prompt) ||
|
|
94
|
-
REVIEWER_LABEL_PATTERN.test(prompt) ||
|
|
95
|
-
REVIEWER_ACTION_EN_PATTERN.test(prompt) ||
|
|
96
|
-
REVIEWER_ACTION_ES_PATTERN.test(prompt) ||
|
|
97
|
-
REVIEWER_CODE_REVIEW_PATTERN.test(prompt)
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const DEFINITION_PATTERN =
|
|
102
|
-
/\b(scout|reconnaissance|reconocimiento|prior exploration|explora(?:ci[oó]n)? previa|verified context|contexto verificado|asserts\.md|brief\.md)\b/i;
|
|
103
|
-
const WRITING_PATTERN =
|
|
104
|
-
/\b(test|tests|prueba|pruebas|vitest|jest|playwright|typecheck|lint|verification|verificaci[oó]n|verification criteria)\b/i;
|
|
105
|
-
|
|
106
|
-
/** The three pipeline stages this gate requires be declared, in this order. */
|
|
107
|
-
const REQUIRED_STAGES = [
|
|
108
|
-
{
|
|
109
|
-
id: 'definition',
|
|
110
|
-
isDeclared: (prompt) => DEFINITION_PATTERN.test(prompt),
|
|
111
|
-
missing:
|
|
112
|
-
'DEFINITION: does not declare where the context came from (a prior scout, read-only exploration, or an already-written asserts/brief).',
|
|
113
|
-
line: 'Verified context: <path to the cited asserts/brief> — or: a prior scout ran, findings at <path>.',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: 'writing',
|
|
117
|
-
isDeclared: (prompt) => WRITING_PATTERN.test(prompt),
|
|
118
|
-
missing:
|
|
119
|
-
'WRITING: does not declare what is verified nor with what command. A builder does not validate its own work without a criterion written beforehand.',
|
|
120
|
-
line: 'Verification: <exact command> and what is expected. Anything that cannot run is reported OMITTED, never PASS.',
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: 'reviewer',
|
|
124
|
-
isDeclared: reviewerStageDeclared,
|
|
125
|
-
missing:
|
|
126
|
-
'REVIEWER: does not declare who reviews the result. Whoever implements does not validate their own work.',
|
|
127
|
-
line: 'Review: the coordinator reviews the diff and the evidence before closing — or: a read-only `revision`/`contraste` agent reviews it.',
|
|
128
|
-
},
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
function isExempt(toolInput, prompt, builderSubagents, exemptSubagents) {
|
|
132
|
-
const subagentType = String(
|
|
133
|
-
toolInput.subagent_type ?? toolInput.subagentType ?? '',
|
|
134
|
-
).toLowerCase();
|
|
135
|
-
if (exemptSubagents.includes(subagentType)) return true;
|
|
136
|
-
// An unknown type is not assumed to be a builder: the safe side here is not to
|
|
137
|
-
// invent requirements for agents this gate cannot classify.
|
|
138
|
-
if (subagentType && !builderSubagents.includes(subagentType)) return true;
|
|
139
|
-
if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
|
|
140
|
-
if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
|
|
141
|
-
if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
|
|
142
|
-
if (HARNESS_PATTERN.test(prompt)) return true;
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
runGate(
|
|
147
|
-
{
|
|
148
|
-
id: GATE_ID,
|
|
149
|
-
configKey: CONFIG_KEY,
|
|
150
|
-
enabledByDefault: false,
|
|
151
|
-
defaultParams: {
|
|
152
|
-
builderSubagents: DEFAULT_BUILDER_SUBAGENTS,
|
|
153
|
-
exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
({ toolName, toolInput, parameters }) => {
|
|
157
|
-
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
158
|
-
|
|
159
|
-
const prompt = delegationPromptOf(toolInput);
|
|
160
|
-
if (!prompt.trim()) return;
|
|
161
|
-
|
|
162
|
-
const builderSubagents =
|
|
163
|
-
parameters.builderSubagents ?? DEFAULT_BUILDER_SUBAGENTS;
|
|
164
|
-
const exemptSubagents =
|
|
165
|
-
parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
|
|
166
|
-
if (isExempt(toolInput, prompt, builderSubagents, exemptSubagents)) return;
|
|
167
|
-
|
|
168
|
-
const missingStages = REQUIRED_STAGES.filter(
|
|
169
|
-
(stage) => !stage.isDeclared(prompt),
|
|
170
|
-
);
|
|
171
|
-
if (missingStages.length === 0) return;
|
|
172
|
-
|
|
173
|
-
const isHighRisk = HIGH_RISK_PATTERN.test(prompt);
|
|
174
|
-
const details = missingStages.map((stage) => stage.missing).join(' ');
|
|
175
|
-
const lines = missingStages.map((stage) => ` ${stage.line}`).join('\n');
|
|
176
|
-
|
|
177
|
-
deny(
|
|
178
|
-
GATE_ID,
|
|
179
|
-
`This delegation is going to build code but skips ${missingStages.length} pipeline ` +
|
|
180
|
-
`stage(s). ${details} ${
|
|
181
|
-
isHighRisk
|
|
182
|
-
? 'In HIGH-RISK a separate reviewer is not negotiable. '
|
|
183
|
-
: ''
|
|
184
|
-
}PASTE THESE LINES INTO THE PROMPT AND RELAUNCH:\n${lines}`,
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
);
|
|
1
|
+
// implementation-pipeline — denies a delegation that sends a builder subagent straight
|
|
2
|
+
// to writing code without the prompt declaring the pipeline stages around it:
|
|
3
|
+
// definition (where the context came from), writing's own verification plan, and a
|
|
4
|
+
// separate reviewer for validation/QA/closure. Migrated from
|
|
5
|
+
// ~/.claude/hooks/guard-pipeline-de-implementacion.mjs.
|
|
6
|
+
//
|
|
7
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
+
// builderSubagents subagent types that count as builders — the ones this gate
|
|
9
|
+
// holds to the pipeline. Replaces the built-in list wholesale.
|
|
10
|
+
// exemptSubagents subagent types exempt outright: read-only pipeline stages that
|
|
11
|
+
// cannot be required to declare a pipeline around themselves.
|
|
12
|
+
// Replaces the built-in list wholesale.
|
|
13
|
+
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
14
|
+
// its override replaces.
|
|
15
|
+
//
|
|
16
|
+
// ── Off by default, and quiet outside its narrow trigger ───────────────────────────
|
|
17
|
+
// This gate only evaluates a delegation whose prompt declares a STANDARD/HIGH-RISK
|
|
18
|
+
// level, uses an implementation verb, targets a builder subagent, and is not about the
|
|
19
|
+
// harness itself. A prompt that only describes work, or that is read-only/exploratory,
|
|
20
|
+
// never reaches the check.
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
runGate,
|
|
24
|
+
deny,
|
|
25
|
+
toolInGroups,
|
|
26
|
+
delegationPromptOf,
|
|
27
|
+
} from '../../lib/hook-io.mjs';
|
|
28
|
+
|
|
29
|
+
const GATE_ID = 'implementation-pipeline';
|
|
30
|
+
const CONFIG_KEY = 'requireImplementationPipeline';
|
|
31
|
+
|
|
32
|
+
const DEFAULT_BUILDER_SUBAGENTS = [
|
|
33
|
+
'frontend',
|
|
34
|
+
'backend',
|
|
35
|
+
'worker',
|
|
36
|
+
'worker-senior',
|
|
37
|
+
'general-purpose',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const DEFAULT_EXEMPT_SUBAGENTS = [
|
|
41
|
+
'scout',
|
|
42
|
+
'explore',
|
|
43
|
+
'plan',
|
|
44
|
+
'revision',
|
|
45
|
+
'contraste',
|
|
46
|
+
'test-planner',
|
|
47
|
+
'qa',
|
|
48
|
+
'ui',
|
|
49
|
+
'ux',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function withWordBoundary(alternation) {
|
|
53
|
+
return new RegExp(
|
|
54
|
+
`(?:^|[^\\p{L}\\p{N}_])(?:${alternation})(?:[^\\p{L}\\p{N}_]|$)`,
|
|
55
|
+
'iu',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const IMPLEMENTATION_VERBS = withWordBoundary(
|
|
60
|
+
'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
|
|
61
|
+
'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
|
|
62
|
+
'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
|
|
63
|
+
'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
|
|
64
|
+
'escrib(í|e)|escribir|implement\\w*|writ(?:e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
|
|
65
|
+
'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
|
|
69
|
+
* the plugin-wide convention (see risk-level.mjs). */
|
|
70
|
+
const DEMANDING_LEVEL_PATTERN =
|
|
71
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
|
|
72
|
+
const EXEMPT_LEVEL_PATTERN =
|
|
73
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
|
|
74
|
+
const HIGH_RISK_PATTERN =
|
|
75
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\bHIGH-RISK\b/iu;
|
|
76
|
+
|
|
77
|
+
const HARNESS_PATTERN =
|
|
78
|
+
/(\.claude[\\/]|hooks[\\/]|plugins[\\/]gates|settings\.json|[\\/]agents[\\/]\w|[\\/]rules[\\/]\w|[\\/]skills[\\/]\w)/i;
|
|
79
|
+
|
|
80
|
+
// Several small patterns instead of one long alternation: a single big regex here
|
|
81
|
+
// tripped the linter's regex-complexity limit. Any one matching counts as declared.
|
|
82
|
+
const REVIEWER_NAME_PATTERN =
|
|
83
|
+
/\b(reviewer|revisor|separate review|revisi[oó]n separada|contraste|auditor)\b/i;
|
|
84
|
+
const REVIEWER_LABEL_PATTERN = /(review|revisi[oó]n)\s*:/i;
|
|
85
|
+
const REVIEWER_ACTION_EN_PATTERN =
|
|
86
|
+
/\breview\s+(?:the\s+)?(?:diff|result|work)\b/i;
|
|
87
|
+
const REVIEWER_ACTION_ES_PATTERN =
|
|
88
|
+
/\brevisa\s+(?:el\s+)?(?:diff|resultado|trabajo)\b/i;
|
|
89
|
+
const REVIEWER_CODE_REVIEW_PATTERN = /code.review/i;
|
|
90
|
+
|
|
91
|
+
function reviewerStageDeclared(prompt) {
|
|
92
|
+
return (
|
|
93
|
+
REVIEWER_NAME_PATTERN.test(prompt) ||
|
|
94
|
+
REVIEWER_LABEL_PATTERN.test(prompt) ||
|
|
95
|
+
REVIEWER_ACTION_EN_PATTERN.test(prompt) ||
|
|
96
|
+
REVIEWER_ACTION_ES_PATTERN.test(prompt) ||
|
|
97
|
+
REVIEWER_CODE_REVIEW_PATTERN.test(prompt)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const DEFINITION_PATTERN =
|
|
102
|
+
/\b(scout|reconnaissance|reconocimiento|prior exploration|explora(?:ci[oó]n)? previa|verified context|contexto verificado|asserts\.md|brief\.md)\b/i;
|
|
103
|
+
const WRITING_PATTERN =
|
|
104
|
+
/\b(test|tests|prueba|pruebas|vitest|jest|playwright|typecheck|lint|verification|verificaci[oó]n|verification criteria)\b/i;
|
|
105
|
+
|
|
106
|
+
/** The three pipeline stages this gate requires be declared, in this order. */
|
|
107
|
+
const REQUIRED_STAGES = [
|
|
108
|
+
{
|
|
109
|
+
id: 'definition',
|
|
110
|
+
isDeclared: (prompt) => DEFINITION_PATTERN.test(prompt),
|
|
111
|
+
missing:
|
|
112
|
+
'DEFINITION: does not declare where the context came from (a prior scout, read-only exploration, or an already-written asserts/brief).',
|
|
113
|
+
line: 'Verified context: <path to the cited asserts/brief> — or: a prior scout ran, findings at <path>.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'writing',
|
|
117
|
+
isDeclared: (prompt) => WRITING_PATTERN.test(prompt),
|
|
118
|
+
missing:
|
|
119
|
+
'WRITING: does not declare what is verified nor with what command. A builder does not validate its own work without a criterion written beforehand.',
|
|
120
|
+
line: 'Verification: <exact command> and what is expected. Anything that cannot run is reported OMITTED, never PASS.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'reviewer',
|
|
124
|
+
isDeclared: reviewerStageDeclared,
|
|
125
|
+
missing:
|
|
126
|
+
'REVIEWER: does not declare who reviews the result. Whoever implements does not validate their own work.',
|
|
127
|
+
line: 'Review: the coordinator reviews the diff and the evidence before closing — or: a read-only `revision`/`contraste` agent reviews it.',
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function isExempt(toolInput, prompt, builderSubagents, exemptSubagents) {
|
|
132
|
+
const subagentType = String(
|
|
133
|
+
toolInput.subagent_type ?? toolInput.subagentType ?? '',
|
|
134
|
+
).toLowerCase();
|
|
135
|
+
if (exemptSubagents.includes(subagentType)) return true;
|
|
136
|
+
// An unknown type is not assumed to be a builder: the safe side here is not to
|
|
137
|
+
// invent requirements for agents this gate cannot classify.
|
|
138
|
+
if (subagentType && !builderSubagents.includes(subagentType)) return true;
|
|
139
|
+
if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
|
|
140
|
+
if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
|
|
141
|
+
if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
|
|
142
|
+
if (HARNESS_PATTERN.test(prompt)) return true;
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
runGate(
|
|
147
|
+
{
|
|
148
|
+
id: GATE_ID,
|
|
149
|
+
configKey: CONFIG_KEY,
|
|
150
|
+
enabledByDefault: false,
|
|
151
|
+
defaultParams: {
|
|
152
|
+
builderSubagents: DEFAULT_BUILDER_SUBAGENTS,
|
|
153
|
+
exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
({ toolName, toolInput, parameters }) => {
|
|
157
|
+
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
158
|
+
|
|
159
|
+
const prompt = delegationPromptOf(toolInput);
|
|
160
|
+
if (!prompt.trim()) return;
|
|
161
|
+
|
|
162
|
+
const builderSubagents =
|
|
163
|
+
parameters.builderSubagents ?? DEFAULT_BUILDER_SUBAGENTS;
|
|
164
|
+
const exemptSubagents =
|
|
165
|
+
parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
|
|
166
|
+
if (isExempt(toolInput, prompt, builderSubagents, exemptSubagents)) return;
|
|
167
|
+
|
|
168
|
+
const missingStages = REQUIRED_STAGES.filter(
|
|
169
|
+
(stage) => !stage.isDeclared(prompt),
|
|
170
|
+
);
|
|
171
|
+
if (missingStages.length === 0) return;
|
|
172
|
+
|
|
173
|
+
const isHighRisk = HIGH_RISK_PATTERN.test(prompt);
|
|
174
|
+
const details = missingStages.map((stage) => stage.missing).join(' ');
|
|
175
|
+
const lines = missingStages.map((stage) => ` ${stage.line}`).join('\n');
|
|
176
|
+
|
|
177
|
+
deny(
|
|
178
|
+
GATE_ID,
|
|
179
|
+
`This delegation is going to build code but skips ${missingStages.length} pipeline ` +
|
|
180
|
+
`stage(s). ${details} ${
|
|
181
|
+
isHighRisk
|
|
182
|
+
? 'In HIGH-RISK a separate reviewer is not negotiable. '
|
|
183
|
+
: ''
|
|
184
|
+
}PASTE THESE LINES INTO THE PROMPT AND RELAUNCH:\n${lines}`,
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
);
|