@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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +39 -4
  3. package/README.md +34 -5
  4. package/cli/config.mjs +126 -124
  5. package/cli/init.mjs +303 -276
  6. package/cli/install.mjs +281 -175
  7. package/cli/materialize.mjs +103 -102
  8. package/cli/registry.mjs +139 -136
  9. package/cli/smoke-fixtures.json +65 -0
  10. package/cli/task.mjs +140 -140
  11. package/package.json +1 -1
  12. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  13. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  14. package/plugins/gates/hooks/doctor.mjs +207 -207
  15. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  16. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  17. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  18. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  19. package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
  20. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  21. package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
  22. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  23. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  24. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  25. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  26. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  27. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  28. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  29. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  30. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  31. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  32. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  33. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  34. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  35. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  36. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  37. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  38. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  39. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  40. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  41. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  42. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  43. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  44. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  45. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  46. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  47. package/plugins/gates/hooks/hooks.json +61 -0
  48. package/plugins/gates/hooks/lib/config.mjs +179 -172
  49. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  50. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  51. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  52. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  53. package/plugins/tasks/hooks/hooks.json +26 -26
  54. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  55. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  56. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  57. 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
+ );