@event4u/agent-config 4.9.0 → 5.1.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.
Files changed (82) hide show
  1. package/.agent-src/commands/implement-ticket.md +5 -4
  2. package/.agent-src/contexts/execution/roadmap-process-loop.md +30 -4
  3. package/.agent-src/rules/language-and-tone.md +4 -10
  4. package/.agent-src/rules/linked-projects-onboarding-gate.md +82 -0
  5. package/.agent-src/rules/roadmap-progress-sync.md +39 -5
  6. package/.agent-src/scripts/update_roadmap_progress.py +63 -7
  7. package/.agent-src/skills/command-routing/SKILL.md +5 -4
  8. package/.agent-src/skills/roadmap-management/SKILL.md +121 -21
  9. package/.agent-src/skills/roadmap-writing/SKILL.md +63 -0
  10. package/.agent-src/templates/agent-settings.md +16 -0
  11. package/.agent-src/templates/roadmaps.md +22 -1
  12. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +20 -3
  13. package/.claude-plugin/marketplace.json +1 -1
  14. package/CHANGELOG.md +106 -0
  15. package/CONTRIBUTING.md +19 -0
  16. package/README.md +12 -1
  17. package/dist/cli/registry.js +0 -2
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/discovery/deprecation-report.md +1 -1
  20. package/dist/discovery/discovery-manifest.json +36 -14
  21. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  22. package/dist/discovery/discovery-manifest.summary.md +3 -3
  23. package/dist/discovery/orphan-report.md +1 -1
  24. package/dist/discovery/packs.json +6 -5
  25. package/dist/discovery/trust-report.md +3 -3
  26. package/dist/discovery/workspaces.json +5 -4
  27. package/dist/mcp/registry-manifest.json +3 -3
  28. package/dist/router.json +1 -1671
  29. package/docs/architecture.md +1 -1
  30. package/docs/benchmark.md +20 -8
  31. package/docs/benchmarks.md +11 -0
  32. package/docs/catalog.md +3 -2
  33. package/docs/contracts/benchmark-corpus-spec.md +31 -3
  34. package/docs/contracts/command-surface-tiers.md +1 -1
  35. package/docs/contracts/hook-architecture-v1.md +33 -0
  36. package/docs/contracts/migrate-command.md +197 -0
  37. package/docs/contracts/settings-api.md +2 -1
  38. package/docs/contracts/value-dashboard-spec.md +374 -0
  39. package/docs/contracts/value-report-schema.md +150 -0
  40. package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
  41. package/docs/decisions/ADR-032-linked-projects-scope.md +118 -0
  42. package/docs/decisions/INDEX.md +2 -0
  43. package/docs/getting-started.md +1 -1
  44. package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
  45. package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
  46. package/docs/guides/cross-repo-linked-projects.md +86 -0
  47. package/docs/migration/v1-to-v2.md +40 -27
  48. package/docs/value.md +84 -0
  49. package/package.json +8 -8
  50. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  51. package/scripts/_cli/cmd_migrate.py +264 -102
  52. package/scripts/_cli/cmd_settings_migrate.py +2 -1
  53. package/scripts/_dispatch.bash +147 -49
  54. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  55. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  56. package/scripts/_lib/agent_settings.py +20 -3
  57. package/scripts/_lib/install_regenerator.py +129 -0
  58. package/scripts/_lib/linked_projects.py +238 -0
  59. package/scripts/_lib/value_ladder.py +599 -0
  60. package/scripts/_lib/value_report.py +441 -0
  61. package/scripts/bench_rtk_savings.py +320 -0
  62. package/scripts/check_no_local_settings_committed.py +51 -0
  63. package/scripts/compile_router.py +19 -5
  64. package/scripts/expected_perms.json +1 -1
  65. package/scripts/first_run_gate_hook.py +178 -0
  66. package/scripts/hook_manifest.yaml +16 -7
  67. package/scripts/hooks/dispatch_hook.py +27 -0
  68. package/scripts/hooks/dispatch_issues.py +136 -0
  69. package/scripts/hooks_doctor.py +40 -1
  70. package/scripts/install.py +25 -21
  71. package/scripts/lint_agents_layout.py +5 -4
  72. package/scripts/lint_bench_corpus.py +86 -4
  73. package/scripts/lint_global_paths.py +4 -3
  74. package/scripts/lint_marketplace_install_completeness.py +188 -0
  75. package/scripts/lint_value_dashboard.py +218 -0
  76. package/scripts/render_benchmark_md.py +6 -2
  77. package/scripts/render_value_md.py +355 -0
  78. package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
  79. package/scripts/roadmap_progress_hook.py +23 -0
  80. package/scripts/router_telemetry.py +470 -0
  81. package/scripts/validate_frontmatter.py +23 -9
  82. package/scripts/_cli/cmd_migrate_to_global.py +0 -415
