@event4u/agent-config 10.1.0 → 10.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 +1 -1
- package/CHANGELOG.md +110 -167
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
- package/dist/agent-src/commands/roadmap/create.md +4 -1
- package/dist/agent-src/commands/roadmap/next.md +38 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
- package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
- package/dist/agent-src/personas/advisors/first-principles.md +6 -0
- package/dist/agent-src/rules/active-remediation.md +53 -2
- package/dist/agent-src/rules/code-provenance.md +66 -4
- package/dist/agent-src/rules/content-quoting-floor.md +6 -0
- package/dist/agent-src/rules/context-hygiene.md +39 -0
- package/dist/agent-src/rules/design-fidelity.md +49 -3
- package/dist/agent-src/rules/token-budget-discipline.md +3 -2
- package/dist/agent-src/rules/token-efficiency.md +22 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
- package/dist/agent-src/templates/agent-settings.md +12 -28
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
- package/dist/agent-src/templates/persona.md +26 -0
- package/dist/agent-src/templates/roadmaps.md +27 -7
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/dist/agent-src/templates/skill.md +35 -1
- package/dist/cli/commands/commands.candidates.test.js +257 -0
- package/dist/cli/commands/commands.candidates.test.js.map +1 -0
- package/dist/cli/commands/commands.js +199 -0
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
- package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
- package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
- package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
- package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
- package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
- package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
- package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
- package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +881 -204
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +6 -5
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +44 -9
- package/dist/cli-delegate/cmd_settings_get.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +21 -21
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +204 -44
- package/dist/install/install.mjs +9 -21
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1337 -490
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +25 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/_lib/compile_time_toggles.js +4 -3
- package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
- package/dist/server/io/yamlIO.js +0 -2
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -9
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsClasses.js +64 -2
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
- package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +307 -145
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
- package/docs/guidelines/agent-infra/layered-settings.md +1 -2
- package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
- package/docs/guidelines/design-antipatterns.md +79 -10
- package/docs/guidelines/design-fidelity-mechanics.md +120 -2
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/src/config/agent-settings.template.yml +12 -30
- package/src/config/gate-coverage.yml +42 -8
- package/src/config/gate-violation-baselines.json +15 -3
- package/src/config/profiles/balanced.ini +0 -2
- package/src/config/profiles/full.ini +1 -3
- package/src/config/profiles/minimal.ini +0 -2
- package/src/config/review-prompt-binding-baseline.json +19 -0
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
- package/src/scripts/_cli/handoff_sessions.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -0
- package/src/scripts/_lib/agent_settings.ts +25 -2
- package/src/scripts/_lib/bench_telegraph.ts +1 -1
- package/src/scripts/_lib/cc_transcript.ts +25 -0
- package/src/scripts/_lib/compile_time_toggles.ts +4 -3
- package/src/scripts/ai_council/clients.ts +243 -34
- package/src/scripts/cache_realization_report.ts +3 -2
- package/src/scripts/check_ci_local_parity.ts +60 -6
- package/src/scripts/check_review_prompt_binding.ts +553 -0
- package/src/scripts/check_rule_projection_integrity.ts +3 -1
- package/src/scripts/check_suppression_hygiene.ts +19 -3
- package/src/scripts/conformance_scan.ts +282 -9
- package/src/scripts/cost/track.mjs +10 -4
- package/src/scripts/council_cli.ts +33 -3
- package/src/scripts/design_slop_rules.test.ts +37 -6
- package/src/scripts/design_slop_rules.ts +150 -22
- package/src/scripts/dispatch_r2_reviewer.ts +15 -1
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
- package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
- package/src/scripts/hooks/evidence_independence.ts +61 -9
- package/src/scripts/hooks/session_eol_hook.ts +123 -3
- package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
- package/src/scripts/lint_design_antipattern_parity.ts +201 -0
- package/src/scripts/lint_harvest_provenance.ts +483 -0
- package/src/scripts/lint_settings_classes.ts +273 -11
- package/src/scripts/lint_skill_router_head.ts +254 -0
- package/src/scripts/lint_symptom_intake.ts +241 -0
- package/src/scripts/report_consultation_rate.ts +128 -2
- package/src/scripts/report_skill_activation.ts +6 -4
- package/src/scripts/report_skill_obligation_violations.ts +2 -1
- package/src/scripts/run_skill_evals.ts +10 -5
- package/src/scripts/schemas/persona.schema.json +8 -0
- package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
- package/src/scripts/sweep_dead_scan_roots.ts +51 -0
- package/src/server/io/yamlIO.ts +0 -2
- package/src/server/schemas/settings.ts +3 -19
- package/src/shared/settingsClasses.ts +93 -2
- package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* 4. every B row's template default is a conservative value
|
|
16
16
|
* (`false` / `""` / `0` / `[]` / `null` / `{}`) — the invariant that makes
|
|
17
17
|
* "absent" and "declined" the same outcome in a sparse settings file;
|
|
18
|
-
* 5. the contract's own Counts table matches the tallies computed here
|
|
18
|
+
* 5. the contract's own Counts table matches the tallies computed here;
|
|
19
|
+
* 6. every `REMOVED_KEYS` entry names what decides instead, and no key is
|
|
20
|
+
* both removed and live in the template.
|
|
19
21
|
*
|
|
20
22
|
* **Landed at error, not advisory.** The authoring guideline's advisory-first
|
|
21
23
|
* rule exists so a gate cannot be wired to error over a corpus whose hits are
|
|
@@ -48,16 +50,24 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
48
50
|
import { load as parseYaml } from 'js-yaml';
|
|
49
51
|
|
|
50
52
|
import {
|
|
53
|
+
dispositionOf,
|
|
51
54
|
getSettingsLeaf,
|
|
52
55
|
isConservativeDefault,
|
|
53
56
|
isSettingsClass,
|
|
57
|
+
isSettingsDisposition,
|
|
54
58
|
parseDeclaredClassCounts,
|
|
59
|
+
parseDeclaredDispositionCounts,
|
|
55
60
|
parseSettingsClassRows,
|
|
61
|
+
replacementOf,
|
|
56
62
|
SETTINGS_CLASSES,
|
|
63
|
+
SETTINGS_DISPOSITIONS,
|
|
57
64
|
type SettingsClass,
|
|
58
65
|
type SettingsClassRow,
|
|
66
|
+
type SettingsDisposition,
|
|
59
67
|
settingsLeafPaths,
|
|
60
68
|
} from '../shared/settingsClasses.js';
|
|
69
|
+
import { REMOVED_KEYS } from './_lib/agent_settings.js';
|
|
70
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
61
71
|
import { GateLedger } from './_lib/gate_ledger.js';
|
|
62
72
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
63
73
|
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
@@ -117,15 +127,27 @@ export function selfTest(): number {
|
|
|
117
127
|
`\n## Counts\n\n| Class | Keys |\n|---|---|\n| A — preference | ${String(a)} |\n` +
|
|
118
128
|
`| B — consent | ${String(b)} |\n| C — guarded | ${String(c)} |\n` +
|
|
119
129
|
`| **Total** | **${String(total)}** |\n`;
|
|
120
|
-
|
|
130
|
+
// The disposition tallies, in the section the parser scopes to. Defaulted to
|
|
131
|
+
// the clean fixture's shape so a case that does not care about them says so
|
|
132
|
+
// by omission rather than by restating four rows.
|
|
133
|
+
const dispositions = (d: number, u: number, co: number, p: number, total: number): string =>
|
|
134
|
+
`\n## Dispositions\n\n| Disposition | Keys |\n|---|---|\n| derivable | ${String(d)} |\n` +
|
|
135
|
+
`| un-inferrable | ${String(u)} |\n| consent | ${String(co)} |\n| policy | ${String(p)} |\n` +
|
|
136
|
+
`| **Total** | **${String(total)}** |\n`;
|
|
137
|
+
const table = (rows: string, disp = dispositions(0, 1, 1, 0, 2)): string =>
|
|
138
|
+
`# Fixture\n${counts(0, 1, 1, 2)}\n${disp}\n## The table\n\n` +
|
|
139
|
+
`| Key | Class | Default | Why | Disposition |\n|---|---|---|---|---|\n${rows}`;
|
|
121
140
|
|
|
122
|
-
const clean = table(
|
|
141
|
+
const clean = table(
|
|
142
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
143
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n',
|
|
144
|
+
);
|
|
123
145
|
|
|
124
146
|
try {
|
|
125
147
|
return runSelfTest({
|
|
126
148
|
gate: GATE,
|
|
127
|
-
minCases:
|
|
128
|
-
minRejectCases:
|
|
149
|
+
minCases: 12,
|
|
150
|
+
minRejectCases: 10,
|
|
129
151
|
cases: [
|
|
130
152
|
{
|
|
131
153
|
name: 'a template whose every leaf is classified passes',
|
|
@@ -140,7 +162,7 @@ export function selfTest(): number {
|
|
|
140
162
|
seed(
|
|
141
163
|
'unclassified',
|
|
142
164
|
template,
|
|
143
|
-
table('| `beta` | C | `"keep"` | fixture |\n') + counts(0, 0, 1, 1),
|
|
165
|
+
table('| `beta` | C | `"keep"` | fixture | un-inferrable |\n') + counts(0, 0, 1, 1),
|
|
144
166
|
),
|
|
145
167
|
),
|
|
146
168
|
},
|
|
@@ -153,8 +175,9 @@ export function selfTest(): number {
|
|
|
153
175
|
'stale-row',
|
|
154
176
|
template,
|
|
155
177
|
table(
|
|
156
|
-
'| `alpha.one` | B | `false` | fixture
|
|
157
|
-
'| `
|
|
178
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
179
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n' +
|
|
180
|
+
'| `gamma.gone` | A | `1` | fixture | policy |\n',
|
|
158
181
|
),
|
|
159
182
|
),
|
|
160
183
|
),
|
|
@@ -167,7 +190,90 @@ export function selfTest(): number {
|
|
|
167
190
|
seed(
|
|
168
191
|
'bad-class',
|
|
169
192
|
template,
|
|
170
|
-
table(
|
|
193
|
+
table(
|
|
194
|
+
'| `alpha.one` | D | `false` | fixture | consent |\n' +
|
|
195
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n',
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'a leaf with a class but no disposition is rejected',
|
|
202
|
+
expect: 'reject',
|
|
203
|
+
run: () =>
|
|
204
|
+
run(
|
|
205
|
+
seed(
|
|
206
|
+
'no-disposition',
|
|
207
|
+
template,
|
|
208
|
+
table(
|
|
209
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
210
|
+
'| `beta` | C | `"keep"` | fixture |\n',
|
|
211
|
+
dispositions(0, 0, 1, 0, 2),
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'a disposition outside the closed vocabulary is rejected',
|
|
218
|
+
expect: 'reject',
|
|
219
|
+
run: () =>
|
|
220
|
+
run(
|
|
221
|
+
seed(
|
|
222
|
+
'bad-disposition',
|
|
223
|
+
template,
|
|
224
|
+
table(
|
|
225
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
226
|
+
'| `beta` | C | `"keep"` | fixture | obsolete |\n',
|
|
227
|
+
dispositions(0, 0, 1, 0, 2),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'a `derivable` row that names no replacement is rejected — the clause IS the falsifier',
|
|
234
|
+
expect: 'reject',
|
|
235
|
+
run: () =>
|
|
236
|
+
run(
|
|
237
|
+
seed(
|
|
238
|
+
'derivable-without-replacement',
|
|
239
|
+
template,
|
|
240
|
+
table(
|
|
241
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
242
|
+
'| `beta` | C | `"keep"` | fixture | derivable |\n',
|
|
243
|
+
dispositions(1, 0, 1, 0, 2),
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'a `derivable` row that names its replacement passes',
|
|
250
|
+
expect: 'accept',
|
|
251
|
+
run: () =>
|
|
252
|
+
run(
|
|
253
|
+
seed(
|
|
254
|
+
'derivable-with-replacement',
|
|
255
|
+
template,
|
|
256
|
+
table(
|
|
257
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
258
|
+
'| `beta` | C | `"keep"` | fixture | derivable — the probe reads it |\n',
|
|
259
|
+
dispositions(1, 0, 1, 0, 2),
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'a Dispositions table that disagrees with the rows is rejected',
|
|
266
|
+
expect: 'reject',
|
|
267
|
+
run: () =>
|
|
268
|
+
run(
|
|
269
|
+
seed(
|
|
270
|
+
'bad-disposition-counts',
|
|
271
|
+
template,
|
|
272
|
+
table(
|
|
273
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
274
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n',
|
|
275
|
+
dispositions(7, 7, 7, 7, 28),
|
|
276
|
+
),
|
|
171
277
|
),
|
|
172
278
|
),
|
|
173
279
|
},
|
|
@@ -191,8 +297,10 @@ export function selfTest(): number {
|
|
|
191
297
|
seed(
|
|
192
298
|
'bad-counts',
|
|
193
299
|
template,
|
|
194
|
-
`# Fixture\n${counts(9, 9, 9, 27)}\n
|
|
195
|
-
'
|
|
300
|
+
`# Fixture\n${counts(9, 9, 9, 27)}\n${dispositions(0, 1, 1, 0, 2)}\n` +
|
|
301
|
+
'## The table\n\n| Key | Class | Default | Why | Disposition |\n|---|---|---|---|---|\n' +
|
|
302
|
+
'| `alpha.one` | B | `false` | fixture | consent |\n' +
|
|
303
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n',
|
|
196
304
|
),
|
|
197
305
|
),
|
|
198
306
|
},
|
|
@@ -201,6 +309,29 @@ export function selfTest(): number {
|
|
|
201
309
|
expect: 'reject',
|
|
202
310
|
run: () => run(seed('dead-root', null, clean)),
|
|
203
311
|
},
|
|
312
|
+
{
|
|
313
|
+
name: 'a template leaf that is also a REMOVED_KEYS entry is rejected (check 6)',
|
|
314
|
+
expect: 'reject',
|
|
315
|
+
// `REMOVED_KEYS` is compiled into the loader, so a fixture root
|
|
316
|
+
// cannot seed it — but it can seed the other side of the
|
|
317
|
+
// contradiction. A fixture that declares `quality.wait_for_remote_ci`
|
|
318
|
+
// as a live, fully classified leaf is a tree where the loader
|
|
319
|
+
// would warn "ignored" about a value the schema honours, which
|
|
320
|
+
// is exactly the state check 6 exists to refuse. Classified and
|
|
321
|
+
// counted correctly on purpose: every other check passes here,
|
|
322
|
+
// so a rejection can only come from check 6.
|
|
323
|
+
run: () =>
|
|
324
|
+
run(
|
|
325
|
+
seed(
|
|
326
|
+
'removed-and-live',
|
|
327
|
+
'quality:\n wait_for_remote_ci: false\nbeta: "keep"\n',
|
|
328
|
+
table(
|
|
329
|
+
'| `quality.wait_for_remote_ci` | B | `false` | fixture | consent |\n' +
|
|
330
|
+
'| `beta` | C | `"keep"` | fixture | un-inferrable |\n',
|
|
331
|
+
),
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
},
|
|
204
335
|
],
|
|
205
336
|
});
|
|
206
337
|
} finally {
|
|
@@ -331,6 +462,98 @@ function main(): number {
|
|
|
331
462
|
ledger.complete(target);
|
|
332
463
|
}
|
|
333
464
|
|
|
465
|
+
// --- the disposition axis -------------------------------------------------
|
|
466
|
+
// Orthogonal to A/B/C: that answers who may WRITE a key, this answers whether
|
|
467
|
+
// the key should EXIST. Checked against the same leaf set, in the same gate,
|
|
468
|
+
// so a key with no disposition fails the mechanism that already counts it —
|
|
469
|
+
// rather than sitting in a second artefact that can drift from the template.
|
|
470
|
+
const dispositionTally: Record<SettingsDisposition, number> = {
|
|
471
|
+
derivable: 0,
|
|
472
|
+
'un-inferrable': 0,
|
|
473
|
+
consent: 0,
|
|
474
|
+
policy: 0,
|
|
475
|
+
};
|
|
476
|
+
for (const leaf of leaves) {
|
|
477
|
+
const row = rowByKey.get(leaf);
|
|
478
|
+
if (row === undefined) {
|
|
479
|
+
continue; // already reported as unclassified above
|
|
480
|
+
}
|
|
481
|
+
const label = dispositionOf(row.disposition);
|
|
482
|
+
if (label === null || label === '') {
|
|
483
|
+
findings.push(
|
|
484
|
+
`${CONTRACT_RELATIVE}:${String(row.line)} \`${leaf}\` has no disposition — add one of ` +
|
|
485
|
+
`${SETTINGS_DISPOSITIONS.join(' | ')}. A key with no disposition is the finding: ` +
|
|
486
|
+
'nobody has said whether it should exist.',
|
|
487
|
+
);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (!isSettingsDisposition(label)) {
|
|
491
|
+
findings.push(
|
|
492
|
+
`${CONTRACT_RELATIVE}:${String(row.line)} \`${leaf}\` has disposition '${label}' — ` +
|
|
493
|
+
`allowed: ${SETTINGS_DISPOSITIONS.join(' | ')}.`,
|
|
494
|
+
);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
dispositionTally[label] += 1;
|
|
498
|
+
if (label === 'derivable' && replacementOf(row.disposition) === '') {
|
|
499
|
+
// The replacement clause is what makes `derivable` falsifiable. Without
|
|
500
|
+
// it the label degrades into a synonym for "inconvenient to keep", and
|
|
501
|
+
// the direction of this axis pushes every borderline row toward it.
|
|
502
|
+
findings.push(
|
|
503
|
+
`${CONTRACT_RELATIVE}:${String(row.line)} \`${leaf}\` is \`derivable\` but names no ` +
|
|
504
|
+
'replacement — write `derivable — <what decides instead>`. A derivable row that ' +
|
|
505
|
+
'cannot name its replacement is reclassified, not deleted.',
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// The anti-regrowth ratchet. `derivable` is a deletion queue, so its size is
|
|
511
|
+
// a debt count and may only fall: a NEW key classified `derivable` is a key
|
|
512
|
+
// that should not have been added, and it raises the number. The 56-day
|
|
513
|
+
// non-stagnation clause in the shared baseline mechanism is the other half —
|
|
514
|
+
// a queue that never drains is a direction nobody is walking.
|
|
515
|
+
//
|
|
516
|
+
// Deliberately scoped to the real repo root. Under `--root` (self-test
|
|
517
|
+
// fixtures) the baseline describes a different tree entirely, and judging a
|
|
518
|
+
// two-key fixture against this repository's surface would make every fixture
|
|
519
|
+
// report a loose ratchet.
|
|
520
|
+
if (_rootOverride(process.argv.slice(2)) === null) {
|
|
521
|
+
const verdict = checkRatchet({
|
|
522
|
+
gate: `${GATE}:derivable-surface`,
|
|
523
|
+
actual: dispositionTally.derivable,
|
|
524
|
+
repoRoot: REPO_ROOT,
|
|
525
|
+
});
|
|
526
|
+
if (!verdict.ok) {
|
|
527
|
+
findings.push(verdict.message);
|
|
528
|
+
} else if (verdict.status === 'improved' && !QUIET) {
|
|
529
|
+
process.stdout.write(`ℹ️ ${verdict.message}\n`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const declaredDisposition = parseDeclaredDispositionCounts(contractText);
|
|
534
|
+
const dispositionExpected: Array<[string, number | null, number]> = [
|
|
535
|
+
['derivable', declaredDisposition.derivable, dispositionTally.derivable],
|
|
536
|
+
['un-inferrable', declaredDisposition['un-inferrable'], dispositionTally['un-inferrable']],
|
|
537
|
+
['consent', declaredDisposition.consent, dispositionTally.consent],
|
|
538
|
+
['policy', declaredDisposition.policy, dispositionTally.policy],
|
|
539
|
+
['Total', declaredDisposition.total, leaves.length],
|
|
540
|
+
];
|
|
541
|
+
for (const [label, stated, actual] of dispositionExpected) {
|
|
542
|
+
if (stated === null) {
|
|
543
|
+
findings.push(
|
|
544
|
+
`${CONTRACT_RELATIVE} the Dispositions table has no '${label}' row — a published count ` +
|
|
545
|
+
'with no row is a count nothing can check.',
|
|
546
|
+
);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (stated !== actual) {
|
|
550
|
+
findings.push(
|
|
551
|
+
`${CONTRACT_RELATIVE} Dispositions says ${label} = ${String(stated)}, the table holds ` +
|
|
552
|
+
`${String(actual)} — a derived number beside a mechanism that can compute it.`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
334
557
|
const declared = parseDeclaredClassCounts(contractText);
|
|
335
558
|
const expected: Array<[string, number | null, number]> = [
|
|
336
559
|
['A', declared.A, tally.A],
|
|
@@ -354,6 +577,45 @@ function main(): number {
|
|
|
354
577
|
}
|
|
355
578
|
}
|
|
356
579
|
|
|
580
|
+
// Check 6 — the DELETION side of the surface.
|
|
581
|
+
//
|
|
582
|
+
// Checks 1-5 all police what is added: a new leaf needs a row, a class, a
|
|
583
|
+
// disposition, a conservative default. Nothing policed what is removed, and
|
|
584
|
+
// that is the asymmetry this axis lives on — the whole direction of the
|
|
585
|
+
// roadmap behind it is deletion, so the un-gated half was the half doing the
|
|
586
|
+
// work. Two contradictions are decidable here and neither is visible to the
|
|
587
|
+
// loader that prints the warning:
|
|
588
|
+
//
|
|
589
|
+
// a. a reason that names no replacement. The `derivable` rows above are
|
|
590
|
+
// already held to "name what decides instead" because that clause is the
|
|
591
|
+
// falsifier; a deleted key is the same claim after the fact, and a
|
|
592
|
+
// warning that says only "removed" sends the reader looking for a
|
|
593
|
+
// successor key that does not exist.
|
|
594
|
+
// b. a key that is BOTH removed and live. Re-adding a leaf while its
|
|
595
|
+
// `REMOVED_KEYS` entry stands makes the loader warn "ignored" about a
|
|
596
|
+
// value the schema now honours — the reader is told the opposite of what
|
|
597
|
+
// happens.
|
|
598
|
+
//
|
|
599
|
+
// What this does NOT check, stated because the step it serves asks for it:
|
|
600
|
+
// that the replacement mechanism actually EXISTS, and that its commit
|
|
601
|
+
// precedes the deletion. Neither is decidable from these two files; both stay
|
|
602
|
+
// model-carried, exactly as the A/B/C judgement itself does.
|
|
603
|
+
const liveLeaves = new Set(leaves);
|
|
604
|
+
for (const [removedKey, reason] of REMOVED_KEYS) {
|
|
605
|
+
if (reason.trim() === '') {
|
|
606
|
+
findings.push(
|
|
607
|
+
`src/scripts/_lib/agent_settings.ts \`${removedKey}\` is in REMOVED_KEYS with an empty ` +
|
|
608
|
+
'reason — name what decides instead, not that it is gone.',
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (liveLeaves.has(removedKey)) {
|
|
612
|
+
findings.push(
|
|
613
|
+
`${TEMPLATE_RELATIVE} \`${removedKey}\` is a live template leaf AND a REMOVED_KEYS ` +
|
|
614
|
+
'entry — the loader would warn that a honoured value is ignored. Drop one side.',
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
357
619
|
ledger.report();
|
|
358
620
|
|
|
359
621
|
if (findings.length > 0) {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Router-head gate — makes the published K6 cap true for NEW skills.
|
|
4
|
+
*
|
|
5
|
+
* `src/agent-src/templates/skill.md` has carried "K6: Under 400 lines" as a
|
|
6
|
+
* checklist item since long before this gate. A checklist item is a statement
|
|
7
|
+
* of intent that nothing checks; four skills exceed it today. This gate turns
|
|
8
|
+
* the existing contract into an enforced one, without re-litigating the number.
|
|
9
|
+
*
|
|
10
|
+
* The rule: a SKILL.md over MAX_HEAD_LINES must be a router head — an entry
|
|
11
|
+
* file (when-to-use, mode table, routing) plus per-mode bodies in `tasks/` or
|
|
12
|
+
* `references/` inside the skill folder. Over the cap with neither directory
|
|
13
|
+
* present is a monolith and fails.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THIS GATE DOES NOT CLAIM. It is not an activation or token argument.
|
|
16
|
+
* The host loads SKILL.md whole on trigger, and whether it then follows a
|
|
17
|
+
* pointer into `tasks/` is host behaviour nothing in this tree observes — so
|
|
18
|
+
* "splitting saves tokens on trigger" is unmeasurable here and is not asserted.
|
|
19
|
+
* ADR-202's lesson binds: an activation claim goes behind a measured instrument
|
|
20
|
+
* or it is not made. The defensible claim is narrower and sufficient: the cap
|
|
21
|
+
* was already the repo's position, and now a new skill cannot quietly ignore it.
|
|
22
|
+
*
|
|
23
|
+
* GRANDFATHERING is a shrink-only ratchet. GRANDFATHERED below is seeded with
|
|
24
|
+
* the measured offender set. Entries may be REMOVED when a skill is
|
|
25
|
+
* restructured or drops under the cap; adding one is the move this gate exists
|
|
26
|
+
* to prevent, and the diff makes any addition visible in review. The list is an
|
|
27
|
+
* inline constant rather than a JSON baseline on purpose — a baseline file
|
|
28
|
+
* grows without anyone reading it, and this one should be read every time.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ./scripts-run src/scripts/lint_skill_router_head
|
|
32
|
+
* ./scripts-run src/scripts/lint_skill_router_head --quiet
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 = clean · 2 = any finding OR usage error.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
42
|
+
|
|
43
|
+
const _FILE = fileURLToPath(import.meta.url);
|
|
44
|
+
const _HERE = path.dirname(_FILE);
|
|
45
|
+
const REPO = path.resolve(_HERE, '..', '..');
|
|
46
|
+
const SKILLS_REL = 'src/skills';
|
|
47
|
+
|
|
48
|
+
/** The K6 cap, in lines. Mirrors src/agent-src/templates/skill.md § K6. */
|
|
49
|
+
export const MAX_HEAD_LINES = 400;
|
|
50
|
+
|
|
51
|
+
/** Directories a router head may route into. `references/` is the established
|
|
52
|
+
* convention (9 skills use it today); `tasks/` is permitted by the contract
|
|
53
|
+
* for per-mode procedure bodies. */
|
|
54
|
+
export const MODE_BODY_DIRS: readonly string[] = ['tasks', 'references'];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shrink-only allowlist — skills already over the cap when the gate landed.
|
|
58
|
+
* Measured, not guessed. REMOVE entries as skills are restructured; never add.
|
|
59
|
+
*/
|
|
60
|
+
export const GRANDFATHERED: readonly string[] = [
|
|
61
|
+
'ai-council', // 1055
|
|
62
|
+
'skill-writing', // 767
|
|
63
|
+
'roadmap-management', // 552
|
|
64
|
+
'quality-tools', // 445
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export interface Finding {
|
|
68
|
+
readonly skill: string;
|
|
69
|
+
readonly lines: number;
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Count lines the way `wc -l` does, so the gate and a shell check agree. */
|
|
74
|
+
export function countLines(text: string): number {
|
|
75
|
+
if (text.length === 0) return 0;
|
|
76
|
+
const n = text.split('\n').length;
|
|
77
|
+
return text.endsWith('\n') ? n - 1 : n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Does this skill folder carry at least one mode-body directory? */
|
|
81
|
+
export function hasModeBodies(skillDir: string, dirs: readonly string[] = MODE_BODY_DIRS): boolean {
|
|
82
|
+
return dirs.some((d) => {
|
|
83
|
+
try {
|
|
84
|
+
return fs.statSync(path.join(skillDir, d)).isDirectory();
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SkillHead {
|
|
92
|
+
readonly name: string;
|
|
93
|
+
readonly lines: number;
|
|
94
|
+
readonly hasBodies: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read every `<skillsRoot>/<name>/SKILL.md`. */
|
|
98
|
+
export function collectSkillHeads(skillsRoot: string): SkillHead[] {
|
|
99
|
+
let entries: fs.Dirent[];
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const heads: SkillHead[] = [];
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (!e.isDirectory()) continue;
|
|
108
|
+
const dir = path.join(skillsRoot, e.name);
|
|
109
|
+
const head = path.join(dir, 'SKILL.md');
|
|
110
|
+
let text: string;
|
|
111
|
+
try {
|
|
112
|
+
text = fs.readFileSync(head, 'utf-8');
|
|
113
|
+
} catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
heads.push({ name: e.name, lines: countLines(text), hasBodies: hasModeBodies(dir) });
|
|
117
|
+
}
|
|
118
|
+
return heads.sort((a, b) => a.name.localeCompare(b.name));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A head fails when it is over the cap, carries no mode-body directory, and is
|
|
123
|
+
* not grandfathered. Two separate results are reported so the caller can
|
|
124
|
+
* distinguish a live violation from allowlist drift.
|
|
125
|
+
*/
|
|
126
|
+
export function evaluate(
|
|
127
|
+
heads: readonly SkillHead[],
|
|
128
|
+
grandfathered: readonly string[] = GRANDFATHERED,
|
|
129
|
+
max: number = MAX_HEAD_LINES,
|
|
130
|
+
): { readonly findings: Finding[]; readonly staleAllowlist: string[] } {
|
|
131
|
+
const allow = new Set(grandfathered);
|
|
132
|
+
const findings: Finding[] = [];
|
|
133
|
+
const stillOver = new Set<string>();
|
|
134
|
+
|
|
135
|
+
for (const h of heads) {
|
|
136
|
+
if (h.lines <= max) continue;
|
|
137
|
+
stillOver.add(h.name);
|
|
138
|
+
if (allow.has(h.name)) continue;
|
|
139
|
+
if (h.hasBodies) continue;
|
|
140
|
+
findings.push({
|
|
141
|
+
skill: h.name,
|
|
142
|
+
lines: h.lines,
|
|
143
|
+
message:
|
|
144
|
+
`SKILL.md is ${h.lines} lines (cap ${max}) with no ${MODE_BODY_DIRS.map((d) => `${d}/`).join(' or ')} ` +
|
|
145
|
+
'directory — over the cap a skill restructures as a router head (entry head + one file per mode), ' +
|
|
146
|
+
'per templates/skill.md § The router-head contract. The allowlist is shrink-only and does not accept new entries.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// An allowlist entry for a skill that no longer exceeds the cap (or no
|
|
151
|
+
// longer exists) is the ratchet failing to tighten: the entry silently
|
|
152
|
+
// buys nothing and would re-permit a regression. Reported, not fatal —
|
|
153
|
+
// removing it is a one-line follow-up, not a reason to red the build.
|
|
154
|
+
const staleAllowlist = grandfathered.filter((g) => !stillOver.has(g));
|
|
155
|
+
|
|
156
|
+
return { findings, staleAllowlist };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
class ExitCode extends Error {
|
|
162
|
+
readonly code: number;
|
|
163
|
+
constructor(code: number) {
|
|
164
|
+
super(`exit ${code}`);
|
|
165
|
+
this.code = code;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseArgs(argv: string[]): { readonly quiet: boolean } {
|
|
170
|
+
let quiet = false;
|
|
171
|
+
for (const a of argv) {
|
|
172
|
+
if (a === '--quiet') {
|
|
173
|
+
quiet = true;
|
|
174
|
+
} else if (a === '-h' || a === '--help') {
|
|
175
|
+
process.stdout.write('usage: lint_skill_router_head [--quiet]\n');
|
|
176
|
+
throw new ExitCode(0);
|
|
177
|
+
} else {
|
|
178
|
+
process.stderr.write(`❌ lint_skill_router_head: unrecognized argument: ${a}\n`);
|
|
179
|
+
throw new ExitCode(2);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { quiet };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
186
|
+
const args = parseArgs(argv);
|
|
187
|
+
|
|
188
|
+
// The scanned root is the scope signal. A moved or renamed src/skills would
|
|
189
|
+
// otherwise produce zero heads and a clean green over nothing at all.
|
|
190
|
+
try {
|
|
191
|
+
assertWatchlistResolves({
|
|
192
|
+
gate: 'lint_skill_router_head',
|
|
193
|
+
candidates: [`${SKILLS_REL}/skill-writing/SKILL.md`],
|
|
194
|
+
repoRoot: REPO,
|
|
195
|
+
});
|
|
196
|
+
} catch (exc) {
|
|
197
|
+
if (exc instanceof DeadScopeError) {
|
|
198
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
199
|
+
return 2;
|
|
200
|
+
}
|
|
201
|
+
throw exc;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const heads = collectSkillHeads(path.join(REPO, SKILLS_REL));
|
|
205
|
+
const { findings, staleAllowlist } = evaluate(heads);
|
|
206
|
+
|
|
207
|
+
for (const s of staleAllowlist) {
|
|
208
|
+
process.stderr.write(
|
|
209
|
+
`⚠️ lint_skill_router_head: '${s}' is grandfathered but no longer over the cap — remove it from GRANDFATHERED so the ratchet keeps its teeth\n`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (findings.length > 0) {
|
|
214
|
+
for (const f of findings) {
|
|
215
|
+
process.stderr.write(`❌ ${SKILLS_REL}/${f.skill}/SKILL.md: ${f.message}\n`);
|
|
216
|
+
}
|
|
217
|
+
process.stderr.write(`❌ lint_skill_router_head: ${findings.length} finding(s)\n`);
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!args.quiet) {
|
|
222
|
+
const over = heads.filter((h) => h.lines > MAX_HEAD_LINES).length;
|
|
223
|
+
process.stdout.write(
|
|
224
|
+
`✅ lint_skill_router_head: ${heads.length} skill head(s) scanned · ` +
|
|
225
|
+
`${over} over the ${MAX_HEAD_LINES}-line cap, all grandfathered or routed · ` +
|
|
226
|
+
`allowlist holds ${GRANDFATHERED.length} entry(ies), shrink-only\n`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Robust "am I the entry script?" — realpath-compares argv[1] to this file. */
|
|
233
|
+
function _isCliEntry(): boolean {
|
|
234
|
+
const a = process.argv[1];
|
|
235
|
+
if (!a) return false;
|
|
236
|
+
if (a === _FILE || pathToFileURL(path.resolve(a)).href === import.meta.url) return true;
|
|
237
|
+
try {
|
|
238
|
+
return fs.realpathSync(a) === fs.realpathSync(_FILE);
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (_isCliEntry()) {
|
|
244
|
+
try {
|
|
245
|
+
process.exit(main());
|
|
246
|
+
} catch (exc) {
|
|
247
|
+
if (exc instanceof ExitCode) {
|
|
248
|
+
process.exit(exc.code);
|
|
249
|
+
}
|
|
250
|
+
throw exc;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { REPO, SKILLS_REL, ExitCode };
|