@devrik-tools/claude-gates 0.1.2 → 0.2.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 +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -1,166 +1,180 @@
|
|
|
1
|
-
// mandatory-flow — denies an implementation delegation when there is no live task
|
|
2
|
-
// pointer on disk backed by a contract file. Declaring pipeline stages in prose (what
|
|
3
|
-
// implementation-pipeline.mjs checks) is not the same as a live task actually existing
|
|
4
|
-
// on disk; this gate checks the disk fact, not the prompt's wording. Migrated from
|
|
5
|
-
// ~/.claude/hooks/guard-flujo-obligatorio.mjs.
|
|
6
|
-
//
|
|
7
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
-
// activePointerPath path (relative to the project root) to the file naming the
|
|
9
|
-
// active task/pipeline slug.
|
|
10
|
-
// exemptSubagents subagent types exempt outright (read-only pipeline stages).
|
|
11
|
-
// Replaces the built-in list wholesale.
|
|
12
|
-
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
13
|
-
// its override replaces.
|
|
14
|
-
//
|
|
15
|
-
// ── Off by default, and quiet outside its narrow trigger ───────────────────────────
|
|
16
|
-
// Only STANDARD/HIGH-RISK implementation delegations, on non-exempt subagents, not
|
|
17
|
-
// about the harness itself, reach the disk check.
|
|
18
|
-
|
|
19
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
/(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
);
|
|
1
|
+
// mandatory-flow — denies an implementation delegation when there is no live task
|
|
2
|
+
// pointer on disk backed by a contract file. Declaring pipeline stages in prose (what
|
|
3
|
+
// implementation-pipeline.mjs checks) is not the same as a live task actually existing
|
|
4
|
+
// on disk; this gate checks the disk fact, not the prompt's wording. Migrated from
|
|
5
|
+
// ~/.claude/hooks/guard-flujo-obligatorio.mjs.
|
|
6
|
+
//
|
|
7
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
+
// activePointerPath path (relative to the project root) to the file naming the
|
|
9
|
+
// active task/pipeline slug.
|
|
10
|
+
// exemptSubagents subagent types exempt outright (read-only pipeline stages).
|
|
11
|
+
// Replaces the built-in list wholesale.
|
|
12
|
+
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
13
|
+
// its override replaces.
|
|
14
|
+
//
|
|
15
|
+
// ── Off by default, and quiet outside its narrow trigger ───────────────────────────
|
|
16
|
+
// Only STANDARD/HIGH-RISK implementation delegations, on non-exempt subagents, not
|
|
17
|
+
// about the harness itself, reach the disk check.
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join, normalize, sep } from 'node:path';
|
|
21
|
+
import {
|
|
22
|
+
runGate,
|
|
23
|
+
deny,
|
|
24
|
+
toolInGroups,
|
|
25
|
+
delegationPromptOf,
|
|
26
|
+
} from '../../lib/hook-io.mjs';
|
|
27
|
+
|
|
28
|
+
const GATE_ID = 'mandatory-flow';
|
|
29
|
+
const CONFIG_KEY = 'requireLiveTaskWhenImplementing';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_ACTIVE_POINTER_PATH = join('.ai', 'pipeline', 'ACTIVA');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_EXEMPT_SUBAGENTS = [
|
|
34
|
+
'explore',
|
|
35
|
+
'plan',
|
|
36
|
+
'scout',
|
|
37
|
+
'revision',
|
|
38
|
+
'contraste',
|
|
39
|
+
'test-planner',
|
|
40
|
+
'qa',
|
|
41
|
+
'ui',
|
|
42
|
+
'ux',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Contract files that count as evidence a task's contract exists on disk. */
|
|
46
|
+
const TASK_CONTRACT_FILES = [
|
|
47
|
+
'asserts.md',
|
|
48
|
+
'task.json',
|
|
49
|
+
'spec.md',
|
|
50
|
+
'requirements.md',
|
|
51
|
+
'acceptance.md',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function withWordBoundary(alternation) {
|
|
55
|
+
return new RegExp(
|
|
56
|
+
`(?:^|[^\\p{L}\\p{N}_])(?:${alternation})(?:[^\\p{L}\\p{N}_]|$)`,
|
|
57
|
+
'iu',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const IMPLEMENTATION_VERBS = withWordBoundary(
|
|
62
|
+
'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
|
|
63
|
+
'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
|
|
64
|
+
'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
|
|
65
|
+
'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
|
|
66
|
+
'escrib(í|e)|escribir|implement\\w*|writ(?:e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
|
|
67
|
+
'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
|
|
71
|
+
* the plugin-wide convention (see risk-level.mjs). */
|
|
72
|
+
const DEMANDING_LEVEL_PATTERN =
|
|
73
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
|
|
74
|
+
const EXEMPT_LEVEL_PATTERN =
|
|
75
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
|
|
76
|
+
|
|
77
|
+
const HARNESS_PATTERN =
|
|
78
|
+
/(\.claude[\\/]|hooks[\\/]|plugins[\\/]gates|settings\.json|[\\/]agents[\\/]\w|[\\/]rules[\\/]\w|[\\/]skills[\\/]\w)/i;
|
|
79
|
+
|
|
80
|
+
function isExempt(toolInput, prompt, exemptSubagents) {
|
|
81
|
+
const subagentType = String(
|
|
82
|
+
toolInput.subagent_type ?? toolInput.subagentType ?? '',
|
|
83
|
+
).toLowerCase();
|
|
84
|
+
if (exemptSubagents.includes(subagentType)) return true;
|
|
85
|
+
if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
|
|
86
|
+
if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
|
|
87
|
+
if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
|
|
88
|
+
if (HARNESS_PATTERN.test(prompt)) return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasTaskContract(taskDirectory) {
|
|
93
|
+
return TASK_CONTRACT_FILES.some((file) => {
|
|
94
|
+
try {
|
|
95
|
+
return existsSync(join(taskDirectory, file));
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The pointer file's trimmed content, or '' when it cannot be read or the slug
|
|
103
|
+
* attempts path traversal. A slug is a directory name, not a path: rejecting any
|
|
104
|
+
* segment separator or '..' closes off `join(cwd, '.ai', 'pipeline', slug)` escaping
|
|
105
|
+
* that directory to accept an unrelated file elsewhere on disk as the task contract. */
|
|
106
|
+
function readSlug(pointerPath) {
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = readFileSync(pointerPath, 'utf8').trim();
|
|
110
|
+
} catch {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
if (!raw) return '';
|
|
114
|
+
const normalized = normalize(raw);
|
|
115
|
+
const hasTraversal = normalized
|
|
116
|
+
.split(/[\\/]/)
|
|
117
|
+
.some((segment) => segment === '..' || segment === '.');
|
|
118
|
+
if (hasTraversal || normalized.includes(sep) || normalized.includes('/')) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
return normalized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Denies for whichever of the three live-task facts is missing, or does nothing. */
|
|
125
|
+
function checkLiveTask(pointerPath) {
|
|
126
|
+
if (!existsSync(pointerPath)) {
|
|
127
|
+
deny(
|
|
128
|
+
GATE_ID,
|
|
129
|
+
`This delegation is going to implement, but there is no live task: ${pointerPath} ` +
|
|
130
|
+
'does not exist. Start a task pointing to it before delegating, or declare ' +
|
|
131
|
+
'LEVEL: QUESTION/MICRO if this is not implementation.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const slug = readSlug(pointerPath);
|
|
136
|
+
if (!slug) {
|
|
137
|
+
deny(
|
|
138
|
+
GATE_ID,
|
|
139
|
+
`${pointerPath} exists but is empty. An active-task pointer with no slug is not ` +
|
|
140
|
+
'a live task. Write the task slug into it before delegating.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const taskDirectory = join(process.cwd(), '.ai', 'pipeline', slug);
|
|
145
|
+
if (!hasTaskContract(taskDirectory)) {
|
|
146
|
+
deny(
|
|
147
|
+
GATE_ID,
|
|
148
|
+
`The active task '${slug}' has no contract on disk: none of ` +
|
|
149
|
+
`${TASK_CONTRACT_FILES.join(', ')} exists under ${taskDirectory}. ` +
|
|
150
|
+
'Write the contract before implementing.',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
runGate(
|
|
156
|
+
{
|
|
157
|
+
id: GATE_ID,
|
|
158
|
+
configKey: CONFIG_KEY,
|
|
159
|
+
enabledByDefault: false,
|
|
160
|
+
defaultParams: {
|
|
161
|
+
activePointerPath: DEFAULT_ACTIVE_POINTER_PATH,
|
|
162
|
+
exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
({ toolName, toolInput, parameters }) => {
|
|
166
|
+
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
167
|
+
|
|
168
|
+
const prompt = delegationPromptOf(toolInput);
|
|
169
|
+
if (!prompt.trim()) return;
|
|
170
|
+
|
|
171
|
+
const exemptSubagents =
|
|
172
|
+
parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
|
|
173
|
+
if (isExempt(toolInput, prompt, exemptSubagents)) return;
|
|
174
|
+
|
|
175
|
+
const activePointerPath = String(
|
|
176
|
+
parameters.activePointerPath ?? DEFAULT_ACTIVE_POINTER_PATH,
|
|
177
|
+
);
|
|
178
|
+
checkLiveTask(join(process.cwd(), activePointerPath));
|
|
179
|
+
},
|
|
180
|
+
);
|
|
@@ -1,85 +1,79 @@
|
|
|
1
|
-
import { runGate, warn } from '../../lib/hook-io.mjs';
|
|
2
|
-
|
|
3
|
-
const GATE_ID = 'neutral-spanish';
|
|
4
|
-
const CONFIG_KEY = 'warnNonNeutralSpanish';
|
|
5
|
-
|
|
6
|
-
const MAX_REPORTED_MARKERS = 8;
|
|
7
|
-
|
|
8
|
-
// Data strings, not identifiers — never add these to a cSpell dictionary.
|
|
9
|
-
const DEFAULT_REGIONAL_MARKERS = [
|
|
10
|
-
'tenés',
|
|
11
|
-
'podés',
|
|
12
|
-
'querés',
|
|
13
|
-
'sabés',
|
|
14
|
-
'hacés',
|
|
15
|
-
'decís',
|
|
16
|
-
'venís',
|
|
17
|
-
'sos',
|
|
18
|
-
'fijate',
|
|
19
|
-
'mirá',
|
|
20
|
-
'pará',
|
|
21
|
-
'esperá',
|
|
22
|
-
'dale',
|
|
23
|
-
'mandale',
|
|
24
|
-
'contame',
|
|
25
|
-
'fíjate vos',
|
|
26
|
-
'acá',
|
|
27
|
-
'allá',
|
|
28
|
-
'laburo',
|
|
29
|
-
'laburar',
|
|
30
|
-
'quilombo',
|
|
31
|
-
'posta',
|
|
32
|
-
'che',
|
|
33
|
-
'boludo',
|
|
34
|
-
'pibe',
|
|
35
|
-
'guita',
|
|
36
|
-
'al pedo',
|
|
37
|
-
'un toque',
|
|
38
|
-
'capaz que',
|
|
39
|
-
'de una',
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
function extractText(toolName, toolInput) {
|
|
43
|
-
if (toolName
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
warn(
|
|
81
|
-
GATE_ID,
|
|
82
|
-
`Text being written contains regional Spanish markers: ${unique.join(', ')}. Prefer neutral Spanish unless this is a literal quote or data.`,
|
|
83
|
-
);
|
|
84
|
-
},
|
|
85
|
-
);
|
|
1
|
+
import { runGate, warn, toolInGroups, writtenContentOf } from '../../lib/hook-io.mjs';
|
|
2
|
+
|
|
3
|
+
const GATE_ID = 'neutral-spanish';
|
|
4
|
+
const CONFIG_KEY = 'warnNonNeutralSpanish';
|
|
5
|
+
|
|
6
|
+
const MAX_REPORTED_MARKERS = 8;
|
|
7
|
+
|
|
8
|
+
// Data strings, not identifiers — never add these to a cSpell dictionary.
|
|
9
|
+
const DEFAULT_REGIONAL_MARKERS = [
|
|
10
|
+
'tenés',
|
|
11
|
+
'podés',
|
|
12
|
+
'querés',
|
|
13
|
+
'sabés',
|
|
14
|
+
'hacés',
|
|
15
|
+
'decís',
|
|
16
|
+
'venís',
|
|
17
|
+
'sos',
|
|
18
|
+
'fijate',
|
|
19
|
+
'mirá',
|
|
20
|
+
'pará',
|
|
21
|
+
'esperá',
|
|
22
|
+
'dale',
|
|
23
|
+
'mandale',
|
|
24
|
+
'contame',
|
|
25
|
+
'fíjate vos',
|
|
26
|
+
'acá',
|
|
27
|
+
'allá',
|
|
28
|
+
'laburo',
|
|
29
|
+
'laburar',
|
|
30
|
+
'quilombo',
|
|
31
|
+
'posta',
|
|
32
|
+
'che',
|
|
33
|
+
'boludo',
|
|
34
|
+
'pibe',
|
|
35
|
+
'guita',
|
|
36
|
+
'al pedo',
|
|
37
|
+
'un toque',
|
|
38
|
+
'capaz que',
|
|
39
|
+
'de una',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function extractText(toolName, toolInput) {
|
|
43
|
+
if (!toolInGroups(toolName, ['write'])) return '';
|
|
44
|
+
return writtenContentOf(toolInput);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
runGate(
|
|
48
|
+
{
|
|
49
|
+
id: GATE_ID,
|
|
50
|
+
configKey: CONFIG_KEY,
|
|
51
|
+
enabledByDefault: true,
|
|
52
|
+
defaultParams: {
|
|
53
|
+
regionalMarkers: DEFAULT_REGIONAL_MARKERS,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
({ toolName, toolInput, parameters }) => {
|
|
57
|
+
const text = extractText(toolName, toolInput).toLowerCase();
|
|
58
|
+
if (!text) return;
|
|
59
|
+
|
|
60
|
+
const hits = [];
|
|
61
|
+
for (const marker of parameters.regionalMarkers) {
|
|
62
|
+
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
63
|
+
// \p{L} (any Unicode letter) as the non-boundary class, instead of a hand-written
|
|
64
|
+
// accented-character set, so accented word boundaries are handled correctly without
|
|
65
|
+
// spelling out literal accented characters in the source (the project keeps its
|
|
66
|
+
// spell-check dictionary untouched, so no word list may lean on this being a string).
|
|
67
|
+
const pattern = new RegExp(`(^|[^\\p{L}])${escaped}([^\\p{L}]|$)`, 'iu');
|
|
68
|
+
if (pattern.test(text)) hits.push(marker);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hits.length === 0) return;
|
|
72
|
+
|
|
73
|
+
const unique = [...new Set(hits)].slice(0, MAX_REPORTED_MARKERS);
|
|
74
|
+
warn(
|
|
75
|
+
GATE_ID,
|
|
76
|
+
`Text being written contains regional Spanish markers: ${unique.join(', ')}. Prefer neutral Spanish unless this is a literal quote or data.`,
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
+
|
|
11
|
+
function runGate(payload, { config } = {}) {
|
|
12
|
+
const project = mkdtempSync(join(tmpdir(), 'neutral-spanish-edge-'));
|
|
13
|
+
mkdirSync(join(project, '.git'));
|
|
14
|
+
if (config) {
|
|
15
|
+
mkdirSync(join(project, '.ai'));
|
|
16
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
+
}
|
|
18
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
+
input: JSON.stringify(payload),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
cwd: project,
|
|
22
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
23
|
+
});
|
|
24
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isWarn(result) {
|
|
28
|
+
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// FIXED: extractText() now gates on toolInGroups(toolName, ['write']) and reads through
|
|
32
|
+
// writtenContentOf() (lib/hook-io.mjs), which covers NotebookEdit's new_source field.
|
|
33
|
+
test('FIXED: NotebookEdit carrying regional Spanish is detected', () => {
|
|
34
|
+
const payload = {
|
|
35
|
+
tool_name: 'NotebookEdit',
|
|
36
|
+
tool_input: {
|
|
37
|
+
notebook_path: '/repo/notebook.ipynb',
|
|
38
|
+
cell_id: 'abc',
|
|
39
|
+
new_source: 'vos tenés que revisar esto, dale',
|
|
40
|
+
cell_type: 'markdown',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
assert.ok(isWarn(runGate(payload)), 'gate now detects regional Spanish written via NotebookEdit');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// FIXED: write_to_file is recognized by toolInGroups(toolName, ['write']) (native name in
|
|
47
|
+
// TOOL_GROUPS.write) and its `content` field is read through writtenContentOf().
|
|
48
|
+
test('FIXED: write_to_file with regional Spanish content is detected', () => {
|
|
49
|
+
const payload = {
|
|
50
|
+
tool_name: 'write_to_file',
|
|
51
|
+
tool_input: {
|
|
52
|
+
file_path: '/repo/notes.md',
|
|
53
|
+
content: 'che, mirá el laburo que hicimos acá',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
assert.ok(isWarn(runGate(payload)), 'gate now detects regional Spanish written via write_to_file');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// EDGE CASE 3: false-positive check on legitimate neutral words that are substrings-adjacent
|
|
60
|
+
// to markers. "sos" (voseo for "you are") is in the marker list; verify a word that merely
|
|
61
|
+
// CONTAINS "sos" as a substring (e.g. "sospecha", "hermosos") is NOT flagged, since the
|
|
62
|
+
// pattern uses \p{L} boundaries.
|
|
63
|
+
test('OK: word boundary prevents false positive on words containing a marker as substring', () => {
|
|
64
|
+
const result = runGate({
|
|
65
|
+
tool_name: 'Write',
|
|
66
|
+
tool_input: { file_path: '/repo/notes.md', content: 'Tengo una sospecha sobre estos hermosos resultados.' },
|
|
67
|
+
});
|
|
68
|
+
assert.equal(result, null, 'false positive: "sos" matched inside sospecha/hermosos');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// EDGE CASE 4: "dale" is in the marker list as Rioplatense slang, but is this over-broad?
|
|
72
|
+
// "dale" only appears as that exact word — check it correctly fires only for that token,
|
|
73
|
+
// not, e.g., as part of "dalear" or similar. This confirms no over-matching regression.
|
|
74
|
+
test('OK: marker "dale" does not fire on a word merely containing it', () => {
|
|
75
|
+
const result = runGate({
|
|
76
|
+
tool_name: 'Write',
|
|
77
|
+
tool_input: { file_path: '/repo/notes.md', content: 'Actualiza el dataleer o el dalext.' },
|
|
78
|
+
});
|
|
79
|
+
assert.equal(result, null, 'false positive: "dale" matched inside a longer word');
|
|
80
|
+
});
|