@@ -1,27 +1,37 @@
1
- """``agent-config migrate`` — one-shot migration off legacy install paths.
2
-
3
- P3.5/P3.6 of road-to-portable-runtime-and-update-check.md. Migrates a
4
- consumer project from the legacy composer / npm install paths onto
5
- the ``npx``-only runtime described in ``docs/architecture.md``.
6
-
7
- Steps performed (idempotent):
8
-
9
- 1. Detect legacy install signals (composer.json entry, package.json
10
- devDependency, in-project symlinks pointing at vendor/ or
11
- node_modules/).
12
- 2. Remove the package entry from composer.json / package.json
13
- in-place, preserving sibling keys + formatting.
14
- 3. Delete agent-config managed symlinks that point inside the legacy
15
- install dirs. User-added links elsewhere are preserved with a
16
- warning.
17
- 4. Write a fresh ``.agent-settings.yml`` (only if missing) with
18
- ``agent_config_version`` pinned to the running version.
19
- 5. Update the consumer's ``.gitignore`` block (legacy paths out, new
20
- project-scope entries in).
21
- 6. Print a summary so the developer can review + commit.
22
-
23
- Re-runs on an already-migrated repo emit ``already migrated`` and
24
- exit 0.
1
+ """``agent-config migrate`` — one-shot, opinionated migration off every
2
+ legacy install / state shape.
3
+
4
+ Contract: ``docs/contracts/migrate-command.md``.
5
+
6
+ Source roadmap: ``agents/roadmaps/road-to-one-migrate-command.md``. The
7
+ unified command collapses the legacy ``migrate``, ``migrate-state``,
8
+ and ``migrate-to-global`` triplet into a single, opinionated entry
9
+ point. The only flag is ``--dry-run`` (preview vs. apply).
10
+
11
+ Apply order (fixed; foundation-first):
12
+
13
+ 1. Strip ``@event4u/agent-config`` from ``package.json``
14
+ (``dependencies`` / ``devDependencies``).
15
+ 2. Strip ``event4u/agent-config`` from ``composer.json``
16
+ (``require`` / ``require-dev``).
17
+ 3. Delete managed symlinks (``.augment``, ``.claude``, ``.cursor``,
18
+ ``.clinerules``, ``.windsurfrules``) whose target points into a
19
+ legacy install dir (``vendor/`` or ``node_modules/``). Preserve
20
+ user-managed symlinks pointing elsewhere with a warning.
21
+ 4. Migrate ``.implement-ticket-state.json`` ``.work-state.json`` if
22
+ a v0 payload is present (the v0 source is renamed ``.bak``).
23
+ 5. Hard-delete legacy project-local config:
24
+ ``.agent-settings.yml``, ``.agent-user.yml``,
25
+ ``settings/.agent-settings.yml``, ``settings/.agent-user.yml``.
26
+ Remove the ``settings/`` directory if it becomes empty.
27
+ 6. Remove the empty ``agent-config/`` shell directory at the project
28
+ root, if present and empty.
29
+ 7. Refresh the ``.gitignore`` agent-config managed block to the
30
+ canonical shape.
31
+
32
+ Re-runs on a fully-migrated repo emit ``already migrated`` and exit 0
33
+ without touching the filesystem. ``--dry-run`` runs the same
34
+ detection and prints what would change without mutating disk.
25
35
  """
26
36
  from __future__ import annotations
27
37
 
@@ -30,7 +40,7 @@ import json
30
40
  import re
31
41
  import sys
32
42
  from pathlib import Path
33
- from typing import Iterable, Optional
43
+ from typing import Optional
34
44
 
35
45
  from scripts._lib.agent_settings import resolve_project_root
36
46
 
@@ -52,8 +62,14 @@ GITIGNORE_NEW_BODY = (
52
62
  "agents/runtime/council/responses/\n"
53
63
  "agents/runtime/council/sessions/\n"
54
64
  )
65
+ LEGACY_SETTINGS_FILES = (".agent-settings.yml", ".agent-user.yml")
66
+ LEGACY_STATE_FILENAME = ".implement-ticket-state.json"
67
+ LEGACY_STATE_V1_FILENAME = ".work-state.json"
68
+ LEGACY_AGENT_CONFIG_SHELL = "agent-config"
55
69
 
56
70
 
71
+ # ---------- detection ----------
72
+
57
73
  def _detect_npm(pkg_json: Path) -> bool:
58
74
  if not pkg_json.is_file():
59
75
  return False
@@ -82,6 +98,69 @@ def _detect_composer(composer_json: Path) -> bool:
82
98
  return False
83
99
 
84
100
 
101
+ def _classify_symlink(link: Path) -> Optional[str]:
102
+ """Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
103
+ if not link.is_symlink():
104
+ return None
105
+ try:
106
+ target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
107
+ except OSError:
108
+ return None
109
+ target_str = str(target)
110
+ if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
111
+ return "legacy"
112
+ return "user"
113
+
114
+
115
+ def _detect_legacy_state(project: Path) -> bool:
116
+ """A v0 state file is present at the project root."""
117
+ return (project / LEGACY_STATE_FILENAME).is_file()
118
+
119
+
120
+ def _detect_legacy_settings(project: Path) -> list[Path]:
121
+ """Return the list of legacy settings files present, in deletion order."""
122
+ found: list[Path] = []
123
+ for name in LEGACY_SETTINGS_FILES:
124
+ flat = project / name
125
+ if flat.is_file():
126
+ found.append(flat)
127
+ typed = project / "settings" / name
128
+ if typed.is_file():
129
+ found.append(typed)
130
+ return found
131
+
132
+
133
+ def _detect_empty_shell(project: Path) -> bool:
134
+ """An empty ``agent-config/`` directory at the project root."""
135
+ shell = project / LEGACY_AGENT_CONFIG_SHELL
136
+ if not shell.is_dir() or shell.is_symlink():
137
+ return False
138
+ try:
139
+ return not any(shell.iterdir())
140
+ except OSError:
141
+ return False
142
+
143
+
144
+ def _detect_already_migrated(project: Path) -> bool:
145
+ """A repo counts as migrated when no legacy signal remains."""
146
+ if _detect_npm(project / "package.json"):
147
+ return False
148
+ if _detect_composer(project / "composer.json"):
149
+ return False
150
+ for name in MANAGED_SYMLINKS:
151
+ if _classify_symlink(project / name) == "legacy":
152
+ return False
153
+ if _detect_legacy_state(project):
154
+ return False
155
+ if _detect_legacy_settings(project):
156
+ return False
157
+ if _detect_empty_shell(project):
158
+ return False
159
+ return True
160
+
161
+
162
+ # ---------- apply primitives ----------
163
+
85
164
  def _strip_npm_entry(pkg_json: Path) -> bool:
