@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +95 -142
- package/agents/templates/.ai-council.yml.example +31 -51
- package/dist/agent-src/commands/analyze/conformance.md +15 -4
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/session-canary.md +28 -0
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
- package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
- package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
- package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
- package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
- package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1038 -197
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +9 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- 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 +654 -411
- package/dist/install/install.mjs +39 -29
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1580 -617
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +39 -25
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +15 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +23 -3
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +11 -6
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +16 -9
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/config/agent-settings.template.yml +38 -36
- package/src/config/ci-local-parity.yml +44 -0
- package/src/config/gate-coverage.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_lib/agent_settings.ts +39 -25
- package/src/scripts/_lib/environment_detector.ts +52 -1
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +62 -24
- package/src/scripts/before_complete_hook.ts +39 -0
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/check_bridge_derivation.ts +89 -3
- package/src/scripts/check_ci_local_parity.ts +74 -0
- package/src/scripts/check_references.ts +15 -1
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/conformance_scan.ts +200 -9
- package/src/scripts/council_cli.ts +95 -3
- package/src/scripts/hook_manifest.yaml +30 -9
- package/src/scripts/hooks/block_no_verify.ts +82 -9
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/probe_promissory_closing.ts +180 -0
- package/src/scripts/probe_session_canary.ts +180 -0
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/server/schemas/settings.ts +14 -13
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /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
|
-
#
|
|
1194
|
-
#
|
|
1195
|
-
#
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
#
|
|
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
|
|
937
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
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
|
|
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
|
|
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
|
|
927
|
-
*
|
|
928
|
-
*
|
|
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
|
|
932
|
-
if (
|
|
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
|
-
|
|
939
|
-
process.stderr.write(`${key} was removed (
|
|
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
|
+
}
|