@event4u/agent-config 4.3.0 → 4.6.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/.agent-src/commands/agents/user/init.md +7 -13
- package/.agent-src/commands/agents/user/show.md +4 -4
- package/.agent-src/commands/post-as/me.md +6 -6
- package/.agent-src/contexts/execution/autonomy-mechanics.md +1 -1
- package/.agent-src/contexts/execution/cheap-question-mechanics.md +4 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +28 -227
- package/config/discovery/packs.yml +9 -1
- package/config/discovery/workspaces.yml +14 -1
- package/dist/cli/agent-config.js +15 -10
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/commands/uiServe.js +27 -11
- package/dist/cli/commands/uiServe.js.map +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +51 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +25 -4
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +4 -4
- package/dist/install/selectedTools.js +52 -0
- package/dist/install/selectedTools.js.map +1 -0
- package/dist/install/toolDetection.js +104 -0
- package/dist/install/toolDetection.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/server/app.js +36 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/routes/ping.js +17 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/wizard.js +280 -34
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/serverInfo.js +54 -0
- package/dist/server/serverInfo.js.map +1 -0
- package/dist/shared/userMd/formAdapter.js +1 -5
- package/dist/shared/userMd/formAdapter.js.map +1 -1
- package/dist/shared/userMd/schema.js +2 -1
- package/dist/shared/userMd/schema.js.map +1 -1
- package/dist/ui/assets/index-BAJQeVdX.js +40 -0
- package/dist/ui/assets/index-BAJQeVdX.js.map +1 -0
- package/dist/ui/assets/index-BbWWuFrF.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/archive/CHANGELOG-pre-4.0.0.md +80 -0
- package/docs/archive/CHANGELOG-pre-4.5.0.md +289 -0
- package/docs/contracts/agent-user-schema.md +1 -3
- package/docs/contracts/discovery-manifest.schema.json +3 -1
- package/docs/contracts/gui-wizard.md +103 -8
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +27 -0
- package/docs/decisions/ADR-021-deployment-shape.md +2 -2
- package/docs/deploy/connector-setup.md +2 -2
- package/docs/deploy/policy-cookbook.md +2 -2
- package/docs/examples/agent-user.example.md +0 -1
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/build_discovery_manifest.py +4 -0
- package/scripts/condense.py +56 -1
- package/scripts/condense_memory.py +8 -2
- package/scripts/install.py +89 -43
- package/scripts/lint_discovery_vocabulary.py +8 -0
- package/dist/ui/assets/index-BDAhhpDV.js +0 -40
- package/dist/ui/assets/index-BDAhhpDV.js.map +0 -1
- package/dist/ui/assets/index-BXZILUxe.css +0 -1
|
@@ -90,7 +90,12 @@ Versioned under `/api/v1/`. Selected routes:
|
|
|
90
90
|
| POST | `/api/v1/wizard/state` | Persist state between steps |
|
|
91
91
|
| GET | `/api/v1/wizard/manifest` | Locked discovery-manifest (extended mode) |
|
|
92
92
|
| GET | `/api/v1/wizard/auto-detect` | Project-signal evidence for the `ai-tools` step (extended mode) |
|
|
93
|
+
| GET | `/api/v1/wizard/detect-tools` | Native AI-tool presence (home/app/`$PATH`) for Step-1 pre-select + badge |
|
|
94
|
+
| GET | `/api/v1/wizard/detect-rtk` | rtk presence + per-OS install hint (Editor-and-tooling step) |
|
|
95
|
+
| GET | `/api/v1/wizard/ai-council` | AI-council scalar subset + provider key presence (extended mode) |
|
|
96
|
+
| POST | `/api/v1/wizard/ai-council` | Comment-preserving scalar merge into `.ai-council.yml` |
|
|
93
97
|
| POST | `/api/v1/wizard/finish` | 2PC commit of settings + user-identity |
|
|
98
|
+
| POST | `/api/v1/shutdown` | Browser-close shutdown beacon (`navigator.sendBeacon` target; real-serve only) |
|
|
94
99
|
| POST | `/api/v1/wizard/apply` | **Single real-apply route.** `dry_run:true` → buffered plan preview; otherwise SSE-streams `scripts/install.py --apply-payload` |
|
|
95
100
|
| GET | `/api/v1/install/detect` | Scope + project shape + tool presence |
|
|
96
101
|
| POST | `/api/v1/install/plan` | Plan preview (per-tool file counts + conflicts) for the Review step |
|
|
@@ -107,6 +112,36 @@ an `Origin` allow-list (browser-issued requests), and a per-server bearer
|
|
|
107
112
|
token (`Authorization: Bearer <token>`, minted at boot, surfaced in the
|
|
108
113
|
`?token=` URL). A bad token / Host / Origin returns `403`.
|
|
109
114
|
|
|
115
|
+
### Browser-lifecycle shutdown
|
|
116
|
+
|
|
117
|
+
In real-serve (`runUiServe`), the server stops itself when the browser that
|
|
118
|
+
drives it goes away — the local process should not outlive its only client:
|
|
119
|
+
|
|
120
|
+
- The SPA ([`src/ui/serverLifecycle.ts`](../../src/ui/serverLifecycle.ts))
|
|
121
|
+
heartbeats `GET /api/v1/ping` every 30s while the tab is visible and the
|
|
122
|
+
user has interacted within the last 30 min. On `pagehide` (window/tab
|
|
123
|
+
close) it fires `navigator.sendBeacon('/api/v1/shutdown?token=…')` (the
|
|
124
|
+
token rides as a query param because `sendBeacon` cannot set headers); and
|
|
125
|
+
once the user has been idle for 30 min it fires the same beacon instead of
|
|
126
|
+
a ping, so the server stops even with the tab still open.
|
|
127
|
+
- The server (`createApp` `idleShutdown` option, passed only by `runUiServe`)
|
|
128
|
+
exits on that beacon, and — as a backstop for crashes where neither beacon
|
|
129
|
+
is delivered — via an idle timer that **arms only after the first authed
|
|
130
|
+
request** (so headless / `--allow-headless` manual-connect servers are
|
|
131
|
+
never killed before the operator attaches) and fires after 30 min of
|
|
132
|
+
silence.
|
|
133
|
+
|
|
134
|
+
On boot, `runUiServe` records `{pid, port, url}` to
|
|
135
|
+
`~/.event4u/agent-config/local-server.json`
|
|
136
|
+
([`src/server/serverInfo.ts`](../../src/server/serverInfo.ts)) and removes it
|
|
137
|
+
on graceful exit. A fresh `agent-config init` (via `scripts/install.py`
|
|
138
|
+
`_kill_stale_wizard_server`) reads that record, terminates a still-running
|
|
139
|
+
prior instance, and starts a new server — so init always lands on step 1.
|
|
140
|
+
|
|
141
|
+
`createApp` is inert (no watchdog, no `/api/v1/shutdown` route) unless
|
|
142
|
+
`idleShutdown` is supplied, so the in-process test harness
|
|
143
|
+
([`tests/server/helpers.ts`](../../tests/server/helpers.ts)) is unaffected.
|
|
144
|
+
|
|
110
145
|
## Real apply — single source of truth
|
|
111
146
|
|
|
112
147
|
`POST /api/v1/wizard/apply` is the only write path:
|
|
@@ -185,16 +220,76 @@ effect):
|
|
|
185
220
|
|
|
186
221
|
| `extendedSteps` | Steps | Layout |
|
|
187
222
|
|---|---|---|
|
|
188
|
-
| `false` |
|
|
189
|
-
| `true` |
|
|
223
|
+
| `false` | 9 | `welcome → editor → personality → cost → roadmap-quality → memory → ai-council → user-md → review` |
|
|
224
|
+
| `true` | 13 | `welcome → ai-tools → roles → packs → editor → personality → cost → roadmap-quality → memory → ai-council → user-md → modules → review` |
|
|
225
|
+
|
|
226
|
+
The project-specific `modules` step (writes `.agent-project-settings.yml`, not
|
|
227
|
+
the global `.agent-settings.yml`) sits at the **end** of the extended flow,
|
|
228
|
+
just before `review` — global/user settings come first, the project step
|
|
229
|
+
comes last. Because it is no longer part of the install-only lead,
|
|
230
|
+
`setup` mode now walks it too (the lead it skips is `ai-tools → roles →
|
|
231
|
+
packs`).
|
|
232
|
+
|
|
233
|
+
The `welcome` step (Step 1, both modes) collects **name + language** up front —
|
|
234
|
+
pulled out of the user-md step so the agent knows who it's talking to before
|
|
235
|
+
anything else. Name pre-fills from the OS account (`GET /api/v1/ping`
|
|
236
|
+
`systemUser`) when empty; language pre-fills from the browser locale
|
|
237
|
+
(`navigator.language`) when no `.agent-user.yml` exists yet. In install mode
|
|
238
|
+
the user-md step hides its name + language fields (collected here); setup mode
|
|
239
|
+
skips the welcome step (it lands on the first settings step) and keeps those
|
|
240
|
+
fields in the user-md form.
|
|
241
|
+
|
|
242
|
+
The `roles` step presents the discovery **workspaces** as the *area*
|
|
243
|
+
(Engineering, Product, Finance, Founder, GTM, Ops, …; the maintainer workspace
|
|
244
|
+
is hidden) — each tile shows the area label, then advisory `example_roles`
|
|
245
|
+
(e.g. Engineering → "Developer, CTO"; Finance → "CFO") and the description. The
|
|
246
|
+
selected workspace ids become `.agent-user.yml` `role[]` (the example roles are
|
|
247
|
+
UI hints, not the stored value) and recommend each domain's `default_packs` on
|
|
248
|
+
the packs step. In install mode the user-md
|
|
249
|
+
step therefore hides its role field (collected here instead); setup mode keeps
|
|
250
|
+
the role field since it skips the roles step.
|
|
251
|
+
|
|
252
|
+
The `ai-council` step (road-to-wizard-ux-improvements § Phase 8) configures the
|
|
253
|
+
wizard-controlled scalar subset of `.ai-council.yml` (enable, per-member
|
|
254
|
+
enable + low-impact, global transport mode, debate rounds, cost budget, the
|
|
255
|
+
non-locked `decision_resolution` classes) via `GET`/`POST /api/v1/wizard/ai-council`;
|
|
256
|
+
the file is written with comment-preserving `replaceScalar` edits.
|
|
190
257
|
|
|
191
258
|
The step shapes themselves are declared in
|
|
192
|
-
[`src/ui/wizard/steps.ts`](../../src/ui/wizard/steps.ts) — the
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`
|
|
197
|
-
|
|
259
|
+
[`src/ui/wizard/steps.ts`](../../src/ui/wizard/steps.ts) — the always-first
|
|
260
|
+
`welcome` step plus the four extended-only lead steps (`ai-tools`, `roles`,
|
|
261
|
+
`packs`, `modules`) carry no `paths` and use dedicated renderers in
|
|
262
|
+
`WizardPage.tsx`.
|
|
263
|
+
`getWizardSteps({ extended })` is the single resolver; the UI consumes the
|
|
264
|
+
active list via `getActiveSteps()` / `activeTotalSteps()` so a server toggle
|
|
265
|
+
takes effect on the next reload without a code change.
|
|
266
|
+
|
|
267
|
+
### AI-tools / roles / packs selection rules (Steps 2-4)
|
|
268
|
+
|
|
269
|
+
- **AI-tools pre-selection.** `detect-tools` returns `tools` (installed on the
|
|
270
|
+
machine) and `configured` (the user's prior selection, persisted to
|
|
271
|
+
`~/.event4u/agent-config/wizard-tools.json` on each real apply). On a repeat
|
|
272
|
+
run the wizard pre-selects exactly the `configured` tools; only on a genuine
|
|
273
|
+
first run (no prior selection) does it fall back to pre-selecting every
|
|
274
|
+
installed tool.
|
|
275
|
+
- **Roles → packs recommendation.** Each selected role contributes its
|
|
276
|
+
workspace `default_packs`; the union pre-selects packs on Step 3 (plus
|
|
277
|
+
auto-detected project packs). The recommendation stops clobbering the
|
|
278
|
+
selection once the user manually edits a pack. Each pack tile also badges
|
|
279
|
+
the workspace(s) it belongs to (from the pack's `workspaces`), highlighting
|
|
280
|
+
the badges that match a role the user picked on Step 2.
|
|
281
|
+
- **Step 2 framework persistence.** A language tile (`cluster`, e.g. PHP)
|
|
282
|
+
gates its framework children in the UI but never destroys their stored
|
|
283
|
+
selection. Turning a language off disables — but keeps checked — its
|
|
284
|
+
children, so a PHP off→on round-trip restores the exact Laravel-on /
|
|
285
|
+
Symfony-off choice. `resolveSelectedPacks()` filters disabled children at
|
|
286
|
+
submit time, so a remembered-but-gated framework is not installed.
|
|
287
|
+
- **No-autodetect packs.** Some packs are never pre-selected from project
|
|
288
|
+
signals (`python` — a non-engineer may have python installed but not need
|
|
289
|
+
it). The pack stays available to tick manually.
|
|
290
|
+
- **Empty-selection gate.** The AI-tools, roles, and packs steps each block
|
|
291
|
+
Next until at least one effective selection exists (≥ 1 tool, ≥ 1 role, and
|
|
292
|
+
≥ 1 installable pack respectively).
|
|
198
293
|
|
|
199
294
|
### `GET /api/v1/wizard/state` payload
|
|
200
295
|
|
|
@@ -219,6 +219,33 @@ examples instead. No ADR-014 issued.
|
|
|
219
219
|
|
|
220
220
|
Driven by [`agents/roadmaps/monorepo-phase-1-frontmatter-metadata.md`](../../agents/roadmaps/monorepo-phase-1-frontmatter-metadata.md).
|
|
221
221
|
|
|
222
|
+
### 2026-05-27 — Additive advisory `cluster:` key on packs
|
|
223
|
+
|
|
224
|
+
Pack entries in [`config/discovery/packs.yml`](../../config/discovery/packs.yml)
|
|
225
|
+
may carry an optional `cluster: <language-id>` field. It groups a framework
|
|
226
|
+
pack under a programming-language tile in the setup wizard's capability-packs
|
|
227
|
+
step (e.g. `laravel: cluster: php`, `react: cluster: typescript`). Like
|
|
228
|
+
`requires_hint`, it is **advisory only** — the installer does not act on it; it
|
|
229
|
+
drives the wizard UI's collapsible language→framework grouping. The value must
|
|
230
|
+
be a known pack id (and not self-referential), enforced by
|
|
231
|
+
[`scripts/lint_discovery_vocabulary.py`](../../scripts/lint_discovery_vocabulary.py)
|
|
232
|
+
and emitted into the discovery manifest. Additive, no vocabulary rename.
|
|
233
|
+
|
|
234
|
+
Driven by [`agents/roadmaps/road-to-wizard-ux-improvements.md`](../../agents/roadmaps/road-to-wizard-ux-improvements.md) § Phase 4 (AI-council-resolved: reuse `packs.yml` as the single source of truth rather than a second mapping file).
|
|
235
|
+
|
|
236
|
+
### 2026-05-27 — Additive advisory `example_roles:` key on workspaces
|
|
237
|
+
|
|
238
|
+
Workspace entries in [`config/discovery/workspaces.yml`](../../config/discovery/workspaces.yml)
|
|
239
|
+
may carry an optional `example_roles: [<title>, …]` list of illustrative job
|
|
240
|
+
titles (e.g. `engineering: [Developer, CTO]`, `finance: [CFO]`). The wizard's
|
|
241
|
+
roles step shows the workspace as the *area* and these titles as examples under
|
|
242
|
+
it. **Advisory and free-form** — NOT a closed vocabulary: the stored
|
|
243
|
+
`.agent-user.yml` `role[]` is the workspace id, never these examples; nothing
|
|
244
|
+
acts on the strings beyond display. Emitted into the discovery manifest;
|
|
245
|
+
allowed by [`discovery-manifest.schema.json`](../contracts/discovery-manifest.schema.json).
|
|
246
|
+
Additive, no vocabulary rename. (Same change cleaned the `finance` label from
|
|
247
|
+
"Finance / CFO" to "Finance".)
|
|
248
|
+
|
|
222
249
|
## Cross-references
|
|
223
250
|
|
|
224
251
|
- [ADR-007 — Agent Discovery Scopes](ADR-007-agent-discovery-scopes.md):
|
|
@@ -25,7 +25,7 @@ Companion artefacts:
|
|
|
25
25
|
- Roadmap: [`agents/roadmaps/road-to-internal-ai-os-deployment.md`](../../agents/roadmaps/road-to-internal-ai-os-deployment.md)
|
|
26
26
|
- Artefacts: [`packages/core/deploy/`](../../packages/core/deploy/)
|
|
27
27
|
- Env contract: [`docs/deploy/env-vars.md`](../deploy/env-vars.md)
|
|
28
|
-
- Council question (drafted, not invoked — no keys): [`agents/tmp/council-question-deployment-shape.md`](../../agents/
|
|
28
|
+
- Council question (drafted, not invoked — no keys): [`agents/tmp/council-question-deployment-shape.md`](../../agents/old/council-question-deployment-shape.md)
|
|
29
29
|
- Predecessor ADR: [`ADR-016`](ADR-016-installer-architecture.md) — installer architecture (agent-mode protocol the GUI server wraps).
|
|
30
30
|
|
|
31
31
|
## Context
|
|
@@ -141,7 +141,7 @@ orchestrator-agnostic.
|
|
|
141
141
|
## Open questions (council-deferred)
|
|
142
142
|
|
|
143
143
|
The accompanying council question file
|
|
144
|
-
[`agents/tmp/council-question-deployment-shape.md`](../../agents/
|
|
144
|
+
[`agents/tmp/council-question-deployment-shape.md`](../../agents/old/council-question-deployment-shape.md)
|
|
145
145
|
has not yet been run (no provider keys configured). A maintainer with
|
|
146
146
|
keys should run it and either ratify or supersede this ADR.
|
|
147
147
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
> and Phase 3 (central policy) shipping first.
|
|
7
7
|
>
|
|
8
8
|
> Open design questions live in
|
|
9
|
-
> [`agents/tmp/council-question-connector-scope.md`](../../agents/
|
|
9
|
+
> [`agents/tmp/council-question-connector-scope.md`](../../agents/old/council-question-connector-scope.md).
|
|
10
10
|
|
|
11
11
|
## Audience
|
|
12
12
|
|
|
@@ -124,6 +124,6 @@ All of the above land in Phase 5, contingent on Phases 2 + 3.
|
|
|
124
124
|
## Cross-references
|
|
125
125
|
|
|
126
126
|
- 🚧 Reserved ADR slot: `docs/decisions/ADR-025-connector-scope.md`.
|
|
127
|
-
- Council question: [`agents/tmp/council-question-connector-scope.md`](../../agents/
|
|
127
|
+
- Council question: [`agents/tmp/council-question-connector-scope.md`](../../agents/old/council-question-connector-scope.md).
|
|
128
128
|
- Quickstart: [`quickstart.md`](quickstart.md).
|
|
129
129
|
- Policy cookbook: [`policy-cookbook.md`](policy-cookbook.md).
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> before code lands. Every section below is normative-once-shipped.
|
|
8
8
|
>
|
|
9
9
|
> Open design questions live in
|
|
10
|
-
> [`agents/tmp/council-question-central-policy.md`](../../agents/
|
|
10
|
+
> [`agents/tmp/council-question-central-policy.md`](../../agents/old/council-question-central-policy.md).
|
|
11
11
|
|
|
12
12
|
## Audience
|
|
13
13
|
|
|
@@ -125,6 +125,6 @@ All of the above land in Phase 3. Until then, per-user
|
|
|
125
125
|
## Cross-references
|
|
126
126
|
|
|
127
127
|
- 🚧 Reserved ADR slot: `docs/decisions/ADR-023-central-policy.md`.
|
|
128
|
-
- Council question: [`agents/tmp/council-question-central-policy.md`](../../agents/
|
|
128
|
+
- Council question: [`agents/tmp/council-question-central-policy.md`](../../agents/old/council-question-central-policy.md).
|
|
129
129
|
- Env contract: [`env-vars.md`](env-vars.md) (`POLICY_PATH`).
|
|
130
130
|
- Quickstart: [`quickstart.md`](quickstart.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Universal AI Agent OS \u2014 audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -284,6 +284,7 @@ def _build(strict: bool) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
|
284
284
|
"description": w["description"],
|
|
285
285
|
"default_packs": list(w.get("default_packs") or []),
|
|
286
286
|
**({"optional_packs": list(w["optional_packs"])} if w.get("optional_packs") else {}),
|
|
287
|
+
**({"example_roles": list(w["example_roles"])} if w.get("example_roles") else {}),
|
|
287
288
|
}
|
|
288
289
|
for w in workspaces
|
|
289
290
|
]
|
|
@@ -302,6 +303,8 @@ def _build(strict: bool) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
|
302
303
|
}
|
|
303
304
|
if p.get("requires_hint"):
|
|
304
305
|
item["requires_hint"] = list(p["requires_hint"])
|
|
306
|
+
if p.get("cluster"):
|
|
307
|
+
item["cluster"] = p["cluster"]
|
|
305
308
|
pk_out.append(item)
|
|
306
309
|
|
|
307
310
|
if strict and unassigned:
|
|
@@ -546,6 +549,7 @@ def _packs_view(manifest: dict[str, Any]) -> dict[str, Any]:
|
|
|
546
549
|
"description": p["description"],
|
|
547
550
|
"workspaces": list(p.get("workspaces", [])),
|
|
548
551
|
"requires_hint": list(p.get("requires_hint", [])),
|
|
552
|
+
"cluster": p.get("cluster"),
|
|
549
553
|
"trust_level_default": p.get("trust_level_default"),
|
|
550
554
|
"artefact_count": len(ids),
|
|
551
555
|
"artefacts": ids,
|
package/scripts/condense.py
CHANGED
|
@@ -1060,6 +1060,59 @@ def generate_user_type_symlinks() -> int:
|
|
|
1060
1060
|
return total
|
|
1061
1061
|
|
|
1062
1062
|
|
|
1063
|
+
def generate_plugin_hooks() -> int:
|
|
1064
|
+
"""Generate ``hooks/hooks.json`` at the plugin root from the hook manifest.
|
|
1065
|
+
|
|
1066
|
+
Claude Code plugins auto-discover hooks at ``<plugin-root>/hooks/hooks.json``
|
|
1067
|
+
(Plugins reference). The agent-config plugin's source is the repo root
|
|
1068
|
+
(``.claude-plugin/marketplace.json``), so the file lands at
|
|
1069
|
+
``PROJECT_ROOT/hooks/hooks.json``.
|
|
1070
|
+
|
|
1071
|
+
Delivering the Claude lifecycle hooks via **plugin scope** — instead of
|
|
1072
|
+
writing them into the shared ``.claude/settings.json`` ``hooks`` array —
|
|
1073
|
+
means no shared ``hooks`` array in any settings file, so there is no
|
|
1074
|
+
collision with a neighbour tool's hooks or with a developer's
|
|
1075
|
+
``settings.local.json``. Claude Code merges plugin-scope and
|
|
1076
|
+
settings-scope hooks and dedups by command string.
|
|
1077
|
+
|
|
1078
|
+
The command is rooted at ``$CLAUDE_PROJECT_DIR`` so it resolves from
|
|
1079
|
+
plugin scope regardless of cwd; the universal dispatcher then reads
|
|
1080
|
+
``scripts/hook_manifest.yaml`` at runtime to fan out to the active
|
|
1081
|
+
concerns (each a no-op when its feature is disabled).
|
|
1082
|
+
"""
|
|
1083
|
+
manifest_path = PROJECT_ROOT / "scripts" / "hook_manifest.yaml"
|
|
1084
|
+
if not manifest_path.exists():
|
|
1085
|
+
print(" ⚠️ scripts/hook_manifest.yaml not found — skipping plugin hooks",
|
|
1086
|
+
file=sys.stderr)
|
|
1087
|
+
return 0
|
|
1088
|
+
|
|
1089
|
+
manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {}
|
|
1090
|
+
claude_events = manifest.get("platforms", {}).get("claude", {}) or {}
|
|
1091
|
+
aliases = manifest.get("native_event_aliases", {}).get("claude", {}) or {}
|
|
1092
|
+
# Reverse the native→agent-config map so we can emit native event names.
|
|
1093
|
+
ac_to_native = {ac: native for native, ac in aliases.items()}
|
|
1094
|
+
|
|
1095
|
+
hooks: dict[str, list] = {}
|
|
1096
|
+
for ac_event, concerns in claude_events.items():
|
|
1097
|
+
if not concerns:
|
|
1098
|
+
continue
|
|
1099
|
+
native = ac_to_native.get(ac_event)
|
|
1100
|
+
if native is None:
|
|
1101
|
+
continue
|
|
1102
|
+
command = (
|
|
1103
|
+
'"$CLAUDE_PROJECT_DIR"/agent-config dispatch:hook '
|
|
1104
|
+
f"--platform claude --event {ac_event} --native-event {native}"
|
|
1105
|
+
)
|
|
1106
|
+
hooks[native] = [{"hooks": [{"type": "command", "command": command}]}]
|
|
1107
|
+
|
|
1108
|
+
hooks_dir = PROJECT_ROOT / "hooks"
|
|
1109
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
1110
|
+
out = hooks_dir / "hooks.json"
|
|
1111
|
+
out.write_text(json.dumps({"hooks": hooks}, indent=2) + "\n", encoding="utf-8")
|
|
1112
|
+
info(f" ✅ Generated hooks/hooks.json ({len(hooks)} Claude plugin hooks)")
|
|
1113
|
+
return len(hooks)
|
|
1114
|
+
|
|
1115
|
+
|
|
1063
1116
|
def generate_tools() -> None:
|
|
1064
1117
|
"""Generate all tool-specific directories and files.
|
|
1065
1118
|
|
|
@@ -1074,6 +1127,7 @@ def generate_tools() -> None:
|
|
|
1074
1127
|
generate_gemini_md()
|
|
1075
1128
|
skills = generate_claude_skills() if _tool_active("claude-code") else 0
|
|
1076
1129
|
commands = generate_claude_commands() if _tool_active("claude-code") else 0
|
|
1130
|
+
plugin_hooks = generate_plugin_hooks() if _tool_active("claude-code") else 0
|
|
1077
1131
|
personas = generate_persona_symlinks()
|
|
1078
1132
|
user_types = generate_user_type_symlinks()
|
|
1079
1133
|
cursor_mdc = generate_cursor_mdc_rules() if _tool_active("cursor") else 0
|
|
@@ -1082,7 +1136,8 @@ def generate_tools() -> None:
|
|
|
1082
1136
|
windsurf_wf = generate_windsurf_workflows() if _tool_active("windsurf") else 0
|
|
1083
1137
|
summary = (
|
|
1084
1138
|
f"✅ generate-tools — rules={rules} skills={skills} "
|
|
1085
|
-
f"commands={commands}
|
|
1139
|
+
f"commands={commands} plugin_hooks={plugin_hooks} "
|
|
1140
|
+
f"personas={personas} user_types={user_types} "
|
|
1086
1141
|
f"cursor_mdc={cursor_mdc} windsurf_rules={windsurf_modern} "
|
|
1087
1142
|
f"cursor_commands={cursor_cmds} windsurf_workflows={windsurf_wf} "
|
|
1088
1143
|
f"windsurfrules={windsurfrules}"
|
|
@@ -37,6 +37,11 @@ RE_NUMBERED = re.compile(r"^>?\s*\d+\.\s")
|
|
|
37
37
|
RE_STATUS = re.compile(r"^\s*(?:❌|⚠️|✅)")
|
|
38
38
|
RE_IRONLAW = re.compile(r"^[A-Z][A-Z0-9 ,.\-_/']{3,}$")
|
|
39
39
|
RE_BACKTICK_SPAN = re.compile(r"`[^`\n]+`")
|
|
40
|
+
# Spans frozen byte-for-byte inside prose lines. Backtick spans first so a
|
|
41
|
+
# slash-bearing span inside backticks is captured whole; then any non-whitespace
|
|
42
|
+
# run containing `/` or `\` — bare URLs, markdown link/image targets, and file
|
|
43
|
+
# paths whose segments (`is`, `the`, `a`) would otherwise be eaten as drop-tokens.
|
|
44
|
+
RE_PRESERVE_SPAN = re.compile(r"`[^`\n]+`|\S*[/\\]\S*")
|
|
40
45
|
RE_FRONTMATTER = re.compile(r"^---\s*$")
|
|
41
46
|
WORD_RE = re.compile(r"\b[A-Za-z]+\b")
|
|
42
47
|
DROP_TOKENS = {"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
|
@@ -50,10 +55,11 @@ def _condense_words(text: str) -> str:
|
|
|
50
55
|
|
|
51
56
|
|
|
52
57
|
def _condense_prose_line(line: str) -> str:
|
|
53
|
-
"""Condense a prose line; preserve backtick
|
|
58
|
+
"""Condense a prose line; preserve backtick spans, URLs, link targets, and
|
|
59
|
+
slash-bearing paths byte-for-byte (see ``RE_PRESERVE_SPAN``)."""
|
|
54
60
|
parts: list[str] = []
|
|
55
61
|
last = 0
|
|
56
|
-
for span in
|
|
62
|
+
for span in RE_PRESERVE_SPAN.finditer(line):
|
|
57
63
|
parts.append(_condense_words(line[last:span.start()]))
|
|
58
64
|
parts.append(span.group(0))
|
|
59
65
|
last = span.end()
|
package/scripts/install.py
CHANGED
|
@@ -32,6 +32,7 @@ import os
|
|
|
32
32
|
import re
|
|
33
33
|
import shlex
|
|
34
34
|
import shutil
|
|
35
|
+
import signal
|
|
35
36
|
import subprocess
|
|
36
37
|
import sys
|
|
37
38
|
import threading
|
|
@@ -1082,52 +1083,25 @@ def ensure_augment_user_hooks(package_root: Path, force: bool) -> list[dict[str,
|
|
|
1082
1083
|
# concern fires on the same logical surface across platforms — the
|
|
1083
1084
|
# contract from agents/settings/contexts/hardening-pattern.md § Cross-platform
|
|
1084
1085
|
# parity.
|
|
1085
|
-
CLAUDE_DISPATCHER_BINDINGS = (
|
|
1086
|
-
("session_start", "SessionStart"),
|
|
1087
|
-
("session_end", "SessionEnd"),
|
|
1088
|
-
("stop", "Stop"),
|
|
1089
|
-
("user_prompt_submit", "UserPromptSubmit"),
|
|
1090
|
-
("post_tool_use", "PostToolUse"),
|
|
1091
|
-
)
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
def _claude_dispatch_block(ac_event: str, native: str) -> dict:
|
|
1095
|
-
"""Single hook entry routing the event through the universal dispatcher."""
|
|
1096
|
-
return {
|
|
1097
|
-
"hooks": [
|
|
1098
|
-
{
|
|
1099
|
-
"type": "command",
|
|
1100
|
-
"command": (
|
|
1101
|
-
f"./agent-config dispatch:hook "
|
|
1102
|
-
f"--platform claude --event {ac_event} "
|
|
1103
|
-
f"--native-event {native}"
|
|
1104
|
-
),
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
1086
|
def ensure_claude_bridge(project_root: Path, force: bool) -> list[dict[str, Any]]:
|
|
1111
|
-
"""Deploy .claude/settings.json with plugin enablement
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1087
|
+
"""Deploy .claude/settings.json with plugin enablement only.
|
|
1088
|
+
|
|
1089
|
+
Claude lifecycle hooks are delivered via **plugin scope**
|
|
1090
|
+
(`hooks/hooks.json`, generated by `condense.generate_plugin_hooks`), not
|
|
1091
|
+
written into the shared `.claude/settings.json` `hooks` array. Writing
|
|
1092
|
+
them here would monopolise that array and collide with any neighbour
|
|
1093
|
+
tool's hooks or a developer's `settings.local.json` — Claude Code merges
|
|
1094
|
+
plugin-scope and settings-scope hooks at runtime and dedups by command
|
|
1095
|
+
string, so plugin delivery needs no `hooks` block here.
|
|
1096
|
+
|
|
1097
|
+
The plugin id matches `.claude-plugin/marketplace.json`
|
|
1098
|
+
(`<plugin>@<marketplace>` = `agent-config@event4u-agent-config`) and the
|
|
1099
|
+
documented install command in docs/installation.md. Idempotent:
|
|
1100
|
+
`enabledPlugins` is a dict-merge, so the key coexists with any other
|
|
1101
|
+
plugin a neighbour tool enabled.
|
|
1121
1102
|
"""
|
|
1122
|
-
per_event: dict[str, list] = {}
|
|
1123
|
-
for ac_event, native in CLAUDE_DISPATCHER_BINDINGS:
|
|
1124
|
-
per_event.setdefault(native, []).append(
|
|
1125
|
-
_claude_dispatch_block(ac_event, native)
|
|
1126
|
-
)
|
|
1127
|
-
|
|
1128
1103
|
bridge = {
|
|
1129
|
-
"enabledPlugins": {"agent-
|
|
1130
|
-
"hooks": per_event,
|
|
1104
|
+
"enabledPlugins": {"agent-config@event4u-agent-config": True},
|
|
1131
1105
|
}
|
|
1132
1106
|
return merge_json_file(
|
|
1133
1107
|
project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json",
|
|
@@ -4335,6 +4309,75 @@ def _wizard_cli_dist(project_root: Path) -> Path | None:
|
|
|
4335
4309
|
return cli if cli.exists() else None
|
|
4336
4310
|
|
|
4337
4311
|
|
|
4312
|
+
def _server_info_path() -> Path:
|
|
4313
|
+
"""Path of the running-server record written by `ui:serve`."""
|
|
4314
|
+
return Path.home() / ".event4u" / "agent-config" / "local-server.json"
|
|
4315
|
+
|
|
4316
|
+
|
|
4317
|
+
def _pid_is_agent_config(pid: int) -> bool:
|
|
4318
|
+
"""Best-effort check that `pid` is one of our wizard servers.
|
|
4319
|
+
|
|
4320
|
+
Guards against signalling an unrelated process that recycled the pid.
|
|
4321
|
+
Uses `ps` (POSIX); on platforms without it we conservatively return
|
|
4322
|
+
False so we never kill the wrong process.
|
|
4323
|
+
"""
|
|
4324
|
+
try:
|
|
4325
|
+
out = subprocess.run( # noqa: S603,S607 - fixed argv, pid is an int
|
|
4326
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
4327
|
+
capture_output=True,
|
|
4328
|
+
text=True,
|
|
4329
|
+
timeout=5,
|
|
4330
|
+
check=False,
|
|
4331
|
+
)
|
|
4332
|
+
except (OSError, subprocess.SubprocessError):
|
|
4333
|
+
return False
|
|
4334
|
+
return "agent-config" in out.stdout.lower()
|
|
4335
|
+
|
|
4336
|
+
|
|
4337
|
+
def _kill_stale_wizard_server() -> None:
|
|
4338
|
+
"""Terminate a previously-launched wizard server, if one is recorded.
|
|
4339
|
+
|
|
4340
|
+
`agent-config init` should always start fresh: a stale server (left
|
|
4341
|
+
from an earlier run) is stopped so the new instance owns the port and
|
|
4342
|
+
the wizard re-enters at step 1. Best-effort — every failure is ignored.
|
|
4343
|
+
"""
|
|
4344
|
+
path = _server_info_path()
|
|
4345
|
+
try:
|
|
4346
|
+
info = json.loads(path.read_text(encoding="utf-8"))
|
|
4347
|
+
except (OSError, ValueError):
|
|
4348
|
+
return
|
|
4349
|
+
pid = info.get("pid")
|
|
4350
|
+
if not isinstance(pid, int):
|
|
4351
|
+
path.unlink(missing_ok=True)
|
|
4352
|
+
return
|
|
4353
|
+
try:
|
|
4354
|
+
os.kill(pid, 0) # liveness probe
|
|
4355
|
+
except OSError:
|
|
4356
|
+
path.unlink(missing_ok=True) # already gone
|
|
4357
|
+
return
|
|
4358
|
+
if not _pid_is_agent_config(pid):
|
|
4359
|
+
return # pid reused by an unrelated process — leave it alone
|
|
4360
|
+
try:
|
|
4361
|
+
os.kill(pid, signal.SIGTERM)
|
|
4362
|
+
except OSError:
|
|
4363
|
+
path.unlink(missing_ok=True)
|
|
4364
|
+
return
|
|
4365
|
+
# Wait up to ~3s for a graceful exit, then force-kill.
|
|
4366
|
+
for _ in range(30):
|
|
4367
|
+
try:
|
|
4368
|
+
os.kill(pid, 0)
|
|
4369
|
+
except OSError:
|
|
4370
|
+
break
|
|
4371
|
+
time.sleep(0.1)
|
|
4372
|
+
else:
|
|
4373
|
+
try:
|
|
4374
|
+
os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
|
|
4375
|
+
except OSError:
|
|
4376
|
+
pass
|
|
4377
|
+
path.unlink(missing_ok=True)
|
|
4378
|
+
print("(Stopped the previous wizard server.)")
|
|
4379
|
+
|
|
4380
|
+
|
|
4338
4381
|
def _wizard_spawn(project_root: Path) -> int:
|
|
4339
4382
|
"""Spawn the wizard, await readiness, hand off to the child.
|
|
4340
4383
|
|
|
@@ -4343,6 +4386,9 @@ def _wizard_spawn(project_root: Path) -> int:
|
|
|
4343
4386
|
Never raises into the parent — every error surfaces as a printed
|
|
4344
4387
|
fallback line and a 0 return.
|
|
4345
4388
|
"""
|
|
4389
|
+
# Always start fresh: stop any server left running by a prior init.
|
|
4390
|
+
_kill_stale_wizard_server()
|
|
4391
|
+
|
|
4346
4392
|
cli = _wizard_cli_dist(project_root)
|
|
4347
4393
|
if cli is None:
|
|
4348
4394
|
print(
|
|
@@ -101,6 +101,14 @@ def lint(quiet: bool) -> int:
|
|
|
101
101
|
for hint in pk.get("requires_hint", []) or []:
|
|
102
102
|
if hint not in pack_ids:
|
|
103
103
|
errors.append(f"packs.yml '{pid}'.requires_hint → unknown pack '{hint}'")
|
|
104
|
+
# cluster (road-to-wizard-ux-improvements § Phase 4): advisory wizard
|
|
105
|
+
# grouping; the value must be a known pack id and not self-referential.
|
|
106
|
+
cluster = pk.get("cluster")
|
|
107
|
+
if cluster is not None:
|
|
108
|
+
if cluster not in pack_ids:
|
|
109
|
+
errors.append(f"packs.yml '{pid}'.cluster → unknown pack '{cluster}'")
|
|
110
|
+
elif cluster == pid:
|
|
111
|
+
errors.append(f"packs.yml '{pid}'.cluster → must not reference itself")
|
|
104
112
|
|
|
105
113
|
# 4. Bidirectional integrity (council HIGH fold-in).
|
|
106
114
|
pack_by_id = {pk.get("id"): pk for pk in packs}
|