@event4u/agent-config 14.17.0 → 14.19.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 +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- 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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* (measured: ADR-035 still asserts a rejection ADR-232 reopened).
|
|
26
26
|
* - **review_trigger** — verbatim, plus a state.
|
|
27
27
|
*
|
|
28
|
-
* `indeterminate` is a FIRST-CLASS trigger result, not a failure.
|
|
28
|
+
* `indeterminate` is a FIRST-CLASS trigger result, not a failure. Almost every
|
|
29
29
|
* `review_trigger` in the corpus is a semantic condition ("reopen when the
|
|
30
30
|
* capacity premise changes", "if a fifth band appears"), so forcing them to a
|
|
31
31
|
* boolean would convert uncertainty into either permission or blockage. Both
|
|
@@ -33,6 +33,29 @@
|
|
|
33
33
|
* trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
|
|
34
34
|
* result means: this may not be presented as an unqualified lock — route it.
|
|
35
35
|
*
|
|
36
|
+
* THE DATED SUB-CLASS, AND WHY THE 2026-08-19 REJECTION DOES NOT REACH IT
|
|
37
|
+
* ----------------------------------------------------------------------
|
|
38
|
+
* The sentence above used to open "Every `review_trigger` in the corpus is a
|
|
39
|
+
* semantic condition"; it is weakened rather than deleted because the claim was
|
|
40
|
+
* true when written and one record has since falsified it.
|
|
41
|
+
*
|
|
42
|
+
* What those two seats rejected was forcing SEMANTIC conditions to a boolean.
|
|
43
|
+
* A trigger that OPENS with a calendar date is not one: `fired` / `not-fired`
|
|
44
|
+
* is decided by comparing that date to `asOf()`, which is arithmetic and
|
|
45
|
+
* carries no interpretation of the tree, so a second reader of the same commit
|
|
46
|
+
* gets the same answer. That is the entire carve-out — a leading
|
|
47
|
+
* `Expiry YYYY-MM-DD`, or a bare leading ISO date, and nothing else. A
|
|
48
|
+
* condition that merely MENTIONS a date somewhere in its prose ("revisit at the
|
|
49
|
+
* 2026-11-10 review date, or earlier on either observable event") is a semantic
|
|
50
|
+
* condition and stays `indeterminate`, which is what keeps this from being the
|
|
51
|
+
* rejected grammar by increments.
|
|
52
|
+
*
|
|
53
|
+
* `dated-unparsed` exists so the carve-out cannot fail silently. A trigger that
|
|
54
|
+
* announces itself as dated and whose date this parser cannot read is a defect
|
|
55
|
+
* in the record or in the parser; returning `indeterminate` for it would hide
|
|
56
|
+
* that defect behind the exact state every trigger had before the carve-out
|
|
57
|
+
* existed, where no test could ever see it as a regression.
|
|
58
|
+
*
|
|
36
59
|
* It also prints the two descriptive axes (`provenance`, `evidence`) and
|
|
37
60
|
* `authority_basis`, and for an accepted E0/E1 record that does NOT carry
|
|
38
61
|
* `authority_basis: owner_intent` it prints
|
|
@@ -73,12 +96,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
73
96
|
import {
|
|
74
97
|
type AdrFrontmatter,
|
|
75
98
|
authorityBasisOf,
|
|
99
|
+
dated_trigger_day,
|
|
100
|
+
DATED_TRIGGER_PROBE,
|
|
101
|
+
iso_day,
|
|
76
102
|
evidenceOf,
|
|
77
103
|
isLowEvidenceAccepted,
|
|
78
104
|
provenanceOf,
|
|
79
105
|
readAdrFrontmatter,
|
|
80
106
|
readAdrFrontmatterScalars,
|
|
81
107
|
} from './_lib/adr_frontmatter.js';
|
|
108
|
+
import { asOf } from './_lib/as_of.js';
|
|
82
109
|
import { GateLedger } from './_lib/gate_ledger.js';
|
|
83
110
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
84
111
|
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
@@ -149,8 +176,12 @@ export const SURFACES_NOT_SCANNED = [
|
|
|
149
176
|
*/
|
|
150
177
|
export const PARTIAL_COVERAGE: readonly string[] = [];
|
|
151
178
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
179
|
+
/**
|
|
180
|
+
* A trigger state. `none` means the ADR never recorded a reopen condition;
|
|
181
|
+
* `dated-unparsed` means it recorded one that announces a date this parser
|
|
182
|
+
* could not read, which is a defect and not an answer.
|
|
183
|
+
*/
|
|
184
|
+
export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired' | 'dated-unparsed';
|
|
154
185
|
|
|
155
186
|
export interface CiteResult {
|
|
156
187
|
ref: string;
|
|
@@ -469,12 +500,21 @@ export function amendment_blocks(body: string): string[] {
|
|
|
469
500
|
/**
|
|
470
501
|
* A trigger's state, from what is mechanically decidable and nothing else.
|
|
471
502
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
503
|
+
* Two clauses return a decided answer, and both read a leading calendar date
|
|
504
|
+
* and nothing else — see `DATED_TRIGGER_RE` for why the anchor is the safety
|
|
505
|
+
* property. Every semantic trigger still returns `indeterminate`, which is a
|
|
506
|
+
* first-class result here and not a failure.
|
|
507
|
+
*
|
|
508
|
+
* The clock is read ONLY when a dated trigger is actually present — `asOf()`
|
|
509
|
+
* warns on stderr for an unpinned run, and a corpus with no dated trigger has
|
|
510
|
+
* no verdict that depends on the hour, so announcing one there would be noise
|
|
511
|
+
* on a stream other tools read beside the payload.
|
|
512
|
+
*
|
|
513
|
+
* @param asOfDate the "now" to compare against; defaults to the `asOf()` seam,
|
|
514
|
+
* so a CI run compares against the commit date and is
|
|
515
|
+
* reproducible from the commit alone.
|
|
476
516
|
*/
|
|
477
|
-
export function trigger_state(fm: Record<string, string
|
|
517
|
+
export function trigger_state(fm: Record<string, string>, asOfDate?: Date): TriggerState {
|
|
478
518
|
const t = (fm['review_trigger'] ?? '').trim();
|
|
479
519
|
if (t === '') return 'none';
|
|
480
520
|
// The transitional migration value records that no condition has been
|
|
@@ -485,6 +525,9 @@ export function trigger_state(fm: Record<string, string>): TriggerState {
|
|
|
485
525
|
// nobody has evaluated. Found in completion review; the field did not
|
|
486
526
|
// exist in this file's corpus when the mapping was written.
|
|
487
527
|
if (t.toLowerCase() === 'unclassified') return 'none';
|
|
528
|
+
const day = dated_trigger_day(t);
|
|
529
|
+
if (day !== null) return iso_day(asOfDate ?? asOf()) >= day ? 'fired' : 'not-fired';
|
|
530
|
+
if (DATED_TRIGGER_PROBE.test(t)) return 'dated-unparsed';
|
|
488
531
|
return 'indeterminate';
|
|
489
532
|
}
|
|
490
533
|
|
|
@@ -530,6 +573,15 @@ export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
|
|
|
530
573
|
if (r.trigger_state === 'none') {
|
|
531
574
|
return 'LIVE, NO REOPEN CONDITION — the ADR records no `review_trigger`, so nothing would ever reopen it on its own. Treat that as a defect in the ADR, not as strength of the lock.';
|
|
532
575
|
}
|
|
576
|
+
if (r.trigger_state === 'fired') {
|
|
577
|
+
return 'LIVE, TRIGGER FIRED — the reopen condition is a date and that date has passed. The record still binds until someone acts on it, and citing it without naming the fired trigger presents a lapsed review as a current one.';
|
|
578
|
+
}
|
|
579
|
+
if (r.trigger_state === 'not-fired') {
|
|
580
|
+
return 'LIVE, TRIGGER NOT YET FIRED — the reopen condition is a date still in the future. This is the one state in which a trigger genuinely adds nothing to the citation, and it stops being true on a known day.';
|
|
581
|
+
}
|
|
582
|
+
if (r.trigger_state === 'dated-unparsed') {
|
|
583
|
+
return 'LIVE, DATED TRIGGER UNREADABLE — the reopen condition announces a date this tool could not parse. That is a defect in the record or in the parser, not a verdict: fix the trigger to open with `Expiry YYYY-MM-DD` rather than reading this as "unknown".';
|
|
584
|
+
}
|
|
533
585
|
return 'LIVE, TRIGGER INDETERMINATE — the reopen condition is semantic and this tool cannot decide it. Not an unqualified lock: evaluate the condition against the current tree and route the result.';
|
|
534
586
|
}
|
|
535
587
|
|
|
@@ -823,21 +875,25 @@ export interface CorpusRow {
|
|
|
823
875
|
/** Does the record carry a `review_trigger` at all? */
|
|
824
876
|
has_trigger: boolean;
|
|
825
877
|
/** Only meaningful when `has_trigger`; see `TRIGGER_VERDICTS`. */
|
|
826
|
-
trigger:
|
|
878
|
+
trigger: TriggerState;
|
|
827
879
|
reopen_policy: string;
|
|
828
880
|
/** Cited anywhere OUTSIDE `docs/decisions/`. */
|
|
829
881
|
cited_outside: boolean;
|
|
830
882
|
}
|
|
831
883
|
|
|
832
884
|
/**
|
|
833
|
-
* The
|
|
885
|
+
* The states a record CARRYING a trigger can be in, and why `none` is not one.
|
|
886
|
+
*
|
|
887
|
+
* `none` is outside the denominator, not a bucket inside it: the survey asks
|
|
888
|
+
* that these sum to "the number of ADRs carrying a trigger", and a record with
|
|
889
|
+
* no trigger carries none.
|
|
834
890
|
*
|
|
835
|
-
* `
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
891
|
+
* `dated-unparsed` was added with the dated sub-class and belongs in the sum
|
|
892
|
+
* for the same reason `none` does not — a record in that state DOES carry a
|
|
893
|
+
* trigger. Leaving it out would have made the sum line report a mismatch, i.e.
|
|
894
|
+
* announced a defect in the survey where the defect is in the ADR.
|
|
839
895
|
*/
|
|
840
|
-
export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate'] as const;
|
|
896
|
+
export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate', 'dated-unparsed'] as const;
|
|
841
897
|
|
|
842
898
|
/** Every ADR file, surveyed. */
|
|
843
899
|
export function corpus_survey(repo_root: string = REPO_ROOT): CorpusRow[] {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// External-AI clients for the council — ported from the retired Python
|
|
2
2
|
// `src/scripts/ai_council/clients.py` (ADR-200).
|
|
3
3
|
//
|
|
4
|
-
// Mirrors the contract from `scripts/skill_trigger_eval.
|
|
4
|
+
// Mirrors the contract from `src/scripts/skill_trigger_eval.ts`:
|
|
5
5
|
// - Tokens come exclusively from `~/.event4u/agent-config/<provider>.key`
|
|
6
6
|
// (legacy `~/.config/agent-config/<provider>.key` is read as a fallback so
|
|
7
7
|
// pre-2.4 installs keep working until the user moves the files into the new
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autonomy_friction_corpus — how many host confirmations each payload class
|
|
3
|
+
* costs, measured through the real dispatcher.
|
|
4
|
+
*
|
|
5
|
+
* WHY A CONFIRMATION COUNT AND NOT A GATE COUNT. "Three gates fire" says
|
|
6
|
+
* nothing about what the operator experiences; "this call costs one
|
|
7
|
+
* confirmation" is the quantity the round is about. A corpus counting gates
|
|
8
|
+
* would go green on a change that fired fewer gates and prompted just as often.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT MEASURES, STATED NARROWLY. The number of confirmations THIS PACKAGE
|
|
11
|
+
* leaves on the table for a call — 0 when the dispatcher hands the host an
|
|
12
|
+
* explicit `permissionDecision: allow`, 1 when it emits no permission field and
|
|
13
|
+
* the host's own machinery decides. It is NOT a measurement of what the host
|
|
14
|
+
* finally does: a consumer whose settings already allow the call is not
|
|
15
|
+
* prompted either way, and a confined session ignores our allow entirely
|
|
16
|
+
* (`permissionDecision=allow ignored: a confined session takes grants only
|
|
17
|
+
* from its command line`). Reading this as a host-behaviour measurement is the
|
|
18
|
+
* fact-about-our-plumbing-written-as-a-fact-about-the-host collapse the hook
|
|
19
|
+
* contract warns about three times.
|
|
20
|
+
*
|
|
21
|
+
* A REFUSAL IS NOT A CONFIRMATION. A case the dispatcher blocks (exit 2) is
|
|
22
|
+
* reported as `refused` and scores no confirmations — counting a refusal as
|
|
23
|
+
* friction would reward removing a gate, which is the opposite of the point.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* ./scripts-run src/scripts/autonomy_friction_corpus [--json]
|
|
27
|
+
* ./agent-config … (no verb — this is a package-internal measurement)
|
|
28
|
+
*/
|
|
29
|
+
import { spawnSync } from "node:child_process";
|
|
30
|
+
import * as fs from "node:fs";
|
|
31
|
+
import * as os from "node:os";
|
|
32
|
+
import * as path from "node:path";
|
|
33
|
+
import process from "node:process";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..");
|
|
38
|
+
const ENTRY = path.join(REPO_ROOT, "src", "scripts", "hooks", "dispatch_entry.ts");
|
|
39
|
+
const TSX_BIN = path.join(
|
|
40
|
+
REPO_ROOT,
|
|
41
|
+
"node_modules",
|
|
42
|
+
".bin",
|
|
43
|
+
process.platform === "win32" ? "tsx.cmd" : "tsx",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** One payload class, with the confirmations it is expected to cost. */
|
|
47
|
+
export interface FrictionCase {
|
|
48
|
+
id: string;
|
|
49
|
+
/** The class the owner named, in their words. */
|
|
50
|
+
klass: string;
|
|
51
|
+
tool: string;
|
|
52
|
+
input: Record<string, unknown>;
|
|
53
|
+
/** Confirmations this call SHOULD cost. */
|
|
54
|
+
expected: number;
|
|
55
|
+
/** Why that number, in one line. */
|
|
56
|
+
because: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One measured result. */
|
|
60
|
+
export interface FrictionResult extends FrictionCase {
|
|
61
|
+
observed: number;
|
|
62
|
+
refused: boolean;
|
|
63
|
+
pass: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The six classes the round named, plus the directory-flag form.
|
|
68
|
+
*
|
|
69
|
+
* The seventh is not padding: cases 1 and 2 are the shape the canon stopped
|
|
70
|
+
* teaching, and without the shape that replaced them the corpus would record
|
|
71
|
+
* the cost and not the fix.
|
|
72
|
+
*/
|
|
73
|
+
export const CASES: readonly FrictionCase[] = [
|
|
74
|
+
{
|
|
75
|
+
id: "directory-change",
|
|
76
|
+
klass: "directory change",
|
|
77
|
+
tool: "Bash",
|
|
78
|
+
input: { command: "cd sub" },
|
|
79
|
+
expected: 1,
|
|
80
|
+
because:
|
|
81
|
+
"a bare `cd` is not category A — the canon's answer is a directory flag, " +
|
|
82
|
+
"not an allow for changing directory",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "directory-change-compounded",
|
|
86
|
+
klass: "directory change compounded with a status read",
|
|
87
|
+
tool: "Bash",
|
|
88
|
+
input: { command: "cd sub && git status" },
|
|
89
|
+
expected: 1,
|
|
90
|
+
because:
|
|
91
|
+
"a compound command is refused before its argv is read — a safe head " +
|
|
92
|
+
"token must not be able to carry a second command",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "directory-flag-status",
|
|
96
|
+
klass: "the directory-flag form of the case above",
|
|
97
|
+
tool: "Bash",
|
|
98
|
+
input: { command: "git -C sub status" },
|
|
99
|
+
expected: 0,
|
|
100
|
+
because: "one simple command, a read-only git subcommand, inside the working tree",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "test-runner-in-subdirectory",
|
|
104
|
+
klass: "a test runner invoked from a subdirectory",
|
|
105
|
+
tool: "Bash",
|
|
106
|
+
input: { command: "vitest run sub" },
|
|
107
|
+
expected: 0,
|
|
108
|
+
because: "a test runner admitted by name — the name IS the operation",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "workspace-package-manager",
|
|
112
|
+
klass: "a workspace package manager",
|
|
113
|
+
tool: "Bash",
|
|
114
|
+
input: { command: "npm run test" },
|
|
115
|
+
expected: 0,
|
|
116
|
+
because: "a declared subcommand with a declared script name",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "consequence-authorized",
|
|
120
|
+
klass: "a consequence operation with the authorizing phrase present",
|
|
121
|
+
tool: "Bash",
|
|
122
|
+
input: { command: "terraform apply" },
|
|
123
|
+
expected: 1,
|
|
124
|
+
because:
|
|
125
|
+
"the phrase changes the RECORD, never the prompt — after ADR-254 the " +
|
|
126
|
+
"Hard Floor is model-held and this package grants nothing",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "consequence-unauthorized",
|
|
130
|
+
klass: "the same operation with the authorizing phrase absent",
|
|
131
|
+
tool: "Bash",
|
|
132
|
+
input: { command: "terraform apply" },
|
|
133
|
+
expected: 1,
|
|
134
|
+
because:
|
|
135
|
+
"identical to the case above BY DESIGN — that the two are the same " +
|
|
136
|
+
"number is the finding, not a defect in the corpus",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/** Run one case through the real dispatcher and count its confirmations. */
|
|
141
|
+
export function measure(c: FrictionCase, workspace: string): FrictionResult {
|
|
142
|
+
const payload = JSON.stringify({
|
|
143
|
+
session_id: "autonomy-friction-corpus",
|
|
144
|
+
cwd: workspace,
|
|
145
|
+
hook_event_name: "PreToolUse",
|
|
146
|
+
tool_name: c.tool,
|
|
147
|
+
tool_input: c.input,
|
|
148
|
+
});
|
|
149
|
+
const r = spawnSync(
|
|
150
|
+
TSX_BIN,
|
|
151
|
+
[ENTRY, "--platform", "claude", "--event", "pre_tool_use", "--project-dir", workspace],
|
|
152
|
+
{
|
|
153
|
+
encoding: "utf-8",
|
|
154
|
+
cwd: workspace,
|
|
155
|
+
input: payload,
|
|
156
|
+
timeout: 120_000,
|
|
157
|
+
env: { ...process.env, AGENT_CONFIG_PACKAGE_ROOT: REPO_ROOT },
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
const refused = r.status === 2;
|
|
161
|
+
const allowed = !refused && (r.stdout ?? "").includes('"permissionDecision":"allow"');
|
|
162
|
+
const observed = refused || allowed ? 0 : 1;
|
|
163
|
+
return { ...c, observed, refused, pass: refused || observed === c.expected };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function runCorpus(): FrictionResult[] {
|
|
167
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "ac-friction-"));
|
|
168
|
+
fs.mkdirSync(path.join(workspace, "sub"), { recursive: true });
|
|
169
|
+
try {
|
|
170
|
+
return CASES.map((c) => measure(c, workspace));
|
|
171
|
+
} finally {
|
|
172
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function render(results: readonly FrictionResult[]): string {
|
|
177
|
+
const out: string[] = [];
|
|
178
|
+
out.push("autonomy_friction_corpus — confirmations per payload class");
|
|
179
|
+
out.push("");
|
|
180
|
+
out.push(" measured through the real dispatcher; a confirmation is one this");
|
|
181
|
+
out.push(" package leaves to the host, NOT one the host necessarily shows.");
|
|
182
|
+
out.push("");
|
|
183
|
+
for (const r of results) {
|
|
184
|
+
const mark = r.pass ? "✅" : "❌";
|
|
185
|
+
const obs = r.refused ? "refused" : String(r.observed);
|
|
186
|
+
out.push(` ${mark} ${r.id.padEnd(30)} expected ${r.expected} observed ${obs}`);
|
|
187
|
+
out.push(` ${r.klass}`);
|
|
188
|
+
out.push(` ${r.because}`);
|
|
189
|
+
}
|
|
190
|
+
const failures = results.filter((r) => !r.pass);
|
|
191
|
+
out.push("");
|
|
192
|
+
out.push(
|
|
193
|
+
failures.length === 0
|
|
194
|
+
? ` ${results.length} case(s), every observation matches its expectation`
|
|
195
|
+
: ` ${failures.length} of ${results.length} case(s) miss their expectation: ` +
|
|
196
|
+
failures.map((f) => f.id).join(", "),
|
|
197
|
+
);
|
|
198
|
+
return out.join("\n") + "\n";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
202
|
+
const json = argv.includes("--json");
|
|
203
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
204
|
+
process.stdout.write(
|
|
205
|
+
"usage: autonomy_friction_corpus [--json]\n" +
|
|
206
|
+
" Reports the confirmations each payload class costs, measured\n" +
|
|
207
|
+
" through the real dispatcher. Exit 1 when an observation misses\n" +
|
|
208
|
+
" its expectation.\n",
|
|
209
|
+
);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
const results = runCorpus();
|
|
213
|
+
process.stdout.write(json ? `${JSON.stringify(results, null, 2)}\n` : render(results));
|
|
214
|
+
return results.every((r) => r.pass) ? 0 : 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _isCliEntry(): boolean {
|
|
218
|
+
const argv1 = process.argv[1];
|
|
219
|
+
if (argv1 === undefined) return false;
|
|
220
|
+
try {
|
|
221
|
+
return fs.realpathSync(argv1) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (_isCliEntry()) {
|
|
228
|
+
process.exit(main());
|
|
229
|
+
}
|
|
@@ -413,10 +413,20 @@ function render(): string {
|
|
|
413
413
|
{
|
|
414
414
|
const rows = collectEnforcement();
|
|
415
415
|
const s = summariseEnforcement(rows);
|
|
416
|
+
// The split ships WITH the figure, and the word `yet` travels only with
|
|
417
|
+
// the half it is true of. `kernel_denied` rules cannot carry an
|
|
418
|
+
// `enforced_by` field at all — `block_kernel_rule_writes` refuses the
|
|
419
|
+
// write with no agent-accessible override — so publishing them as "not
|
|
420
|
+
// declared yet" promises a future that does not exist for them. They
|
|
421
|
+
// still count as uncovered — the counting policy is unchanged; only the
|
|
422
|
+
// claim about their future is.
|
|
416
423
|
L.push(
|
|
417
424
|
`**Axis 1 — enforcement level per rule.** ${s.total} rules · ` +
|
|
418
425
|
`${s.blocking} blocking (${s.blocking_pct}%) · ${s.observer} observer · ` +
|
|
419
|
-
`${s.local_only} local-only · ${s.undeclared} undeclared
|
|
426
|
+
`${s.local_only} local-only · ${s.undeclared} undeclared — of which ` +
|
|
427
|
+
`${s.kernel_denied} kernel-denied (\`block_kernel_rule_writes\` refuses an ` +
|
|
428
|
+
`\`enforced_by\` write on a kernel rule, so no declaration is reachable ` +
|
|
429
|
+
`for them at all) and ${s.undeclared - s.kernel_denied} not declared yet.`,
|
|
420
430
|
);
|
|
421
431
|
L.push('');
|
|
422
432
|
// The denominator, WITH the frame that produced it. Until 2026-08-23 the
|
|
@@ -436,7 +446,10 @@ function render(): string {
|
|
|
436
446
|
L.push(`| \`${r.id}\` | ${r.effective} | ${decl} |`);
|
|
437
447
|
}
|
|
438
448
|
L.push('');
|
|
439
|
-
L.push(
|
|
449
|
+
L.push(
|
|
450
|
+
`Undeclared rules (${s.undeclared}, of which ${s.kernel_denied} kernel-denied) ` +
|
|
451
|
+
'carry no row — an honest gap beats a false claim.',
|
|
452
|
+
);
|
|
440
453
|
}
|
|
441
454
|
L.push('');
|
|
442
455
|
{
|