@event4u/agent-config 2.4.0 → 2.4.1

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": "2.4.0",
9
+ "version": "2.4.1",
10
10
  "keywords": [
11
11
  "agent-config",
12
12
  "skills",
package/CHANGELOG.md CHANGED
@@ -343,6 +343,15 @@ our recommendation order, not its support status.
343
343
  users" tension without removing any path that an existing user
344
344
  might rely on.
345
345
 
346
+ ## [2.4.1](https://github.com/event4u-app/agent-config/compare/2.4.0...2.4.1) (2026-05-13)
347
+
348
+ ### Bug Fixes
349
+
350
+ * **install:** write lockfile to canonical event4u namespace ([ca57607](https://github.com/event4u-app/agent-config/commit/ca5760785f150903bcad74e278b59e534f83634d))
351
+ * **install:** source global deploy from .agent-src/ subdirectories ([f151caf](https://github.com/event4u-app/agent-config/commit/f151caf854d9ce8519bc244d441a7c8bf73e7fde))
352
+
353
+ Tests: 3521 (+0 since 2.4.0)
354
+
346
355
  ## [2.4.0](https://github.com/event4u-app/agent-config/compare/2.3.0...2.4.0) (2026-05-13)
347
356
 
348
357
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -422,6 +422,7 @@ def _uninstall_project(opts: argparse.Namespace) -> int:
422
422
 
423
423
  def _uninstall_global(opts: argparse.Namespace) -> int:
424
424
  lock_path = installed_lock.lockfile_path()
425
+ write_path = installed_lock.lockfile_write_path()
425
426
  lock = installed_lock.read_lockfile(lock_path)
426
427
  if lock is None and not opts.force:
427
428
  print(f"❌ no global lockfile at {lock_path}", file=sys.stderr)
@@ -442,15 +443,24 @@ def _uninstall_global(opts: argparse.Namespace) -> int:
442
443
  removed_names.append(tool)
443
444
  if lock is not None and not opts.dry_run:
444
445
  remaining = [t for t in lock.get("tools", []) if t not in tools]
446
+ version = lock.get("agent_config_version", "")
445
447
  if remaining:
446
- installed_lock.write_lockfile(remaining, version=lock.get("agent_config_version", ""))
448
+ installed_lock.write_lockfile(version, remaining, path=write_path)
449
+ # Drop the legacy file if it differs from the canonical write
450
+ # target so the namespace migration completes on uninstall.
451
+ if lock_path != write_path:
452
+ try:
453
+ lock_path.unlink()
454
+ except OSError:
455
+ pass
447
456
  print(f"✅ lockfile updated ({len(tools)} entries removed, {len(remaining)} kept)")
448
457
  else:
449
- try:
450
- lock_path.unlink()
451
- print(f"✅ lockfile deleted ({lock_path})")
452
- except OSError as exc:
453
- print(f"⚠️ could not delete lockfile: {exc}")
458
+ for target in {lock_path, write_path}:
459
+ try:
460
+ target.unlink()
461
+ except OSError:
462
+ pass
463
+ print(f"✅ lockfile deleted ({write_path})")
454
464
  return 0
455
465
 
456
466
 
@@ -243,17 +243,18 @@ def _refresh_global_lockfile(version: str, *, out=sys.stdout) -> None:
243
243
  when ``update`` flips the pin. Atomic write goes through
244
244
  ``installed_lock.write_lockfile``.
245
245
  """
246
- lock_path = installed_lock.lockfile_path()
247
- existing = installed_lock.read_lockfile(path=lock_path)
246
+ read_path = installed_lock.lockfile_path()
247
+ write_path = installed_lock.lockfile_write_path()
248
+ existing = installed_lock.read_lockfile(path=read_path)
248
249
  if existing is None:
249
250
  return
250
251
  recorded = existing.get("agent_config_version")
251
252
  tools = list(existing.get("tools", []))
252
- if recorded == version:
253
- print(f"ℹ️ {lock_path} already records {version}.", file=out)
253
+ if recorded == version and read_path == write_path:
254
+ print(f"ℹ️ {write_path} already records {version}.", file=out)
254
255
  return
255
- installed_lock.write_lockfile(version, tools, path=lock_path)
256
- print(f"✅ Refreshed global lockfile at {lock_path}.", file=out)
256
+ installed_lock.write_lockfile(version, tools, path=write_path)
257
+ print(f"✅ Refreshed global lockfile at {write_path}.", file=out)
257
258
 
258
259
 
259
260
  def _detect_installed_version() -> str:
@@ -2,7 +2,7 @@
2
2
 
3
3
  Claude Desktop has no filesystem convention for skills; the Customize →
4
4
  Skills UI accepts a ZIP per skill via the Upload button. This module
5
- walks ``<package_root>/.claude/skills/*`` and produces one
5
+ walks ``<package_root>/.agent-src/skills/*`` and produces one
6
6
  ``<skill-name>.zip`` per directory into ``dest_dir``.
7
7
 
8
8
  Contract:
@@ -51,8 +51,8 @@ def _walk_skill_files(skill_dir: Path) -> list[tuple[Path, tuple[str, ...]]]:
51
51
  """Return ``[(abs_path, rel_parts), ...]`` for every file in the skill.
52
52
 
53
53
  Symlinks are followed (``os.walk(..., followlinks=True)``) so a
54
- bundle from a symlinked entry under ``.claude/skills/`` contains the
55
- actual target content, not a dangling symlink.
54
+ bundle from a symlinked entry under ``.agent-src/skills/`` contains
55
+ the actual target content, not a dangling symlink.
56
56
  """
57
57
  out: list[tuple[Path, tuple[str, ...]]] = []
58
58
  resolved = skill_dir.resolve()
@@ -121,7 +121,7 @@ def build_skill_bundles(
121
121
  ``curation`` optionally restricts the build to the given skill
122
122
  names; ``None`` bundles every skill folder containing ``SKILL.md``.
123
123
  """
124
- skills_root = package_root / ".claude" / "skills"
124
+ skills_root = package_root / ".agent-src" / "skills"
125
125
  if not skills_root.is_dir():
126
126
  return []
127
127
  dest_dir.mkdir(parents=True, exist_ok=True)
@@ -50,7 +50,7 @@ _TOOL_RE = re.compile(r"^\s*-\s*([A-Za-z0-9_\-.]+)\s*$")
50
50
 
51
51
 
52
52
  def lockfile_path(env: Optional[dict] = None) -> Path:
53
- """Return the active lockfile path, honoring the env override.
53
+ """Return the active lockfile path for **reads**, honoring overrides.
54
54
 
55
55
  Resolution order:
56
56
 
@@ -59,9 +59,10 @@ def lockfile_path(env: Optional[dict] = None) -> Path:
59
59
  3. ``~/.config/agent-config/installed.lock`` (legacy fallback, read-only).
60
60
  4. Canonical write target under the new namespace (Step 2 fallthrough).
61
61
 
62
- Writers always end up at (4) when no lockfile exists yet; readers
63
- benefit from (3) so pre-2.4 installs keep working while the
64
- migration shim has not yet run.
62
+ Readers benefit from (3) so pre-2.4 installs keep working while the
63
+ migration shim has not yet run. Writers must use
64
+ :func:`lockfile_write_path` so a stale legacy file does not anchor
65
+ subsequent writes to the deprecated location.
65
66
  """
66
67
  env = env if env is not None else os.environ
67
68
  override = env.get(LOCKFILE_ENV)
@@ -73,6 +74,24 @@ def lockfile_path(env: Optional[dict] = None) -> Path:
73
74
  return user_global_paths.write_target("installed.lock", env=env)
74
75
 
75
76
 
77
+ def lockfile_write_path(env: Optional[dict] = None) -> Path:
78
+ """Return the canonical write target for the lockfile.
79
+
80
+ Unlike :func:`lockfile_path`, this never falls back to the legacy
81
+ ``~/.config/agent-config/`` location. Honors the
82
+ ``$AGENT_CONFIG_INSTALLED_LOCK`` override for tests, otherwise pins
83
+ to ``~/.event4u/agent-config/installed.lock``. Callers in
84
+ ``init``, ``update``, and ``uninstall`` use this so writes always
85
+ land in the new namespace regardless of whether a stale legacy
86
+ lockfile is still present.
87
+ """
88
+ env = env if env is not None else os.environ
89
+ override = env.get(LOCKFILE_ENV)
90
+ if override:
91
+ return Path(override).expanduser()
92
+ return user_global_paths.write_target("installed.lock", env=env)
93
+
94
+
76
95
  def read_lockfile(path: Optional[Path] = None) -> Optional[dict]:
77
96
  """Parse ``path`` (or the default) into a dict; return ``None`` if absent.
78
97
 
@@ -2092,36 +2092,41 @@ PROJECT_BRIDGE_MARKERS = {
2092
2092
  # ``_write_claude_desktop_marker`` rather than via this map.
2093
2093
  #
2094
2094
  # Tools that follow the markdown-skills convention (anchors lifted from
2095
- # nextlevelbuilder/ui-ux-pro-max-skill) deploy ``.claude/skills``
2096
- # the universal Anthropic-shaped skill bundle — into ``<anchor>/skills/``
2097
- # (or ``<anchor>/steering/`` for kiro). ``.claude/rules`` is also copied
2095
+ # nextlevelbuilder/ui-ux-pro-max-skill) deploy the universal Anthropic-
2096
+ # shaped skill bundle — sourced from ``.agent-src/`` (the npm-shipped
2097
+ # canonical asset tree) into ``<anchor>/skills/`` (or
2098
+ # ``<anchor>/steering/`` for kiro). ``.agent-src/rules`` is also copied
2098
2099
  # where the destination is a true rules-aware tool root.
2100
+ #
2101
+ # All source paths reference ``.agent-src/<subdir>`` because that is the
2102
+ # only asset tree included in the npm tarball (see ``package.json#files``).
2103
+ # The legacy ``.augment/``, ``.claude/``, ``.cursor/`` projections only
2104
+ # exist in the development checkout — they are not shipped.
2099
2105
  _CLAUDE_SKILL_BUNDLE: list[tuple[str, str]] = [
2100
- (".claude/rules", "rules"),
2101
- (".claude/skills", "skills"),
2102
- (".claude/personas", "personas"),
2106
+ (".agent-src/rules", "rules"),
2107
+ (".agent-src/skills", "skills"),
2108
+ (".agent-src/personas", "personas"),
2103
2109
  ]
2104
2110
  GLOBAL_DEPLOY_SOURCES: dict[str, list[tuple[str, str]]] = {
2105
2111
  "claude-code": _CLAUDE_SKILL_BUNDLE,
2106
2112
  "augment": [
2107
- (".augment/rules", "rules"),
2108
- (".augment/skills", "skills"),
2109
- (".augment/commands", "commands"),
2110
- (".augment/contexts", "contexts"),
2111
- (".augment/personas", "personas"),
2112
- (".augment/templates", "templates"),
2113
+ (".agent-src/rules", "rules"),
2114
+ (".agent-src/skills", "skills"),
2115
+ (".agent-src/commands", "commands"),
2116
+ (".agent-src/contexts", "contexts"),
2117
+ (".agent-src/personas", "personas"),
2118
+ (".agent-src/templates", "templates"),
2113
2119
  ],
2114
2120
  "cursor": [
2115
- (".cursor/rules", "rules"),
2116
- (".cursor/commands", "commands"),
2117
- (".cursor/personas", "personas"),
2121
+ (".agent-src/rules", "rules"),
2122
+ (".agent-src/commands", "commands"),
2123
+ (".agent-src/personas", "personas"),
2118
2124
  ],
2119
2125
  "windsurf": [
2120
- (".windsurf/rules", "rules"),
2121
- (".windsurf/workflows", "workflows"),
2126
+ (".agent-src/rules", "rules"),
2122
2127
  ],
2123
2128
  "cline": [
2124
- (".clinerules", ""),
2129
+ (".agent-src/rules", ""),
2125
2130
  ],
2126
2131
  # Markdown-skills tools — mirror the universal skill bundle into the
2127
2132
  # tool-specific anchor. Subpath matches the reference repo's
@@ -2142,9 +2147,9 @@ GLOBAL_DEPLOY_SOURCES: dict[str, list[tuple[str, str]]] = {
2142
2147
  # Kiro reads from `steering/` not `skills/` (per
2143
2148
  # platforms/kiro.json#folderStructure.skillPath).
2144
2149
  "kiro": [
2145
- (".claude/rules", "rules"),
2146
- (".claude/skills", "steering"),
2147
- (".claude/personas", "personas"),
2150
+ (".agent-src/rules", "rules"),
2151
+ (".agent-src/skills", "steering"),
2152
+ (".agent-src/personas", "personas"),
2148
2153
  ],
2149
2154
  }
2150
2155
 
@@ -2988,14 +2993,15 @@ def install_global(
2988
2993
 
2989
2994
  lock_mod = _load_installed_lock_module()
2990
2995
  installed_version = lock_mod.current_package_version()
2991
- lock_path = lock_mod.lockfile_path()
2992
- ok, recorded = lock_mod.check_version(installed_version, path=lock_path)
2996
+ read_path = lock_mod.lockfile_path()
2997
+ write_path = lock_mod.lockfile_write_path()
2998
+ ok, recorded = lock_mod.check_version(installed_version, path=read_path)
2993
2999
 
2994
3000
  if not ok and not force:
2995
3001
  if not QUIET:
2996
3002
  print()
2997
3003
  warn("Refusing global install: lockfile version mismatch.")
2998
- info(f" Lockfile: {lock_path}")
3004
+ info(f" Lockfile: {read_path}")
2999
3005
  info(f" Recorded version: {recorded}")
3000
3006
  info(f" Current package: {installed_version}")
3001
3007
  info(" Fix: run `agent-config update`")
@@ -3013,10 +3019,10 @@ def install_global(
3013
3019
  continue
3014
3020
  print(f" {tool_id:<15} → {anchor}")
3015
3021
 
3016
- existing = lock_mod.read_lockfile(path=lock_path) or {}
3022
+ existing = lock_mod.read_lockfile(path=read_path) or {}
3017
3023
  existing_tools = list(existing.get("tools", []))
3018
3024
  merged_tools = sorted(set(existing_tools) | set(tools))
3019
- written = lock_mod.write_lockfile(installed_version, merged_tools, path=lock_path)
3025
+ written = lock_mod.write_lockfile(installed_version, merged_tools, path=write_path)
3020
3026
 
3021
3027
  if not QUIET:
3022
3028
  print()