@event4u/agent-config 4.9.0 → 5.0.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 (65) hide show
  1. package/.agent-src/commands/implement-ticket.md +5 -4
  2. package/.agent-src/rules/language-and-tone.md +4 -10
  3. package/.agent-src/skills/command-routing/SKILL.md +5 -4
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/CHANGELOG.md +73 -0
  6. package/CONTRIBUTING.md +19 -0
  7. package/README.md +11 -0
  8. package/dist/cli/registry.js +0 -2
  9. package/dist/cli/registry.js.map +1 -1
  10. package/dist/discovery/deprecation-report.md +1 -1
  11. package/dist/discovery/discovery-manifest.json +5 -5
  12. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  13. package/dist/discovery/discovery-manifest.summary.md +1 -1
  14. package/dist/discovery/orphan-report.md +1 -1
  15. package/dist/discovery/packs.json +2 -2
  16. package/dist/discovery/trust-report.md +1 -1
  17. package/dist/discovery/workspaces.json +2 -2
  18. package/dist/mcp/registry-manifest.json +2 -2
  19. package/dist/router.json +1 -1671
  20. package/docs/benchmark.md +20 -8
  21. package/docs/benchmarks.md +11 -0
  22. package/docs/contracts/benchmark-corpus-spec.md +31 -3
  23. package/docs/contracts/command-surface-tiers.md +1 -1
  24. package/docs/contracts/hook-architecture-v1.md +33 -0
  25. package/docs/contracts/migrate-command.md +197 -0
  26. package/docs/contracts/settings-api.md +2 -1
  27. package/docs/contracts/value-dashboard-spec.md +374 -0
  28. package/docs/contracts/value-report-schema.md +150 -0
  29. package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
  30. package/docs/decisions/INDEX.md +1 -0
  31. package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
  32. package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
  33. package/docs/migration/v1-to-v2.md +40 -27
  34. package/docs/value.md +84 -0
  35. package/package.json +8 -8
  36. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  37. package/scripts/_cli/cmd_migrate.py +264 -102
  38. package/scripts/_cli/cmd_settings_migrate.py +2 -1
  39. package/scripts/_dispatch.bash +147 -49
  40. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  41. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  42. package/scripts/_lib/install_regenerator.py +129 -0
  43. package/scripts/_lib/value_ladder.py +599 -0
  44. package/scripts/_lib/value_report.py +441 -0
  45. package/scripts/bench_rtk_savings.py +320 -0
  46. package/scripts/compile_router.py +19 -5
  47. package/scripts/expected_perms.json +1 -1
  48. package/scripts/first_run_gate_hook.py +178 -0
  49. package/scripts/hook_manifest.yaml +16 -7
  50. package/scripts/hooks/dispatch_hook.py +27 -0
  51. package/scripts/hooks/dispatch_issues.py +136 -0
  52. package/scripts/hooks_doctor.py +40 -1
  53. package/scripts/install.py +25 -21
  54. package/scripts/lint_agents_layout.py +5 -4
  55. package/scripts/lint_bench_corpus.py +86 -4
  56. package/scripts/lint_global_paths.py +4 -3
  57. package/scripts/lint_marketplace_install_completeness.py +188 -0
  58. package/scripts/lint_value_dashboard.py +218 -0
  59. package/scripts/render_benchmark_md.py +6 -2
  60. package/scripts/render_value_md.py +355 -0
  61. package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
  62. package/scripts/roadmap_progress_hook.py +23 -0
  63. package/scripts/router_telemetry.py +470 -0
  64. package/scripts/validate_frontmatter.py +23 -9
  65. package/scripts/_cli/cmd_migrate_to_global.py +0 -415
@@ -101,8 +101,12 @@ Tier 1 — power-user (release shape, audit, migration):
101
101
  Usage: explain config | explain rule <name>
102
102
  | explain route "<text>"
103
103
  Flags: --json | --project=<path>
104
- migrate One-shot migration off legacy composer / npm install paths
105
- Flags: --dry-run (detect only)
104
+ migrate One-shot, opinionated migration off every legacy install /
105
+ state shape removes composer / npm package entries,
106
+ deletes legacy symlinks + project-local config, migrates
107
+ the v0 work-engine state file, refreshes .gitignore.
108
+ Wizard recreates fresh config. Single flag: --dry-run
109
+ (preview only). Contract: docs/contracts/migrate-command.md
106
110
  first-run Guided first-run setup — cost profile, settings, tooling
