@event4u/agent-config 9.35.0 → 10.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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +95 -142
  3. package/agents/templates/.ai-council.yml.example +31 -51
  4. package/dist/agent-src/commands/analyze/conformance.md +15 -4
  5. package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
  6. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  7. package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
  8. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  9. package/dist/agent-src/rules/session-canary.md +28 -0
  10. package/dist/agent-src/rules/ui-audit-gate.md +35 -0
  11. package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
  12. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  13. package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
  14. package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
  15. package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
  16. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  17. package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
  18. package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
  19. package/dist/agent-src/templates/AGENTS.md +1 -1
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
  22. package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
  23. package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
  24. package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
  25. package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
  26. package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
  27. package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
  28. package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
  29. package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
  30. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  31. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  32. package/dist/cli-delegate/cmd_conformance.js +1038 -197
  33. package/dist/cli-delegate/cmd_doctor.js +4 -4
  34. package/dist/cli-delegate/cmd_explain.js +2 -2
  35. package/dist/cli-delegate/cmd_export.js +4 -4
  36. package/dist/cli-delegate/cmd_handoff.js +3 -3
  37. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  38. package/dist/cli-delegate/cmd_migrate.js +2 -2
  39. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  40. package/dist/cli-delegate/cmd_prune.js +4 -4
  41. package/dist/cli-delegate/cmd_refresh.js +1 -1
  42. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  43. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +9 -2
  45. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  46. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  47. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  48. package/dist/cli-delegate/cmd_sync.js +4 -4
  49. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  50. package/dist/cli-delegate/cmd_update.js +1 -1
  51. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  52. package/dist/cli-delegate/cmd_validate.js +4 -4
  53. package/dist/cli-delegate/cmd_versions.js +1 -1
  54. package/dist/discovery/deprecation-report.md +1 -1
  55. package/dist/discovery/discovery-manifest.json +14 -14
  56. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  57. package/dist/discovery/discovery-manifest.summary.md +1 -1
  58. package/dist/discovery/dormancy-report.md +1 -1
  59. package/dist/discovery/orphan-report.md +1 -1
  60. package/dist/discovery/packs.json +2 -2
  61. package/dist/discovery/trust-report.md +1 -1
  62. package/dist/discovery/workspaces.json +2 -2
  63. package/dist/hooks/dispatch.js +654 -411
  64. package/dist/install/install.mjs +39 -29
  65. package/dist/mcp/registry-manifest.json +1 -1
  66. package/dist/mcp/server.json +2 -2
  67. package/dist/mcp/server.mjs +1580 -617
  68. package/dist/router.json +1 -1
  69. package/dist/scripts/_lib/agent_settings.js +39 -25
  70. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  71. package/dist/scripts/condense.js +15 -2
  72. package/dist/scripts/condense.js.map +1 -1
  73. package/dist/server/routes/wizard.js +23 -3
  74. package/dist/server/routes/wizard.js.map +1 -1
  75. package/dist/server/schemas/settings.js +11 -6
  76. package/dist/server/schemas/settings.js.map +1 -1
  77. package/docs/contracts/settings-classes.md +16 -9
  78. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
  79. package/package.json +2 -2
  80. package/src/agent-src/templates/AGENTS.md +1 -1
  81. package/src/config/agent-settings.template.yml +38 -36
  82. package/src/config/ci-local-parity.yml +44 -0
  83. package/src/config/gate-coverage.yml +12 -0
  84. package/src/scripts/_cli/cmd_doctor.ts +32 -0
  85. package/src/scripts/_cli/cmd_migrate.ts +26 -3
  86. package/src/scripts/_cli/cmd_refresh.ts +29 -0
  87. package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
  88. package/src/scripts/_lib/agent_settings.ts +39 -25
  89. package/src/scripts/_lib/environment_detector.ts +52 -1
  90. package/src/scripts/_lib/hook_settings.ts +79 -0
  91. package/src/scripts/_lib/ui_surface.ts +97 -0
  92. package/src/scripts/ai_council/config.ts +62 -24
  93. package/src/scripts/before_complete_hook.ts +39 -0
  94. package/src/scripts/capture_skill_catalogue.ts +483 -0
  95. package/src/scripts/check_bridge_derivation.ts +89 -3
  96. package/src/scripts/check_ci_local_parity.ts +74 -0
  97. package/src/scripts/check_references.ts +15 -1
  98. package/src/scripts/condense.ts +16 -2
  99. package/src/scripts/conformance_scan.ts +200 -9
  100. package/src/scripts/council_cli.ts +95 -3
  101. package/src/scripts/hook_manifest.yaml +30 -9
  102. package/src/scripts/hooks/block_no_verify.ts +82 -9
  103. package/src/scripts/hooks/concern_registry.ts +2 -0
  104. package/src/scripts/hooks/design_slop_hook.ts +1 -1
  105. package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
  106. package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
  107. package/src/scripts/lint_roadmap_complexity.ts +60 -0
  108. package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
  109. package/src/scripts/probe_promissory_closing.ts +180 -0
  110. package/src/scripts/probe_session_canary.ts +180 -0
  111. package/src/scripts/report_consultation_rate.ts +358 -0
  112. package/src/scripts/report_skill_activation.ts +5 -1
  113. package/src/server/schemas/settings.ts +14 -13
  114. /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
  115. /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
  116. /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
  117. /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
