@event4u/agent-config 9.35.0 → 9.36.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 +61 -165
- 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/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-ZAXH3ZWU.js → chunk-JP53LNVT.js} +9 -0
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NR7HSN3P.js} +2 -2
- package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-SFGARMAB.js} +11 -1
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-U7CMAIOC.js} +7 -2
- package/dist/cli-delegate/cmd_conformance.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +3 -3
- package/dist/cli-delegate/cmd_migrate.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +3 -3
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +8 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +3 -3
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +3 -3
- package/dist/cli-delegate/cmd_uninstall.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +12 -12
- 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 +530 -323
- package/dist/install/install.mjs +18 -4
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -5
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +5 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +4 -2
- 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 +32 -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/hook_settings.ts +79 -0
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/hook_manifest.yaml +18 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- 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/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/server/schemas/settings.ts +9 -0
- /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
|
@@ -180,8 +180,8 @@ dispatch runs, not WHETHER the layer exists, so they keep their own C rows.
|
|
|
180
180
|
|---|---|
|
|
181
181
|
| A — preference | 27 |
|
|
182
182
|
| B — consent | 3 |
|
|
183
|
-
| C — guarded |
|
|
184
|
-
| **Total** | **
|
|
183
|
+
| C — guarded | 114 |
|
|
184
|
+
| **Total** | **144** |
|
|
185
185
|
|
|
186
186
|
The total is every leaf in the template, where *leaf* means anything that is not
|
|
187
187
|
a **non-empty** map. An empty map (like the former `subagents.host_capabilities: {}`) is a real
|
|
@@ -236,6 +236,7 @@ Rows follow template order, so a diff against the template reads straight down.
|
|
|
236
236
|
| `project.pr_template` | C | `.github/pull_request_template.md` | filesystem path whose contents reach an outbound PR |
|
|
237
237
|
| `project.upstream_repo` | C | `""` | destination of outbound improvement PRs |
|
|
238
238
|
| `project.improvement_pr_branch_prefix` | A | `improve/agent-` | branch-name cosmetics |
|
|
239
|
+
| `project.audience` | C | `public` | C-test 4 — it governs the agent's own reasoning discipline: `self` makes the § 8-pre demand gate inert. Who a project is built for is a fact only its maintainer knows, so the agent never infers it and never asks; hand-edit or the GUI write route. The default is today's behaviour, so an install that never sets it is unchanged |
|
|
239
240
|
| `github.pr_reply_method` | A | `create_review_comment` | picks between two endpoints of one operation |
|
|
240
241
|
| `augment.rules_use_symlinks` | A | `false` | reversible install mechanics |
|
|
241
242
|
| `eloquent.access_style` | A | `getters_setters` | code convention |
|
|
@@ -324,6 +325,7 @@ Rows follow template order, so a diff against the template reads straight down.
|
|
|
324
325
|
| `hooks.injection_scan.enabled` | C | `false` | the prompt-injection scanner |
|
|
325
326
|
| `hooks.rtk_wrap.enabled` | C | `false` | configures code that runs on every tool call |
|
|
326
327
|
| `hooks.design_slop.enabled` | C | `false` | configures code that runs on every tool call |
|
|
328
|
+
| `hooks.ui_route_nudge.enabled` | C | `false` | configures code that runs on every tool call |
|
|
327
329
|
| `hooks.code_graph.enabled` | C | `false` | configures code that runs on every tool call |
|
|
328
330
|
| `hooks.turn_end_gate.enabled` | C | `false` | arms the only concern that can REFUSE a turn-end; an agent must never be able to switch its own delivery gate on |
|
|
329
331
|
| `hooks.turn_end_gate.promissory` | C | `true` | which refusal the turn-end gate may raise; inert while the master switch is off |
|
|
@@ -108,6 +108,22 @@ feature" ask, before checks 8a–8c. It is a three-question advisor, NOT a
|
|
|
108
108
|
product-management framework — surface it in one short block, then proceed on
|
|
109
109
|
the user's answer (never block; `improve-before-implement`'s golden rule holds).
|
|
110
110
|
|
|
111
|
+
**Read the addressee before the questions.** The ladder below measures **market
|
|
112
|
+
demand**, and market demand is a meaningful quantity only where a market is
|
|
113
|
+
intended. Who the project is built for is `project.audience` — resolve it with
|
|
114
|
+
`agent-config settings:get project.audience`, which reports the value and the
|
|
115
|
+
file it came from, rather than opening one file. An absent value resolves to
|
|
116
|
+
`public`: this package has no defaults layer that supplies one at read time, so
|
|
117
|
+
absent means *nobody said*, and the conservative reading of that is the shipped
|
|
118
|
+
default.
|
|
119
|
+
|
|
120
|
+
| `project.audience` | What 8-pre does |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `self` | **Inert.** One line — "demand gate skipped (audience: self)" — then straight to 8a. No deferral, no demand question. |
|
|
123
|
+
| `internal` | Question 2 only ("what breaks without it?"). Questions 1 and 3 are already answered — the requester is the team. |
|
|
124
|
+
| `client` | The requester is the client, not a segment. Question 2 survives; 1 and 3 are answered by the engagement. |
|
|
125
|
+
| `public` | **Today's behaviour, unchanged** — all three questions and the full ladder. |
|
|
126
|
+
|
|
111
127
|
Three questions:
|
|
112
128
|
|
|
113
129
|
1. **Who requested this?** (a real user / segment, or an internal hunch?)
|
|
@@ -118,16 +134,42 @@ Map the answers to a compressed feature-demand hierarchy and recommend:
|
|
|
118
134
|
|
|
119
135
|
| Level | Signal | Recommendation |
|
|
120
136
|
|---|---|---|
|
|
137
|
+
| L-self | The maintainer needs it for their own work — no user population is intended | **Build** |
|
|
121
138
|
| L0 | Founder/agent anxiety — "it feels missing" | **Defer** — validate demand first |
|
|
122
139
|
| L1 | One anecdotal request, no pattern | **Defer** — watch for repetition |
|
|
123
140
|
| L2 | Repeated requests, no measured impact | **Validate** — instrument before building |
|
|
124
141
|
| L3 | Blocks activation/retention for a real segment | **Build** |
|
|
125
142
|
| L4 | Users are churning / deals lost without it | **Build now** |
|
|
126
143
|
|
|
127
|
-
Build
|
|
128
|
-
one missing evidence named.
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
Build at **L-self**, **L3**, or **L4**; L0–L2 get a defer/validate
|
|
145
|
+
recommendation with the one missing evidence named.
|
|
146
|
+
|
|
147
|
+
`L-self` is not a weaker L3. L3 and L4 are defined exclusively over third-party
|
|
148
|
+
users — "a real segment", "users are churning" — so a project with no intended
|
|
149
|
+
user population cannot reach either, not because it is bad but because the scale
|
|
150
|
+
has no value for it. Its ceiling on the L0–L4 ladder is L0, whose recommendation
|
|
151
|
+
is *defer*, which would defer a single-user tool forever. `L-self` is the level
|
|
152
|
+
that ladder is missing, not a discount on the ones it has.
|
|
153
|
+
|
|
154
|
+
**The artefact consequence.** At `L-self`, or at `audience: self` / `internal`,
|
|
155
|
+
never write a roadmap gate, exit criterion, or opening condition that names an
|
|
156
|
+
external user population, a market, or an external measurement. A gate whose
|
|
157
|
+
condition depends on a quantity the project cannot produce is not a gate — it is
|
|
158
|
+
a permanent no in the shape of an answer. This is where the advisory reading
|
|
159
|
+
actually breaks: in conversation "never block" holds, but when a roadmap needs a
|
|
160
|
+
falsifiable opening condition the agent reaches for the only vocabulary it has
|
|
161
|
+
for *should this exist*, and an advisory ladder becomes a number, then a gate.
|
|
162
|
+
|
|
163
|
+
This is advisory — the user decides; a "just build it" answer proceeds
|
|
164
|
+
immediately to 8a. No network, no case-memory API (the source's remote lookup is
|
|
165
|
+
dropped — a lethal-trifecta egress concern).
|
|
166
|
+
|
|
167
|
+
**Enforcement, honestly.** Nothing reads `project.audience` to change what this
|
|
168
|
+
section does. § 8-pre is prose and the branch table above is model-carried —
|
|
169
|
+
the same honesty boundary `security-sensitive-stop` and `untrusted-input-defense`
|
|
170
|
+
state for their own obligations. The one deterministic backstop sits downstream,
|
|
171
|
+
where the damage actually lands: `lint_roadmap_complexity` warns when a roadmap
|
|
172
|
+
gate rests on an external population.
|
|
131
173
|
|
|
132
174
|
### 8a. Is the request clear?
|
|
133
175
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.36.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"build:install-bundle": "esbuild src/scripts/install.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/install/install.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);\"",
|
|
69
69
|
"build:hooks": "esbuild src/scripts/hooks/dispatch_entry.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/hooks/dispatch.js --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_HOOKS_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\"",
|
|
70
70
|
"build:mcp-bundle": "esbuild src/scripts/mcp_server/__main__.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/mcp/server.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_MCP_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\"",
|
|
71
|
-
"build:cli-delegate": "esbuild src/scripts/_cli/cmd_*.ts --bundle --splitting --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outdir=dist/cli-delegate --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);\"",
|
|
71
|
+
"build:cli-delegate": "esbuild src/scripts/_cli/cmd_*.ts --bundle --splitting --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --define:__AGENT_CONFIG_CLI_DELEGATE__=true --outdir=dist/cli-delegate --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);\"",
|
|
72
72
|
"build:ui": "vite build",
|
|
73
73
|
"tokens:build": "node tokens/build-ac.mjs && node tokens/build-as.mjs",
|
|
74
74
|
"tokens:check": "node tokens/build-ac.mjs --check && node tokens/build-as.mjs --check && node tokens/check-contrast.mjs",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
## Pointers
|
|
15
15
|
|
|
16
16
|
- **Filling out this AGENTS.md** — section templates, capability bullets, multi-agent entry flow, monorepo layout: [`consumer-agents-md-guide`](.augment/contexts/contracts/consumer-agents-md-guide.md).
|
|
17
|
-
- **Behavior rules (always
|
|
17
|
+
- **Behavior rules** (9 always-on; rest routed) — [`.augment/rules/`](.augment/rules/). Kernel rules are **immutable**; tighten only via the override registry.
|
|
18
18
|
- **Skills (on-demand expertise)** — domain skills surfaced by description, invoked on trigger match: [`.augment/skills/`](.augment/skills/).
|
|
19
19
|
- **Commands (workflows)** — slash-commands the agent runs end-to-end (`/work`, `/create-pr`, …): [`.augment/commands/`](.augment/commands/).
|
|
20
20
|
- **Project-specific docs** — your own architecture notes, roadmaps, sessions, contexts: [`agents/`](agents/).
|
|
@@ -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
|
|
@@ -1248,6 +1266,20 @@ hooks:
|
|
|
1248
1266
|
design_slop:
|
|
1249
1267
|
enabled: false
|
|
1250
1268
|
|
|
1269
|
+
# PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4).
|
|
1270
|
+
# Default-OFF. When enabled, a Write/Edit to a UI surface with no design
|
|
1271
|
+
# consultation latched this session WARNS (exit 2, never blocks) naming the
|
|
1272
|
+
# route: run existing-ui-audit, then the fe-design loop. A read or search
|
|
1273
|
+
# touching fe-design / existing-ui-audit / design-review / design-intelligence
|
|
1274
|
+
# latches consultation and silences it for the rest of the session, so the
|
|
1275
|
+
# agent that did the right thing is never nagged. Anti-loop: at most 2 nudges
|
|
1276
|
+
# per session. It does NOT read the two UI rules: the UI-surface decision
|
|
1277
|
+
# comes from _lib/ui_surface.ts, so this runs parallel to them rather than
|
|
1278
|
+
# consuming their triggers. Host-limited (pre_tool_use fires on ~2-3 hosts) —
|
|
1279
|
+
# elsewhere the obligation stays model-carried, as the two UI rules declare.
|
|
1280
|
+
ui_route_nudge:
|
|
1281
|
+
enabled: false
|
|
1282
|
+
|
|
1251
1283
|
# PreToolUse code-graph nudge (ADR-124 Phase 4). Default-OFF. When enabled
|
|
1252
1284
|
# AND a native code-graph engine has built a cache (or the repo ships a
|
|
1253
1285
|
# graph.json / SCIP index), WARNS once per session (exit 2, never blocks) as
|
|
@@ -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 (
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui_surface — the ONE definition of "this path is a UI surface".
|
|
3
|
+
*
|
|
4
|
+
* Three consumers need the same answer and were about to disagree about it:
|
|
5
|
+
* the anti-slop PreToolUse hook (which had the only copy), the UI-turn
|
|
6
|
+
* definition the catalogue analyzer counts against, and the route nudge.
|
|
7
|
+
* A UI-turn rate measured against one extension set and a nudge fired against
|
|
8
|
+
* another would produce a consultation rate that no fix could ever move,
|
|
9
|
+
* because the denominator and the trigger would be different populations.
|
|
10
|
+
*
|
|
11
|
+
* `.blade.php` is deliberately part of the set and was absent from the hook's
|
|
12
|
+
* original regex: a two-part extension does not match a `\.(…)$` alternation,
|
|
13
|
+
* so every Blade template — the single largest UI surface in a Laravel
|
|
14
|
+
* consumer — read as a non-UI file. That is why `isUiPath` matches on the
|
|
15
|
+
* path tail rather than on `path.extname`, which returns `.php` for
|
|
16
|
+
* `page.blade.php` and would silently reproduce the same hole.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Single-segment UI extensions, without the leading dot. */
|
|
20
|
+
export const UI_EXTENSIONS = [
|
|
21
|
+
'html',
|
|
22
|
+
'htm',
|
|
23
|
+
'css',
|
|
24
|
+
'scss',
|
|
25
|
+
'sass',
|
|
26
|
+
'less',
|
|
27
|
+
'vue',
|
|
28
|
+
'svelte',
|
|
29
|
+
'astro',
|
|
30
|
+
'jsx',
|
|
31
|
+
'tsx',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Multi-segment UI suffixes. These cannot be expressed as an extension
|
|
36
|
+
* alternation — `path.extname('x.blade.php')` is `.php`.
|
|
37
|
+
*/
|
|
38
|
+
export const UI_COMPOUND_SUFFIXES = ['.blade.php'] as const;
|
|
39
|
+
|
|
40
|
+
/** Equivalent to the historical hook regex, plus the compound suffixes. */
|
|
41
|
+
export const UI_EXT = new RegExp(
|
|
42
|
+
`(\\.(${UI_EXTENSIONS.join('|')})|${UI_COMPOUND_SUFFIXES.map((s) => s.replace(/\./g, '\\.')).join('|')})$`,
|
|
43
|
+
'i',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** True when `p` names a file on a UI surface. Case-insensitive. */
|
|
47
|
+
export function isUiPath(p: string): boolean {
|
|
48
|
+
return UI_EXT.test(p);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Directory fragments that mark a UI tree across frameworks. The two UI rules
|
|
53
|
+
* carried only the Laravel pair (`resources/views/`, `resources/js/`), so a
|
|
54
|
+
* React, Vue, Next or Svelte consumer writing to `src/components/` matched no
|
|
55
|
+
* path trigger at all. Kept as fragments, not anchored prefixes: a consumer's
|
|
56
|
+
* UI tree is rarely at the repo root.
|
|
57
|
+
*
|
|
58
|
+
* `app/` is deliberately NOT in this list although it is the Next.js router
|
|
59
|
+
* directory. In a Laravel consumer `app/` is the entire backend, so the
|
|
60
|
+
* fragment would fire on every controller, model and job — a trigger that
|
|
61
|
+
* over-fires on the largest directory of a whole ecosystem is worse than the
|
|
62
|
+
* gap it closes. Next.js route files are caught by extension instead
|
|
63
|
+
* (`page.tsx`, `layout.tsx` match `isUiPath`), which is the precise signal.
|
|
64
|
+
*/
|
|
65
|
+
export const UI_PATH_FRAGMENTS = [
|
|
66
|
+
'resources/views/',
|
|
67
|
+
'resources/js/',
|
|
68
|
+
// `components/` already subsumes `src/components/` under substring
|
|
69
|
+
// matching — listing both here would be dead weight. The rules' own
|
|
70
|
+
// `path_prefix` list keeps both because matching there is anchored.
|
|
71
|
+
'components/',
|
|
72
|
+
'pages/',
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fragments that sit INSIDE a UI tree and are not UI. Checked first, because a
|
|
77
|
+
* substring fragment cannot express "pages/ but not pages/api/" on its own —
|
|
78
|
+
* and `pages/api/` is server-only Next.js code, the exact over-fire the `app/`
|
|
79
|
+
* exclusion above exists to avoid. Leaving it in would have put backend files
|
|
80
|
+
* in the pre-registered UI-turn denominator, which is worse than the nudge
|
|
81
|
+
* firing on them: a rate is harder to un-break than a warning.
|
|
82
|
+
*/
|
|
83
|
+
export const UI_TREE_EXCLUSIONS = ['pages/api/'] as const;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when `p` sits inside a conventional UI tree.
|
|
87
|
+
*
|
|
88
|
+
* Note this does NOT require a UI extension — that is the point: a `.js` file
|
|
89
|
+
* under `resources/js/` is UI. The cost is that a non-code file in a UI tree
|
|
90
|
+
* (a README, a fixture) also matches; callers that need the narrower answer
|
|
91
|
+
* combine this with `isUiPath`.
|
|
92
|
+
*/
|
|
93
|
+
export function isUiTreePath(p: string): boolean {
|
|
94
|
+
const normalized = p.replace(/\\/g, '/').toLowerCase();
|
|
95
|
+
if (UI_TREE_EXCLUSIONS.some((fragment) => normalized.includes(fragment))) return false;
|
|
96
|
+
return UI_PATH_FRAGMENTS.some((fragment) => normalized.includes(fragment));
|
|
97
|
+
}
|