107
111
  keys:install-anthropic Install the Anthropic API key for the AI Council
108
112
  (interactive, /dev/tty only, writes ~/.config/agent-config/anthropic.key 0600)
@@ -138,10 +142,6 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
138
142
  ~/.event4u/agent-config/ (the global-only consumer surface,
139
143
  ADR-020). Idempotent; --force overwrites a non-empty global
140
144
  file, --dry-run lists intended copies with zero writes.
141
- migrate-to-global One-shot legacy → global-only migration (Phase 5,
142
- road-to-global-only-install.md). Copy → verify → move →
143
- bridge. Runs lint_global_paths.py first. Flags:
144
- --dry-run, --force, --rollback, --skip-perms-gate.
145
145
  hooks:install Install the combined pre-commit hook (roadmap-progress
146
146
  + ADR-013 artefact frontmatter lint).
147
147
  (use --print to dump it, --force to overwrite an existing hook)
@@ -156,8 +156,6 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
156
156
  Usage: hooks:replay --platform <name> --event <event>
157
157
  --payload <path|event-name> [--native-event <native>]
158
158
  [--manifest <path>] [--json] [--dry-run]
159
- migrate-state Migrate a legacy .implement-ticket-state.json file
160
- to the v1 .work-state.json schema (preserves .bak)
161
159
  memory:lookup Retrieve memory entries (text or JSON envelope)
162
160
  memory:signal Append a provisional intake signal (memory proposal)
163
161
  memory:hash Hash a memory entry (YAML or JSON stdin)
@@ -187,7 +185,7 @@ EOF
187
185
  if [[ "$tier" == "0" ]]; then
188
186
  cat <<'EOF'
189
187
 
190
- (Hidden: 15 Tier-1 + 26 Tier-2 commands. Run `./agent-config --help --tier=1`
188
+ (Hidden: 15 Tier-1 + 24 Tier-2 commands. Run `./agent-config --help --tier=1`
191
189
  or `--tier=all` to see them. Tier criteria: docs/contracts/command-surface-tiers.md.)
192
190
  EOF
193
191
  fi
@@ -245,7 +243,6 @@ Examples (Tier 2):
245
243
  ./agent-config settings:check
246
244
  ./agent-config hooks:install
247
245
  ./agent-config hooks:replay --platform augment --event post_tool_use --payload post_tool_use --json
248
- ./agent-config migrate-state
249
246
  ./agent-config memory:lookup --types domain-invariants --key billing
250
247
  ./agent-config memory:signal --type architecture-decision --path src/Foo.php --body "…"
251
248
  ./agent-config memory:check --path agents/memory
@@ -407,21 +404,6 @@ cmd_work() {
407
404
  exec env PYTHONPATH="$engine_root" python3 -m work_engine "$@"
408
405
  }
409
406
 
410
- cmd_migrate_state() {
411
- require_python3
412
- local engine_root="$PACKAGE_ROOT/.agent-src/templates/scripts"
413
- if [[ ! -d "$engine_root/work_engine/migration" ]]; then
414
- echo "❌ agent-config: work_engine.migration module not found at $engine_root/work_engine/migration" >&2
415
- echo " Reinstall the package and retry." >&2
416
- return 1
417
- fi
418
- # -W ignore::RuntimeWarning suppresses the known sys.modules notice from
419
- # `python3 -m pkg.subpkg.module` when the parent package eagerly imports
420
- # the submodule via its CLI module. The migration is non-invasive and
421
- # the warning is cosmetic; suppressing here avoids touching the engine.
422
- exec env PYTHONPATH="$engine_root" python3 -W ignore::RuntimeWarning -m work_engine.migration.v0_to_v1 "$@"
423
- }
424
-
425
407
  cmd_memory_lookup() {
426
408
  require_python3
427
409
  local script
@@ -560,26 +542,43 @@ cmd_chat_history_checkpoint() {
560
542
  cmd_hooks_install() {
561
543
  local force=false
562
544
  local print_only=false
545
+ local claude_mode=false
546
+ local regen_mode=false
563
547
  for arg in "$@"; do
564
548
  case "$arg" in
565
549
  --force) force=true ;;
566
550
  --print) print_only=true ;;
551
+ --claude|--lifecycle) claude_mode=true ;;
552
+ --regen) regen_mode=true ;;
567
553
  -h|--help)
