@event4u/agent-config 15.0.0 → 16.0.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 +142 -348
- package/dist/agent-src/commands/roadmap/create.md +3 -2
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +50 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-H32DGUJH.js} +14 -89
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +831 -272
- package/dist/cli-delegate/cmd_explain.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +13 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- 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 +1274 -571
- package/dist/install/install.mjs +1 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1218 -632
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/server/io/yamlIO.js +50 -0
- package/package.json +1 -1
- package/src/config/continuity-surface.json +7 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +399 -122
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +39 -3
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +4 -1
- package/src/scripts/install.ts +1 -1
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +8 -1
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/release.ts +52 -64
- package/src/scripts/release_drill.ts +193 -14
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +1 -1
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/server/io/yamlIO.ts +48 -0
|
@@ -18,60 +18,97 @@
|
|
|
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
|
-
* THE
|
|
22
|
-
*
|
|
23
|
-
* `ci_delivery.
|
|
24
|
-
* move UP"
|
|
25
|
-
* twice. ADR-264 resolved that against the practice
|
|
26
|
-
*
|
|
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`.
|
|
21
|
+
* THE CEILING IS MEASURED AT THE BASE REF, NOT STORED
|
|
22
|
+
* ----------------------------------------------------
|
|
23
|
+
* It used to be a stored `ci_delivery.grace_ceiling`, whose own text said *"It
|
|
24
|
+
* may never move UP"* while `grace_ceiling_history` recorded it moving up
|
|
25
|
+
* twice. ADR-264 resolved that against the practice and left the sentence
|
|
26
|
+
* unenforced; `_lib/standing_bound_ratchet.ts` became the enforcement.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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.
|
|
28
|
+
* `road-to-delivery-for-every-host` 4.4 retires the stored number entirely. An
|
|
29
|
+
* AI council (2/2, 2026-09-10, under a written owner delegation) converged on
|
|
36
30
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
31
|
+
* ceiling = max(design_ceiling, payload_at_base_ref + active grants)
|
|
32
|
+
*
|
|
33
|
+
* for one reason: a measured ceiling captures every merged reduction
|
|
34
|
+
* automatically, where a stored one stays at its last hand-written number — so
|
|
35
|
+
* payload a merge removed can be added straight back into the space it freed.
|
|
36
|
+
* The ordinary path is therefore ZERO NET GROWTH while the tree is over design,
|
|
37
|
+
* and the design ceiling once it is at or below. No per-PR headroom: both seats
|
|
38
|
+
* refused one because a percentage or a fixed allowance against a moving base
|
|
39
|
+
* authorises cumulative growth (138,413 × 1.05^10 ≈ 225,000).
|
|
40
|
+
*
|
|
41
|
+
* Three prerequisites came with the verdict and all three are in the pipeline
|
|
42
|
+
* below. The base measurement FAILS CLOSED under `--require-base`, because a
|
|
43
|
+
* base that cannot be read would otherwise grant an unbounded budget. The
|
|
44
|
+
* shrink-only ratchet moved with the ceiling: `design_ceiling` and every
|
|
45
|
+
* existing exception grant and watermark are now the bounded numbers. And the
|
|
46
|
+
* catalogue exhaustiveness audit refuses payload sitting outside every measured
|
|
47
|
+
* bucket, which defeats a stored ceiling exactly as it would defeat this one.
|
|
48
|
+
*
|
|
49
|
+
* A change whose offsetting reduction is genuinely unsafe takes a recorded,
|
|
50
|
+
* human-approved, dated grant in `src/config/preamble-payload-exceptions.json`.
|
|
51
|
+
* 96.8 % of the last 250 merged pull requests moved this payload by zero or
|
|
52
|
+
* less, so the ordinary path stays autonomous and only the tail reaches a
|
|
53
|
+
* person. `_lib/measured_payload_ceiling.ts` carries the mechanism and the
|
|
54
|
+
* reasoning, including why no numeric cap is enforced.
|
|
55
|
+
*
|
|
56
|
+
* ADR-265 still holds for what this does NOT do: the 128-token Iron Law reserve
|
|
57
|
+
* stays unshipped, because a mechanism that GRANTS budget cannot rest on a
|
|
58
|
+
* verifier the change under review can edit.
|
|
59
|
+
*
|
|
60
|
+
* Exit codes: 0 within budget · 1 over budget, a bound rose, the ceiling could
|
|
61
|
+
* not be measured under `--require-base`, or payload sits outside the catalogue
|
|
62
|
+
* · 2 misuse / unreadable budget.
|
|
39
63
|
*/
|
|
40
|
-
import { execFileSync } from 'node:child_process';
|
|
41
64
|
import * as fs from 'node:fs';
|
|
42
|
-
import * as os from 'node:os';
|
|
43
65
|
import * as path from 'node:path';
|
|
44
66
|
|
|
67
|
+
import { asOf } from './_lib/as_of.js';
|
|
45
68
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
46
69
|
import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
|
|
47
70
|
import { PREFIX_STABLE_SURFACES, prefixStableRoots } from './_lib/prefix_stable_surfaces.js';
|
|
48
71
|
import { HOST_SURFACES } from './_lib/host_projection_reach.js';
|
|
49
72
|
import { attributeGrowth, buildLedger, renderAttribution } from './_lib/asset_delivery_ledger.js';
|
|
50
73
|
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
74
|
+
import { extractSurfacesAtRef, measurePayloadAtRef } from './_lib/base_ref_payload.js';
|
|
75
|
+
import {
|
|
76
|
+
type CeilingReading,
|
|
77
|
+
computeCeiling,
|
|
78
|
+
EXCEPTIONS_CONFIG_PATH,
|
|
79
|
+
readExceptions,
|
|
80
|
+
renderCeiling,
|
|
81
|
+
} from './_lib/measured_payload_ceiling.js';
|
|
82
|
+
import { auditCatalogue, type CatalogueAudit } from './_lib/payload_catalogue_completeness.js';
|
|
51
83
|
import {
|
|
52
84
|
assertBoundsDidNotRise,
|
|
53
85
|
type BoundsRatchetVerdict,
|
|
86
|
+
boundsFrom,
|
|
54
87
|
type GitRunner,
|
|
55
88
|
realGit,
|
|
56
89
|
} from './_lib/standing_bound_ratchet.js';
|
|
57
90
|
|
|
58
91
|
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
92
|
+
/**
|
|
93
|
+
* Where this SCRIPT lives — the default tree to measure, and nothing more.
|
|
94
|
+
*
|
|
95
|
+
* Under prerequisite 3 of the measured-ceiling verdict, CI runs this file as it
|
|
96
|
+
* exists at the BASE ref against the HEAD tree, so that a pull request cannot
|
|
97
|
+
* edit the code that measures it. There the two diverge and `--repo-root`
|
|
98
|
+
* carries the tree; everywhere else they are the same directory.
|
|
99
|
+
*/
|
|
59
100
|
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
60
|
-
|
|
101
|
+
|
|
102
|
+
/** The budget config inside a given tree. */
|
|
103
|
+
function budgetFileIn(repoRoot: string): string {
|
|
104
|
+
return path.join(repoRoot, 'src', 'config', 'preamble-payload-budget.json');
|
|
105
|
+
}
|
|
106
|
+
const BUDGET_FILE = budgetFileIn(REPO_ROOT);
|
|
61
107
|
|
|
62
108
|
interface Budget {
|
|
63
109
|
baseline_tokens: number;
|
|
64
110
|
headroom_pct: number;
|
|
65
111
|
target_tokens: { median: number; p95: number };
|
|
66
|
-
/**
|
|
67
|
-
* `ci_delivery.grace_ceiling`, or `null` when the file carries none.
|
|
68
|
-
*
|
|
69
|
-
* Read here only so the shrink-only ratchet has a head-side value on a local
|
|
70
|
-
* run that passes no `--ceiling`. The number the payload is COMPARED against
|
|
71
|
-
* still arrives through the flag, so this does not become a second home for
|
|
72
|
-
* the ceiling.
|
|
73
|
-
*/
|
|
74
|
-
grace_ceiling: number | null;
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
export interface BudgetVerdict {
|
|
@@ -94,16 +131,19 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
|
|
|
94
131
|
throw new Error(`${file}: baseline_tokens and headroom_pct must both be numbers`);
|
|
95
132
|
}
|
|
96
133
|
const target = (raw['target_tokens'] ?? {}) as Record<string, unknown>;
|
|
97
|
-
const delivery = (raw['ci_delivery'] ?? {}) as Record<string, unknown>;
|
|
98
|
-
const grace = Number(delivery['grace_ceiling']);
|
|
99
134
|
return {
|
|
100
135
|
baseline_tokens: baseline,
|
|
101
136
|
headroom_pct: headroom,
|
|
102
137
|
target_tokens: { median: Number(target['median']), p95: Number(target['p95']) },
|
|
103
|
-
grace_ceiling: Number.isFinite(grace) ? grace : null,
|
|
104
138
|
};
|
|
105
139
|
}
|
|
106
140
|
|
|
141
|
+
/** The raw budget JSON, for the bound derivation that must see head and base
|
|
142
|
+
* through the SAME function. `readBudget` narrows; this does not. */
|
|
143
|
+
function readBudgetRaw(file: string): unknown {
|
|
144
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
/** Look one surface root up by id. Throws on an unknown id — a renamed surface
|
|
108
148
|
* must fail loudly here rather than degrade this gate to measuring nothing. */
|
|
109
149
|
function surfaceRoot(id: string): string {
|
|
@@ -358,105 +398,196 @@ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling
|
|
|
358
398
|
export interface Decision {
|
|
359
399
|
verdict: BudgetVerdict;
|
|
360
400
|
bounds: BoundsRatchetVerdict;
|
|
361
|
-
/**
|
|
401
|
+
/** How the ceiling was arrived at, and whether it may be trusted. */
|
|
402
|
+
ceiling: CeilingReading;
|
|
403
|
+
/** Payload the census does not enumerate. Empty findings = complete. */
|
|
404
|
+
catalogue: CatalogueAudit;
|
|
405
|
+
/** False when the payload is over the ceiling, a bound rose, the ceiling
|
|
406
|
+
* could not be measured under `--require-base`, or payload escaped the
|
|
407
|
+
* catalogue. */
|
|
362
408
|
ok: boolean;
|
|
363
409
|
}
|
|
364
410
|
|
|
365
411
|
export interface DecideOptions {
|
|
366
412
|
repoRoot?: string;
|
|
367
413
|
budgetFile?: string;
|
|
368
|
-
overrideCeiling?: number;
|
|
369
414
|
env?: NodeJS.ProcessEnv;
|
|
370
415
|
git?: GitRunner;
|
|
371
416
|
/** Test seam: pin the base ref instead of resolving it. `null` = none resolved. */
|
|
372
417
|
baseRef?: string | null;
|
|
373
|
-
/** Refuse instead of skipping when the
|
|
418
|
+
/** Refuse instead of skipping when the base ref cannot be read. */
|
|
374
419
|
requireBase?: boolean;
|
|
420
|
+
/** Test seam: pin today's date for the exception-expiry branch. */
|
|
421
|
+
today?: string;
|
|
422
|
+
/** Test seam: supply the base payload instead of measuring the ref. */
|
|
423
|
+
basePayload?: number | null;
|
|
424
|
+
/**
|
|
425
|
+
* Exception ids whose approval event was VERIFIED against the platform.
|
|
426
|
+
*
|
|
427
|
+
* Empty by default, which is the safe reading: a grant nobody confirmed is
|
|
428
|
+
* a grant the diff asserted about itself. Only a caller with platform
|
|
429
|
+
* access can populate this, which is why it is not read from the tree.
|
|
430
|
+
*/
|
|
431
|
+
verifiedApprovals?: readonly string[];
|
|
375
432
|
}
|
|
376
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The measurement, the ceiling it is compared against, and everything that can
|
|
436
|
+
* refuse independently of the size question.
|
|
437
|
+
*
|
|
438
|
+
* `evaluate` above answers "how big is the tree" and touches no ref; this
|
|
439
|
+
* answers "may this tree ship" and needs the base one. They stay separate
|
|
440
|
+
* because a census that silently depended on a remote ref would behave
|
|
441
|
+
* differently in a shallow clone, and the census is the half other callers use.
|
|
442
|
+
*
|
|
443
|
+
* ORDER MATTERS ONLY IN ONE PLACE: the ceiling is established BEFORE the size
|
|
444
|
+
* comparison, because a run that could not measure its ceiling has no
|
|
445
|
+
* business reporting a tree as within one.
|
|
446
|
+
*/
|
|
377
447
|
export function decide(opts: DecideOptions = {}): Decision {
|
|
378
448
|
const repoRoot = opts.repoRoot ?? REPO_ROOT;
|
|
379
449
|
const git = opts.git ?? realGit;
|
|
380
|
-
const
|
|
381
|
-
const
|
|
450
|
+
const budgetFile = opts.budgetFile ?? budgetFileIn(repoRoot);
|
|
451
|
+
const budget = readBudget(budgetFile);
|
|
452
|
+
const design = Math.round(budget.baseline_tokens * (1 + budget.headroom_pct / 100));
|
|
453
|
+
|
|
382
454
|
const baseRef =
|
|
383
455
|
opts.baseRef !== undefined ? opts.baseRef : resolveBaseRef(repoRoot, opts.env ?? process.env, git);
|
|
456
|
+
|
|
457
|
+
// The base payload goes through THIS file's own census, so base and head
|
|
458
|
+
// cannot be measured by two different definitions — the property that keeps
|
|
459
|
+
// a measured ceiling from drifting against the number it bounds.
|
|
460
|
+
const sumPayload = (root: string): number =>
|
|
461
|
+
measureDeterministicPayload(root).reduce((n, b) => n + b.tokens, 0);
|
|
462
|
+
let basePayload: number | null;
|
|
463
|
+
let baseNote: string | null;
|
|
464
|
+
if (opts.basePayload !== undefined) {
|
|
465
|
+
basePayload = opts.basePayload;
|
|
466
|
+
baseNote = basePayload === null ? 'the base payload was pinned to null by the caller' : null;
|
|
467
|
+
} else if (baseRef === null || baseRef.trim() === '') {
|
|
468
|
+
basePayload = null;
|
|
469
|
+
baseNote = 'no base ref resolved';
|
|
470
|
+
} else {
|
|
471
|
+
const reading = measurePayloadAtRef({ repoRoot, ref: baseRef, measure: sumPayload });
|
|
472
|
+
basePayload = reading.tokens;
|
|
473
|
+
baseNote = reading.note;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const exceptionsFile = path.join(repoRoot, EXCEPTIONS_CONFIG_PATH);
|
|
477
|
+
const ex = readExceptions(exceptionsFile);
|
|
478
|
+
const ceiling = computeCeiling({
|
|
479
|
+
designCeiling: design,
|
|
480
|
+
basePayload,
|
|
481
|
+
baseNote,
|
|
482
|
+
headPayload: sumPayload(repoRoot),
|
|
483
|
+
exceptions: ex.exceptions,
|
|
484
|
+
exceptionErrors: ex.errors,
|
|
485
|
+
verifiedApprovals: opts.verifiedApprovals ?? [],
|
|
486
|
+
// Through the as-of seam, not the wall clock. An exception expiry is
|
|
487
|
+
// exactly the kind of verdict that must be reproducible from the
|
|
488
|
+
// commit: a grant that expires overnight would make the same tree pass
|
|
489
|
+
// and then fail with nothing in the diff to explain it.
|
|
490
|
+
today: opts.today ?? asOf().toISOString().slice(0, 10),
|
|
491
|
+
requireBase: opts.requireBase === true,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const verdict = evaluate(repoRoot, budgetFile, ceiling.ceiling);
|
|
495
|
+
|
|
496
|
+
// The head-side bounds go through `boundsFrom` exactly as the base-side
|
|
497
|
+
// ones do, from the raw configs rather than the narrowed ones — one
|
|
498
|
+
// derivation for both sides, which is what the grace ceiling never had.
|
|
499
|
+
let headBounds: Record<string, number> = { design_ceiling: design };
|
|
500
|
+
try {
|
|
501
|
+
const raw = readBudgetRaw(budgetFile);
|
|
502
|
+
let rawEx: unknown = null;
|
|
503
|
+
try {
|
|
504
|
+
rawEx = JSON.parse(fs.readFileSync(exceptionsFile, 'utf-8'));
|
|
505
|
+
} catch {
|
|
506
|
+
/* absent ledger contributes no bounds */
|
|
507
|
+
}
|
|
508
|
+
headBounds = boundsFrom(raw, rawEx) ?? headBounds;
|
|
509
|
+
} catch {
|
|
510
|
+
/* readBudget above already threw on an unusable file */
|
|
511
|
+
}
|
|
384
512
|
const bounds = assertBoundsDidNotRise({
|
|
385
513
|
repoRoot,
|
|
386
514
|
baseRef,
|
|
387
515
|
git,
|
|
388
|
-
|
|
516
|
+
headBounds,
|
|
389
517
|
requireBase: opts.requireBase === true,
|
|
390
518
|
});
|
|
391
|
-
|
|
519
|
+
|
|
520
|
+
const catalogue = auditCatalogue(repoRoot);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
verdict,
|
|
524
|
+
bounds,
|
|
525
|
+
ceiling,
|
|
526
|
+
catalogue,
|
|
527
|
+
// THE SIZE COMPARISON IS ONLY MADE AGAINST A CEILING THAT WAS
|
|
528
|
+
// ESTABLISHED. An advisory run that could not read the base has no
|
|
529
|
+
// bound to compare against, and falling back to the design ceiling
|
|
530
|
+
// would report a 28 %-over tree as OVER BUDGET for a reason that has
|
|
531
|
+
// nothing to do with the change — the exact "I could not look" /
|
|
532
|
+
// "I looked and it grew" conflation the enforcing refusal spells out
|
|
533
|
+
// in its own message. So it reports and does not gate.
|
|
534
|
+
//
|
|
535
|
+
// This is not a hole: the CI step passes `--require-base`, where an
|
|
536
|
+
// unestablished ceiling REFUSES. It was invisible until the stored
|
|
537
|
+
// ceiling was deleted, because that number was the fallback.
|
|
538
|
+
ok:
|
|
539
|
+
bounds.ok &&
|
|
540
|
+
ceiling.ok &&
|
|
541
|
+
catalogue.findings.length === 0 &&
|
|
542
|
+
(ceiling.verified ? verdict.withinBudget : true),
|
|
543
|
+
};
|
|
392
544
|
}
|
|
393
545
|
|
|
394
546
|
/**
|
|
395
|
-
* Per-asset attribution for a refusal, against the
|
|
547
|
+
* Per-asset attribution for a refusal, against the base tree.
|
|
396
548
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
549
|
+
* Every failure mode returns `null` — the caller treats attribution as an
|
|
550
|
+
* explanation, never as a precondition for refusing. A gate that failed to
|
|
551
|
+
* refuse because it could not explain itself would be strictly worse than one
|
|
552
|
+
* that refuses without the explanation.
|
|
553
|
+
*
|
|
554
|
+
* It reads the base tree through `_lib/base_ref_payload`, the same
|
|
555
|
+
* materialisation the ceiling uses. It used to shell `git archive` directly,
|
|
556
|
+
* which silently drops `export-ignore` paths — harmless for the two roots this
|
|
557
|
+
* ledger reads, and a live 746-token defect for the ceiling. Both now go
|
|
558
|
+
* through the one reader rather than through two that differ in a way nobody
|
|
559
|
+
* would notice until the numbers disagreed.
|
|
401
560
|
*/
|
|
402
|
-
function attributeGrowthAgainstBase(): string[] | null {
|
|
561
|
+
function attributeGrowthAgainstBase(repoRoot: string, baseRef: string | null): string[] | null {
|
|
562
|
+
if (baseRef === null || baseRef.trim() === '') return null;
|
|
563
|
+
const t = extractSurfacesAtRef({ repoRoot, ref: baseRef });
|
|
564
|
+
if (t.tree === null) return null;
|
|
403
565
|
try {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const tar = execFileSync('git', ['archive', base, '--', rel], {
|
|
418
|
-
cwd: REPO_ROOT,
|
|
419
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
420
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
421
|
-
});
|
|
422
|
-
// `-f -` is not optional: without it BSD tar reads its
|
|
423
|
-
// default device rather than stdin, and the extraction
|
|
424
|
-
// silently produces nothing — which reads downstream as
|
|
425
|
-
// "the base tree was unavailable" rather than as a bug.
|
|
426
|
-
execFileSync('tar', ['-x', '-f', '-', '-C', tmp], {
|
|
427
|
-
input: tar,
|
|
428
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
429
|
-
});
|
|
430
|
-
} catch {
|
|
431
|
-
/* root absent at base — nothing to extract */
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
const [rulesRel, skillsRel] = prefixStableRoots();
|
|
435
|
-
const before = buildLedger(
|
|
436
|
-
path.join(tmp, rulesRel ?? ''),
|
|
437
|
-
path.join(tmp, skillsRel ?? ''),
|
|
438
|
-
tmp,
|
|
439
|
-
);
|
|
440
|
-
const after = buildLedger(
|
|
441
|
-
path.join(REPO_ROOT, rulesRel ?? ''),
|
|
442
|
-
path.join(REPO_ROOT, skillsRel ?? ''),
|
|
443
|
-
REPO_ROOT,
|
|
444
|
-
);
|
|
445
|
-
if (before.rows.length === 0) return null;
|
|
446
|
-
return renderAttribution(attributeGrowth(before.rows, after.rows));
|
|
447
|
-
} finally {
|
|
448
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
449
|
-
}
|
|
566
|
+
const [rulesRel, skillsRel] = prefixStableRoots();
|
|
567
|
+
const before = buildLedger(
|
|
568
|
+
path.join(t.tree, rulesRel ?? ''),
|
|
569
|
+
path.join(t.tree, skillsRel ?? ''),
|
|
570
|
+
t.tree,
|
|
571
|
+
);
|
|
572
|
+
const after = buildLedger(
|
|
573
|
+
path.join(repoRoot, rulesRel ?? ''),
|
|
574
|
+
path.join(repoRoot, skillsRel ?? ''),
|
|
575
|
+
repoRoot,
|
|
576
|
+
);
|
|
577
|
+
if (before.rows.length === 0) return null;
|
|
578
|
+
return renderAttribution(attributeGrowth(before.rows, after.rows));
|
|
450
579
|
} catch {
|
|
451
580
|
return null;
|
|
581
|
+
} finally {
|
|
582
|
+
t.dispose();
|
|
452
583
|
}
|
|
453
584
|
}
|
|
454
585
|
|
|
455
586
|
/** The stderr header for a refused bound. Exported so both refusals are testable. */
|
|
456
587
|
export function boundsRefusalHeader(b: BoundsRatchetVerdict): string {
|
|
457
588
|
return b.verified
|
|
458
|
-
? '❌
|
|
459
|
-
: '❌ the standing-payload
|
|
589
|
+
? '❌ a shrink-only standing-payload bound rose in this change:\n'
|
|
590
|
+
: '❌ the standing-payload bounds could not be VERIFIED, and this run requires it:\n';
|
|
460
591
|
}
|
|
461
592
|
|
|
462
593
|
/**
|
|
@@ -475,42 +606,96 @@ export function boundsRefusalHeader(b: BoundsRatchetVerdict): string {
|
|
|
475
606
|
* reserved for the ADVISORY skip, where nothing was refused at all.
|
|
476
607
|
*/
|
|
477
608
|
export function renderBounds(b: BoundsRatchetVerdict): string {
|
|
609
|
+
const label = 'shrink-only bound ratchet';
|
|
478
610
|
if (b.note !== null && b.ok) {
|
|
479
|
-
return ` ${
|
|
611
|
+
return ` ${label.padEnd(38)} ${'SKIPPED'.padStart(8)} — ${b.note}\n`;
|
|
480
612
|
}
|
|
481
613
|
if (!b.verified) {
|
|
482
614
|
return (
|
|
483
|
-
` ${
|
|
484
|
-
`${b.note ?? 'the
|
|
615
|
+
` ${label.padEnd(38)} ${'UNVERIFIED'.padStart(8)} — ` +
|
|
616
|
+
`${b.note ?? 'the bounds could not be read'} (this run requires it)\n`
|
|
485
617
|
);
|
|
486
618
|
}
|
|
487
|
-
const
|
|
619
|
+
const n = b.baseBounds === null ? 0 : Object.keys(b.baseBounds).length;
|
|
488
620
|
const state = b.ok ? 'ok' : 'ROSE';
|
|
489
621
|
return (
|
|
490
|
-
` ${
|
|
491
|
-
`${
|
|
622
|
+
` ${label.padEnd(38)} ${state.padStart(8)} — ` +
|
|
623
|
+
`${String(n)} bound(s) at ${b.baseRef ?? 'n/a'}, shrink-only (ADR-264)\n`
|
|
492
624
|
);
|
|
493
625
|
}
|
|
494
626
|
|
|
495
627
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
496
628
|
const json = argv.includes('--format=json') || argv.includes('--json');
|
|
497
|
-
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
629
|
+
|
|
630
|
+
// `--ceiling <n>` is GONE. It existed to hand the gate the stored grace
|
|
631
|
+
// ceiling, and the ceiling is measured now. Refusing loudly rather than
|
|
632
|
+
// ignoring it is the point: a caller still passing a number believes it is
|
|
633
|
+
// setting the bound, and silently measuring something else would be the
|
|
634
|
+
// most expensive kind of no-op. The replacement for a deliberate, bounded
|
|
635
|
+
// widening is an approved entry in the exceptions ledger.
|
|
636
|
+
if (argv.includes('--ceiling')) {
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
'❌ preamble-payload budget: --ceiling was removed. The ceiling is now MEASURED as\n' +
|
|
639
|
+
' max(design_ceiling, payload at the base ref + active grants), so a caller-supplied\n' +
|
|
640
|
+
` number cannot set it. To widen it deliberately, record an approved grant in\n` +
|
|
641
|
+
` ${EXCEPTIONS_CONFIG_PATH} — it carries an approver, a reason, a watermark and an\n` +
|
|
642
|
+
' expiry, which a flag never did.\n',
|
|
643
|
+
);
|
|
644
|
+
return 2;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// `--require-base`: refuse instead of skipping when the base ref cannot be
|
|
648
|
+
// read. An AI council (2/2, 2026-09-10) made this blocking for the move to
|
|
649
|
+
// a base-measured ceiling, because an unreadable base costs a comparison
|
|
650
|
+
// under a stored ceiling and grants an unbounded budget under a measured
|
|
651
|
+
// one. Opt-in rather than the default, and rather than derived from
|
|
652
|
+
// `GITHUB_ACTIONS`: a shallow clone, a first commit and a detached build
|
|
653
|
+
// all legitimately have no base, and a gate that reds on a developer's
|
|
654
|
+
// machine gets switched off.
|
|
512
655
|
const requireBase = argv.includes('--require-base');
|
|
513
656
|
|
|
657
|
+
// `--approved <id>` (repeatable): this caller VERIFIED that exception's
|
|
658
|
+
// approval event against the platform. Nothing in the tree can establish
|
|
659
|
+
// that — an `approved_by` field in the diff requesting the grant is
|
|
660
|
+
// self-asserted data, which was the 2026-09-11 council's hardest pushback
|
|
661
|
+
// in both seats. So the fact arrives from outside, from a caller that
|
|
662
|
+
// queried the platform, and a grant nobody confirmed contributes zero
|
|
663
|
+
// tokens and refuses the run.
|
|
664
|
+
const verifiedApprovals: string[] = [];
|
|
665
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
666
|
+
if (argv[i] !== '--approved') continue;
|
|
667
|
+
const id = argv[i + 1];
|
|
668
|
+
if (id === undefined || id.startsWith('--')) {
|
|
669
|
+
process.stderr.write('❌ preamble-payload budget: --approved needs an exception id.\n');
|
|
670
|
+
return 2;
|
|
671
|
+
}
|
|
672
|
+
verifiedApprovals.push(id);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// `--repo-root <path>`: measure THIS tree, rather than the one this script
|
|
676
|
+
// happens to sit in. Prerequisite 3 of the measured-ceiling verdict — CI
|
|
677
|
+
// runs the gate as it exists at the BASE ref against the HEAD tree, so a
|
|
678
|
+
// pull request cannot edit the code that measures it. Both seats chose this
|
|
679
|
+
// over code-owner review on the gate, which would put the sole maintainer
|
|
680
|
+
// in the path of every gate-editing pull request. Its honest limit,
|
|
681
|
+
// anthropic's words: "the mechanism does not prevent the exploit; it makes
|
|
682
|
+
// the exploit auditable" — a weakening merged first and exploited second is
|
|
683
|
+
// visible in `git log` and is not blocked.
|
|
684
|
+
const ri = argv.indexOf('--repo-root');
|
|
685
|
+
const repoRootArg = ri !== -1 ? argv[ri + 1] : undefined;
|
|
686
|
+
if (ri !== -1 && (repoRootArg === undefined || repoRootArg.startsWith('--'))) {
|
|
687
|
+
process.stderr.write('❌ preamble-payload budget: --repo-root needs a path.\n');
|
|
688
|
+
return 2;
|
|
689
|
+
}
|
|
690
|
+
const repoRoot = repoRootArg === undefined ? REPO_ROOT : path.resolve(repoRootArg);
|
|
691
|
+
if (!fs.existsSync(budgetFileIn(repoRoot))) {
|
|
692
|
+
process.stderr.write(
|
|
693
|
+
`❌ preamble-payload budget: ${repoRoot} carries no ${'src/config/preamble-payload-budget.json'} — ` +
|
|
694
|
+
'that is not a tree this gate can measure.\n',
|
|
695
|
+
);
|
|
696
|
+
return 2;
|
|
697
|
+
}
|
|
698
|
+
|
|
514
699
|
// `--host <id>` / `--project-rules-dir <path>`: the additive host reading
|
|
515
700
|
// (AI council 2026-09-09, option 1A). Mutually exclusive on purpose — a call
|
|
516
701
|
// passing both is asking two different questions and would silently get one
|
|
@@ -541,7 +726,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
541
726
|
if (hostArg !== undefined || rulesDirArg !== undefined) {
|
|
542
727
|
try {
|
|
543
728
|
host = measureHostPayload(
|
|
544
|
-
|
|
729
|
+
repoRoot,
|
|
545
730
|
hostArg !== undefined
|
|
546
731
|
? { host: hostArg }
|
|
547
732
|
: { rulesDirOverride: rulesDirArg as string },
|
|
@@ -554,10 +739,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
554
739
|
|
|
555
740
|
let decision: Decision;
|
|
556
741
|
try {
|
|
557
|
-
decision = decide({
|
|
558
|
-
...(override === undefined ? {} : { overrideCeiling: override }),
|
|
559
|
-
requireBase,
|
|
560
|
-
});
|
|
742
|
+
decision = decide({ repoRoot, requireBase, verifiedApprovals });
|
|
561
743
|
} catch (err) {
|
|
562
744
|
process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
|
|
563
745
|
return 2;
|
|
@@ -602,10 +784,28 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
602
784
|
},
|
|
603
785
|
host_payload: host,
|
|
604
786
|
ok: decision.ok,
|
|
787
|
+
measured_ceiling: {
|
|
788
|
+
ceiling: decision.ceiling.ceiling,
|
|
789
|
+
design_ceiling: decision.ceiling.designCeiling,
|
|
790
|
+
base_payload: decision.ceiling.basePayload,
|
|
791
|
+
effective_base: decision.ceiling.effectiveBase,
|
|
792
|
+
active_grants: decision.ceiling.activeGrants,
|
|
793
|
+
active_exception_ids: decision.ceiling.activeIds,
|
|
794
|
+
verified: decision.ceiling.verified,
|
|
795
|
+
ok: decision.ceiling.ok,
|
|
796
|
+
note: decision.ceiling.note,
|
|
797
|
+
violations: decision.ceiling.violations,
|
|
798
|
+
},
|
|
799
|
+
catalogue_completeness: {
|
|
800
|
+
ok: decision.catalogue.findings.length === 0,
|
|
801
|
+
trees_seen: decision.catalogue.treesSeen,
|
|
802
|
+
files_scanned: decision.catalogue.filesScanned,
|
|
803
|
+
findings: decision.catalogue.findings,
|
|
804
|
+
},
|
|
605
805
|
bound_ratchet: {
|
|
606
806
|
ok: decision.bounds.ok,
|
|
607
807
|
base_ref: decision.bounds.baseRef,
|
|
608
|
-
|
|
808
|
+
base_bounds: decision.bounds.baseBounds,
|
|
609
809
|
note: decision.bounds.note,
|
|
610
810
|
violations: decision.bounds.violations,
|
|
611
811
|
},
|
|
@@ -626,7 +826,14 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
626
826
|
` ${'measured total'.padEnd(38)} ${String(verdict.measured).padStart(8)} tok ` +
|
|
627
827
|
`(baseline ${verdict.baseline}, ${sign}${delta}; ceiling ${verdict.ceiling})\n`,
|
|
628
828
|
);
|
|
829
|
+
process.stdout.write(renderCeiling(decision.ceiling));
|
|
629
830
|
process.stdout.write(renderBounds(decision.bounds));
|
|
831
|
+
process.stdout.write(
|
|
832
|
+
` ${'catalogue completeness'.padEnd(38)} ` +
|
|
833
|
+
`${(decision.catalogue.findings.length === 0 ? 'ok' : 'GAPS').padStart(8)} — ` +
|
|
834
|
+
`${String(decision.catalogue.treesSeen)} projected tree(s), ` +
|
|
835
|
+
`${String(decision.catalogue.filesScanned)} file(s) scanned for stray rule payload\n`,
|
|
836
|
+
);
|
|
630
837
|
|
|
631
838
|
// The host reading prints AFTER the source verdict and never touches the
|
|
632
839
|
// exit code below. The label says which surface each number describes,
|
|
@@ -653,6 +860,30 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
653
860
|
}
|
|
654
861
|
}
|
|
655
862
|
|
|
863
|
+
// THE CEILING IS ESTABLISHED BEFORE THE SIZE QUESTION. A run that could not
|
|
864
|
+
// measure its own ceiling has no business reporting a tree as within one,
|
|
865
|
+
// and an expired grant is a refusal whatever the payload does.
|
|
866
|
+
if (!decision.ceiling.ok) {
|
|
867
|
+
process.stderr.write(
|
|
868
|
+
(decision.ceiling.verified
|
|
869
|
+
? '❌ the standing-payload exception ledger refuses this run:\n'
|
|
870
|
+
: '❌ the standing-payload ceiling could not be MEASURED, and this run requires it:\n') +
|
|
871
|
+
decision.ceiling.violations.map((v) => ` · ${v}\n`).join(''),
|
|
872
|
+
);
|
|
873
|
+
return 1;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Payload the census cannot see is payload no ceiling bounds. Refused
|
|
877
|
+
// BEFORE the size comparison for the same reason: a green "within budget"
|
|
878
|
+
// over an incomplete catalogue is a measurement of the wrong set.
|
|
879
|
+
if (decision.catalogue.findings.length > 0) {
|
|
880
|
+
process.stderr.write(
|
|
881
|
+
'❌ standing payload sits outside every measured bucket:\n' +
|
|
882
|
+
decision.catalogue.findings.map((v) => ` · ${v}\n`).join(''),
|
|
883
|
+
);
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
886
|
+
|
|
656
887
|
// The bound is checked BEFORE the size question and independently of it. A
|
|
657
888
|
// change that lifts its own ceiling has already defeated the gate, and
|
|
658
889
|
// reporting that as "within budget" would be the config-weakening move
|
|
@@ -668,6 +899,19 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
668
899
|
return 1;
|
|
669
900
|
}
|
|
670
901
|
|
|
902
|
+
// An advisory run that never established a ceiling reports the number and
|
|
903
|
+
// stops. Saying "over budget" against a bound nobody read would send the
|
|
904
|
+
// reader to shrink a rule over a fetch problem — the same misdiagnosis the
|
|
905
|
+
// `--require-base` refusal is worded to prevent, one layer up.
|
|
906
|
+
if (!decision.ceiling.verified) {
|
|
907
|
+
process.stdout.write(
|
|
908
|
+
'⚠️ size comparison SKIPPED — the ceiling was not established in this run, so there is\n' +
|
|
909
|
+
' nothing to compare the measurement against. This is NOT a pass on the payload: pass\n' +
|
|
910
|
+
' --require-base (as CI does) to make an unestablished ceiling a refusal instead.\n',
|
|
911
|
+
);
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
|
|
671
915
|
if (!verdict.withinBudget) {
|
|
672
916
|
// road-to-delivered-cost-truth 2.2 — a gate names its own "no". The
|
|
673
917
|
// ceiling message alone states a fact and leaves the reader to find the
|
|
@@ -680,13 +924,46 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
680
924
|
// build or a first commit legitimately has none. A gate that failed to
|
|
681
925
|
// refuse because it could not explain itself would be strictly worse
|
|
682
926
|
// than one that refuses without the explanation.
|
|
683
|
-
const attribution = attributeGrowthAgainstBase();
|
|
927
|
+
const attribution = attributeGrowthAgainstBase(repoRoot, decision.bounds.baseRef);
|
|
928
|
+
|
|
929
|
+
// REJECTION INSTRUMENTATION — prerequisite 4a's converged floor.
|
|
930
|
+
//
|
|
931
|
+
// Both council seats refused to derive an exception cap from the 250-PR
|
|
932
|
+
// delta distribution, for one reason: the sample is CENSORED by the very
|
|
933
|
+
// ratchet a cap would relax, so every percentile describes what got
|
|
934
|
+
// through rather than what was attempted. The thing that uncensors it is
|
|
935
|
+
// recording the deltas that were BLOCKED, which nothing did.
|
|
936
|
+
//
|
|
937
|
+
// This line is that record, and its limits are worth stating rather than
|
|
938
|
+
// implying: it is a log line in a CI job, not a database. It captures
|
|
939
|
+
// attempts that reached this gate and failed; it cannot see a change
|
|
940
|
+
// nobody pushed. It is deliberately machine-greppable from a workflow
|
|
941
|
+
// log so the distribution can be rebuilt later without a schema anybody
|
|
942
|
+
// has to maintain today.
|
|
943
|
+
process.stdout.write(
|
|
944
|
+
'payload-rejection: ' +
|
|
945
|
+
JSON.stringify({
|
|
946
|
+
measured: verdict.measured,
|
|
947
|
+
ceiling: verdict.ceiling,
|
|
948
|
+
attempted_delta: decision.ceiling.effectiveBase === null
|
|
949
|
+
? null
|
|
950
|
+
: verdict.measured - decision.ceiling.effectiveBase,
|
|
951
|
+
design_ceiling: decision.ceiling.designCeiling,
|
|
952
|
+
base_payload: decision.ceiling.basePayload,
|
|
953
|
+
base_ref: decision.bounds.baseRef,
|
|
954
|
+
active_grants: decision.ceiling.activeGrants,
|
|
955
|
+
buckets: verdict.buckets.map((b) => ({ name: b.name, tokens: b.tokens })),
|
|
956
|
+
}) +
|
|
957
|
+
'\n',
|
|
958
|
+
);
|
|
959
|
+
|
|
684
960
|
process.stderr.write(
|
|
685
961
|
`❌ per-spawn preamble payload grew past the ratchet: ${verdict.measured} > ${verdict.ceiling} tok.\n` +
|
|
686
962
|
` Every rule and skill description here is re-written on EVERY subagent spawn, so growth\n` +
|
|
687
963
|
` is paid per spawn, not once. Shrink the addition, or migrate the prose out of the\n` +
|
|
688
|
-
` standing rule. The
|
|
689
|
-
`
|
|
964
|
+
` standing rule. The ceiling may NOT be widened to fit it — it is MEASURED at the base\n` +
|
|
965
|
+
` ref, so there is no number to edit. A change whose offsetting reduction is genuinely\n` +
|
|
966
|
+
` unsafe takes an approved, dated grant in ${EXCEPTIONS_CONFIG_PATH}.\n`,
|
|
690
967
|
);
|
|
691
968
|
// Three distinct states, and conflating the last two is a diagnostic
|
|
692
969
|
// defect rather than a cosmetic one: "I could not look" and "I looked
|