@@ -355,6 +355,24 @@ project:
355
355
  # Branch prefix for improvement PRs
356
356
  improvement_pr_branch_prefix: "improve/agent-"
357
357
 
358
+ # Who this project is built for. Read by the demand gate
359
+ # (docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md
360
+ # § 8-pre), whose L0-L4 ladder measures MARKET demand and is therefore
361
+ # meaningless where no market is intended.
362
+ # self -> a tool its maintainer builds for themselves. The demand gate
363
+ # is inert; work is classified L-self (build), never deferred
364
+ # for lack of a user population that is not wanted.
365
+ # internal -> a team / company internal tool. Only "what breaks without
366
+ # it?" survives; the requester is known.
367
+ # client -> built for a named client. The client is the requester, not a
368
+ # market segment.
369
+ # public -> a product with an intended market. Full three-question gate
370
+ # and the full ladder.
371
+ # Default `public` = the behaviour every install had before this key existed.
372
+ # Absent resolves to `public` for the same reason: this package has no
373
+ # defaults layer, so absent means nobody said.
374
+ audience: public
375
+
358
376
  # --- GitHub integration ---
359
377
  github:
360
378
  # API method for replying to PR review comments
@@ -1190,42 +1208,12 @@ hooks:
1190
1208
  injection_scan:
1191
1209
  enabled: false
1192
1210
 
1193
- # Stop-slot turn-end gate (road-to-conformance-round5 Phase 3). Default-OFF,
1194
- # and it is the only concern in this file that can REFUSE a turn-end rather
1195
- # than a tool call — so it soaks behind an explicit opt-in before it binds.
1196
- #
1197
- # Round 5 measured what it is for: both BLOCKING carriers reached zero
1198
- # violations, neither ADVISORY carrier did, and 19 wrong-language replies
1199
- # survived a pin that had fired 26-35 seconds earlier. A reminder at higher
1200
- # frequency is the same failed mechanism running more often; a check at
1201
- # delivery that can say no is a different one.
1202
- #
1203
- # enabled — master switch. false = the concern is a no-op costing one
1204
- # settings read per turn-end.
1205
- # promissory — detector A: the closing paragraph promises work not yet
1206
- # performed while asking the user nothing (20 measured).
1207
- # language — detector B: the user-visible prose is not in the pinned
1208
- # language, with code, quotes, paths and identifiers already
1209
- # excluded (19 measured).
1210
- # verification — detector C: the turn changed a file and then ran nothing that
1211
- # could have checked it — no test, type-check, lint or build call
1212
- # follows the LAST edit. Ordering is the point: a run before the
1213
- # final edit did not exercise it, which is the freshness clause
1214
- # verify-before-complete states. Reads the turn's tool calls, not
1215
- # the prose. Deliberately narrow on what counts as verification —
1216
- # a missed detection costs one unguarded turn, a false refusal
1217
- # teaches you to switch the gate off.
1218
- #
1219
- # All three detectors default ON *within* the master switch, so opting in needs
1220
- # one line, and any one can be silenced without editing the hook manifest.
1221
- # A refused turn continues in the same turn and is never refused twice —
1222
- # re-entrancy is `stop_hook_active` plus a marker keyed on the last user
1223
- # message, never on the reply.
1224
- turn_end_gate:
1225
- enabled: false
1226
- promissory: true
1227
- language: true
1228
- verification: true
1211
+ # NOTE — `turn_end_gate` has no keys here any more. The stop-slot turn-end
1212
+ # gate is ALWAYS ARMED (2026-08-12); `hooks.turn_end_gate.*` was removed and
1213
+ # a leftover block warns once on stderr and is ignored. Whether it fires is
1214
+ # decided by each detector's own trigger — no promise in the closing
1215
+ # paragraph, no language-pin mismatch, no unverified edit ⇒ the turn ends.
1216
+ # See `src/scripts/hooks/turn_end_gate_hook.ts` § "Always armed".
1229
1217
 
1230
1218
  # PreToolUse RTK-wrap nudge (token-saving Phase 3). Default-OFF. When enabled
