@event4u/agent-config 10.1.0 → 10.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 +1 -1
- package/CHANGELOG.md +109 -0
- package/README.md +1 -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/rules/context-hygiene.md +39 -0
- 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/roadmap-writing/SKILL.md +1 -1
- 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/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/dist/cli/registry.js +1 -1
- 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-E4RUZKXB.js} +165 -38
- 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 +11 -11
- 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 +88 -20
- package/dist/install/install.mjs +9 -21
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1336 -489
- 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/layered-settings.md +1 -2
- package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
- 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-violation-baselines.json +9 -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/scripts/_cli/cmd_session_recycle.ts +66 -9
- package/src/scripts/_cli/handoff_sessions.ts +3 -2
- 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 +228 -33
- package/src/scripts/cache_realization_report.ts +3 -2
- package/src/scripts/check_rule_projection_integrity.ts +3 -1
- 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/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/lint_settings_classes.ts +273 -11
- package/src/scripts/lint_symptom_intake.ts +241 -0
- package/src/scripts/report_consultation_rate.ts +2 -1
- package/src/scripts/report_skill_activation.ts +6 -4
- package/src/scripts/report_skill_obligation_violations.ts +2 -1
- 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,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Warn-only gate: operator symptom entries under `agents/tickets/symptoms/`
|
|
4
|
+
* must be resolved within 30 days of the reported date.
|
|
5
|
+
*
|
|
6
|
+
* "Resolved" means the entry carries exactly one of the two blocks the
|
|
7
|
+
* directory's `README.md` defines — `## confirmed:` (a defect with `file:line`
|
|
8
|
+
* at a pinned commit) or `## null:` (checked, not reproducible, with evidence).
|
|
9
|
+
* A `null:` is a first-class outcome: the point of the gate is that somebody
|
|
10
|
+
* looked, not that a defect was found.
|
|
11
|
+
*
|
|
12
|
+
* **Warn-only by design, and the design is the load-bearing part.** Intake has
|
|
13
|
+
* to stay cheaper than the work it triggers, so an unresolved report is a
|
|
14
|
+
* reminder, never a build failure — a gate that reds CI because a human has not
|
|
15
|
+
* yet investigated a symptom would train people to stop filing symptoms, which
|
|
16
|
+
* is the exact opposite of what the surface is for
|
|
17
|
+
* (`agents/roadmaps/archive/road-to-symptom-driven-harvest-loop.md` Phase 1 Step 2).
|
|
18
|
+
* The only exit-1 path is a dead scan scope.
|
|
19
|
+
*
|
|
20
|
+
* Scope: `agents/tickets/symptoms/*.md`, one level, no recursion.
|
|
21
|
+
* `README.md` and `_`-prefixed files (the template) are counted as scanned but
|
|
22
|
+
* never evaluated — so the gate keeps a non-zero denominator while the surface
|
|
23
|
+
* exists, and does not red the moment the last entry is resolved away.
|
|
24
|
+
*
|
|
25
|
+
* Exit codes: 0 = clean or warnings only, 1 = dead scan scope.
|
|
26
|
+
* `--quiet` is a bare argv membership check, matching the sibling roadmap gates.
|
|
27
|
+
* `--now=YYYY-MM-DD` overrides today (tests and reproducible runs only).
|
|
28
|
+
*/
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
|
+
|
|
33
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
34
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
35
|
+
|
|
36
|
+
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
37
|
+
|
|
38
|
+
const SYMPTOM_DIR = path.join('agents', 'tickets', 'symptoms');
|
|
39
|
+
|
|
40
|
+
/** Days after `reported:` at which an unresolved entry starts warning. */
|
|
41
|
+
export const STALE_AFTER_DAYS = 30;
|
|
42
|
+
|
|
43
|
+
const _HERE = path.resolve(fileURLToPath(import.meta.url));
|
|
44
|
+
|
|
45
|
+
const REPORTED_RE = /^reported:\s*(\d{4}-\d{2}-\d{2})\s*$/m;
|
|
46
|
+
const CONFIRMED_RE = /^##\s+confirmed:\s*$/m;
|
|
47
|
+
const NULL_RE = /^##\s+null:\s*$/m;
|
|
48
|
+
|
|
49
|
+
export interface SymptomFinding {
|
|
50
|
+
readonly file: string;
|
|
51
|
+
readonly kind: 'unresolved' | 'no-reported-date';
|
|
52
|
+
readonly ageDays: number | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Walk up from CWD until a dir containing `agents/tickets` is found. */
|
|
56
|
+
function _repoRoot(): string {
|
|
57
|
+
let cur = process.cwd();
|
|
58
|
+
const chain = [cur];
|
|
59
|
+
for (;;) {
|
|
60
|
+
const parent = path.dirname(cur);
|
|
61
|
+
if (parent === cur) break;
|
|
62
|
+
chain.push(parent);
|
|
63
|
+
cur = parent;
|
|
64
|
+
}
|
|
65
|
+
for (const candidate of chain) {
|
|
66
|
+
try {
|
|
67
|
+
if (fs.statSync(path.join(candidate, 'agents', 'tickets')).isDirectory()) {
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// keep walking
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return process.cwd();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _isDir(p: string): boolean {
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(p).isDirectory();
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every `*.md` directly under the symptoms dir, sorted. */
|
|
86
|
+
export function listMarkdown(base: string): string[] {
|
|
87
|
+
if (!_isDir(base)) return [];
|
|
88
|
+
let entries: fs.Dirent[];
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return entries
|
|
95
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
96
|
+
.map((e) => e.name)
|
|
97
|
+
.sort();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** An entry is a report; `README.md` and `_`-prefixed files are not. */
|
|
101
|
+
export function isEntry(name: string): boolean {
|
|
102
|
+
return name.endsWith('.md') && name !== 'README.md' && !name.startsWith('_');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _daysBetween(fromIso: string, now: Date): number | null {
|
|
106
|
+
const parsed = Date.parse(`${fromIso}T00:00:00Z`);
|
|
107
|
+
if (Number.isNaN(parsed)) return null;
|
|
108
|
+
return Math.floor((now.getTime() - parsed) / 86_400_000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function evaluate(text: string, name: string, now: Date): SymptomFinding | null {
|
|
112
|
+
const reported = REPORTED_RE.exec(text);
|
|
113
|
+
if (reported === null) {
|
|
114
|
+
return { file: name, kind: 'no-reported-date', ageDays: null };
|
|
115
|
+
}
|
|
116
|
+
if (CONFIRMED_RE.test(text) || NULL_RE.test(text)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const age = _daysBetween(reported[1] as string, now);
|
|
120
|
+
if (age === null) {
|
|
121
|
+
return { file: name, kind: 'no-reported-date', ageDays: null };
|
|
122
|
+
}
|
|
123
|
+
if (age <= STALE_AFTER_DAYS) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return { file: name, kind: 'unresolved', ageDays: age };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _now(): Date {
|
|
130
|
+
const flag = process.argv.slice(2).find((a) => a.startsWith('--now='));
|
|
131
|
+
if (flag !== undefined) {
|
|
132
|
+
const parsed = Date.parse(`${flag.slice('--now='.length)}T00:00:00Z`);
|
|
133
|
+
if (!Number.isNaN(parsed)) return new Date(parsed);
|
|
134
|
+
}
|
|
135
|
+
return new Date();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function main(): number {
|
|
139
|
+
const root = _repoRoot();
|
|
140
|
+
const base = path.join(root, SYMPTOM_DIR);
|
|
141
|
+
const names = listMarkdown(base);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// The scanned unit counts README + template too, deliberately: this gate
|
|
145
|
+
// must not go dead the moment the last open entry is resolved, and the
|
|
146
|
+
// directory always carries its convention file while the surface exists.
|
|
147
|
+
assertScanned({
|
|
148
|
+
gate: 'lint_symptom_intake',
|
|
149
|
+
scanned: names.length,
|
|
150
|
+
units: 'symptom file(s)',
|
|
151
|
+
roots: [SYMPTOM_DIR],
|
|
152
|
+
});
|
|
153
|
+
} catch (exc) {
|
|
154
|
+
if (exc instanceof DeadScopeError) {
|
|
155
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
throw exc;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const now = _now();
|
|
162
|
+
const findings: SymptomFinding[] = [];
|
|
163
|
+
let entryCount = 0;
|
|
164
|
+
|
|
165
|
+
// Per-target accounting: README.md and the template are planned too, so the
|
|
166
|
+
// ledger states out loud that they were seen and deliberately not evaluated
|
|
167
|
+
// — an unaccounted `continue` is the silent-skip defect this module catches.
|
|
168
|
+
const ledger = new GateLedger('lint_symptom_intake');
|
|
169
|
+
ledger.plan(names);
|
|
170
|
+
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
if (!isEntry(name)) {
|
|
173
|
+
ledger.outOfScope(name, 'not_applicable_kind');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
entryCount += 1;
|
|
177
|
+
let text: string;
|
|
178
|
+
try {
|
|
179
|
+
text = fs.readFileSync(path.join(base, name), 'utf-8');
|
|
180
|
+
} catch {
|
|
181
|
+
ledger.skip(name, 'binary_content');
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const finding = evaluate(text, name, now);
|
|
185
|
+
if (finding !== null) {
|
|
186
|
+
findings.push(finding);
|
|
187
|
+
ledger.fail(name, finding.kind);
|
|
188
|
+
} else {
|
|
189
|
+
ledger.complete(name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (findings.length === 0) {
|
|
194
|
+
ledger.report();
|
|
195
|
+
if (!QUIET) {
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
`✅ lint-symptom-intake: ${entryCount} entry(ies) resolved or within ` +
|
|
198
|
+
`${STALE_AFTER_DAYS} days.\n`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ledger.report();
|
|
205
|
+
process.stdout.write(`⚠️ lint-symptom-intake: ${findings.length} unresolved symptom entry(ies):\n`);
|
|
206
|
+
for (const f of findings) {
|
|
207
|
+
if (f.kind === 'no-reported-date') {
|
|
208
|
+
process.stdout.write(` ${SYMPTOM_DIR}/${f.file} — no valid \`reported: YYYY-MM-DD\` in frontmatter\n`);
|
|
209
|
+
} else {
|
|
210
|
+
process.stdout.write(
|
|
211
|
+
` ${SYMPTOM_DIR}/${f.file} — reported ${String(f.ageDays)} days ago, ` +
|
|
212
|
+
'no `## confirmed:` or `## null:` block\n',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write('\n');
|
|
217
|
+
process.stdout.write(' Add whichever block is true (see agents/tickets/symptoms/README.md):\n');
|
|
218
|
+
process.stdout.write(' • `## confirmed:` — defect + file:line at a pinned commit + owning roadmap\n');
|
|
219
|
+
process.stdout.write(' • `## null:` — what was checked, and why it does not reproduce\n');
|
|
220
|
+
process.stdout.write(' A null is a result, not a failure to act. This gate never fails the build.\n');
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _isCliEntry(): boolean {
|
|
225
|
+
if (process.argv[1] === undefined) return false;
|
|
226
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
227
|
+
if (import.meta.url === argvUrl) return true;
|
|
228
|
+
try {
|
|
229
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
230
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
231
|
+
return here === argv;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
238
|
+
process.exitCode = main();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export { QUIET, SYMPTOM_DIR, main };
|
|
@@ -46,6 +46,7 @@ import * as os from 'node:os';
|
|
|
46
46
|
import * as path from 'node:path';
|
|
47
47
|
import { fileURLToPath } from 'node:url';
|
|
48
48
|
|
|
49
|
+
import { projectStoreSlug } from './_lib/cc_transcript.js';
|
|
49
50
|
import {
|
|
50
51
|
isConsultation,
|
|
51
52
|
isUiWrite,
|
|
@@ -107,7 +108,7 @@ export interface RateReport {
|
|
|
107
108
|
* reads a clean zero out of a store it never found.
|
|
108
109
|
*/
|
|
109
110
|
export function defaultStore(cwd: string): string {
|
|
110
|
-
return path.join(os.homedir(), '.claude', 'projects', cwd
|
|
111
|
+
return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
/**
|
|
@@ -52,6 +52,8 @@ import * as os from 'node:os';
|
|
|
52
52
|
import * as path from 'node:path';
|
|
53
53
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
54
|
|
|
55
|
+
import { projectStoreSlug } from './_lib/cc_transcript.js';
|
|
56
|
+
|
|
55
57
|
const _HERE = fileURLToPath(import.meta.url);
|
|
56
58
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
57
59
|
|
|
@@ -114,11 +116,11 @@ export interface UsageReport {
|
|
|
114
116
|
|
|
115
117
|
/** Default store for the current working directory, mangled the way the host does. */
|
|
116
118
|
export function defaultStore(cwd: string): string {
|
|
117
|
-
//
|
|
118
|
-
// `<repo>/.claude/worktrees/x` lands in `…-agent-config--claude-worktrees-x`.
|
|
119
|
-
//
|
|
119
|
+
// Every non-alphanumeric is flattened, not only separators: a worktree at
|
|
120
|
+
// `<repo>/.claude/worktrees/x+y` lands in `…-agent-config--claude-worktrees-x-y`.
|
|
121
|
+
// A narrower class returns a directory that does not exist, and the caller
|
|
120
122
|
// then reads a clean zero out of a store it never found.
|
|
121
|
-
return path.join(os.homedir(), '.claude', 'projects', cwd
|
|
123
|
+
return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export function measureUsage(store: string, limit: number): UsageReport {
|
|
@@ -59,6 +59,7 @@ import * as path from 'node:path';
|
|
|
59
59
|
import process from 'node:process';
|
|
60
60
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
61
61
|
|
|
62
|
+
import { projectStoreSlug } from './_lib/cc_transcript.js';
|
|
62
63
|
import { entryText, isSidechain, toolUses as _toolUses } from './_lib/transcript_entry.js';
|
|
63
64
|
import { censusSkills, DETERMINISTIC_RE, SKILLS_ROOT } from './report_skill_activation.js';
|
|
64
65
|
|
|
@@ -408,7 +409,7 @@ export interface Sk2Report {
|
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
export function defaultStore(cwd: string): string {
|
|
411
|
-
return path.join(os.homedir(), '.claude', 'projects', cwd
|
|
412
|
+
return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
export function scanStore(repoRoot: string, store: string, limit: number): Sk2Report {
|
package/src/server/io/yamlIO.ts
CHANGED
|
@@ -144,8 +144,6 @@ export const TEMPLATE_PLACEHOLDER_DEFAULTS: Readonly<Record<string, string>> = {
|
|
|
144
144
|
__DISCIPLINE_PROFILE__: 'auto',
|
|
145
145
|
__USER_TYPE__: '',
|
|
146
146
|
__CHAT_HISTORY_FREQUENCY__: 'per_turn',
|
|
147
|
-
__CHAT_HISTORY_MAX_SIZE_KB__: '2048',
|
|
148
|
-
__CHAT_HISTORY_ON_OVERFLOW__: 'rotate',
|
|
149
147
|
};
|
|
150
148
|
|
|
151
149
|
/** Replace every known template placeholder with its default value. */
|