@event4u/agent-config 14.21.0 → 14.23.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +13 -13
- 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 +1813 -946
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +21 -3
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/hook-token-budget.json +7 -6
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Exit codes: 0 green · 1 over budget · 2 misuse / unreadable input.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { createHash } from 'node:crypto';
|
|
27
27
|
import * as fs from 'node:fs';
|
|
28
28
|
import * as path from 'node:path';
|
|
@@ -30,6 +30,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
30
|
|
|
31
31
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
32
32
|
import { runSelfTest } from './_lib/gate_self_test.js';
|
|
33
|
+
import {
|
|
34
|
+
type EligibilityVerdict,
|
|
35
|
+
evaluateEntry,
|
|
36
|
+
type GrammarProbe,
|
|
37
|
+
loadManifest,
|
|
38
|
+
} from './_lib/packed_binary_predicate.js';
|
|
33
39
|
|
|
34
40
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
35
41
|
const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
|
|
@@ -306,19 +312,26 @@ export function payloadFingerprint(entryPath: string, size: number): string {
|
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
/**
|
|
309
|
-
*
|
|
310
|
-
* text and these 15 are not. `archive` is empty and stays a hard zero.
|
|
315
|
+
* Bound exceptions exist for `dotfile` and `no-extension` ONLY.
|
|
311
316
|
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
+
* `binary` and `archive` are not exceptable HERE and never will be: this table
|
|
318
|
+
* is prose-bound, and prose is exactly what a binary must not be admitted on.
|
|
319
|
+
* `archive` remains a hard zero with no admission path of any kind. `binary`
|
|
320
|
+
* has one since 2026-09-08 — `src/config/packed-binary-manifest.json`, gated by
|
|
321
|
+
* the mechanically verified predicate in `_lib/packed_binary_predicate.ts`,
|
|
322
|
+
* which re-derives every claim from the packed bytes. Adding a `binary` row to
|
|
323
|
+
* the table below still does nothing. That is AC-6 of
|
|
324
|
+
* `road-to-scan-that-fails-closed` read literally, after an AI council
|
|
325
|
+
* (2026-09-07, 2 seats, unanimous over two rounds) resolved the split between
|
|
326
|
+
* reading "at zero" as the ratchet and as the observed count. It chose the
|
|
327
|
+
* observed count, and chose disposition 3 — ship a textual representation —
|
|
328
|
+
* over generating fixtures at dry-run time, on the measurement that 12 of the
|
|
329
|
+
* 15 asset-bearing media fixtures ALREADY ship an ASCII placeholder at their
|
|
330
|
+
* documented path (`FIXTURE-<adapter-id>-<ext>`). The three 69-byte PNGs that
|
|
331
|
+
* used to sit here were the outliers, not the norm; they now carry the sibling
|
|
332
|
+
* convention and the class is empty by observation.
|
|
317
333
|
*/
|
|
318
334
|
const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
|
|
319
|
-
{ fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
|
|
320
|
-
{ fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
321
|
-
{ fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
322
335
|
{ fingerprint: '47e4328e71652a0375ade7eb4dac8a3e4b6df1c6e4e74caa75287266676cc9a6', path: 'agents/templates/.ai-council.yml.example', size: 30561, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one. RE-PINNED 2026-09-07, 29,872 -> 30,561 bytes: the exception is path-AND-size-bound by design, so this branch adding a `disabled_reason` honest-null block to the gemini seat in the template invalidated it and the gate refused the entry as unaccounted. The re-pin excepts the SAME path for the SAME reason at its measured new size — it does not widen the class, raise a cap, or except anything the gate was not already carrying.' },
|
|
323
336
|
{ fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
|
|
324
337
|
{ fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
|
|
@@ -360,16 +373,25 @@ export interface TypeClassification {
|
|
|
360
373
|
/**
|
|
361
374
|
* Classify the whole payload by type and report what is not accounted for.
|
|
362
375
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
376
|
+
* `archive` stays a closed class: no admission path, so an entry in it is
|
|
377
|
+
* always a violation. `binary` is admitted only through `eligible` — the
|
|
378
|
+
* caller supplies the verdicts, this function never decides eligibility itself
|
|
379
|
+
* — and an INELIGIBLE binary is unaccounted exactly as before.
|
|
380
|
+
*
|
|
381
|
+
* The counts are the OBSERVED payload and are never reduced by eligibility. A
|
|
382
|
+
* caller compares `observed` against `allowed` (the number of entries that
|
|
383
|
+
* passed the predicate); nothing here subtracts an excused file from a count,
|
|
384
|
+
* because a measurement an exception can move is not a measurement.
|
|
385
|
+
*
|
|
386
|
+
* Dotfile and extensionless entries are each bound to a path AND a size, so
|
|
387
|
+
* replacing one with different content re-fires the check on that entry rather
|
|
388
|
+
* than passing inside a count.
|
|
368
389
|
*/
|
|
369
390
|
export function classifyPayloadTypes(
|
|
370
391
|
files: readonly PackFile[],
|
|
371
392
|
root = REPO_ROOT,
|
|
372
393
|
head: (p: string) => Uint8Array | null = (p) => readHead(p, root),
|
|
394
|
+
eligible: ReadonlySet<string> = new Set<string>(),
|
|
373
395
|
): TypeClassification {
|
|
374
396
|
const counts: Record<PayloadType | 'unreadable', number> = {
|
|
375
397
|
archive: 0,
|
|
@@ -387,7 +409,11 @@ export function classifyPayloadTypes(
|
|
|
387
409
|
counts[type] += 1;
|
|
388
410
|
if (type === 'text') continue;
|
|
389
411
|
const fingerprint = payloadFingerprint(f.path, f.size);
|
|
390
|
-
|
|
412
|
+
// A binary is accounted for ONLY by the mechanical predicate. It is
|
|
413
|
+
// never matched against the prose table, so a `binary` row added there
|
|
414
|
+
// would still do nothing.
|
|
415
|
+
if (type === 'binary' && eligible.has(f.path)) continue;
|
|
416
|
+
if (type !== 'binary' && type !== 'archive' && byFingerprint.has(fingerprint)) {
|
|
391
417
|
used.add(fingerprint);
|
|
392
418
|
continue;
|
|
393
419
|
}
|
|
@@ -400,6 +426,127 @@ export function classifyPayloadTypes(
|
|
|
400
426
|
};
|
|
401
427
|
}
|
|
402
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Binary eligibility over the packed payload.
|
|
431
|
+
*
|
|
432
|
+
* The probe is a CHILD PROCESS running the engine's own loader
|
|
433
|
+
* (`code_graph/grammar_probe.ts`), not a reimplementation here: a second
|
|
434
|
+
* loader inside the gate could accept bytes the engine rejects, which is the
|
|
435
|
+
* one failure mode that would let a broken grammar ship while the gate called
|
|
436
|
+
* it valid.
|
|
437
|
+
*
|
|
438
|
+
* A probe that cannot run at all yields `null` for every file, which FAILS
|
|
439
|
+
* conditions 8 and 9 rather than passing them — an unverifiable binary is not
|
|
440
|
+
* an eligible one.
|
|
441
|
+
*/
|
|
442
|
+
export interface BinaryEligibilityReport {
|
|
443
|
+
/** Paths that passed every condition. */
|
|
444
|
+
readonly eligible: ReadonlySet<string>;
|
|
445
|
+
/** Every packed binary, eligible or not, with its failing conditions. */
|
|
446
|
+
readonly verdicts: readonly EligibilityVerdict[];
|
|
447
|
+
/** True count of binaries in the payload — never reduced by eligibility. */
|
|
448
|
+
readonly observedCount: number;
|
|
449
|
+
/** True byte total of those binaries. */
|
|
450
|
+
readonly observedBytes: number;
|
|
451
|
+
/** Entries that passed the predicate. Compliance is observed <= allowed. */
|
|
452
|
+
readonly allowedCount: number;
|
|
453
|
+
/** Byte total of the allowed entries. */
|
|
454
|
+
readonly allowedBytes: number;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function evaluateBinaryEligibility(
|
|
458
|
+
files: readonly PackFile[],
|
|
459
|
+
root = REPO_ROOT,
|
|
460
|
+
probeAll: (paths: readonly string[]) => Map<string, GrammarProbe | null> = probeGrammars,
|
|
461
|
+
): BinaryEligibilityReport {
|
|
462
|
+
const manifest = loadManifest(root);
|
|
463
|
+
const packedPaths = new Set(files.map((f) => f.path));
|
|
464
|
+
const binaries = files.filter((f) => classifyEntry(f.path, readHead(f.path, root)) === 'binary');
|
|
465
|
+
const probes = probeAll(binaries.map((b) => b.path));
|
|
466
|
+
|
|
467
|
+
const verdicts: EligibilityVerdict[] = [];
|
|
468
|
+
for (const b of binaries) {
|
|
469
|
+
let bytes: Uint8Array | null = null;
|
|
470
|
+
try {
|
|
471
|
+
bytes = fs.readFileSync(path.join(root, b.path));
|
|
472
|
+
} catch {
|
|
473
|
+
bytes = null;
|
|
474
|
+
}
|
|
475
|
+
verdicts.push(
|
|
476
|
+
evaluateEntry({
|
|
477
|
+
filePath: b.path,
|
|
478
|
+
packedPaths,
|
|
479
|
+
manifest,
|
|
480
|
+
bytes,
|
|
481
|
+
probe: probes.get(b.path) ?? null,
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const eligible = new Set(verdicts.filter((v) => v.eligible).map((v) => v.path));
|
|
486
|
+
const sizeOf = new Map(files.map((f) => [f.path, f.size]));
|
|
487
|
+
return {
|
|
488
|
+
eligible,
|
|
489
|
+
verdicts,
|
|
490
|
+
observedCount: binaries.length,
|
|
491
|
+
observedBytes: binaries.reduce((n, b) => n + b.size, 0),
|
|
492
|
+
allowedCount: eligible.size,
|
|
493
|
+
allowedBytes: [...eligible].reduce((n, p) => n + (sizeOf.get(p) ?? 0), 0),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Run the production loader over `paths` in one child process. */
|
|
498
|
+
function probeGrammars(paths: readonly string[]): Map<string, GrammarProbe | null> {
|
|
499
|
+
const out = new Map<string, GrammarProbe | null>();
|
|
500
|
+
for (const p of paths) out.set(p, null);
|
|
501
|
+
if (paths.length === 0) return out;
|
|
502
|
+
const script = path.join(REPO_ROOT, 'src/scripts/code_graph/grammar_probe.ts');
|
|
503
|
+
const res = spawnSync('npx', ['tsx', script, ...paths.map((p) => path.join(REPO_ROOT, p))], {
|
|
504
|
+
cwd: REPO_ROOT,
|
|
505
|
+
encoding: 'utf-8',
|
|
506
|
+
});
|
|
507
|
+
if (res.status !== 0 || !res.stdout.trim()) return out;
|
|
508
|
+
try {
|
|
509
|
+
const rows = JSON.parse(res.stdout.trim()) as Array<{
|
|
510
|
+
path: string;
|
|
511
|
+
ok: boolean;
|
|
512
|
+
abi?: number;
|
|
513
|
+
grammar_id?: string;
|
|
514
|
+
}>;
|
|
515
|
+
for (const r of rows) {
|
|
516
|
+
const rel = path.relative(REPO_ROOT, r.path);
|
|
517
|
+
if (r.ok && typeof r.abi === 'number' && typeof r.grammar_id === 'string') {
|
|
518
|
+
out.set(rel, { abi: r.abi, grammar_id: r.grammar_id });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
/* an unparseable probe leaves every entry null, which fails 8 and 9 */
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The always-printed binary line: the measurement, then the allowance. */
|
|
528
|
+
export function binaryLine(r: BinaryEligibilityReport): string {
|
|
529
|
+
return (
|
|
530
|
+
`binary: observed ${String(r.observedCount)} (${r.observedBytes.toLocaleString('en-US')} B) · ` +
|
|
531
|
+
`allowed ${String(r.allowedCount)} (${r.allowedBytes.toLocaleString('en-US')} B)`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Compliance is `observed <= allowed`.
|
|
537
|
+
*
|
|
538
|
+
* Stated as its own check rather than inferred from the unaccounted list,
|
|
539
|
+
* because the two can only ever agree by construction and a gate that relies on
|
|
540
|
+
* that is one refactor away from silently accepting an excess.
|
|
541
|
+
*/
|
|
542
|
+
export function binaryComplianceViolations(r: BinaryEligibilityReport): string[] {
|
|
543
|
+
if (r.observedCount <= r.allowedCount) return [];
|
|
544
|
+
return [
|
|
545
|
+
`binary payload: observed ${String(r.observedCount)} entries but only ${String(r.allowedCount)} are ` +
|
|
546
|
+
'allowed by src/config/packed-binary-manifest.json under the eligibility predicate',
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
|
|
403
550
|
/** One line naming every type class and its count. */
|
|
404
551
|
export function payloadTypeLine(c: TypeClassification): string {
|
|
405
552
|
const parts = [...PAYLOAD_TYPES, 'unreadable' as const]
|
|
@@ -413,11 +560,17 @@ export function typeClassViolations(c: TypeClassification): string[] {
|
|
|
413
560
|
return c.unbound.map(
|
|
414
561
|
(u) =>
|
|
415
562
|
`payload type \`${u.type}\`: ${u.path} (${String(u.size)} bytes) is not accounted for. ` +
|
|
416
|
-
(u.type === 'archive'
|
|
417
|
-
? '
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
563
|
+
(u.type === 'archive'
|
|
564
|
+
? 'Archive is a closed class with no admission path of any kind. ' +
|
|
565
|
+
'Exclude it from package.json files[].'
|
|
566
|
+
: u.type === 'binary'
|
|
567
|
+
? 'A binary ships only via src/config/packed-binary-manifest.json, and only when every ' +
|
|
568
|
+
'condition of the eligibility predicate holds on the packed bytes ' +
|
|
569
|
+
'(_lib/packed_binary_predicate.ts). Add or correct its manifest entry, or exclude it ' +
|
|
570
|
+
'from package.json files[]. The failing conditions are listed above.'
|
|
571
|
+
: 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ' +
|
|
572
|
+
`If it belongs in the published surface, add a BoundPayloadException with ` +
|
|
573
|
+
`fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`),
|
|
421
574
|
);
|
|
422
575
|
}
|
|
423
576
|
|
|
@@ -664,6 +817,81 @@ function selfTest(): number {
|
|
|
664
817
|
? 1
|
|
665
818
|
: 0,
|
|
666
819
|
},
|
|
820
|
+
{
|
|
821
|
+
// THE PROSE-PATH SENSITIVITY PROOF. `LICENSE` at 1064 bytes
|
|
822
|
+
// carries a real bound exception, so under the pre-2026-09-07
|
|
823
|
+
// code this entry passed. It is fed binary head bytes here, and
|
|
824
|
+
// binary outranks no-extension in `classifyEntry`, so a
|
|
825
|
+
// prose-exceptable `binary` class would accept it. It must not.
|
|
826
|
+
//
|
|
827
|
+
// STILL LOAD-BEARING after the 2026-09-08 reopening, and more
|
|
828
|
+
// so: `binary` is admissible again, but ONLY through the
|
|
829
|
+
// mechanically verified manifest. This row is what goes red if
|
|
830
|
+
// anyone ever wires the prose table back into the class.
|
|
831
|
+
name: 'type/binary: a manifest-eligible binary IS admitted',
|
|
832
|
+
expect: 'accept',
|
|
833
|
+
// THE OPEN-PATH ACCEPTING CONTROL. The council's own argument
|
|
834
|
+
// against the closed class was that a control nobody saw fire
|
|
835
|
+
// is a claim; the same holds in reverse for a path nobody saw
|
|
836
|
+
// accept. Without this row every binary assertion here is a
|
|
837
|
+
// rejection, and a predicate that only ever refuses is
|
|
838
|
+
// indistinguishable from the hard zero it replaced.
|
|
839
|
+
run: () =>
|
|
840
|
+
typeClassViolations(
|
|
841
|
+
classifyPayloadTypes(
|
|
842
|
+
[{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
|
|
843
|
+
REPO_ROOT,
|
|
844
|
+
() => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
|
|
845
|
+
new Set(['src/vendor/grammars/tree-sitter-php.wasm']),
|
|
846
|
+
),
|
|
847
|
+
).length > 0
|
|
848
|
+
? 1
|
|
849
|
+
: 0,
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'type/binary: the SAME path is refused when eligibility did not pass',
|
|
853
|
+
expect: 'reject',
|
|
854
|
+
// The paired negative. Same path, same bytes, empty eligible
|
|
855
|
+
// set — so the admission demonstrably comes from the predicate
|
|
856
|
+
// and not from the path looking familiar.
|
|
857
|
+
run: () =>
|
|
858
|
+
typeClassViolations(
|
|
859
|
+
classifyPayloadTypes(
|
|
860
|
+
[{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
|
|
861
|
+
REPO_ROOT,
|
|
862
|
+
() => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
|
|
863
|
+
new Set<string>(),
|
|
864
|
+
),
|
|
865
|
+
).length > 0
|
|
866
|
+
? 1
|
|
867
|
+
: 0,
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: 'type/binary: an entry with a VALID bound exception is still refused once it reads binary',
|
|
871
|
+
expect: 'reject',
|
|
872
|
+
run: () =>
|
|
873
|
+
typeClassViolations(
|
|
874
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
|
|
875
|
+
Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x00, 0x00]),
|
|
876
|
+
),
|
|
877
|
+
).length > 0
|
|
878
|
+
? 1
|
|
879
|
+
: 0,
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
// The negative control for the row above: the SAME exceptable
|
|
883
|
+
// path with ordinary text still passes on its exception.
|
|
884
|
+
name: 'type/extensionless: the same bound entry with text content still passes',
|
|
885
|
+
expect: 'accept',
|
|
886
|
+
run: () =>
|
|
887
|
+
typeClassViolations(
|
|
888
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
|
|
889
|
+
new TextEncoder().encode('Apache License\n'),
|
|
890
|
+
),
|
|
891
|
+
).length > 0
|
|
892
|
+
? 1
|
|
893
|
+
: 0,
|
|
894
|
+
},
|
|
667
895
|
{
|
|
668
896
|
name: 'type/extensionless: an unbound extensionless entry is refused',
|
|
669
897
|
expect: 'reject',
|
|
@@ -781,11 +1009,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
781
1009
|
// still knows the population it was red over.
|
|
782
1010
|
process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
|
|
783
1011
|
|
|
784
|
-
|
|
1012
|
+
// Binary eligibility, decided on the PACKED bytes before classification so
|
|
1013
|
+
// the classifier is told which paths passed and never decides itself.
|
|
1014
|
+
const binaryReport = evaluateBinaryEligibility(pack.files);
|
|
1015
|
+
const types = classifyPayloadTypes(pack.files, REPO_ROOT, (p) => readHead(p, REPO_ROOT), binaryReport.eligible);
|
|
785
1016
|
const errors = [
|
|
786
1017
|
...evaluate(budget, pack),
|
|
787
1018
|
...classifyPayload(pack.files),
|
|
788
1019
|
...typeClassViolations(types),
|
|
1020
|
+
...binaryComplianceViolations(binaryReport),
|
|
789
1021
|
];
|
|
790
1022
|
const { perSkill, total } = skillBytes(pack.files);
|
|
791
1023
|
if (argv.includes('--json')) {
|
|
@@ -798,6 +1030,13 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
798
1030
|
// what a reader needs to judge a violation: "one unaccounted binary" reads
|
|
799
1031
|
// very differently against a payload of 3 binaries than against one of 0.
|
|
800
1032
|
process.stdout.write(` payload types: ${payloadTypeLine(types)}\n`);
|
|
1033
|
+
// The observed binary count and byte total on EVERY run, green or red.
|
|
1034
|
+
// Eligibility must never move a measurement, so both totals are printed
|
|
1035
|
+
// beside the allowance rather than folded into it.
|
|
1036
|
+
process.stdout.write(` ${binaryLine(binaryReport)}\n`);
|
|
1037
|
+
for (const v of binaryReport.verdicts.filter((x) => !x.eligible)) {
|
|
1038
|
+
for (const f of v.failures) process.stdout.write(` ${v.path}: ${f}\n`);
|
|
1039
|
+
}
|
|
801
1040
|
for (const unused of types.unusedExceptions) {
|
|
802
1041
|
// Not an error: an unbuilt tree legitimately packs fewer files. Named
|
|
803
1042
|
// so a stale exception is visible rather than accumulating unread.
|
|
@@ -18,7 +18,24 @@
|
|
|
18
18
|
* bucket has no local source at all — the census reports that one as an explicit
|
|
19
19
|
* residual rather than pretending to measure it.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* THE BOUND IS SHRINK-ONLY, AND NOW CHECKED
|
|
22
|
+
* ------------------------------------------
|
|
23
|
+
* `ci_delivery.why_a_grace_ceiling` says of the grace ceiling *"It may never
|
|
24
|
+
* move UP"*, and `grace_ceiling_history` in the same file records it moving up
|
|
25
|
+
* twice. ADR-264 resolved that against the practice — the sentence stands — and
|
|
26
|
+
* left the sentence unenforced, which is exactly the state that produced both
|
|
27
|
+
* raises. `_lib/standing_bound_ratchet.ts` is the enforcement: the effective
|
|
28
|
+
* ceiling is compared against the one at the base ref and a rise refuses the run,
|
|
29
|
+
* whether it arrived by editing the config or by passing a bigger `--ceiling`.
|
|
30
|
+
*
|
|
31
|
+
* It is also ALL that `road-to-a-standing-budget-with-headroom` step 1.3 ships.
|
|
32
|
+
* That step designed a 128-token Iron Law reserve; an AI council refused to
|
|
33
|
+
* activate it 2/2 on 2026-09-08 because the verifier is inside the change under
|
|
34
|
+
* review, so a protected approval record grants nothing. ADR-265 carries the
|
|
35
|
+
* verdict and the activation prerequisites.
|
|
36
|
+
*
|
|
37
|
+
* Exit codes: 0 within budget · 1 over budget or the bound rose · 2 misuse /
|
|
38
|
+
* unreadable budget.
|
|
22
39
|
*/
|
|
23
40
|
import { execFileSync } from 'node:child_process';
|
|
24
41
|
import * as fs from 'node:fs';
|
|
@@ -29,6 +46,13 @@ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
|
29
46
|
import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
|
|
30
47
|
import { PREFIX_STABLE_SURFACES, prefixStableRoots } from './_lib/prefix_stable_surfaces.js';
|
|
31
48
|
import { attributeGrowth, buildLedger, renderAttribution } from './_lib/asset_delivery_ledger.js';
|
|
49
|
+
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
50
|
+
import {
|
|
51
|
+
assertBoundsDidNotRise,
|
|
52
|
+
type BoundsRatchetVerdict,
|
|
53
|
+
type GitRunner,
|
|
54
|
+
realGit,
|
|
55
|
+
} from './_lib/standing_bound_ratchet.js';
|
|
32
56
|
|
|
33
57
|
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
34
58
|
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
@@ -38,6 +62,15 @@ interface Budget {
|
|
|
38
62
|
baseline_tokens: number;
|
|
39
63
|
headroom_pct: number;
|
|
40
64
|
target_tokens: { median: number; p95: number };
|
|
65
|
+
/**
|
|
66
|
+
* `ci_delivery.grace_ceiling`, or `null` when the file carries none.
|
|
67
|
+
*
|
|
68
|
+
* Read here only so the shrink-only ratchet has a head-side value on a local
|
|
69
|
+
* run that passes no `--ceiling`. The number the payload is COMPARED against
|
|
70
|
+
* still arrives through the flag, so this does not become a second home for
|
|
71
|
+
* the ceiling.
|
|
72
|
+
*/
|
|
73
|
+
grace_ceiling: number | null;
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
export interface BudgetVerdict {
|
|
@@ -60,10 +93,13 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
|
|
|
60
93
|
throw new Error(`${file}: baseline_tokens and headroom_pct must both be numbers`);
|
|
61
94
|
}
|
|
62
95
|
const target = (raw['target_tokens'] ?? {}) as Record<string, unknown>;
|
|
96
|
+
const delivery = (raw['ci_delivery'] ?? {}) as Record<string, unknown>;
|
|
97
|
+
const grace = Number(delivery['grace_ceiling']);
|
|
63
98
|
return {
|
|
64
99
|
baseline_tokens: baseline,
|
|
65
100
|
headroom_pct: headroom,
|
|
66
101
|
target_tokens: { median: Number(target['median']), p95: Number(target['p95']) },
|
|
102
|
+
grace_ceiling: Number.isFinite(grace) ? grace : null,
|
|
67
103
|
};
|
|
68
104
|
}
|
|
69
105
|
|
|
@@ -142,6 +178,47 @@ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling
|
|
|
142
178
|
return { measured, baseline: budget.baseline_tokens, ceiling, withinBudget: measured <= ceiling, buckets };
|
|
143
179
|
}
|
|
144
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The measurement plus the shrink-only bound check.
|
|
183
|
+
*
|
|
184
|
+
* `evaluate` above answers "how big is the tree" and touches no ref;
|
|
185
|
+
* this answers "may this tree ship" and needs the base one. They stay separate
|
|
186
|
+
* because a census that silently depended on a remote ref would behave
|
|
187
|
+
* differently in a shallow clone, and the census is the half other callers use.
|
|
188
|
+
*/
|
|
189
|
+
export interface Decision {
|
|
190
|
+
verdict: BudgetVerdict;
|
|
191
|
+
bounds: BoundsRatchetVerdict;
|
|
192
|
+
/** False when the payload is over the ceiling, or the ceiling itself rose. */
|
|
193
|
+
ok: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface DecideOptions {
|
|
197
|
+
repoRoot?: string;
|
|
198
|
+
budgetFile?: string;
|
|
199
|
+
overrideCeiling?: number;
|
|
200
|
+
env?: NodeJS.ProcessEnv;
|
|
201
|
+
git?: GitRunner;
|
|
202
|
+
/** Test seam: pin the base ref instead of resolving it. `null` = none resolved. */
|
|
203
|
+
baseRef?: string | null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function decide(opts: DecideOptions = {}): Decision {
|
|
207
|
+
const repoRoot = opts.repoRoot ?? REPO_ROOT;
|
|
208
|
+
const git = opts.git ?? realGit;
|
|
209
|
+
const budget = readBudget(opts.budgetFile);
|
|
210
|
+
const verdict = evaluate(repoRoot, opts.budgetFile, opts.overrideCeiling);
|
|
211
|
+
const baseRef =
|
|
212
|
+
opts.baseRef !== undefined ? opts.baseRef : resolveBaseRef(repoRoot, opts.env ?? process.env, git);
|
|
213
|
+
const bounds = assertBoundsDidNotRise({
|
|
214
|
+
repoRoot,
|
|
215
|
+
baseRef,
|
|
216
|
+
git,
|
|
217
|
+
headGraceCeiling: opts.overrideCeiling ?? budget.grace_ceiling ?? 0,
|
|
218
|
+
});
|
|
219
|
+
return { verdict, bounds, ok: bounds.ok && verdict.withinBudget };
|
|
220
|
+
}
|
|
221
|
+
|
|
145
222
|
/**
|
|
146
223
|
* Per-asset attribution for a refusal, against the merge-base tree.
|
|
147
224
|
*
|
|
@@ -203,6 +280,26 @@ function attributeGrowthAgainstBase(): string[] | null {
|
|
|
203
280
|
}
|
|
204
281
|
}
|
|
205
282
|
|
|
283
|
+
/**
|
|
284
|
+
* The bound check, rendered for a human — on BOTH paths, green and red.
|
|
285
|
+
*
|
|
286
|
+
* A check that only speaks when it fails is a check nobody audits until it is
|
|
287
|
+
* already load-bearing. Printing the compared ref and the earlier bound on the
|
|
288
|
+
* passing path is also the only way a reader can tell "verified and unchanged"
|
|
289
|
+
* from "skipped because no base ref resolved", which are different facts.
|
|
290
|
+
*/
|
|
291
|
+
function renderBounds(b: BoundsRatchetVerdict): string {
|
|
292
|
+
if (b.note !== null) {
|
|
293
|
+
return ` ${'grace ceiling ratchet'.padEnd(38)} ${'SKIPPED'.padStart(8)} — ${b.note}\n`;
|
|
294
|
+
}
|
|
295
|
+
const base = b.baseGraceCeiling === null ? 'n/a' : String(b.baseGraceCeiling);
|
|
296
|
+
const state = b.ok ? 'ok' : 'ROSE';
|
|
297
|
+
return (
|
|
298
|
+
` ${'grace ceiling ratchet'.padEnd(38)} ${state.padStart(8)} — ` +
|
|
299
|
+
`${base} at ${b.baseRef ?? 'n/a'}, shrink-only (ADR-264)\n`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
206
303
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
207
304
|
const json = argv.includes('--format=json') || argv.includes('--json');
|
|
208
305
|
// `--ceiling <n>`: the grace ceiling the CI step reads out of
|
|
@@ -211,13 +308,14 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
211
308
|
// tighter-than-design value is IGNORED rather than honoured — see `evaluate`.
|
|
212
309
|
const ci = argv.indexOf('--ceiling');
|
|
213
310
|
const override = ci !== -1 && argv[ci + 1] !== undefined ? Number(argv[ci + 1]) : undefined;
|
|
214
|
-
let
|
|
311
|
+
let decision: Decision;
|
|
215
312
|
try {
|
|
216
|
-
|
|
313
|
+
decision = decide(override === undefined ? {} : { overrideCeiling: override });
|
|
217
314
|
} catch (err) {
|
|
218
315
|
process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
|
|
219
316
|
return 2;
|
|
220
317
|
}
|
|
318
|
+
const verdict = decision.verdict;
|
|
221
319
|
|
|
222
320
|
// A ratchet over a measurement of nothing always passes: move
|
|
223
321
|
// `dist/agent-src/` and every census returns zero, which is trivially under
|
|
@@ -239,8 +337,24 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
239
337
|
}
|
|
240
338
|
|
|
241
339
|
if (json) {
|
|
242
|
-
process.stdout.write(
|
|
243
|
-
|
|
340
|
+
process.stdout.write(
|
|
341
|
+
JSON.stringify(
|
|
342
|
+
{
|
|
343
|
+
...verdict,
|
|
344
|
+
ok: decision.ok,
|
|
345
|
+
bound_ratchet: {
|
|
346
|
+
ok: decision.bounds.ok,
|
|
347
|
+
base_ref: decision.bounds.baseRef,
|
|
348
|
+
base_grace_ceiling: decision.bounds.baseGraceCeiling,
|
|
349
|
+
note: decision.bounds.note,
|
|
350
|
+
violations: decision.bounds.violations,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
null,
|
|
354
|
+
2,
|
|
355
|
+
) + '\n',
|
|
356
|
+
);
|
|
357
|
+
return decision.ok ? 0 : 1;
|
|
244
358
|
}
|
|
245
359
|
|
|
246
360
|
for (const b of verdict.buckets) {
|
|
@@ -252,6 +366,20 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
252
366
|
` ${'measured total'.padEnd(38)} ${String(verdict.measured).padStart(8)} tok ` +
|
|
253
367
|
`(baseline ${verdict.baseline}, ${sign}${delta}; ceiling ${verdict.ceiling})\n`,
|
|
254
368
|
);
|
|
369
|
+
process.stdout.write(renderBounds(decision.bounds));
|
|
370
|
+
|
|
371
|
+
// The bound is checked BEFORE the size question and independently of it. A
|
|
372
|
+
// change that lifts its own ceiling has already defeated the gate, and
|
|
373
|
+
// reporting that as "within budget" would be the config-weakening move
|
|
374
|
+
// wearing a green checkmark.
|
|
375
|
+
if (!decision.bounds.ok) {
|
|
376
|
+
process.stderr.write(
|
|
377
|
+
'❌ the standing-payload ceiling rose in this change:\n' +
|
|
378
|
+
decision.bounds.violations.map((v) => ` · ${v}\n`).join(''),
|
|
379
|
+
);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
|
|
255
383
|
if (!verdict.withinBudget) {
|
|
256
384
|
// road-to-delivered-cost-truth 2.2 — a gate names its own "no". The
|
|
257
385
|
// ceiling message alone states a fact and leaves the reader to find the
|
|
@@ -268,8 +396,9 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
268
396
|
process.stderr.write(
|
|
269
397
|
`❌ per-spawn preamble payload grew past the ratchet: ${verdict.measured} > ${verdict.ceiling} tok.\n` +
|
|
270
398
|
` Every rule and skill description here is re-written on EVERY subagent spawn, so growth\n` +
|
|
271
|
-
` is paid per spawn, not once.
|
|
272
|
-
`
|
|
399
|
+
` is paid per spawn, not once. Shrink the addition, or migrate the prose out of the\n` +
|
|
400
|
+
` standing rule. The grace ceiling may NOT be raised to fit it: ADR-264, enforced by\n` +
|
|
401
|
+
` the shrink-only ratchet above.\n`,
|
|
273
402
|
);
|
|
274
403
|
// Three distinct states, and conflating the last two is a diagnostic
|
|
275
404
|
// defect rather than a cosmetic one: "I could not look" and "I looked
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
* `skipped/`, template/README/open-questions) MUST:
|
|
20
20
|
* 1. Be parseable by the dashboard's `PHASE_RE` (>= one `## Phase <id>`).
|
|
21
21
|
* 2. Have at least one trackable checkbox under every parsed phase.
|
|
22
|
+
* 3. Not declare a RETIRED `status:` value.
|
|
23
|
+
*
|
|
24
|
+
* `status: draft` is the one exemption. It used to have a second, `carrier`,
|
|
25
|
+
* which ADR-262 deleted: a `status:` value never again decides whether an
|
|
26
|
+
* autonomous run may act on a roadmap, so a receiver of deferred work is now
|
|
27
|
+
* ordinary estate and passes normal trackability. A surviving `status: carrier`
|
|
28
|
+
* is REJECTED here with the migration diagnostic rather than silently ignored —
|
|
29
|
+
* deleting a vocabulary term quietly would leave old files wearing a value
|
|
30
|
+
* nothing reads, and the next author would infer a meaning from its presence.
|
|
31
|
+
* The retired set and its diagnostic live in `_lib/retired_status.ts`.
|
|
22
32
|
*
|
|
23
33
|
* Exit codes:
|
|
24
34
|
* 0 — every active roadmap has parseable phases with >= 1 checkbox per phase.
|
|
@@ -31,6 +41,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
31
41
|
|
|
32
42
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
33
43
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
44
|
+
import { declaresRetiredStatus, retiredStatusDiagnostic } from './_lib/retired_status.js';
|
|
34
45
|
|
|
35
46
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
36
47
|
|
|
@@ -49,15 +60,6 @@ const PHASE_RE =
|
|
|
49
60
|
// FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
|
|
50
61
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
|
|
51
62
|
const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
|
|
52
|
-
/**
|
|
53
|
-
* A carrier holds obligations deferred out of an archived parent. It is not
|
|
54
|
-
* schedulable work — every item in it has an unmet resumption trigger — and it
|
|
55
|
-
* has no `## Phase` headings by construction, so it is skipped exactly where a
|
|
56
|
-
* draft is. It differs from a draft in what protects it: a draft may be deleted
|
|
57
|
-
* freely, a carrier may not, and `lint_carrier_integrity` reds when one loses
|
|
58
|
-
* its receiver.
|
|
59
|
-
*/
|
|
60
|
-
const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
|
|
61
63
|
|
|
62
64
|
const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
|
|
63
65
|
'template.md',
|
|
@@ -101,11 +103,6 @@ function is_draft(fm: Map<string, string>): boolean {
|
|
|
101
103
|
return DRAFT_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/** True for a `status: carrier` roadmap — see CARRIER_VALUES. */
|
|
105
|
-
function is_carrier(fm: Map<string, string>): boolean {
|
|
106
|
-
return CARRIER_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
106
|
/** Mirror update_roadmap_progress.is_roadmap_candidate (POSIX path parts). */
|
|
110
107
|
function is_roadmap_candidate(p: string): boolean {
|
|
111
108
|
const name = path.basename(p);
|
|
@@ -182,7 +179,7 @@ function find_active_roadmaps(root: string): string[] {
|
|
|
182
179
|
continue;
|
|
183
180
|
}
|
|
184
181
|
const text = fs.readFileSync(p, 'utf-8');
|
|
185
|
-
if (is_draft(parse_frontmatter(text))
|
|
182
|
+
if (is_draft(parse_frontmatter(text))) {
|
|
186
183
|
continue;
|
|
187
184
|
}
|
|
188
185
|
out.push(p);
|
|
@@ -193,6 +190,13 @@ function find_active_roadmaps(root: string): string[] {
|
|
|
193
190
|
/** Return human-readable violation strings for a single roadmap. */
|
|
194
191
|
function violations_for(p: string): string[] {
|
|
195
192
|
const text = fs.readFileSync(p, 'utf-8');
|
|
193
|
+
const retired = declaresRetiredStatus(text);
|
|
194
|
+
if (retired !== null) {
|
|
195
|
+
// Returned alone: a file wearing a deleted status has a migration to do
|
|
196
|
+
// before its phase structure is worth reporting on, and stacking phase
|
|
197
|
+
// findings on top would bury the one finding that has to be read first.
|
|
198
|
+
return [`${p}: ${retiredStatusDiagnostic(retired)}`];
|
|
199
|
+
}
|
|
196
200
|
PHASE_RE.lastIndex = 0;
|
|
197
201
|
const matches: Array<{ start: number; end: number; id: string; name: string | undefined }> =
|
|
198
202
|
[];
|
|
@@ -359,7 +363,6 @@ export {
|
|
|
359
363
|
CHECKBOX_RE,
|
|
360
364
|
PHASE_RE,
|
|
361
365
|
is_draft,
|
|
362
|
-
is_carrier,
|
|
363
366
|
is_roadmap_candidate,
|
|
364
367
|
parse_frontmatter,
|
|
365
368
|
find_active_roadmaps,
|