@event4u/agent-config 4.7.2 → 4.9.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 +37 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/install/wizard-plan.js +8 -0
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/decisions/ADR-030-claude-code-command-projection.md +147 -0
- package/docs/decisions/INDEX.md +1 -0
- 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/_lib/installed_lock.py +44 -0
- package/scripts/install.py +247 -11
- package/scripts/inventory_abstraction_budget.py +616 -0
package/scripts/install.py
CHANGED
|
@@ -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": {
|
|
1157
|
+
"enabledPlugins": {CLAUDE_PLUGIN_ID: True},
|
|
1105
1158
|
}
|
|
1106
1159
|
return merge_json_file(
|
|
1107
|
-
|
|
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
|
-
-
|
|
2966
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
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
|
|
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:
|
|
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
|