1231
1219
  # AND `rtk` is on PATH (a live probe — not a self-reported flag), warns (exit
@@ -1248,6 +1236,20 @@ hooks:
1248
1236
  design_slop:
1249
1237
  enabled: false
1250
1238
 
1239
+ # PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4).
1240
+ # Default-OFF. When enabled, a Write/Edit to a UI surface with no design
1241
+ # consultation latched this session WARNS (exit 2, never blocks) naming the
1242
+ # route: run existing-ui-audit, then the fe-design loop. A read or search
1243
+ # touching fe-design / existing-ui-audit / design-review / design-intelligence
1244
+ # latches consultation and silences it for the rest of the session, so the
1245
+ # agent that did the right thing is never nagged. Anti-loop: at most 2 nudges
1246
+ # per session. It does NOT read the two UI rules: the UI-surface decision
1247
+ # comes from _lib/ui_surface.ts, so this runs parallel to them rather than
1248
+ # consuming their triggers. Host-limited (pre_tool_use fires on ~2-3 hosts) —
1249
+ # elsewhere the obligation stays model-carried, as the two UI rules declare.
1250
+ ui_route_nudge:
1251
+ enabled: false
1252
+
1251
1253
  # PreToolUse code-graph nudge (ADR-124 Phase 4). Default-OFF. When enabled
1252
1254
  # AND a native code-graph engine has built a cache (or the repo ships a
1253
1255
  # graph.json / SCIP index), WARNS once per session (exit 2, never blocks) as
@@ -20,6 +20,38 @@
20
20
  # plus the checker makes each direction a declared, reviewable choice instead of an
21
21
  # accident.
22
22
  #
23
+ # THE THIRD DIMENSION — `preflight`, added round 7, REPORT-ONLY
24
+ # ------------------------------------------------------------
25
+ # The two directions above compare CI against the local MIRROR (`task ci` +
26
+ # `task consistency` + the pre-push hook). `task ci` INCLUDES `preflight`, so
27
+ # neither direction can see the gap that actually costs push cycles: the pass a
28
+ # contributor is TOLD to run before pushing is `task preflight`, a deliberate
29
+ # subset of `ci`.
30
+ #
31
+ # Measured 2026-08-12: preflight reaches **22** gates; **209** CI-enforced gates
32
+ # are locally reachable and run in NEITHER preflight nor anywhere else the
33
+ # pre-push hook reaches — i.e. the set that can only fail after a push. Round 7
34
+ # traced three "green locally, red remotely" cycles to it (the Thin-Root char cap,
35
+ # the ratchet in the Node-Tests shard, derived-page freshness).
36
+ #
37
+ # The first reading said 221 and subtracted preflight alone, which counted gates
38
+ # the pre-push hook already runs and therefore cannot fail remotely — corrected
39
+ # by the round's own completion review (finding 4). Both figures are kept because
40
+ # the correction is the point: a number published as "the deliverable" was
41
+ # computed over a superset.
42
+ #
43
+ # It is REPORT-ONLY, and deliberately not a declared list, for two reasons:
44
+ # 1. The subset is intentional. preflight's own docstring excludes
45
+ # check_enforcement_coverage at a measured 30.7 s because "a pre-push gate
46
+ # that doubles the hook teaches people to skip it". Failing on the gap would
47
+ # demand preflight grow into ci — the opposite of that decision.
48
+ # 2. 209 per-entry declarations would BE the allowlist-growth antipattern the
49
+ # `autonomous-execution` rule caps at 20: past that count, the list is the
50
+ # problem and not the exemption.
51
+ #
52
+ # What the report removes is the IMPLICATION that a green preflight predicts a
53
+ # green CI. It does not, and now it says so on every run.
54
+ #
23
55
  # THE CONTRACT
24
56
  # ------------
25
57
  # The checker DERIVES both sides — it never trusts a hand-maintained list of gates:
@@ -152,6 +184,18 @@ ci_only:
152
184
  # worth remote enforcement, OR it should simply be added to a workflow. Prefer
153
185
  # adding it: a local-only gate is the direction that let real defects merge.
154
186
  local_only:
187
+ - id: roadmap_progress_hook
188
+ class: git-hook-scoped
189
+ reason: >
190
+ A git PRE-COMMIT hook script, invoked by the installed hook that
191
+ `install-hooks.sh` writes. CI has no commit to intercept — the commit has
192
+ already happened by the time a workflow runs — so there is no remote
193
+ equivalent to wire it into. Undeclared since main's `fd8c56512`
194
+ ("point the pre-commit roadmap gate at the live script path"), which moved
195
+ the hook onto the real script path and thereby made it visible to this
196
+ checker for the first time; it turned the gate red on the trunk, where
197
+ round 7 found it. Declared rather than wired: the committed-tree equivalent
198
+ is `roadmap:progress-check`, which already runs remotely.
155
199
  - id: check_knowledge_sharing
