@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,251 +1,256 @@
|
|
|
1
|
-
// sdd-specs — denies implementation (a write to the catalog moving a feature past
|
|
2
|
-
// `spec_ready`, or a delegation prompt that implements) when the feature/task it targets
|
|
3
|
-
// has no non-empty contract on disk (requirements/design/tasks or brief/asserts).
|
|
4
|
-
// Migrated from ~/.claude/hooks/guard-sdd-specs.mjs.
|
|
5
|
-
//
|
|
6
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
7
|
-
// catalogLocations paths (relative to the project root) searched for the feature
|
|
8
|
-
// catalog. Replaces the built-in list wholesale.
|
|
9
|
-
// exemptSubagents subagent types exempt from the spec requirement (read-only
|
|
10
|
-
// stages of the pipeline that cannot require a contract of
|
|
11
|
-
// themselves). 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
|
-
// ── Auto-off when there is no SDD harness ───────────────────────────────────────────
|
|
16
|
-
// This gate only acts once a catalog is found at one of `catalogLocations` (or the
|
|
17
|
-
// project's own `.ai/config.json` declares adoption). A project that never adopted the
|
|
18
|
-
// spec-driven harness has no catalog and no declared adoption, so every check below is
|
|
19
|
-
// skipped and the write/delegation is allowed — imposing phases on a project that never
|
|
20
|
-
// asked for them is the false positive that gets a gate disabled.
|
|
21
|
-
//
|
|
22
|
-
// ── Two surfaces inspected ───────────────────────────────────────────────────────────
|
|
23
|
-
// A write to the catalog is checked against the catalog's own declared statuses: a
|
|
24
|
-
// feature marked `spec_ready`/`in_progress`/`done` needs its contract tree in place. A
|
|
25
|
-
// delegation prompt is checked when it declares a STANDARD/HIGH-RISK level and an
|
|
26
|
-
// implementation verb: the contract tree must have non-empty requirements/design/tasks
|
|
27
|
-
// (or brief/asserts) for the feature it targets.
|
|
28
|
-
|
|
29
|
-
import { existsSync, statSync } from 'node:fs';
|
|
30
|
-
import { dirname, join } from 'node:path';
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1
|
+
// sdd-specs — denies implementation (a write to the catalog moving a feature past
|
|
2
|
+
// `spec_ready`, or a delegation prompt that implements) when the feature/task it targets
|
|
3
|
+
// has no non-empty contract on disk (requirements/design/tasks or brief/asserts).
|
|
4
|
+
// Migrated from ~/.claude/hooks/guard-sdd-specs.mjs.
|
|
5
|
+
//
|
|
6
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
7
|
+
// catalogLocations paths (relative to the project root) searched for the feature
|
|
8
|
+
// catalog. Replaces the built-in list wholesale.
|
|
9
|
+
// exemptSubagents subagent types exempt from the spec requirement (read-only
|
|
10
|
+
// stages of the pipeline that cannot require a contract of
|
|
11
|
+
// themselves). 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
|
+
// ── Auto-off when there is no SDD harness ───────────────────────────────────────────
|
|
16
|
+
// This gate only acts once a catalog is found at one of `catalogLocations` (or the
|
|
17
|
+
// project's own `.ai/config.json` declares adoption). A project that never adopted the
|
|
18
|
+
// spec-driven harness has no catalog and no declared adoption, so every check below is
|
|
19
|
+
// skipped and the write/delegation is allowed — imposing phases on a project that never
|
|
20
|
+
// asked for them is the false positive that gets a gate disabled.
|
|
21
|
+
//
|
|
22
|
+
// ── Two surfaces inspected ───────────────────────────────────────────────────────────
|
|
23
|
+
// A write to the catalog is checked against the catalog's own declared statuses: a
|
|
24
|
+
// feature marked `spec_ready`/`in_progress`/`done` needs its contract tree in place. A
|
|
25
|
+
// delegation prompt is checked when it declares a STANDARD/HIGH-RISK level and an
|
|
26
|
+
// implementation verb: the contract tree must have non-empty requirements/design/tasks
|
|
27
|
+
// (or brief/asserts) for the feature it targets.
|
|
28
|
+
|
|
29
|
+
import { existsSync, statSync } from 'node:fs';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import {
|
|
32
|
+
runGate,
|
|
33
|
+
deny,
|
|
34
|
+
toolInGroups,
|
|
35
|
+
writtenContentOf,
|
|
36
|
+
writtenPathOf,
|
|
37
|
+
delegationPromptOf,
|
|
38
|
+
} from '../../lib/hook-io.mjs';
|
|
39
|
+
|
|
40
|
+
const GATE_ID = 'sdd-specs';
|
|
41
|
+
const CONFIG_KEY = 'requireSpecBeforeImplementing';
|
|
42
|
+
|
|
43
|
+
const CATALOG_FILE_NAME = 'feature_list.json';
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CATALOG_LOCATIONS = [
|
|
46
|
+
join('.ai', CATALOG_FILE_NAME),
|
|
47
|
+
CATALOG_FILE_NAME,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const DEFAULT_EXEMPT_SUBAGENTS = [
|
|
51
|
+
'explore',
|
|
52
|
+
'plan',
|
|
53
|
+
'scout',
|
|
54
|
+
'revision',
|
|
55
|
+
'contraste',
|
|
56
|
+
'test-planner',
|
|
57
|
+
'qa',
|
|
58
|
+
'ui',
|
|
59
|
+
'ux',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const ADVANCED_STATUSES = new Set(['spec_ready', 'in_progress', 'done']);
|
|
63
|
+
|
|
64
|
+
/** Files that count as a feature's contract; at least one must be non-empty. */
|
|
65
|
+
const CONTRACT_FILES = [
|
|
66
|
+
'requirements.md',
|
|
67
|
+
'design.md',
|
|
68
|
+
'tasks.md',
|
|
69
|
+
'brief.md',
|
|
70
|
+
'asserts.md',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function withWordBoundary(alternation) {
|
|
74
|
+
return new RegExp(
|
|
75
|
+
`(?<![\\p{L}\\p{N}_])(${alternation})(?![\\p{L}\\p{N}_])`,
|
|
76
|
+
'iu',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const IMPLEMENTATION_VERBS = withWordBoundary(
|
|
81
|
+
'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
|
|
82
|
+
'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
|
|
83
|
+
'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
|
|
84
|
+
'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
|
|
85
|
+
'escrib(í|e)|escribir|implement\\w*|writ(e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
|
|
86
|
+
'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
/** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
|
|
90
|
+
* the plugin-wide convention (see risk-level.mjs). */
|
|
91
|
+
const DEMANDING_LEVEL_PATTERN =
|
|
92
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
|
|
93
|
+
const EXEMPT_LEVEL_PATTERN =
|
|
94
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
|
|
95
|
+
|
|
96
|
+
function fileExistsNonEmpty(path) {
|
|
97
|
+
try {
|
|
98
|
+
return existsSync(path) && statSync(path).size > 0;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findCatalog(catalogLocations, writtenPath) {
|
|
105
|
+
const roots = [process.cwd()];
|
|
106
|
+
// A write can target a catalog that lives elsewhere than cwd (a monorepo subpackage's
|
|
107
|
+
// own .ai/feature_list.json): also resolve catalogLocations relative to the directory
|
|
108
|
+
// of the file actually being written, not only the process cwd.
|
|
109
|
+
if (writtenPath) roots.push(dirname(writtenPath));
|
|
110
|
+
for (const root of roots) {
|
|
111
|
+
for (const relative of catalogLocations) {
|
|
112
|
+
const path = join(root, relative);
|
|
113
|
+
if (existsSync(path)) return path;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Discovers the contract tree root next to wherever the catalog lives, or `.ai/features`. */
|
|
120
|
+
function contractTreeRootFor(catalogPath) {
|
|
121
|
+
const candidates = catalogPath
|
|
122
|
+
? [join(dirname(catalogPath), 'features')]
|
|
123
|
+
: [];
|
|
124
|
+
candidates.push(join(process.cwd(), '.ai', 'features'));
|
|
125
|
+
candidates.push(join(process.cwd(), 'features'));
|
|
126
|
+
for (const candidate of candidates) {
|
|
127
|
+
if (existsSync(candidate)) return candidate;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function contractExistsFor(treeRoot, featureName) {
|
|
133
|
+
if (!treeRoot) return false;
|
|
134
|
+
const featureDirectory = join(treeRoot, featureName);
|
|
135
|
+
if (!existsSync(featureDirectory)) return false;
|
|
136
|
+
return CONTRACT_FILES.some((file) =>
|
|
137
|
+
fileExistsNonEmpty(join(featureDirectory, file)),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseJsonOrNull(text) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(text);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Denies the single first advanced-status feature in the new catalog content that has
|
|
150
|
+
* no contract on disk, or does nothing when every advanced feature has one. */
|
|
151
|
+
function denyIfAdvancedFeatureLacksContract(features, treeRoot) {
|
|
152
|
+
for (const feature of features) {
|
|
153
|
+
if (!feature?.name || !ADVANCED_STATUSES.has(feature.status)) continue;
|
|
154
|
+
if (contractExistsFor(treeRoot, feature.name)) continue;
|
|
155
|
+
deny(
|
|
156
|
+
GATE_ID,
|
|
157
|
+
`Feature '${feature.name}' is set to '${feature.status}' but has no non-empty ` +
|
|
158
|
+
`contract (${CONTRACT_FILES.join(', ')}) under the discovered contract tree.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkCatalogWrite(toolInput, catalogPath, treeRoot) {
|
|
164
|
+
const target = writtenPathOf(toolInput);
|
|
165
|
+
if (!catalogPath || !target.includes(CATALOG_FILE_NAME)) return;
|
|
166
|
+
|
|
167
|
+
const rawContent = writtenContentOf(toolInput);
|
|
168
|
+
const parsed = parseJsonOrNull(rawContent);
|
|
169
|
+
if (parsed === null) {
|
|
170
|
+
// Fail-closed, not fail-open: a write whose content does not parse as JSON but
|
|
171
|
+
// still targets the catalog is suspicious on its own -- a spec_ready/done transition
|
|
172
|
+
// hidden behind a malformed payload must not be silently allowed through.
|
|
173
|
+
deny(
|
|
174
|
+
GATE_ID,
|
|
175
|
+
`The write to ${CATALOG_FILE_NAME} does not parse as JSON. A catalog write that ` +
|
|
176
|
+
'cannot be verified for the spec-contract invariant is not allowed; fix the JSON ' +
|
|
177
|
+
'or write valid content.',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const features = Array.isArray(parsed?.features) ? parsed.features : [];
|
|
181
|
+
denyIfAdvancedFeatureLacksContract(features, treeRoot);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isExemptDelegation(toolInput, prompt, exemptSubagents) {
|
|
185
|
+
const subagentType = String(
|
|
186
|
+
toolInput.subagent_type ?? toolInput.subagentType ?? '',
|
|
187
|
+
).toLowerCase();
|
|
188
|
+
if (exemptSubagents.includes(subagentType)) return true;
|
|
189
|
+
if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
|
|
190
|
+
if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
|
|
191
|
+
if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function featureNamesCitedIn(prompt) {
|
|
196
|
+
const pattern = /\.(?:ai)[\\/]features[\\/]([\w.@-]+)/gi;
|
|
197
|
+
const names = [];
|
|
198
|
+
let match;
|
|
199
|
+
while ((match = pattern.exec(prompt)) !== null) names.push(match[1]);
|
|
200
|
+
return names;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function checkDelegation(toolInput, treeRoot, exemptSubagents) {
|
|
204
|
+
const prompt = delegationPromptOf(toolInput);
|
|
205
|
+
if (!prompt.trim()) return;
|
|
206
|
+
if (isExemptDelegation(toolInput, prompt, exemptSubagents)) return;
|
|
207
|
+
|
|
208
|
+
const citedFeatures = featureNamesCitedIn(prompt);
|
|
209
|
+
if (citedFeatures.length === 0) return; // no citation: nothing this gate can check
|
|
210
|
+
|
|
211
|
+
const missing = citedFeatures.filter(
|
|
212
|
+
(feature) => !contractExistsFor(treeRoot, feature),
|
|
213
|
+
);
|
|
214
|
+
if (missing.length === citedFeatures.length) {
|
|
215
|
+
deny(
|
|
216
|
+
GATE_ID,
|
|
217
|
+
`This implementation delegation cites feature(s) [${missing.join(', ')}] with no ` +
|
|
218
|
+
`non-empty contract (${CONTRACT_FILES.join(', ')}) on disk. Write the contract ` +
|
|
219
|
+
'before implementing.',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
runGate(
|
|
225
|
+
{
|
|
226
|
+
id: GATE_ID,
|
|
227
|
+
configKey: CONFIG_KEY,
|
|
228
|
+
enabledByDefault: false,
|
|
229
|
+
defaultParams: {
|
|
230
|
+
catalogLocations: DEFAULT_CATALOG_LOCATIONS,
|
|
231
|
+
exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
({ toolName, toolInput, parameters }) => {
|
|
235
|
+
const isWrite = toolInGroups(toolName, ['write']);
|
|
236
|
+
const isDelegation = toolInGroups(toolName, ['delegation']);
|
|
237
|
+
if (!isWrite && !isDelegation) return;
|
|
238
|
+
|
|
239
|
+
const catalogLocations =
|
|
240
|
+
parameters.catalogLocations ?? DEFAULT_CATALOG_LOCATIONS;
|
|
241
|
+
const catalogPath = findCatalog(
|
|
242
|
+
catalogLocations,
|
|
243
|
+
isWrite ? writtenPathOf(toolInput) : null,
|
|
244
|
+
);
|
|
245
|
+
if (!catalogPath) return; // no SDD harness adopted: stay silent
|
|
246
|
+
|
|
247
|
+
const treeRoot = contractTreeRootFor(catalogPath);
|
|
248
|
+
|
|
249
|
+
if (isWrite) checkCatalogWrite(toolInput, catalogPath, treeRoot);
|
|
250
|
+
if (isDelegation) {
|
|
251
|
+
const exemptSubagents =
|
|
252
|
+
parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
|
|
253
|
+
checkDelegation(toolInput, treeRoot, exemptSubagents);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
);
|