86
165
  try:
87
166
  data = json.loads(pkg_json.read_text(encoding="utf-8"))
@@ -118,20 +197,6 @@ def _strip_composer_entry(composer_json: Path) -> bool:
118
197
  return changed
119
198
 
120
199
 
121
- def _classify_symlink(link: Path) -> Optional[str]:
122
- """Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
123
- if not link.is_symlink():
124
- return None
125
- try:
126
- target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
127
- except OSError:
128
- return None
129
- target_str = str(target)
130
- if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
131
- return "legacy"
132
- return "user"
133
-
134
-
135
200
  def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
136
201
  removed: list[str] = []
137
202
  preserved: list[str] = []
@@ -149,20 +214,77 @@ def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
149
214
  return removed, preserved
150
215
 
151
216
 
152
- def _write_settings(project: Path, version: str) -> bool:
153
- settings = project / ".agent-settings.yml"
154
- if settings.exists():
217
+ def _migrate_state_file(project: Path) -> Optional[str]:
218
+ """Migrate ``.implement-ticket-state.json`` if v0; return a summary line or None.
219
+
220
+ Raises on conversion error so the caller can surface a non-zero exit.
221
+ """
222
+ source = project / LEGACY_STATE_FILENAME
223
+ if not source.is_file():
224
+ return None
225
+ target = project / LEGACY_STATE_V1_FILENAME
226
+ if target.exists():
227
+ # Migration already happened; just clean up the v0 source.
228
+ try:
229
+ source.unlink()
230
+ return f"removed stale {LEGACY_STATE_FILENAME} (v1 already present)"
231
+ except OSError:
232
+ return None
233
+ migrator = _load_state_migrator()
234
+ if migrator is None:
235
+ return None
236
+ migrator.migrate_file(source, destination=target, backup=True)
237
+ return f"migrated {LEGACY_STATE_FILENAME} → {LEGACY_STATE_V1_FILENAME}"
238
+
239
+
240
+ def _load_state_migrator():
241
+ """Import the v0→v1 state migrator from the shipped engine."""
242
+ pkg_root = Path(__file__).resolve().parents[2]
243
+ engine_root = pkg_root / ".agent-src" / "templates" / "scripts"
244
+ if not (engine_root / "work_engine" / "migration").is_dir():
245
+ return None
246
+ if str(engine_root) not in sys.path:
247
+ sys.path.insert(0, str(engine_root))
248
+ try:
249
+ from work_engine.migration import v0_to_v1 # noqa: PLC0415
250
+ except ImportError:
251
+ return None
252
+ return v0_to_v1
253
+
254
+
255
+ def _delete_legacy_settings(project: Path) -> list[str]:
256
+ """Hard-delete every legacy settings file under ``project``.
257
+
258
+ Returns the list of relative paths actually removed. Removes the
259
+ ``settings/`` directory itself if it becomes empty after the YAML
260
+ sweep.
261
+ """
262
+ removed: list[str] = []
263
+ for path in _detect_legacy_settings(project):
264
+ try:
265
+ path.unlink()
266
+ removed.append(str(path.relative_to(project)))
267
+ except OSError:
268
+ continue
269
+ settings_dir = project / "settings"
270
+ if settings_dir.is_dir() and not settings_dir.is_symlink():
271
+ try:
272
+ if not any(settings_dir.iterdir()):
273
+ settings_dir.rmdir()
274
+ removed.append("settings/")
275
+ except OSError:
276
+ pass
277
+ return removed
278
+
279
+
280
+ def _remove_empty_shell(project: Path) -> bool:
281
+ shell = project / LEGACY_AGENT_CONFIG_SHELL
282
+ if not _detect_empty_shell(project):
155
283
  return False
156
- body = (
157
- "# .agent-settings.yml — generated by `agent-config migrate`.\n"
158
- "# See docs/customization.md for the full key reference.\n"
159
- f'agent_config_version: "{version}"\n'
160
- )
161
- settings.write_text(body, encoding="utf-8")
162
284
  try:
163
- settings.chmod(0o644)
285
+ shell.rmdir()
164
286
  except OSError:
165
- pass
287
+ return False
166
288
  return True
167
289
 
168
290
 
@@ -195,48 +317,54 @@ def _update_gitignore(project: Path) -> bool:
195
317
  return True
196
318
 
197
319
 
198
- def _detect_already_migrated(project: Path) -> bool:
199
- """A repo counts as migrated when no legacy signal remains."""
200
- if _detect_npm(project / "package.json"):
201
- return False
202
- if _detect_composer(project / "composer.json"):
203
- return False
204
- for name in MANAGED_SYMLINKS:
205
- if _classify_symlink(project / name) == "legacy":
206
- return False
207
- return True
208
-
209
-
210
- def main(
211
- argv: Optional[list[str]] = None,
212
- *,
213
- cwd: Optional[Path] = None,
214
- version: Optional[str] = None,
215
- out=sys.stdout,
216
- err=sys.stderr, # noqa: ARG001 — reserved for future error paths
217
- ) -> int:
218
- parser = argparse.ArgumentParser(
219
- prog="agent-config migrate",
220
- description="One-shot migration off legacy composer / npm install paths.",
221
- )
222
- parser.add_argument("--dry-run", action="store_true",
223
- help="Detect only; do not write any files.")
224
- args = parser.parse_args(argv)
225
-
226
- # Phase 3 honor AGENT_CONFIG_PROJECT_ROOT + anchor walk so
227
- # ``agent-config migrate`` invoked from a subdir still targets the
228
- # real project root. ``cwd`` kwarg is preserved for test injection.
229
- project, _ = resolve_project_root(None, cwd=cwd)
230
- version = version or _detect_installed_version()
231
-
232
- if _detect_already_migrated(project):
233
- print("✅ already migrated nothing to do.", file=out)
234
- return 0
320
+ # ---------- plan + apply ----------
321
+
322
+ def _build_plan(project: Path) -> dict:
323
+ """Return a dict describing every detected legacy signal."""
324
+ return {
325
+ "npm": _detect_npm(project / "package.json"),
326
+ "composer": _detect_composer(project / "composer.json"),
327
+ "symlinks_legacy": [
328
+ name for name in MANAGED_SYMLINKS
329
+ if _classify_symlink(project / name) == "legacy"
330
+ ],
331
+ "symlinks_user": [
332
+ name for name in MANAGED_SYMLINKS
333
+ if _classify_symlink(project / name) == "user"
334
+ ],
335
+ "state_file": (project / LEGACY_STATE_FILENAME).is_file(),
336
+ "settings_files": [
337
+ str(p.relative_to(project)) for p in _detect_legacy_settings(project)
338
+ ],
339
+ "empty_shell": _detect_empty_shell(project),
340
+ }
341
+
342
+
343
+ def _format_dry_run(plan: dict, out) -> None:
344
+ lines: list[str] = []
345
+ if plan["npm"]:
346
+ lines.append(f"would remove {PACKAGE_NAME_NPM} from package.json")
347
+ if plan["composer"]:
348
+ lines.append(f"would remove {PACKAGE_NAME_COMPOSER} from composer.json")
349
+ for name in plan["symlinks_legacy"]:
350
+ lines.append(f"would remove legacy symlink {name}")
351
+ for name in plan["symlinks_user"]:
352
+ lines.append(f"would preserve user-managed {name} (review manually)")
353
+ if plan["state_file"]:
354
+ lines.append(
355
+ f"would migrate {LEGACY_STATE_FILENAME} {LEGACY_STATE_V1_FILENAME}"
356
+ )
357
+ for rel in plan["settings_files"]:
358
+ lines.append(f"would delete legacy config {rel}")
359
+ if plan["empty_shell"]:
360
+ lines.append(f"would remove empty {LEGACY_AGENT_CONFIG_SHELL}/ shell")
361
+ lines.append("would refresh .gitignore agent-config block")
362
+ print("ℹ️ legacy install detected — re-run without --dry-run to migrate:", file=out)
363
+ for line in lines:
364
+ print(f" - {line}", file=out)
235
365
 
236
- if args.dry_run:
237
- print("ℹ️ legacy install detected — re-run without --dry-run to migrate.", file=out)
238
- return 0
239
366
 
367
+ def _apply(project: Path, out) -> int:
240
368
  summary: list[str] = []
241
369
  if _strip_npm_entry(project / "package.json"):
242
370
  summary.append(f"removed {PACKAGE_NAME_NPM} from package.json")
@@ -247,8 +375,17 @@ def main(
247
375
  summary.append(f"removed legacy symlink {name}")
248
376
  for name in preserved_links:
249
377
  summary.append(f"preserved user-managed {name} (review manually)")
250
- if _write_settings(project, version):
251
- summary.append(f".agent-settings.yml written (pinned to {version})")
378
+ try:
379
+ state_summary = _migrate_state_file(project)
380
+ except Exception as exc: # noqa: BLE001 — surface as exit-1.
381
+ print(f"❌ state migration failed: {exc}", file=sys.stderr)
382
+ return 1
383
+ if state_summary:
384
+ summary.append(state_summary)
385
+ for rel in _delete_legacy_settings(project):
386
+ summary.append(f"deleted legacy config {rel}")
387
+ if _remove_empty_shell(project):
388
+ summary.append(f"removed empty {LEGACY_AGENT_CONFIG_SHELL}/ shell")
252
389
  if _update_gitignore(project):
253
390
  summary.append(".gitignore agent-config block refreshed")
254
391
 
@@ -259,16 +396,41 @@ def main(
259
396
  return 0
260
397
 
261
398
 
262
- def _detect_installed_version() -> str:
263
- pkg_json = Path(__file__).resolve().parents[2] / "package.json"
264
- try:
265
- data = json.loads(pkg_json.read_text(encoding="utf-8"))
266
- version = data.get("version")
267
- if isinstance(version, str) and version.strip():
268
- return version.strip()
269
- except (OSError, ValueError, json.JSONDecodeError):
270
- pass
271
- return "0.0.0"
399
+ def main(
400
+ argv: Optional[list[str]] = None,
401
+ *,
402
+ cwd: Optional[Path] = None,
403
+ version: Optional[str] = None, # noqa: ARG001 — accepted for test compat; unused.
404
+ out=sys.stdout,
405
+ err=sys.stderr, # noqa: ARG001 — reserved for future error paths.
406
+ ) -> int:
407
+ parser = argparse.ArgumentParser(
408
+ prog="agent-config migrate",
409
+ description=(
410
+ "One-shot, opinionated migration off legacy install / state shapes. "
411
+ "Removes composer / npm package entries, deletes legacy symlinks + "
412
+ "project-local config, migrates the v0 work-engine state file, and "
413
+ "refreshes the .gitignore block. The wizard recreates fresh config."
414
+ ),
415
+ )
416
+ parser.add_argument(
417
+ "--dry-run", action="store_true",
418
+ help="Detect only; print the plan without writing any files.",
419
+ )
420
+ args = parser.parse_args(argv)
421
+
422
+ project, _ = resolve_project_root(None, cwd=cwd)
423
+
424
+ if _detect_already_migrated(project):
425
+ print("✅ already migrated — nothing to do.", file=out)
426
+ return 0
427
+
428
+ if args.dry_run:
429
+ plan = _build_plan(project)
430
+ _format_dry_run(plan, out=out)
431
+ return 0
432
+
433
+ return _apply(project, out=out)
272
434
 
273
435
 
274
436
  if __name__ == "__main__": # pragma: no cover
@@ -4,7 +4,8 @@ Phase 2.4 of ``agents/roadmaps/road-to-global-only-install.md``. Copies
4
4
  an existing project-local ``.agent-settings.yml`` / ``.agent-user.yml``
5
5
  into ``~/.event4u/agent-config/`` so the global-only consumer surface
6
6
  (ADR-020) can take over. Read-only on the source — the destructive
7
- ``move`` step is owned by the Phase 5 ``migrate-to-global`` subcommand.
7
+ ``move`` step is owned by the unified ``agent-config migrate`` command
8
+ (see ``docs/contracts/migrate-command.md``).
8
9
 
9
10
  Idempotent — refuses to overwrite a non-empty global file without
10
11
  ``--force``. ``--dry-run`` lists intended copies; zero writes; exit 0.