568
554
  cat <<'HELP'
569
- agent-config hooks:install — install the combined pre-commit hook
570
- (roadmap-progress + ADR-013 artefact frontmatter lint).
555
+ agent-config hooks:install — install hooks scaffolding.
571
556
 
572
- Usage:
573
- ./agent-config hooks:install [--force] [--print]
557
+ Three modes, picked by flag combination:
558
+
559
+ (no flag) Install the legacy .git/hooks/pre-commit gate
560
+ (roadmap-progress + ADR-013 frontmatter lint).
561
+ Default for backwards compatibility.
574
562
 
575
- Without flags: copies the hook to .git/hooks/pre-commit. Refuses to
576
- overwrite an existing pre-commit hook unless --force is given (the
577
- existing hook may already chain other tooling). Each concern only
578
- runs when relevant files are staged — zero overhead otherwise.
563
+ --claude Wire Claude Code lifecycle hooks: write
564
+ (--lifecycle alias) .claude/settings.json with the plugin enabled +
565
+ ensure the ./agent-config symlink scripts/agent-config
566
+ at the consumer root. Idempotent.
567
+
568
+ --regen Provision the roadmap-progress regenerator at
569
+ .augment/scripts/update_roadmap_progress.py
570
+ (canonical path per docs/contracts/hook-architecture-v1.md
571
+ § Regenerator location). Idempotent.
572
+
573
+ --claude --regen Both. The minimal-viable-scaffolding fix path
574
+ for marketplace-install consumers — see
575
+ road-to-hooks-actually-fire-in-consumers Phase 4.
576
+
577
+ Other flags:
578
+ --print Dump the legacy pre-commit hook script to stdout
579
+ (for manual chaining into husky / lefthook / etc.)
580
+ --force Overwrite an existing .git/hooks/pre-commit (DESTRUCTIVE)
579
581
 
580
- --print dump the hook script to stdout (for manual chaining into an
581
- existing pre-commit script, husky, lefthook, etc.)
582
- --force overwrite an existing .git/hooks/pre-commit (DESTRUCTIVE)
583
582
  HELP
584
583
  return 0 ;;
585
584
  *)
@@ -589,6 +588,30 @@ HELP
589
588
  esac
590
589
  done
591
590
 
