@devrik-tools/claude-gates 0.2.1 → 0.3.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 +7 -6
- package/README.md +16 -7
- package/cli/index.mjs +4 -0
- package/cli/init.mjs +18 -1
- package/cli/install.mjs +26 -1
- package/package.json +5 -2
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +78 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +11 -81
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +140 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +130 -126
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +104 -79
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +138 -123
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +124 -101
- package/plugins/gates/hooks/hooks.json +19 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/register-requests.mjs +7 -3
- package/registry.json +738 -793
- package/cli/__tests__/config.test.mjs +0 -113
- package/cli/__tests__/init-flags.test.mjs +0 -111
- package/cli/__tests__/install.test.mjs +0 -102
- package/cli/__tests__/materialize.test.mjs +0 -95
- package/cli/__tests__/registry-gates-consistency.test.mjs +0 -52
- package/cli/__tests__/selection.test.mjs +0 -200
- package/cli/__tests__/task.test.mjs +0 -124
- package/cli/__tests__/version-consistency.test.mjs +0 -44
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +0 -83
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +0 -85
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +0 -65
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +0 -117
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +0 -104
- package/plugins/gates/hooks/gates/autonomous-mode/test.mjs +0 -58
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +0 -165
- package/plugins/gates/hooks/gates/bash-commands/test.mjs +0 -126
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +0 -151
- package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +0 -111
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +0 -207
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +0 -161
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +0 -69
- package/plugins/gates/hooks/gates/dependency-skills/test.mjs +0 -109
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +0 -68
- package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +0 -68
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/feature-catalog/test.mjs +0 -97
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +0 -88
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +0 -155
- package/plugins/gates/hooks/gates/forge-flow/test.mjs +0 -135
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +0 -66
- package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +0 -86
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +0 -96
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +0 -165
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +0 -85
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +0 -100
- package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +0 -119
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +0 -80
- package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +0 -65
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +0 -71
- package/plugins/gates/hooks/gates/never-assume/test.mjs +0 -95
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +0 -109
- package/plugins/gates/hooks/gates/no-blocking/test.mjs +0 -108
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +0 -106
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +0 -91
- package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +0 -131
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/protected-paths/test.mjs +0 -75
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +0 -102
- package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +0 -99
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +0 -94
- package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +0 -101
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +0 -116
- package/plugins/gates/hooks/gates/risk-level/test.mjs +0 -153
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +0 -58
- package/plugins/gates/hooks/gates/root-cause-first/test.mjs +0 -73
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +0 -124
- package/plugins/gates/hooks/gates/root-whitelist/test.mjs +0 -80
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +0 -78
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +0 -130
- package/plugins/gates/hooks/gates/sdd-specs/test.mjs +0 -163
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +0 -114
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +0 -72
- package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +0 -81
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +0 -63
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +0 -114
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +0 -62
- package/plugins/gates/hooks/gates/tool-map/test.mjs +0 -87
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +0 -173
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +0 -223
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +0 -114
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +0 -189
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +0 -100
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +0 -95
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +0 -180
|
@@ -1,123 +1,138 @@
|
|
|
1
|
-
// no-memory-dependency —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Stage
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
function
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
)
|
|
1
|
+
// no-memory-dependency — denies when a delegation prompt leans on the subagent
|
|
2
|
+
// "remembering" something said earlier in conversation instead of carrying the data itself.
|
|
3
|
+
// A fresh subagent has no access to the delegator's conversation history: if the data
|
|
4
|
+
// matters, it must travel IN the prompt, in a file the subagent reads, or in an
|
|
5
|
+
// already-persisted decision. The dependency on unavailable memory is a complete, objective
|
|
6
|
+
// fact once stage 2 has ruled out a real persistence instruction — so this blocks.
|
|
7
|
+
//
|
|
8
|
+
// ── Two stages, same pattern as intent-flow ─────────────────────────────────────────
|
|
9
|
+
// Stage 1 (cheap): does any memory-dependency phrase appear anywhere? If not, allow.
|
|
10
|
+
// Stage 2 (confirmation, only on candidates): strip quoted text, then discard a phrase
|
|
11
|
+
// that has a REAL deterministic persistence INSTRUCTION nearby (not just a noun/verb
|
|
12
|
+
// mentioned in passing) — "no te olvides de guardar la decision en .ai/decision.md"
|
|
13
|
+
// genuinely depends on a file, not on model memory. A persistence noun/verb that merely
|
|
14
|
+
// co-occurs in the window without forming an instruction TO persist THIS remembered
|
|
15
|
+
// thing (e.g. an unrelated file named elsewhere in the same sentence) does not suppress
|
|
16
|
+
// the deny.
|
|
17
|
+
//
|
|
18
|
+
// ── Escape hatch ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
// A memory phrase can be a false positive: "no te olvides de cerrar el server al final"
|
|
20
|
+
// directs the SUBAGENT'S own future action, not data it must recall from the delegator. For
|
|
21
|
+
// that case the author adds the escapeHatch marker to the prompt to state, explicitly, that
|
|
22
|
+
// no cross-conversation memory is actually required.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
runGate,
|
|
26
|
+
deny,
|
|
27
|
+
toolInGroups,
|
|
28
|
+
delegationPromptOf,
|
|
29
|
+
} from '../../lib/hook-io.mjs';
|
|
30
|
+
import { PERSISTENCE_VERB } from '../../lib/signals.mjs';
|
|
31
|
+
|
|
32
|
+
const GATE_ID = 'no-memory-dependency';
|
|
33
|
+
const CONFIG_KEY = 'warnMemoryDependencyInBrief';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_ESCAPE_HATCH = 'memory-not-needed';
|
|
36
|
+
|
|
37
|
+
const DEFAULT_MEMORY_DEPENDENCY_PATTERNS = [
|
|
38
|
+
'acordate de|acu[eé]rdate de',
|
|
39
|
+
'no olvides|no te olvides',
|
|
40
|
+
'record[aá] que|ten[eé] en cuenta que',
|
|
41
|
+
'como te dije|como ya te dije|ya te dije|te lo dije antes|ya te ped[ií]',
|
|
42
|
+
"remember to|don'?t forget|keep in mind",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// A real persistence INSTRUCTION requires an imperative persistence VERB (an actual
|
|
46
|
+
// directive to save/persist/write THIS remembered thing), not merely a persistence-
|
|
47
|
+
// related NOUN mentioned somewhere nearby (a stray "incident.md" in the same sentence
|
|
48
|
+
// names a file without instructing anything be saved to it). Centralized in
|
|
49
|
+
// lib/signals.mjs (ES+EN) so this class of signal is not duplicated per gate.
|
|
50
|
+
const PERSISTENCE_VERB_PATTERN = PERSISTENCE_VERB;
|
|
51
|
+
|
|
52
|
+
const PERSISTENCE_WINDOW = 80;
|
|
53
|
+
|
|
54
|
+
function withUnicodeWordBoundary(alternatives) {
|
|
55
|
+
return new RegExp(
|
|
56
|
+
`(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
|
|
57
|
+
'iu',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripQuoted(text) {
|
|
62
|
+
return text
|
|
63
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
64
|
+
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
65
|
+
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function allMatches(pattern, text) {
|
|
69
|
+
const flags = pattern.flags.includes('g')
|
|
70
|
+
? pattern.flags
|
|
71
|
+
: `${pattern.flags}g`;
|
|
72
|
+
return [...text.matchAll(new RegExp(pattern.source, flags))];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True only when the window around a memory phrase carries a real persistence
|
|
76
|
+
* INSTRUCTION — an imperative persistence verb — not merely a persistence-related noun
|
|
77
|
+
* mentioned in passing. A noun alone ("el reporte esta en incident.md") names a file
|
|
78
|
+
* without instructing anything be saved to it, so it must NOT suppress the warning. */
|
|
79
|
+
function hasPersistenceInstructionNearby(text, match) {
|
|
80
|
+
const from = Math.max(0, match.index - PERSISTENCE_WINDOW);
|
|
81
|
+
const to = Math.min(
|
|
82
|
+
text.length,
|
|
83
|
+
match.index + match[0].length + PERSISTENCE_WINDOW,
|
|
84
|
+
);
|
|
85
|
+
const window = text.slice(from, to);
|
|
86
|
+
return PERSISTENCE_VERB_PATTERN.test(window);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unresolvedMemoryPhrases(prompt, memoryPattern) {
|
|
90
|
+
if (!memoryPattern.test(prompt)) return []; // stage 1: no candidate, allow cheap
|
|
91
|
+
|
|
92
|
+
const text = stripQuoted(prompt); // stage 2(a)
|
|
93
|
+
const matches = allMatches(memoryPattern, text);
|
|
94
|
+
|
|
95
|
+
return matches
|
|
96
|
+
.filter((match) => !hasPersistenceInstructionNearby(text, match))
|
|
97
|
+
.map((match) => match[0].trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
runGate(
|
|
101
|
+
{
|
|
102
|
+
id: GATE_ID,
|
|
103
|
+
configKey: CONFIG_KEY,
|
|
104
|
+
enabledByDefault: false,
|
|
105
|
+
defaultParams: {
|
|
106
|
+
memoryDependencyPatterns: DEFAULT_MEMORY_DEPENDENCY_PATTERNS,
|
|
107
|
+
escapeHatch: DEFAULT_ESCAPE_HATCH,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
({ toolName, toolInput, parameters }) => {
|
|
111
|
+
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
112
|
+
|
|
113
|
+
const prompt = delegationPromptOf(toolInput);
|
|
114
|
+
if (!prompt.trim()) return;
|
|
115
|
+
|
|
116
|
+
// Explicit opt-out: the author states no cross-conversation memory is actually required.
|
|
117
|
+
const escapeHatch = (
|
|
118
|
+
parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH
|
|
119
|
+
).toLowerCase();
|
|
120
|
+
if (escapeHatch && prompt.toLowerCase().includes(escapeHatch)) return;
|
|
121
|
+
|
|
122
|
+
const memoryPattern = withUnicodeWordBoundary(
|
|
123
|
+
(parameters.memoryDependencyPatterns ?? []).join('|'),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const phrases = unresolvedMemoryPhrases(prompt, memoryPattern);
|
|
127
|
+
if (phrases.length === 0) return;
|
|
128
|
+
|
|
129
|
+
const quotedPhrases = phrases.map((phrase) => `"${phrase}"`).join(', ');
|
|
130
|
+
deny(
|
|
131
|
+
GATE_ID,
|
|
132
|
+
`This delegation depends on the subagent remembering something (${quotedPhrases}), but a ` +
|
|
133
|
+
'fresh subagent has none of this conversation. Put the data IN the prompt, in a file it ' +
|
|
134
|
+
'reads, a flag, or an already-persisted decision. If the phrase directs the subagent’s ' +
|
|
135
|
+
`own future action and needs no recalled data, add the marker "${parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH}" to the prompt.`,
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
@@ -1,101 +1,124 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, relative } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, relative } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
runGate,
|
|
6
|
+
deny,
|
|
7
|
+
toolInGroups,
|
|
8
|
+
writtenPathOf,
|
|
9
|
+
writtenContentOf,
|
|
10
|
+
} from '../../lib/hook-io.mjs';
|
|
11
|
+
|
|
12
|
+
const GATE_ID = 'test-after-implementation';
|
|
13
|
+
const CONFIG_KEY = 'warnTestWrittenAfterImplementation';
|
|
14
|
+
|
|
15
|
+
// Escape hatch: a regression test written AFTER reproducing a bug is a legitimate
|
|
16
|
+
// test-after-implementation case (the project's own rule: reproduce the bug, then write the
|
|
17
|
+
// test that pins it). A deny cannot advise-and-pass, so that legitimate case needs an
|
|
18
|
+
// explicit opt-out — this marker anywhere in the test file's content lets it through.
|
|
19
|
+
const DEFAULT_ESCAPE_HATCH = 'test-after-impl:allow';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_IMPLEMENTATION_EXTENSIONS = [
|
|
22
|
+
'.ts',
|
|
23
|
+
'.tsx',
|
|
24
|
+
'.js',
|
|
25
|
+
'.jsx',
|
|
26
|
+
'.mjs',
|
|
27
|
+
'.vue',
|
|
28
|
+
];
|
|
29
|
+
const TEST_FILE_PATTERN = /\.(spec|test)\.[cm]?[jt]sx?$/i;
|
|
30
|
+
// `git status --porcelain` prefixes each line with a 2-character status code plus a space.
|
|
31
|
+
const PORCELAIN_STATUS_PREFIX_LENGTH = 3;
|
|
32
|
+
|
|
33
|
+
function baseNameWithoutTestSuffix(fileName) {
|
|
34
|
+
return fileName.replace(TEST_FILE_PATTERN, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gitStatusPorcelain(cwd) {
|
|
38
|
+
try {
|
|
39
|
+
const output = execFileSync('git', ['status', '--porcelain'], {
|
|
40
|
+
cwd,
|
|
41
|
+
timeout: 5000,
|
|
42
|
+
}).toString();
|
|
43
|
+
return output
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((line) => line.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
} catch {
|
|
48
|
+
// Not a git repo, or git unavailable — nothing to cross-check against.
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
runGate(
|
|
54
|
+
{
|
|
55
|
+
id: GATE_ID,
|
|
56
|
+
configKey: CONFIG_KEY,
|
|
57
|
+
enabledByDefault: false,
|
|
58
|
+
defaultParams: {
|
|
59
|
+
implementationExtensions: DEFAULT_IMPLEMENTATION_EXTENSIONS,
|
|
60
|
+
escapeHatch: DEFAULT_ESCAPE_HATCH,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
({ toolName, toolInput, parameters }) => {
|
|
64
|
+
if (!toolInGroups(toolName, ['write'])) return;
|
|
65
|
+
|
|
66
|
+
const filePath = writtenPathOf(toolInput);
|
|
67
|
+
if (!TEST_FILE_PATTERN.test(filePath)) return;
|
|
68
|
+
if (existsSync(filePath)) return; // only new test file creation, not edits
|
|
69
|
+
|
|
70
|
+
// Explicit opt-out for a legitimate regression test (bug reproduced first, then pinned).
|
|
71
|
+
const escapeHatch = parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH;
|
|
72
|
+
if (escapeHatch && writtenContentOf(toolInput).includes(escapeHatch))
|
|
73
|
+
return;
|
|
74
|
+
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const statusLines = gitStatusPorcelain(cwd);
|
|
77
|
+
if (statusLines.length === 0) return;
|
|
78
|
+
|
|
79
|
+
const testFileName = basename(filePath);
|
|
80
|
+
const stem = baseNameWithoutTestSuffix(testFileName);
|
|
81
|
+
// `git status --porcelain` reports paths relative to `cwd`; the test's own
|
|
82
|
+
// directory (relative to `cwd`) is the fair comparison, not its absolute path.
|
|
83
|
+
// `relative()` returns '' for the repo root itself, while `dirname()` on a
|
|
84
|
+
// bare file name returns '.' — normalize both to '.' so root-level files compare equal.
|
|
85
|
+
const testDirectoryRelativeToRepo = relative(cwd, dirname(filePath)) || '.';
|
|
86
|
+
|
|
87
|
+
const pairedImplementationChanged = statusLines.some((line) => {
|
|
88
|
+
const changedPath = line.slice(PORCELAIN_STATUS_PREFIX_LENGTH).trim();
|
|
89
|
+
if (
|
|
90
|
+
!parameters.implementationExtensions.some((extension) =>
|
|
91
|
+
changedPath.endsWith(extension),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
return false;
|
|
95
|
+
if (TEST_FILE_PATTERN.test(changedPath)) return false;
|
|
96
|
+
const lastDot = changedPath.lastIndexOf('.');
|
|
97
|
+
const changedBase = basename(
|
|
98
|
+
changedPath,
|
|
99
|
+
lastDot >= 0 ? changedPath.slice(lastDot) : '',
|
|
100
|
+
);
|
|
101
|
+
return (
|
|
102
|
+
changedBase === stem &&
|
|
103
|
+
(dirname(changedPath) || '.') === testDirectoryRelativeToRepo
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!pairedImplementationChanged) return;
|
|
108
|
+
|
|
109
|
+
deny(
|
|
110
|
+
GATE_ID,
|
|
111
|
+
`Creating test ${testFileName} after its paired implementation was already modified ` +
|
|
112
|
+
'(uncommitted). Write the test before or alongside the implementation, not after — a ' +
|
|
113
|
+
'test that passes the first time it runs proves nothing about the change. If this is a ' +
|
|
114
|
+
'regression test for a bug you already reproduced (reproduce first, then pin it), add ' +
|
|
115
|
+
`the marker "${parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH}" in the test content to allow it.`,
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Note: this restores the source guard's (guard-test-despues-de-implementar.mjs) original
|
|
121
|
+
// deny severity. It was warn during the config-key migration; the user's directive is that a
|
|
122
|
+
// gate stays warn only when the defect is genuinely invisible to a hook. Here the defect is a
|
|
123
|
+
// complete, git-verifiable fact, so it denies — with an escape hatch for the legitimate
|
|
124
|
+
// regression-test case. The config key still reads warn* for backward compatibility.
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/block-remote-publish/index.mjs\"",
|
|
20
|
+
"timeout": 30
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
14
24
|
{
|
|
15
25
|
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
16
26
|
"hooks": [
|
|
@@ -311,6 +321,15 @@
|
|
|
311
321
|
"timeout": 30
|
|
312
322
|
}
|
|
313
323
|
]
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"hooks": [
|
|
327
|
+
{
|
|
328
|
+
"type": "command",
|
|
329
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/stop.mjs\"",
|
|
330
|
+
"timeout": 30
|
|
331
|
+
}
|
|
332
|
+
]
|
|
314
333
|
}
|
|
315
334
|
],
|
|
316
335
|
"SessionStart": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Deterministic task tracking for Claude Code: persists tasks the model registers via the CLI, reminds of open tasks on a message counter, and lists active tasks on session start.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|
|
@@ -81,9 +81,13 @@ function readPayload() {
|
|
|
81
81
|
// make something OCCUR deterministically; it is depending on the model's judgment for a
|
|
82
82
|
// judgment call (whether this message describes a new task), same shape as ask-adoption.
|
|
83
83
|
const CLASSIFY_PROMPT =
|
|
84
|
-
'[tasks]
|
|
85
|
-
'
|
|
86
|
-
'
|
|
84
|
+
'[tasks] MANDATORY before you respond: judge whether the message you just received describes ' +
|
|
85
|
+
'a new actionable task (not a question, not small talk, not a follow-up on an already-registered ' +
|
|
86
|
+
'task). If it does, you MUST register it now, before writing your reply — run ' +
|
|
87
|
+
'`claude-gates task add "<title>" [--description <text>] [--size <size>]` from the project root ' +
|
|
88
|
+
'(or the CLI\'s absolute path if `claude-gates` is not on PATH). Do not defer this, do not decide ' +
|
|
89
|
+
'to register it "later", do not silently skip it because the answer seems obvious. If it is not a ' +
|
|
90
|
+
'new task, skip registration — do not fabricate one.';
|
|
87
91
|
|
|
88
92
|
function formatReminder(tasks) {
|
|
89
93
|
const shown = tasks.slice(0, MAX_TASKS_SHOWN);
|