156
200
  class: staging-scoped
157
201
  reason: >
@@ -168,6 +168,18 @@ gates:
168
168
  corpus: "src/rules/*.md frontmatters (111 at baseline)"
169
169
  status: enforced
170
170
 
171
+ - id: lint_rule_skill_pack_reach
172
+ argv: ["--quiet"]
173
+ min_scanned: 90
174
+ corpus: "src/rules/*.md frontmatters joined against src/skills packs and the pack registry (116 rules at baseline)"
175
+ status: enforced
176
+ note: >
177
+ Advisory on findings, enforced on COVERAGE: the gate exits 0 while its
178
+ unreachable-route set is non-empty (12 at introduction), so only this
179
+ floor can catch it going blind. A collapse below 90 would mean it stopped
180
+ reading src/rules, which is exactly the shape that would make "0 findings"
181
+ read as clean.
182
+
171
183
  - id: check_condensation
172
184
  argv: []
173
185
  min_scanned: 380
@@ -3591,10 +3591,42 @@ function main(argv: string[] | null = null): number {
3591
3591
 
3592
3592
  // --- CLI entry ---
3593
3593
 
3594
+ /**
3595
+ * Defined by `build:cli-delegate` only — see `cmd_session_recycle.ts` for why a
3596
+ * single "am I inside a bundle" flag cannot answer "may I run".
3597
+ */
3598
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
3599
+ declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
3600
+
3594
3601
  function _isCliEntry(): boolean {
3602
+ const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
3603
+ const cliDelegate =
3604
+ typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
3605
+ // Inlined into the installer / hook / MCP bundle: never auto-run. This
3606
+ // file previously carried no such guard while its comment described the
3607
+ // pairing, so the four siblings documented one mechanism and implemented
3608
+ // two.
3609
+ if (bundled && !cliDelegate) {
3610
+ return false;
3611
+ }
3595
3612
  if (process.argv[1] === undefined) {
3596
3613
  return false;
3597
3614
  }
3615
+ if (cliDelegate) {
3616
+ // `--splitting` moves this module's body into a shared chunk, so the URL
3617
+ // comparison below weighs the CHUNK against `argv[1]` and never matches.
3618
+ // `agent-config doctor` shipped producing zero bytes and exit 0 for
3619
+ // exactly that reason — a diagnostic reporting success while saying
3620
+ // nothing. The invoked file name is the reliable signal inside this
3621
+ // bundle; the delegate smoke test keeps the literal honest.
3622
+ if (path.basename(process.argv[1], '.js') === 'cmd_doctor') {
3623
+ return true;
3624
+ }
3625
+ // A miss falls THROUGH to the realpath comparison below rather than
3626
+ // returning false: a symlinked or renamed invocation is exactly the
3627
+ // case that fallback exists for, and swallowing it here would rebuild
3628
+ // the silent no-op this change removes.
3629
+ }
3598
3630
  const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
3599
3631
  if (import.meta.url === argvUrl) {
3600
3632
  return true;
@@ -932,14 +932,37 @@ export function main(argv: string[] | null = null, options: MainOptions = {}): n
932
932
  // build's `--define`) disables the auto-run there; under tsx/node it is
933
933
  // undefined, so `typeof` short-circuits and the standalone CLI guard fires
934
934
  // normally.
935
+ // The sentinel above is true in EVERY esbuild target, `build:cli-delegate`
936
+ // included — and there this file is the entry, so refusing on it refused to run
937
+ // the bundle built to run it: `dist/cli-delegate/cmd_migrate.js` produced zero
938
+ // bytes and exit 0, and `agent-config migrate` reported success while doing
939
+ // nothing. `__AGENT_CONFIG_CLI_DELEGATE__` is defined by that one build only, so
940
+ // the pair distinguishes "inlined into the installer" (never run) from "this
941
+ // bundle's own target" (run, subject to the entry check below).
935
942
  declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
936
- const _bundled =
937
- typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
943
+ declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
944
+ const _inForeignBundle =
945
+ typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' &&
946
+ __AGENT_CONFIG_BUNDLE__ &&
947
+ !(typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__);
938
948
  const _HERE = fileURLToPath(import.meta.url);
939
949
  function _isCliEntry(): boolean {
940
950
  if (process.argv[1] === undefined) {
941
951
  return false;
942
952
  }
953
+ if (typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__) {
954
+ // `--splitting` moves this module's body into a shared chunk, so the URL
955
+ // comparison below weighs the CHUNK against `argv[1]` and never matches —
956
+ // `agent-config migrate` shipped producing zero bytes and exit 0. The
957
+ // invoked file name is the reliable signal inside this bundle.
958
+ if (path.basename(process.argv[1], '.js') === 'cmd_migrate') {
959
+ return true;
960
+ }
961
+ // A miss falls THROUGH to the realpath comparison below rather than
962
+ // returning false: a symlinked or renamed invocation is exactly the
963
+ // case that fallback exists for, and swallowing it here would rebuild
964
+ // the silent no-op this change removes.
965
+ }
943
966
  const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
944
967
  if (import.meta.url === argvUrl) {
945
968
  return true;
@@ -958,7 +981,7 @@ function _isCliEntry(): boolean {
958
981
  }
959
982
  }
960
983
 
961
- if (!_bundled && (_isCliEntry() || process.argv[1] === _HERE)) {
984
+ if (!_inForeignBundle && (_isCliEntry() || process.argv[1] === _HERE)) {
962
985
  try {
963
986
  process.exitCode = main(process.argv.slice(2));
964
987
  } catch (exc) {
@@ -449,10 +449,39 @@ export function main(argv: string[] | null = null, options: MainOptions = {}): n
449
449
 
450
450
  // --- CLI entry ---
451
451
 
452
+ /**
453
+ * Defined by `build:cli-delegate` only — see `cmd_session_recycle.ts` for why the
454
+ * URL comparison below cannot decide this inside that bundle.
455
+ */
456
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
457
+ declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
458
+
452
459
  function _isCliEntry(): boolean {
460
+ const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
461
+ const cliDelegate =
462
+ typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
463
+ // Inlined into the installer / hook / MCP bundle: never auto-run. This
464
+ // file previously carried no such guard while its comment described the
465
+ // pairing, so the four siblings documented one mechanism and implemented
466
+ // two.
467
+ if (bundled && !cliDelegate) {
468
+ return false;
469
+ }
453
470
  if (process.argv[1] === undefined) {
454
471
  return false;
455
472
  }
473
+ if (cliDelegate) {
474
+ // Found by running every delegate bundle, not by reading: `--splitting`
475
+ // had moved this body into a shared chunk too, so `agent-config refresh`
476
+ // was a silent exit 0 like doctor, migrate and session:recycle.
477
+ if (path.basename(process.argv[1], '.js') === 'cmd_refresh') {
478
+ return true;
479
+ }
480
+ // A miss falls THROUGH to the realpath comparison below rather than
481
+ // returning false: a symlinked or renamed invocation is exactly the
482
+ // case that fallback exists for, and swallowing it here would rebuild
483
+ // the silent no-op this change removes.
484
+ }
456
485
  const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
457
486
  if (import.meta.url === argvUrl) {
458
487
  return true;
@@ -252,11 +252,49 @@ export function main(argv: string[] = process.argv.slice(2)): number {
252
252
  return result.code;
253
253
  }
254
254
 
255
- // Bundle-safety: never auto-run when inlined into an esbuild bundle.
255
+ /**
256
+ * Bundle-safety: never auto-run when inlined into SOMEONE ELSE'S bundle.
257
+ *
258
+ * `__AGENT_CONFIG_BUNDLE__` alone cannot express that. All four esbuild targets
259
+ * define it, `build:cli-delegate` included — and that bundle's entry points ARE
260
+ * these files, so a guard refusing on the flag refuses to run the very bundle
261
+ * built to run it. That shipped: `dist/cli-delegate/cmd_session_recycle.js`
262
+ * produced zero bytes and exit 0 on every installed copy, so
263
+ * `agent-config session:recycle` reported success while writing no envelope.
264
+ * `cmd_doctor` and `cmd_migrate` carried the same guard and were dead the same
265
+ * way.
266
+ *
267
+ * `__AGENT_CONFIG_CLI_DELEGATE__` is defined by that one build only, so the pair
268
+ * says what a single flag could not: inlined elsewhere (install / hook / MCP
269
+ * bundle) → never run; this bundle's own target → run, and let the `argv[1]`
270
+ * comparison below decide whether THIS module is the entry or a shared chunk
271
+ * pulled in beside it.
272
+ */
256
273
  declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
274
+ declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
257
275
  function _isCliEntry(): boolean {
258
- if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
276
+ const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
277
+ const cliDelegate =
278
+ typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
279
+ if (bundled && !cliDelegate) return false;
259
280
  if (process.argv[1] === undefined) return false;
281
+ if (cliDelegate) {
282
+ // `--splitting` turns the entry file into a re-export shim and moves this
283
+ // module's body into a shared chunk, where `import.meta.url` is the
284
+ // CHUNK's url and can never equal `argv[1]`. The comparison below then
285
+ // silently never fires — which is why two sibling commands shipped dead
286
+ // while others worked by accident, purely by where esbuild happened to
287
+ // place their code. Inside this bundle the invoked file name is the only
288
+ // reliable signal; the smoke test over every delegate bundle is what
289
+ // keeps this literal honest if the file is ever renamed.
290
+ if (path.basename(process.argv[1], '.js') === 'cmd_session_recycle') {
291
+ return true;
292
+ }
293
+ // A miss falls THROUGH to the realpath comparison below rather than
294
+ // returning false: a symlinked or renamed invocation is exactly the
295
+ // case that fallback exists for, and swallowing it here would rebuild
296
+ // the silent no-op this change removes.
297
+ }
260
298
  if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
261
299
  try {
262
300
  return (
@@ -884,31 +884,45 @@ export function load_agent_settings(
884
884
  }
885
885
 
886
886
  /**
887
- * Settings keys deleted by the always-on-orchestration doctrine
888
- * (road-to-always-on-orchestration Phase 1 for the `subagents.*` keys;
889
- * Phase 1 Step 1.3 for `ai_team.enabled`). A leftover value from an older
887
+ * Settings keys this package has DELETED. A leftover value from an older
890
888
  * install is ignored — never applied by any reader — and surfaced with ONE
891
889
  * deprecation line per key per process run (stderr; the exit code never
892
890
  * changes).
893
891
  *
894
- * `ai_team.enabled` joined this list in Step 1.3: the `/team` family's
895
- * availability is now a codex-CLI/auth FACT
896
- * (`src/scripts/ai_team/availability.ts`), the same doctrine already
897
- * applied to `subagents.*` below, not a settings flag. The `ai_team`
898
- * config loader (`src/scripts/ai_team/config.ts`) separately accepts a
899
- * leftover `enabled` key without failing closed, so this warning is the
900
- * only surfaced signal of the deletion.
892
+ * The map carries a per-key REASON rather than one shared sentence, because
893
+ * the list now spans more than one deletion doctrine and a single blanket
894
+ * phrase would mis-attribute every future entry. A reader who hits the line
895
+ * should learn why the key went, not just that it did.
896
+ *
897
+ * `subagents.*` — the always-on-orchestration doctrine
898
+ * (road-to-always-on-orchestration Phase 1): the layer has no per-layer
899
+ * on/off setting any more. `ai_team.enabled` joined in Step 1.3 — the
900
+ * `/team` family's availability is a codex-CLI/auth FACT
901
+ * (`src/scripts/ai_team/availability.ts`), not a flag; the `ai_team` config
902
+ * loader (`src/scripts/ai_team/config.ts`) separately accepts a leftover
903
+ * `enabled` key without failing closed, so this warning is the only surfaced
904
+ * signal of that deletion.
905
+ *
906
+ * `hooks.turn_end_gate.*` — the turn-end gate is always armed (2026-08-12).
907
+ * A default-off safety gate cannot soak, so the switch protecting the soak
908
+ * made the soak impossible; whether the gate fires is now decided by each
909
+ * detector's own trigger conditions. See `turn_end_gate_hook.ts` § "Always
910
+ * armed".
901
911
  */
902
- const REMOVED_ALWAYS_ON_KEYS: readonly string[] = [
903
- 'subagents.enabled',
904
- 'subagents.auto',
905
- 'subagents.host_capabilities',
906
- 'subagents.budget_routing',
907
- 'ai_team.enabled',
908
- ];
912
+ const REMOVED_KEYS: ReadonlyMap<string, string> = new Map([
913
+ ['subagents.enabled', 'always-on orchestration'],
914
+ ['subagents.auto', 'always-on orchestration'],
915
+ ['subagents.host_capabilities', 'always-on orchestration'],
916
+ ['subagents.budget_routing', 'always-on orchestration'],
917
+ ['ai_team.enabled', 'always-on orchestration'],
918
+ ['hooks.turn_end_gate.enabled', 'the turn-end gate is always armed'],
919
+ ['hooks.turn_end_gate.promissory', 'the turn-end gate is always armed'],
920
+ ['hooks.turn_end_gate.language', 'the turn-end gate is always armed'],
921
+ ['hooks.turn_end_gate.verification', 'the turn-end gate is always armed'],
922
+ ]);
909
923
 
910
924
  /** Keys already warned about in THIS process — the "once per run" dedupe. */
911
- const _warnedRemovedAlwaysOnKeys = new Set<string>();
925
+ const _warnedRemovedKeys = new Set<string>();
912
926
 
913
927
  /** Read one dotted path out of a merged settings tree; `undefined` when absent. */
914
928
  function _readDottedSettingsPath(root: SettingsDict, dotted: string): SettingsValue {
@@ -923,20 +937,20 @@ function _readDottedSettingsPath(root: SettingsDict, dotted: string): SettingsVa
923
937
  }
924
938
 
925
939
  /**
926
- * Warn once per process, on stderr, for every {@link REMOVED_ALWAYS_ON_KEYS}
927
- * still present in a resolved settings tree. Never throws, never changes
928
- * what the caller does with `merged` — a pure notification side effect.
940
+ * Warn once per process, on stderr, for every {@link REMOVED_KEYS} entry still
941
+ * present in a resolved settings tree. Never throws, never changes what the
942
+ * caller does with `merged` — a pure notification side effect.
929
943
  */
930
944
  function _warn_removed_always_on_keys(merged: SettingsDict): void {
931
- for (const key of REMOVED_ALWAYS_ON_KEYS) {
932
- if (_warnedRemovedAlwaysOnKeys.has(key)) {
945
+ for (const [key, reason] of REMOVED_KEYS) {
946
+ if (_warnedRemovedKeys.has(key)) {
933
947
  continue;
934
948
  }
935
949
  if (_readDottedSettingsPath(merged, key) === undefined) {
936
950
  continue;
937
951
  }
938
- _warnedRemovedAlwaysOnKeys.add(key);
939
- process.stderr.write(`${key} was removed (always-on orchestration); ignored.\n`);
952
+ _warnedRemovedKeys.add(key);
953
+ process.stderr.write(`${key} was removed (${reason}); ignored.\n`);
940
954
  }
941
955
  }
942
956
 
@@ -220,6 +220,50 @@ export interface DetectEnvironmentOptions {
220
220
  * hardened env and a 2s timeout; pass `() => null` to skip probing.
221
221
  */
222
222
  readonly probeVersion?: VersionProbe;
223
+ /**
224
+ * macOS Keychain presence probe for the Claude Code subscription
225
+ * credential. Omit to shell out to `security find-generic-password`;
226
+ * pass `() => false` to skip it entirely — every test that is not
227
+ * specifically about this probe should, so a developer machine's real
228
+ * Keychain never decides a test outcome.
229
+ */
230
+ readonly probeKeychain?: KeychainProbe;
231
+ }
232
+
233
+ /** `(service) => present?` — never returns the secret, only whether it exists. */
234
+ export type KeychainProbe = (service: string) => boolean;
235
+
236
+ /** Keychain service name Claude Code writes its subscription credential under. */
237
+ export const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
238
+
239
+ /**
240
+ * Is the Claude Code subscription credential in the macOS login Keychain?
241
+ *
242
+ * Claude Code stores its OAuth credential in the Keychain on macOS and only
243
+ * falls back to `~/.claude/.credentials.json` elsewhere. Looking for the file
244
+ * alone therefore reports "no subscription" on the platform where the
245
+ * subscription is most likely to exist — which sent every macOS anthropic
246
+ * member down the metered `api` rung while a paid Claude subscription sat
247
+ * unused on the same machine. That is the defect this probe closes.
248
+ *
249
+ * `-w` is deliberately NOT passed: without it `security` prints attributes and
250
+ * never the password, so the secret cannot reach a log, a report, or a crash
251
+ * dump. Presence is read from the exit code alone.
252
+ */
253
+ function defaultProbeKeychain(service: string): boolean {
254
+ if (process.platform !== 'darwin') return false;
255
+ try {
256
+ const res = spawnSync('security', ['find-generic-password', '-s', service], {
257
+ timeout: VERSION_PROBE_TIMEOUT_MS,
258
+ encoding: 'utf-8',
259
+ windowsHide: true,
260
+ env: hardenedSpawnEnv(),
261
+ });
262
+ if (res.error !== undefined || res.signal !== null) return false;
263
+ return res.status === 0;
264
+ } catch {
265
+ return false;
266
+ }
223
267
  }
224
268
 
225
269
  /**
@@ -298,6 +342,7 @@ function cliSubscriptionEvidence(
298
342
  home: string,
299
343
  env: EnvMap,
300
344
  claudeConfigDir: string,
345
+ probeKeychain: KeychainProbe,
301
346
  ): { source: AuthSource; evidence: string } | null {
302
347
  if (provider === 'anthropic') {
303
348
  const configDir = claudeConfigDir;
@@ -305,6 +350,11 @@ function cliSubscriptionEvidence(
305
350
  const p = path.join(configDir, rel);
306
351
  if (isFile(p)) return { source: 'cli-subscription', evidence: p };
307
352
  }
353
+ // macOS keeps the credential in the Keychain instead of the file above.
354
+ // The evidence string is a locator, never the secret.
355
+ if (probeKeychain(CLAUDE_KEYCHAIN_SERVICE)) {
356
+ return { source: 'cli-subscription', evidence: `keychain:${CLAUDE_KEYCHAIN_SERVICE}` };
357
+ }
308
358
  return null;
309
359
  }
310
360
  if (provider === 'openai') {
@@ -359,11 +409,12 @@ function detectAuth(opts: DetectEnvironmentOptions): {
359
409
  opts.home === undefined && opts.env === undefined
360
410
  ? claude_config_dir()
361
411
  : ((env['CLAUDE_CONFIG_DIR'] ?? '').trim() || path.join(home, '.claude'));
412
+ const probeKeychain = opts.probeKeychain ?? defaultProbeKeychain;
362
413
  const auth: DetectedAuth[] = [];
363
414
  const keys: DetectedKey[] = [];
364
415
 
365
416
  for (const provider of knownProviders()) {
366
- const cli = cliSubscriptionEvidence(provider, home, env, claudeConfigDir);
417
+ const cli = cliSubscriptionEvidence(provider, home, env, claudeConfigDir, probeKeychain);
367
418
  if (cli !== null) {
368
419
  auth.push({ provider, source: cli.source, evidence: cli.evidence });
369
420
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * hook_settings — read a `hooks.<section>.enabled` flag out of
3
+ * `.agent-settings.yml` without dragging a YAML parser into a hook.
4
+ *
5
+ * Every default-OFF hook needs the same two-level lookup, and the tree already
6
+ * carries two hand-rolled copies of it (`code_graph_nudge_hook.enabled` and the
7
+ * equivalent inside `design_slop_hook`). Adding a third copy for the route
8
+ * nudge is what this file prevents; the two existing copies are pre-existing
9
+ * duplication and are deliberately left alone — folding them in is a separate,
10
+ * behaviour-neutral change, not something to smuggle into a feature diff.
11
+ *
12
+ * Fail-closed on the flag, fail-open on the read: an unreadable or malformed
13
+ * settings file returns `false`, which keeps a default-OFF hook off. A hook
14
+ * that turned itself ON because it could not read a file would be the worst
15
+ * possible failure mode for an opt-in nudge.
16
+ */
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+
20
+ const SETTINGS_FILE = '.agent-settings.yml';
21
+
22
+ /**
23
+ * True when `.agent-settings.yml` under `root` carries
24
+ * `hooks:\n <section>:\n enabled: true`.
25
+ *
26
+ * Deliberately indentation-shaped rather than a real parse: the same shape the
27
+ * existing hooks rely on, and a hook must never fail a tool call because a
28
+ * dependency could not load.
29
+ */
30
+ export function hookSectionEnabled(root: string, section: string): boolean {
31
+ const file = path.join(root, SETTINGS_FILE);
32
+ let text: string;
33
+ try {
34
+ if (!fs.statSync(file).isFile()) return false;
35
+ text = fs.readFileSync(file, 'utf-8');
36
+ } catch {
37
+ return false;
38
+ }
39
+
40
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ const sectionPattern = new RegExp(`^(\\s+)${escaped}\\s*:\\s*$`);
42
+ let inHooks = false;
43
+ /** Indent width of the matched section header; null while outside it. */
44
+ let sectionIndent: number | null = null;
45
+
46
+ const indentOf = (line: string): number => line.length - line.replace(/^\s+/, '').length;
47
+
48
+ for (const raw of text.split(/\r\n|\r|\n/)) {
49
+ const line = raw.replace(/\s+$/, '');
50
+ if (!line || line.replace(/^\s+/, '').startsWith('#')) continue;
51
+
52
+ // A non-indented line closes both scopes and may open `hooks:`.
53
+ if (!(line.startsWith(' ') || line.startsWith('\t'))) {
54
+ inHooks = /^hooks\s*:\s*$/.test(line);
55
+ sectionIndent = null;
56
+ continue;
57
+ }
58
+
59
+ if (inHooks) {
60
+ const opened = sectionPattern.exec(line);
61
+ if (opened) {
62
+ // Remember the ACTUAL indent rather than assuming two spaces.
63
+ // The previous version closed the section on any line indented
64
+ // <= 3, so on a 4-space file a sibling section never closed and
65
+ // a default-OFF hook could read another section's enabled: true.
66
+ sectionIndent = opened[1]!.length;
67
+ continue;
68
+ }
69
+ // A key at or above the section header's own depth closes it.
70
+ if (sectionIndent !== null && indentOf(line) <= sectionIndent) {
71
+ sectionIndent = null;
72
+ }
73
+ }
74
+
75
+ if (sectionIndent !== null && /^\s+enabled\s*:\s*true\b/.test(line)) return true;
76
+ }
77
+
78
+ return false;
79
+ }