591
+ # Phase 4 of road-to-hooks-actually-fire-in-consumers: --claude
592
+ # and --regen are mutually compatible with each other but NOT with
593
+ # the legacy pre-commit mode. Routing:
594
+ if $claude_mode || $regen_mode; then
595
+ local rc=0
596
+ if $claude_mode; then
597
+ _hooks_install_claude_lifecycle || rc=$?
598
+ fi
599
+ if $regen_mode && [[ $rc -eq 0 ]]; then
600
+ _hooks_install_regenerator || rc=$?
601
+ fi
602
+ return $rc
603
+ fi
604
+
605
+ # Default-path guidance for callers who passed no flag at all (Phase 4
606
+ # Step 3 — make the new modes discoverable without breaking the
607
+ # legacy default behaviour).
608
+ if [[ $# -eq 0 ]]; then
609
+ echo "ℹ️ hooks:install: no flag given — installing the legacy" >&2
610
+ echo " .git/hooks/pre-commit gate. Use --claude (or --lifecycle)" >&2
611
+ echo " to wire Claude Code hooks, --regen to install the" >&2
612
+ echo " regenerator. See --help for details." >&2
613
+ fi
614
+
592
615
  local hook_src
593
616
  hook_src="$(resolve_script ".agent-src/templates/hooks/pre-commit-roadmap-progress" ".augment/templates/hooks/pre-commit-roadmap-progress")" || return 1
594
617
 
@@ -630,6 +653,92 @@ HELP
630
653
  echo " To uninstall: rm $target"
631
654
  }
632
655
 
656
+ # Phase 4 of road-to-hooks-actually-fire-in-consumers — `--claude`
657
+ # (and its `--lifecycle` alias) path. Writes the minimal-viable Claude
658
+ # Code lifecycle wiring: enables the plugin in `.claude/settings.json`
659
+ # (idempotent dict-merge) AND ensures `./agent-config` is executable
660
+ # at the consumer root (symlink → scripts/agent-config in the package).
661
+ _hooks_install_claude_lifecycle() {
662
+ local settings_dir="$CONSUMER_ROOT/.claude"
663
+ local settings_file="$settings_dir/settings.json"
664
+ mkdir -p "$settings_dir"
665
+
666
+ # Idempotent merge using python3 — bash JSON edits are unsafe with
667
+ # nested keys. Falls back to a fresh-write when the file is absent.
668
+ python3 - "$settings_file" <<'PY' || return 1
669
+ import json
670
+ import sys
671
+ from pathlib import Path
672
+
673
+ target = Path(sys.argv[1])
674
+ data = {}
675
+ if target.is_file():
676
+ try:
677
+ data = json.loads(target.read_text(encoding="utf-8"))
678
+ except json.JSONDecodeError as exc:
679
+ print(f"❌ hooks:install --claude: existing {target} is not valid JSON ({exc})", file=sys.stderr)
680
+ sys.exit(1)
681
+ if not isinstance(data, dict):
682
+ print(f"❌ hooks:install --claude: existing {target} is not a JSON object", file=sys.stderr)
683
+ sys.exit(1)
684
+ enabled = data.setdefault("enabledPlugins", {})
685
+ if not isinstance(enabled, dict):
686
+ print(f"❌ hooks:install --claude: enabledPlugins in {target} is not an object", file=sys.stderr)
687
+ sys.exit(1)
688
+ enabled["agent-config@event4u-agent-config"] = True
689
+ target.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
690
+ print(f"✅ hooks:install --claude: enabled plugin in {target}")
691
+ PY
692
+
693
+ # Symlink the package's agent-config wrapper into the consumer root.
694
+ # Idempotent — replace stale symlinks; skip if already correct.
695
+ local link="$CONSUMER_ROOT/agent-config"
696
+ local link_target
697
+ # The package script lives at <package_root>/scripts/agent-config.
698
+ # Try to resolve the package root via the dispatcher's own location.
699
+ local package_root
700
+ package_root="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")")"
701
+ link_target="$package_root/scripts/agent-config"
702
+
703
+ if [[ ! -e "$link_target" ]]; then
704
+ echo "⚠️ hooks:install --claude: package script not found at $link_target" >&2
705
+ echo " Skipping symlink; the plugin-enable step succeeded." >&2
706
+ return 0
707
+ fi
708
+
709
+ if [[ -L "$link" ]]; then
710
+ local current
711
+ current="$(readlink "$link" 2>/dev/null || true)"
712
+ if [[ "$current" == "$link_target" ]]; then
713
+ echo "✅ hooks:install --claude: ./agent-config symlink already current"
714
+ return 0
715
+ fi
716
+ rm "$link"
717
+ elif [[ -e "$link" ]]; then
718
+ echo "⚠️ hooks:install --claude: ./agent-config exists but is not a symlink — leaving it alone" >&2
719
+ return 0
720
+ fi
721
+
722
+ if ln -s "$link_target" "$link" 2>/dev/null; then
723
+ echo "✅ hooks:install --claude: ./agent-config symlink → $link_target"
724
+ else
725
+ echo "⚠️ hooks:install --claude: could not create ./agent-config symlink" >&2
726
+ return 1
727
+ fi
728
+ }
729
+
730
+ # Phase 4 of road-to-hooks-actually-fire-in-consumers — `--regen` path.
731
+ # Provisions the canonical regenerator via the shared helper module.
732
+ # We invoke the script by path (not module form) so it works from any
733
+ # CONSUMER_ROOT — module form requires the package's `scripts` dir to
734
+ # be on PYTHONPATH which is not the case when invoked from a consumer.
735
+ _hooks_install_regenerator() {
736
+ local package_root
737
+ package_root="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")")"
738
+ python3 "$package_root/scripts/_lib/install_regenerator.py" "$CONSUMER_ROOT" "$package_root" 2>&1
739
+ return $?
740
+ }
741
+
633
742
  # Wrap the interactive key installers under a stable CLI entry. The shell
