@event4u/agent-config 4.7.2 → 4.8.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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Shared agent configuration \u2014 skills for AI coding tools (Claude Code, Augment, Cursor, Cline, Windsurf, Gemini CLI).",
9
- "version": "4.7.2",
9
+ "version": "4.8.0",
10
10
  "keywords": [
11
11
  "agent-config",
12
12
  "skills",
package/CHANGELOG.md CHANGED
@@ -802,6 +802,30 @@ our recommendation order, not its support status.
802
802
  > that forces a new era split (`# Era: 4.6.x`, etc.) — see
803
803
  > [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).
804
804
 
805
+ ## [4.8.0](https://github.com/event4u-app/agent-config/compare/4.7.2...4.8.0) (2026-05-28)
806
+
807
+ ### Features
808
+
809
+ * **install:** close Claude Code global distribution gap ([aa15db9](https://github.com/event4u-app/agent-config/commit/aa15db9651c4fd21f8bd30ef88e3aeeb1eb31e22))
810
+
811
+ ### Bug Fixes
812
+
813
+ * **maintainer:** align .claude/settings.json plugin id ([b59e080](https://github.com/event4u-app/agent-config/commit/b59e0804e874e9c7c95cfc821a31746e4241f61c))
814
+
815
+ ### Documentation
816
+
817
+ * **adr:** record claude-code command-projection strategy (ADR-030) ([706dedb](https://github.com/event4u-app/agent-config/commit/706dedb54f5792a2cf5b7c2401054b30490edeec))
818
+
819
+ ### Tests
820
+
821
+ * **install:** regression coverage for global distribution heal ([bfdbc90](https://github.com/event4u-app/agent-config/commit/bfdbc9053d9032ffd10248835ba03f276631c7b3))
822
+
823
+ ### Chores
824
+
825
+ * gitignore install-time artefacts in maintainer repo ([d75aeac](https://github.com/event4u-app/agent-config/commit/d75aeac4ed8858d2cddc7e3534eeff6bfb1ab036))
826
+
827
+ Tests: 5078 (+14 since 4.7.2)
828
+
805
829
  ## [4.7.2](https://github.com/event4u-app/agent-config/compare/4.7.1...4.7.2) (2026-05-28)
806
830
 
807
831
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  # Discovery — Deprecation Report
2
2
 
3
- - Generated: `2026-05-28T06:18:52Z`
3
+ - Generated: `2026-05-28T11:41:20Z`
4
4
  - Deprecated artefacts: **0**
5
5
 
6
6
  _None. Tree is clean._
@@ -9543,7 +9543,7 @@
9543
9543
  "reason": "scaffold for new SKILL.md authoring"
9544
9544
  }
9545
9545
  ],
9546
- "generated_at": "2026-05-28T06:18:52Z",
9546
+ "generated_at": "2026-05-28T11:41:20Z",
9547
9547
  "packs": [
9548
9548
  {
9549
9549
  "artefact_count": 84,
@@ -1 +1 @@
1
- b1be210896960f4ef344298e27481e6fc8b98055d6694aa89ad2184e4d4c4e26 discovery-manifest.json
1
+ ebdbb29d37f2509c6098a29419ece7ba11030a5709468b1be49bcd3ad42e90e6 discovery-manifest.json
@@ -1,6 +1,6 @@
1
1
  # Discovery Manifest — Summary
2
2
 
3
- - Generated: `2026-05-28T06:18:52Z`
3
+ - Generated: `2026-05-28T11:41:20Z`
4
4
  - Scanner: `d75eba636abb`
5
5
  - Artefacts: **432**
6
6
  - Unassigned: **0**
@@ -1,6 +1,6 @@
1
1
  # Discovery — Orphan Report
2
2
 
3
- - Generated: `2026-05-28T06:18:52Z`
3
+ - Generated: `2026-05-28T11:41:20Z`
4
4
  - Orphan artefacts: **0**
5
5
 
6
6
  > An orphan is an artefact whose declared pack has no other members.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "checksum": "sha256:eb7494e53428947d792e0377cb59e043457819ad3fff0b7ff6bc1011134be766",
3
- "generated_at": "2026-05-28T06:18:52Z",
3
+ "generated_at": "2026-05-28T11:41:20Z",
4
4
  "packs": [
5
5
  {
6
6
  "artefact_count": 84,
@@ -1,6 +1,6 @@
1
1
  # Discovery — Trust Report
2
2
 
3
- - Generated: `2026-05-28T06:18:52Z`
3
+ - Generated: `2026-05-28T11:41:20Z`
4
4
  - Workspaces tracked: **8**
5
5
  - Human-review-required artefacts: **2**
6
6
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "checksum": "sha256:eb7494e53428947d792e0377cb59e043457819ad3fff0b7ff6bc1011134be766",
3
- "generated_at": "2026-05-28T06:18:52Z",
3
+ "generated_at": "2026-05-28T11:41:20Z",
4
4
  "scanner_version": "d75eba636abb",
5
5
  "workspaces": [
6
6
  {
@@ -62,6 +62,14 @@ export const USER_SCOPE_PATHS = {
62
62
  const CLAUDE_SKILL_BUNDLE = [
63
63
  ['.agent-src/rules', 'rules'],
64
64
  ['.agent-src/skills', 'skills'],
65
+ // Commands ship to ~/.claude/commands/ — native Claude Code slash-
66
+ // command surface. Verified empirically 2026-05-28: rich frontmatter
67
+ // (disable-model-invocation, custom fields) tolerated; nested
68
+ // subdirs route as `/<cluster>:<sub>`. Council session 2026-05-28
69
+ // converged on Option B (native slash-only) — see
70
+ // agents/runtime/council/responses/claude-code-distribution.json
71
+ // and scripts/install.py:_CLAUDE_SKILL_BUNDLE for the Python mirror.
72
+ ['.agent-src/commands', 'commands'],
65
73
  ['.agent-src/personas', 'personas'],
66
74
  ];
67
75
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"wizard-plan.js","sourceRoot":"","sources":["../../src/install/wizard-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wizard-plan.js","sourceRoot":"","sources":["../../src/install/wizard-plan.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,7 @@
9
9
  "homepage": "https://github.com/event4u-app/agent-config#readme",
10
10
  "name": "@event4u/agent-config",
11
11
  "repository": "https://github.com/event4u-app/agent-config",
12
- "version": "4.7.2"
12
+ "version": "4.8.0"
13
13
  },
14
14
  "registries": [
15
15
  {
@@ -0,0 +1,147 @@
1
+ ---
2
+ adr: 030
3
+ status: accepted
4
+ date: 2026-05-28
5
+ decision: claude-code-command-projection
6
+ supersedes: —
7
+ superseded_by: —
8
+ phase: road-to-claude-code-global-distribution Phase 3
9
+ type: structural
10
+ review_date: 2026-06-11
11
+ ---
12
+
13
+ # ADR-030 — Claude Code command-projection strategy: native slash-only
14
+
15
+ ## Status
16
+
17
+ **Accepted** · 2026-05-28. Three conditions enumerated below; all three
18
+ verified empirically in the same session, so the decision lands
19
+ **without** soak. The 14-day kill-switch keeps the revert path one
20
+ commit away; review date 2026-06-11 closes the window.
21
+
22
+ ## Context
23
+
24
+ The package ships commands as `.agent-src/commands/<cluster>/<sub>.md`
25
+ (canonical source) and projects them per-tool through
26
+ `scripts/condense.py`. Pre-2026-05-28 the Claude Code projection was a
27
+ single path: `generate_claude_commands` rewrote each command into
28
+ `.claude/skills/<flat-slug>/SKILL.md` with
29
+ `disable-model-invocation: true` in the source frontmatter.
30
+
31
+ The 2026-05-28 diagnostic surfaced the failure mode:
32
+
33
+ - `.claude/commands/` was never populated — commands carried no
34
+ filesystem channel into Claude Code's native slash router.
35
+ - The skills-projection target was filtered out of the
36
+ model-invokable skill list (`disable-model-invocation: true`).
37
+ - `_CLAUDE_SKILL_BUNDLE` shipped `rules` / `skills` / `personas` but
38
+ **no** `commands` subtree.
39
+
40
+ Net result: Claude Code received zero command surface from the package
41
+ outside the cwd-local repo install. Augment Code and Cursor bundles
42
+ already shipped `commands/`; Claude Code was the outlier.
43
+
44
+ ## Decision
45
+
46
+ Adopt **Option B (native slash-only)**:
47
+
48
+ 1. Ship `.agent-src/commands/` directly into Claude Code's native
49
+ slash-command surface at `~/.claude/commands/` via the global
50
+ deploy bundle (`_CLAUDE_SKILL_BUNDLE` in `scripts/install.py` and
51
+ its TypeScript mirror in `src/install/wizard-plan.ts`).
52
+ Subdirectory layout maps 1:1 to the `/<cluster>:<sub>` namespace
53
+ per Claude Code's filesystem-channel convention. No separate
54
+ `condense.py` projector — the source tree already follows the
55
+ target layout, so the global-install copy is the projection.
56
+ 2. Keep `condense.py::generate_claude_commands` (skills-list
57
+ projection at `.claude/skills/`) unchanged **for now** —
58
+ backwards compat for any consumer already discovering commands
59
+ via `.claude/skills/`. Retirement deferred to a separate roadmap
60
+ if needed.
61
+
62
+ Rejected alternatives:
63
+
64
+ - **Option A — dual projection (slash + skills, both active).**
65
+ Conservative, but doubles maintenance surface and keeps the
66
+ filter-out-of-skill-list semantics. Verdict: only useful if Option B
67
+ fails the kill-switch window.
68
+ - **Option C — commands_unsupported (no Claude Code command
69
+ surface).** Eliminates the bug class by removing the feature.
70
+ Verdict: regression in product surface; Claude Code consumers lose
71
+ parity with Augment / Cursor.
72
+
73
+ ### Council convergence
74
+
75
+ Session 2026-05-28, design mode, 2 rounds, $0.06 actual.
76
+ Members: `claude-sonnet-4-5` + `gpt-4o`. Both converged on Option B,
77
+ CONDITIONAL on the three verifications below. Responses captured at
78
+ `agents/runtime/council/responses/claude-code-distribution.json`.
79
+
80
+ ### Three conditions (all verified 2026-05-28)
81
+
82
+ 1. **Plugin loader reads `.claude/commands/` for globally-installed
83
+ plugins (not just cwd-local).**
84
+ Verified empirically: `~/.claude/commands/probe/sub.md` →
85
+ `/probe:sub` routed successfully via
86
+ `echo '' | claude --print "/probe:sub"`. Native filesystem channel
87
+ works for user-scope at top-level AND in subdirectories.
88
+ 2. **Command parser tolerates `disable-model-invocation: true` in
89
+ frontmatter (or strip during projection).**
90
+ Verified empirically: probe command carrying the package's full
91
+ rich frontmatter routed successfully. Subtle behaviour:
92
+ `disable-model-invocation: true` hides the command from `/help`
93
+ listing but keeps it slash-invokable when typed directly — the
94
+ desired UX for heavyweight commands. No frontmatter-strip step
95
+ required.
96
+ 3. **Kill-switch defined: if native slash doesn't resolve in
97
+ production within 14 days, fall back to dual-projection with
98
+ deprecation timeline.**
99
+ The kill-switch is the inverse of Phase 4 Step 1 — remove
100
+ `(".agent-src/commands", "commands")` from `_CLAUDE_SKILL_BUNDLE`
101
+ in `scripts/install.py` and `src/install/wizard-plan.ts`. One-line
102
+ revert per file. Tracked by review date 2026-06-11.
103
+
104
+ ## Consequences
105
+
106
+ **Positive:**
107
+
108
+ - `~/.claude/commands/<cluster>/<sub>.md` lands the package's full
109
+ command surface natively in Claude Code, matching Augment and
110
+ Cursor parity.
111
+ - `disable-model-invocation: true` semantics preserved — heavyweight
112
+ commands stay invokable but hidden from auto-complete, the intended
113
+ UX.
114
+ - No frontmatter mangling — the source command file is the artifact
115
+ that ships, no projection transform needed.
116
+
117
+ **Negative / accepted:**
118
+
119
+ - Two projection paths coexist (`.claude/commands/` for slash routing
120
+ + `.claude/skills/` for the legacy skills-projection list). Cost is
121
+ one extra subtree per install; benefit is no consumer-breaking
122
+ during the 14-day kill-switch window. Retirement of the skills
123
+ projection is a separate decision.
124
+ - Frontmatter shipped to Claude Code carries package-internal fields
125
+ (`tier`, `cluster`, `sub`, `lifecycle`, `trust`, `install`, etc.)
126
+ the runtime ignores. Empirically tolerated; documented as
127
+ belt-and-braces against future runtime tightening.
128
+
129
+ ## Alternatives considered
130
+
131
+ See § Decision above — Option A (dual projection) and Option C
132
+ (commands_unsupported) explicitly rejected with stated rationale.
133
+
134
+ ## References
135
+
136
+ - Roadmap:
137
+ `agents/roadmaps/road-to-claude-code-global-distribution.md` Phase 3.
138
+ - Council session responses:
139
+ `agents/runtime/council/responses/claude-code-distribution.json`.
140
+ - Empirical verification: `~/.claude/commands/probe/sub.md` route
141
+ test, 2026-05-28 (Condition 1 + Condition 2).
142
+ - Mirror implementation: `scripts/install.py::_CLAUDE_SKILL_BUNDLE`
143
+ and `src/install/wizard-plan.ts::CLAUDE_SKILL_BUNDLE` — both
144
+ include `(".agent-src/commands", "commands")` so a global install
145
+ drops `~/.claude/commands/<cluster>/<sub>.md` directly.
146
+ - Predecessor projection: `scripts/condense.py::generate_claude_commands`
147
+ (skills-list, retained pending separate retirement decision).
@@ -33,6 +33,7 @@ _Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._
33
33
  | [ADR-027](ADR-027-changelog-machine-vs-manual.md) | Changelog Machine Vs Manual | accepted | 2026-05-25 | — |
34
34
  | [ADR-028](ADR-028-root-layout.md) | Root Layout | accepted | 2026-05-25 | — |
35
35
  | [ADR-029](ADR-029-multi-workspace-deferred.md) | Multi Workspace Deferred | accepted | 2026-05-25 | — |
36
+ | [ADR-030](ADR-030-claude-code-command-projection.md) | Claude Code Command Projection | accepted | 2026-05-28 | — |
36
37
 
37
38
  ## Unnumbered (legacy)
38
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "4.7.2",
3
+ "version": "4.8.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,
@@ -198,6 +198,50 @@ def check_version(
198
198
  return (recorded == installed_version, recorded)
199
199
 
200
200
 
201
+ _SEMVER_RE = re.compile(r"^\s*v?(\d+)\.(\d+)\.(\d+)")
202
+
203
+
204
+ def _parse_semver(version: str) -> Optional[tuple[int, int, int]]:
205
+ """Parse ``X.Y.Z[-suffix]`` into a ``(major, minor, patch)`` tuple.
206
+
207
+ Returns ``None`` when the leading three numeric segments cannot be
208
+ extracted. Suffixes (``-rc1``, ``+build.5``) are ignored: the
209
+ classification only needs the numeric prefix to decide upgrade vs
210
+ downgrade.
211
+ """
212
+ match = _SEMVER_RE.match(version)
213
+ if not match:
214
+ return None
215
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
216
+
217
+
218
+ def classify_mismatch(
219
+ installed_version: str, recorded: Optional[str],
220
+ ) -> str:
221
+ """Classify the relationship between recorded and installed versions.
222
+
223
+ Returns one of:
224
+ * ``"none"`` — no lockfile yet; install proceeds clean.
225
+ * ``"match"`` — recorded equals installed.
226
+ * ``"upgrade"`` — recorded < installed; auto-heal allowed.
227
+ * ``"downgrade"`` — recorded > installed; refuse without ``--force``.
228
+ * ``"unparseable"`` — recorded shape unrecognizable (pre-1.0, 1.x
229
+ legacy formats from the namespace migration); treated as
230
+ upgrade by the install path.
231
+ """
232
+ if recorded is None:
233
+ return "none"
234
+ if recorded == installed_version:
235
+ return "match"
236
+ rec = _parse_semver(recorded)
237
+ inst = _parse_semver(installed_version)
238
+ if rec is None or inst is None:
239
+ return "unparseable"
240
+ if rec < inst:
241
+ return "upgrade"
242
+ return "downgrade"
243
+
244
+
201
245
  def current_package_version(repo_root: Optional[Path] = None) -> str:
202
246
  """Read ``version`` from the package's own ``package.json``."""
203
247
  if repo_root is None:
@@ -1083,6 +1083,49 @@ def ensure_augment_user_hooks(package_root: Path, force: bool) -> list[dict[str,
1083
1083
  # concern fires on the same logical surface across platforms — the
1084
1084
  # contract from agents/settings/contexts/hardening-pattern.md § Cross-platform
1085
1085
  # parity.
1086
+ # Canonical Claude Code plugin id — must match `.claude-plugin/marketplace.json`
1087
+ # (`<plugin>@<marketplace>` = `agent-config` + `event4u-agent-config`) and the
1088
+ # install command documented in `docs/installation.md`.
1089
+ CLAUDE_PLUGIN_ID = "agent-config@event4u-agent-config"
1090
+
1091
+ # Stale plugin ids written by pre-4.x installer versions. The bridge removes
1092
+ # any of these from `enabledPlugins` on rerun so the canonical id alone
1093
+ # survives. Claude Code silently ignores unresolvable ids (no marketplace
1094
+ # match), so a stale entry leaves the plugin inactive without an error path
1095
+ # the user can see — the heal is the only feedback loop.
1096
+ CLAUDE_LEGACY_PLUGIN_IDS: tuple[str, ...] = (
1097
+ "agent-conf@event4u", # abbreviated form — never matched a real marketplace
1098
+ "agent-config@event4u", # pre-marketplace-rename form (missing `-agent-config` suffix)
1099
+ )
1100
+
1101
+
1102
+ def _heal_legacy_claude_plugin_ids(path: Path) -> list[str]:
1103
+ """Remove known-stale plugin ids from `.claude/settings.json` in place.
1104
+
1105
+ Reads the existing settings file, drops any `enabledPlugins` key whose
1106
+ id appears in `CLAUDE_LEGACY_PLUGIN_IDS`, and writes the file back
1107
+ when anything changed. Returns the list of removed ids so the caller
1108
+ can surface a `success(...)` per heal and treat the operation as a
1109
+ forced refresh for the subsequent `merge_json_file` call.
1110
+
1111
+ No-ops when the file is absent, malformed, or carries no stale ids.
1112
+ The canonical id is left untouched.
1113
+ """
1114
+ if not path.exists():
1115
+ return []
1116
+ data = read_json_file(path)
1117
+ enabled = data.get("enabledPlugins")
1118
+ if not isinstance(enabled, dict):
1119
+ return []
1120
+ removed = [pid for pid in CLAUDE_LEGACY_PLUGIN_IDS if pid in enabled]
1121
+ if not removed:
1122
+ return []
1123
+ for pid in removed:
1124
+ del enabled[pid]
1125
+ write_json_file(path, data)
1126
+ return removed
1127
+
1128
+
1086
1129
  def ensure_claude_bridge(project_root: Path, force: bool) -> list[dict[str, Any]]:
1087
1130
  """Deploy .claude/settings.json with plugin enablement only.
1088
1131
 
@@ -1099,12 +1142,22 @@ def ensure_claude_bridge(project_root: Path, force: bool) -> list[dict[str, Any]
1099
1142
  documented install command in docs/installation.md. Idempotent:
1100
1143
  `enabledPlugins` is a dict-merge, so the key coexists with any other
1101
1144
  plugin a neighbour tool enabled.
1145
+
1146
+ Stale-id heal: before the merge, any pre-4.x ids listed in
1147
+ `CLAUDE_LEGACY_PLUGIN_IDS` are removed from `enabledPlugins`. A heal
1148
+ self-authorises the corrective merge — the merge runs with effective
1149
+ force so the canonical id lands in the same install, even without
1150
+ `--force` on the CLI.
1102
1151
  """
1152
+ target = project_root / ".claude" / "settings.json"
1153
+ healed = _heal_legacy_claude_plugin_ids(target)
1154
+ for pid in healed:
1155
+ success(f".claude/settings.json: removed stale plugin id `{pid}`")
1103
1156
  bridge = {
1104
- "enabledPlugins": {"agent-config@event4u-agent-config": True},
1157
+ "enabledPlugins": {CLAUDE_PLUGIN_ID: True},
1105
1158
  }
1106
1159
  return merge_json_file(
1107
- project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json",
1160
+ target, bridge, force or bool(healed), ".claude/settings.json",
1108
1161
  )
1109
1162
 
1110
1163
 
@@ -2215,6 +2268,17 @@ PROJECT_BRIDGE_MARKERS = {
2215
2268
  _CLAUDE_SKILL_BUNDLE: list[tuple[str, str]] = [
2216
2269
  (".agent-src/rules", "rules"),
2217
2270
  (".agent-src/skills", "skills"),
2271
+ # Commands ship to the native Claude Code user-scope slash-command
2272
+ # surface: `~/.claude/commands/<cluster>/<sub>.md` resolves as
2273
+ # `/<cluster>:<sub>` per Claude Code's filesystem-channel convention
2274
+ # (verified empirically 2026-05-28: top-level + nested + rich
2275
+ # frontmatter all route; heavyweight commands carrying
2276
+ # `disable-model-invocation: true` stay invokable when typed but are
2277
+ # hidden from auto-complete — desired UX). Council session
2278
+ # 2026-05-28 (claude-sonnet-4-5 + gpt-4o, design mode) verdict
2279
+ # Option B (native slash-only). See
2280
+ # `agents/runtime/council/responses/claude-code-distribution.json`.
2281
+ (".agent-src/commands", "commands"),
2218
2282
  (".agent-src/personas", "personas"),
2219
2283
  ]
2220
2284
  GLOBAL_DEPLOY_SOURCES: dict[str, list[tuple[str, str]]] = {
@@ -2956,20 +3020,94 @@ MIGRATE_LEGACY_YAML_FILES = (".agent-settings.yml", ".agent-user.yml")
2956
3020
  MIGRATE_LEGACY_TOOL_DIRS = (".augment", ".claude", ".cursor")
2957
3021
 
2958
3022
 
3023
+ # Package identity used by the maintainer auto-detect. Matches the npm
3024
+ # package name declared in ``package.json`` at the agent-config source
3025
+ # repo root. Refreshing this value requires a coordinated rename
3026
+ # (package.json + this constant + the publish pipeline).
3027
+ AGENT_CONFIG_PACKAGE_NAME = "@event4u/agent-config"
3028
+
3029
+
3030
+ def _is_agent_config_source_repo(project_root: Path) -> tuple[bool, str]:
3031
+ """Return ``(is_source_repo, signature)`` for the maintainer auto-detect.
3032
+
3033
+ Phase Q1 of road-to-claude-code-global-distribution (council Option D —
3034
+ Hybrid auto-detect): treat any of these high-specificity signatures as
3035
+ proof that ``project_root`` is the agent-config source repo, not a
3036
+ consumer project. Hits skip the ADR-020 migration prompt automatically
3037
+ so a maintainer running the wizard does not get their working tree
3038
+ moved into ``.legacy-pre-global-only/``.
3039
+
3040
+ Signatures, in order of cost (cheap-first short-circuit):
3041
+
3042
+ 1. ``package.json`` declares ``"name": "@event4u/agent-config"``.
3043
+ Strongest signal — the npm-published identity of this repo.
3044
+ 2. ``.agent-src.uncondensed/`` exists at ``project_root`` (legacy
3045
+ layout) OR under ``packages/*/`` (current layout). Both shapes
3046
+ are unique to the source repo.
3047
+ 3. ``scripts/install.py`` exists at ``project_root`` AND the file
3048
+ name matches this module (``__file__``). Self-referential — if
3049
+ the installer code path is reading itself, the cwd is the repo
3050
+ that owns the installer.
3051
+
3052
+ The user can force consumer behaviour via ``AGENT_CONFIG_CONSUMER_MODE=1``
3053
+ when testing the wizard's consumer flow from inside the maintainer
3054
+ checkout (end-to-end QA path).
3055
+ """
3056
+ if os.environ.get("AGENT_CONFIG_CONSUMER_MODE") == "1":
3057
+ return False, "consumer-mode-override"
3058
+
3059
+ pkg_json = project_root / "package.json"
3060
+ if pkg_json.is_file():
3061
+ try:
3062
+ data = json.loads(pkg_json.read_text(encoding="utf-8"))
3063
+ except (json.JSONDecodeError, OSError):
3064
+ data = {}
3065
+ if isinstance(data, dict) and data.get("name") == AGENT_CONFIG_PACKAGE_NAME:
3066
+ return True, "package.json:name"
3067
+
3068
+ if (project_root / ".agent-src.uncondensed").is_dir():
3069
+ return True, ".agent-src.uncondensed/"
3070
+ packages_dir = project_root / "packages"
3071
+ if packages_dir.is_dir():
3072
+ for child in packages_dir.iterdir():
3073
+ if (child / ".agent-src.uncondensed").is_dir():
3074
+ return True, f"packages/{child.name}/.agent-src.uncondensed/"
3075
+
3076
+ installer_self = project_root / "scripts" / "install.py"
3077
+ try:
3078
+ if installer_self.is_file() and installer_self.resolve() == Path(__file__).resolve():
3079
+ return True, "scripts/install.py (self)"
3080
+ except OSError:
3081
+ pass
3082
+
3083
+ return False, ""
3084
+
3085
+
2959
3086
  def _detect_legacy_for_migration(project_root: Path) -> list[str]:
2960
3087
  """Return a sorted list of legacy artefact relpaths present in ``project_root``.
2961
3088
 
2962
3089
  Skipped (returns ``[]``) when:
2963
3090
 
2964
3091
  - ``AGENT_CONFIG_DEV_MODE=1`` is set (maintainer dogfood loop),
2965
- - the project root IS the agent-config source repo
2966
- (``.agent-src.uncondensed/`` present),
3092
+ - ``project_root`` IS the agent-config source repo per
3093
+ :func:`_is_agent_config_source_repo` (council Option D auto-detect),
2967
3094
  - the bridge marker already exists (project is already global-only).
2968
3095
  """
2969
3096
  if os.environ.get("AGENT_CONFIG_DEV_MODE") == "1":
2970
3097
  return []
2971
- if (project_root / ".agent-src.uncondensed").is_dir():
3098
+
3099
+ is_source, signature = _is_agent_config_source_repo(project_root)
3100
+ if is_source:
3101
+ if not QUIET:
3102
+ warn(
3103
+ "Maintainer mode auto-detected — agent-config source repo "
3104
+ f"(signature: {signature}). Skipping ADR-020 migration "
3105
+ "prompt; the working tree stays intact. Set "
3106
+ "AGENT_CONFIG_CONSUMER_MODE=1 to override for end-to-end "
3107
+ "consumer-flow testing."
3108
+ )
2972
3109
  return []
3110
+
2973
3111
  if (project_root / CONSUMER_BRIDGE_MARKER_RELPATH).is_file():
2974
3112
  return []
2975
3113
 
@@ -3478,10 +3616,65 @@ def _deploy_global_content(
3478
3616
  written_total += w
3479
3617
  skipped_total += s
3480
3618
  written_paths.extend(paths)
3619
+ # Phase 5 (road-to-claude-code-global-distribution): postcheck.
3620
+ # Every entry in the deploy plan must end with the destination
3621
+ # subpath populated — directory exists AND is non-empty. A
3622
+ # silent partial deploy (no exception raised, no files written
3623
+ # for one of the bundle entries) is the silent-failure class
3624
+ # this phase exists to surface.
3625
+ missing_targets = _verify_deploy_targets(anchor, plan)
3626
+ if missing_targets:
3627
+ if not QUIET:
3628
+ warn(
3629
+ f"{tool_id}: deploy postcheck failed — "
3630
+ f"missing/empty: {', '.join(missing_targets)}"
3631
+ )
3632
+ _emit_progress({
3633
+ "type": "verify_failed",
3634
+ "tool": tool_id,
3635
+ "missing": missing_targets,
3636
+ })
3637
+ results[tool_id] = (
3638
+ written_total, skipped_total, "deploy_failed", written_paths,
3639
+ )
3640
+ continue
3641
+ _emit_progress({"type": "verified", "tool": tool_id})
3481
3642
  results[tool_id] = (written_total, skipped_total, "deployed", written_paths)
3482
3643
  return results
3483
3644
 
3484
3645
 
3646
+ def _verify_deploy_targets(
3647
+ anchor: Path, plan: list[tuple[str, str]],
3648
+ ) -> list[str]:
3649
+ """Return the deploy-plan destination subpaths that did NOT materialise.
3650
+
3651
+ A deploy plan entry ``(src_rel, dest_sub)`` is verified by checking
3652
+ that ``anchor / dest_sub`` (or ``anchor`` when ``dest_sub`` is empty)
3653
+ exists as a directory AND contains at least one entry. An empty
3654
+ directory counts as a failure — the agent-config bundle never
3655
+ legitimately ships an empty subtree, so "empty after deploy" means
3656
+ the copy step silently produced nothing.
3657
+
3658
+ Returns the list of failing ``dest_sub`` values (empty string
3659
+ rewritten to ``"."`` for log clarity). An empty return list means
3660
+ every expected target is populated.
3661
+ """
3662
+ missing: list[str] = []
3663
+ for _, dest_sub in plan:
3664
+ target = anchor / dest_sub if dest_sub else anchor
3665
+ label = dest_sub or "."
3666
+ if not target.is_dir():
3667
+ missing.append(label)
3668
+ continue
3669
+ try:
3670
+ next(target.iterdir())
3671
+ except StopIteration:
3672
+ missing.append(label)
3673
+ except OSError:
3674
+ missing.append(label)
3675
+ return missing
3676
+
3677
+
3485
3678
  def install_global(
3486
3679
  tools: set[str],
3487
3680
  force: bool,
@@ -3525,20 +3718,34 @@ def install_global(
3525
3718
  installed_version = lock_mod.current_package_version()
3526
3719
  read_path = lock_mod.lockfile_path()
3527
3720
  write_path = lock_mod.lockfile_write_path()
3528
- ok, recorded = lock_mod.check_version(installed_version, path=read_path)
3529
-
3530
- if not ok and not force:
3721
+ _, recorded = lock_mod.check_version(installed_version, path=read_path)
3722
+ classification = lock_mod.classify_mismatch(installed_version, recorded)
3723
+
3724
+ # Phase 2 (roadmap road-to-claude-code-global-distribution.md): a stale
3725
+ # lockfile recording a *lower* (or unparseable legacy) version is the
3726
+ # upgrade path — auto-heal by claiming the new version slot and
3727
+ # continuing the install. Only a recorded version *higher* than the
3728
+ # running package is treated as a downgrade and still requires --force.
3729
+ # This kills the silent-refusal trap where users on pre-2.x (recorded:
3730
+ # 1.42.0) installs hit `install.py:3530` and exit 1 without ever
3731
+ # touching `~/.claude/`.
3732
+ if classification == "downgrade" and not force:
3531
3733
  if not QUIET:
3532
3734
  print()
3533
- warn("Refusing global install: lockfile version mismatch.")
3735
+ warn("Refusing global install: lockfile records a newer version.")
3534
3736
  info(f" Lockfile: {read_path}")
3535
3737
  info(f" Recorded version: {recorded}")
3536
3738
  info(f" Current package: {installed_version}")
3537
- info(" Fix: run `agent-config update`")
3538
- info(" Override: re-run with `--force` (replaces the lockfile)")
3739
+ info(" Fix: upgrade the package, or re-run with `--force`")
3539
3740
  print()
3540
3741
  return 1
3541
3742
 
3743
+ if classification in ("upgrade", "unparseable") and not QUIET:
3744
+ info(
3745
+ f"🔄 Upgrading lockfile from {recorded} to {installed_version}, "
3746
+ "redeploying tools"
3747
+ )
3748
+
3542
3749
  if not QUIET:
3543
3750
  print()
3544
3751
  info("Agent Config — Global (user-scope) install [ADR-007]")
@@ -3549,6 +3756,13 @@ def install_global(
3549
3756
  continue
3550
3757
  print(f" {tool_id:<15} → {anchor}")
3551
3758
 
3759
+ # Claim the version slot BEFORE the deploy (Phase 2 Step 2 of the
3760
+ # road-to-claude-code-global-distribution roadmap). The deploy can
3761
+ # fail mid-way; the lockfile must stay on the new version regardless
3762
+ # so subsequent re-runs do not relitigate the upgrade refusal. A
3763
+ # partial deploy retries cleanly on the next invocation — the
3764
+ # lockfile staying stuck on an ancient version is the worse failure
3765
+ # mode this Phase exists to eliminate.
3552
3766
  existing = lock_mod.read_lockfile(path=read_path) or {}
3553
3767
  existing_tools = list(existing.get("tools", []))
3554
3768
  merged_tools = sorted(set(existing_tools) | set(tools))
@@ -3566,6 +3780,28 @@ def install_global(
3566
3780
  package_root = _resolve_package_root_for_global()
3567
3781
  deploy_results = _deploy_global_content(tools, force, package_root, written)
3568
3782
 
3783
+ # Phase 5 (road-to-claude-code-global-distribution): postcheck-driven
3784
+ # lockfile correction. A tool whose deploy postcheck failed
3785
+ # (status="deploy_failed") MUST NOT remain in the lockfile's tools
3786
+ # list — claiming "tool X is installed" without the content on disk
3787
+ # is the silent-failure class this phase exists to surface. Tools
3788
+ # already recorded by a prior successful install stay (the
3789
+ # `failed_tools` set only filters this run's newly-attempted tools).
3790
+ failed_tools = {
3791
+ tool_id
3792
+ for tool_id, (_, _, status, _) in deploy_results.items()
3793
+ if status == "deploy_failed"
3794
+ }
3795
+ if failed_tools:
3796
+ corrected_tools = sorted(set(merged_tools) - failed_tools)
3797
+ if corrected_tools != merged_tools:
3798
+ lock_mod.write_lockfile(installed_version, corrected_tools, path=write_path)
3799
+ if not QUIET:
3800
+ warn(
3801
+ "Lockfile corrected after deploy postcheck — dropped "
3802
+ f"{', '.join(sorted(failed_tools))} (verification failed)."
3803
+ )
3804
+
3569
3805
  # NDJSON progress for the wizard --apply-payload real-apply bridge. One
3570
3806
  # `file` frame per deployed tool unit (coarse, per AI-council 2026-05-27);
3571
3807
  # the GUI maps these to its SSE progress frames. No-op under normal CLI