634
743
  # scripts themselves enforce /dev/tty, 0600, and atomic write — this is
635
744
  # pure routing so consumers never have to know the package layout.
@@ -731,23 +840,14 @@ cmd_settings_check() {
731
840
  # `agent-config settings:migrate` — lift project-local
732
841
  # .agent-settings.yml / .agent-user.yml into ~/.event4u/agent-config/.
733
842
  # Phase 2.4 of road-to-global-only-install.md. Read-only on the source —
734
- # the destructive move step is owned by `migrate-to-global` (Phase 5).
843
+ # the destructive move step is owned by the unified `agent-config migrate`
844
+ # (see docs/contracts/migrate-command.md).
735
845
  # Exit 0 success / no-op, 1 non-empty global without --force or parse error.
736
846
  cmd_settings_migrate() {
737
847
  require_python3
738
848
  exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_settings_migrate "$@"
739
849
  }
740
850
 
741
- # `agent-config migrate-to-global` — Phase 5.1 + 5.3 + 5.5 of
742
- # road-to-global-only-install.md. Order: copy → verify → move → bridge.
743
- # Runs the lint_global_paths.py permissions gate first (Phase 5.0 / A7).
744
- # Flags: --dry-run (zero writes), --force (overwrite non-empty global),
745
- # --rollback (reverse the latest .legacy-pre-global-only/<stamp>/ snapshot).
746
- cmd_migrate_to_global() {
747
- require_python3
748
- exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_migrate_to_global "$@"
749
- }
750
-
751
851
  # `agent-config uninstall` — remove bridge markers (project) or lockfile
752
852
  # entries (global). Idempotent. Pass `--purge` to also delete deployed
753
853
  # content directories under user-scope anchors (destructive). See
@@ -809,7 +909,6 @@ main() {
809
909
  first-run) cmd_first_run "$@" ;;
810
910
  implement-ticket) cmd_implement_ticket "$@" ;;
811
911
  work) cmd_work "$@" ;;
812
- migrate-state) cmd_migrate_state "$@" ;;
813
912
  memory:lookup) cmd_memory_lookup "$@" ;;
814
913
  memory:signal) cmd_memory_signal "$@" ;;
815
914
  memory:hash) cmd_memory_hash "$@" ;;
@@ -841,7 +940,6 @@ main() {
841
940
  validate) cmd_validate "$@" ;;
842
941
  settings:check) cmd_settings_check "$@" ;;
843
942
  settings:migrate) cmd_settings_migrate "$@" ;;
844
- migrate-to-global) cmd_migrate_to_global "$@" ;;
845
943
  uninstall) cmd_uninstall "$@" ;;
846
944
  prune) cmd_prune "$@" ;;
847
945
  doctor) cmd_doctor "$@" ;;
@@ -0,0 +1,129 @@
1
+ """Provision the roadmap-progress regenerator into a consumer.
2
+
3
+ Phase 3 of `road-to-hooks-actually-fire-in-consumers`.
4
+
5
+ The roadmap-progress hook (`scripts/roadmap_progress_hook.py`)
6
+ searches three locations for `update_roadmap_progress.py`. Only the
7
+ **canonical** location is reliable in marketplace-install consumers:
8
+ `.augment/scripts/update_roadmap_progress.py`. This helper pins the
9
+ contract and copies the script idempotently.
10
+
11
+ Canonical paths:
12
+
13
+ | Side | Path |
14
+ |---|---|
15
+ | Source-of-truth (package) | `packages/core/.agent-src.uncondensed/scripts/update_roadmap_progress.py` |
16
+ | Package self-use (dogfooding) | `.agent-src/scripts/update_roadmap_progress.py` (and `.augment/scripts/update_roadmap_progress.py` after `task sync`) |
17
+ | Consumer install target | `<consumer_root>/.augment/scripts/update_roadmap_progress.py` |
18
+
19
+ Used by:
20
+ - `scripts/install.py`'s init / full-install path.
21
+ - `scripts/_dispatch.bash::cmd_hooks_install --regen`.
22
+
23
+ Contract: idempotent; preserves executable bit; never blocks.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import shutil
28
+ import stat
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+
34
+ REGENERATOR_REL = "scripts/update_roadmap_progress.py"
35
+ """Path of the script relative to the package's source-of-truth tree."""
36
+
37
+ CONSUMER_REGENERATOR_REL = ".augment/scripts/update_roadmap_progress.py"
38
+ """Canonical destination path inside a consumer repo."""
39
+
40
+
41
+ def package_source(package_root: Path) -> Optional[Path]:
42
+ """Resolve the package-side source-of-truth for the regenerator.
43
+
44
+ Searches the package layout in priority order:
45
+ 1. `packages/core/.agent-src.uncondensed/scripts/update_roadmap_progress.py`
46
+ 2. `.agent-src/scripts/update_roadmap_progress.py` (condensed projection)
47
+ 3. `.augment/scripts/update_roadmap_progress.py` (tool projection)
48
+
49
+ Returns the first existing file, or None if none exist (which is
50
+ a misconfigured package and should be a hard error at the call
51
+ site).
52
+ """
53
+ candidates = [
54
+ package_root / "packages" / "core" / ".agent-src.uncondensed" / REGENERATOR_REL,
55
+ package_root / ".agent-src" / REGENERATOR_REL,
56
+ package_root / ".augment" / REGENERATOR_REL,
57
+ ]
58
+ for c in candidates:
59
+ if c.is_file():
60
+ return c
61
+ return None
62
+
63
+
64
+ def consumer_target(consumer_root: Path) -> Path:
65
+ """Canonical destination path inside the consumer repo."""
66
+ return consumer_root / CONSUMER_REGENERATOR_REL
67
+
68
+
69
+ def install_regenerator(package_root: Path, consumer_root: Path) -> tuple[bool, str]:
70
+ """Copy the regenerator into the consumer. Idempotent.
71
+
72
+ Returns (success, message). `success=False` means the call site
73
+ should surface the message; the helper never raises.
74
+ """
75
+ source = package_source(package_root)
76
+ if source is None:
77
+ return (
78
+ False,
79
+ "regenerator source not found in package "
80
+ "(searched packages/core/.agent-src.uncondensed/, "
81
+ ".agent-src/, .augment/)",
82
+ )
83
+ target = consumer_target(consumer_root)
84
+ try:
85
+ target.parent.mkdir(parents=True, exist_ok=True)
86
+ # Idempotent: skip the copy if content is byte-identical.
87
+ if target.exists() and target.read_bytes() == source.read_bytes():
88
+ return (True, f"regenerator already current at {target}")
89
+ shutil.copyfile(source, target)
90
+ # Preserve executable bit so the hook can subprocess-call it.
91
+ mode = target.stat().st_mode
92
+ target.chmod(
93
+ mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
94
+ )
95
+ return (True, f"regenerator installed at {target}")
96
+ except OSError as exc:
97
+ return (False, f"failed to install regenerator: {exc}")
98
+
99
+
100
+ def is_installed(consumer_root: Path) -> bool:
101
+ """Quick boolean — does the canonical regenerator exist + is executable?"""
102
+ target = consumer_target(consumer_root)
103
+ if not target.is_file():
104
+ return False
105
+ import os
106
+ return os.access(target, os.X_OK)
107
+
108
+
109
+ def main() -> int:
110
+ """CLI entry point — `python3 -m scripts._lib.install_regenerator <consumer-root>`."""
111
+ if len(sys.argv) < 2:
112
+ print(
113
+ "usage: install_regenerator.py <consumer_root> [<package_root>]",
114
+ file=sys.stderr,
115
+ )
116
+ return 2
117
+ consumer_root = Path(sys.argv[1]).resolve()
118
+ package_root = (
119
+ Path(sys.argv[2]).resolve()
120
+ if len(sys.argv) > 2
121
+ else Path(__file__).resolve().parent.parent.parent
122
+ )
123
+ ok, msg = install_regenerator(package_root, consumer_root)
124
+ print(msg)
125
+ return 0 if ok else 1
126
+
127
+
128
+ if __name__ == "__main__":
129
+ raise SystemExit(main())