@event4u/agent-config 8.3.0 → 8.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.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +4 -426
  2. package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
  3. package/CHANGELOG.md +25 -0
  4. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  5. package/dist/cli/registry.js +1 -0
  6. package/dist/cli/registry.js.map +1 -1
  7. package/dist/discovery/deprecation-report.md +1 -1
  8. package/dist/discovery/discovery-manifest.json +1 -1
  9. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  10. package/dist/discovery/discovery-manifest.summary.md +1 -1
  11. package/dist/discovery/orphan-report.md +1 -1
  12. package/dist/discovery/packs.json +1 -1
  13. package/dist/discovery/trust-report.md +1 -1
  14. package/dist/discovery/workspaces.json +1 -1
  15. package/dist/mcp/registry-manifest.json +1 -1
  16. package/docs/contracts/surface-matrix.md +103 -0
  17. package/package.json +4 -1
  18. package/src/agent-src/README.md +64 -0
  19. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  20. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  21. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  22. package/src/agent-src/templates/AGENTS.md +34 -0
  23. package/src/agent-src/templates/scripts/README.md +195 -0
  24. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  25. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  26. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  27. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  28. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  29. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  30. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  31. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  32. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  33. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  34. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  35. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  36. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  37. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  38. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  39. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  40. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  41. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  42. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  43. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  44. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  45. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  46. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  47. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  48. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  49. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  50. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  51. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  77. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  78. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  79. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  80. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  81. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  82. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  83. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  84. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  99. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  100. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  101. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  102. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  103. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  104. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  105. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  106. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  107. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  108. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  109. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  110. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  111. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  112. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  113. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  114. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  115. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  116. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  117. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  118. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  119. package/src/config/surface-matrix.yml +213 -0
  120. package/src/install/atomic.ts +111 -0
  121. package/src/install/conflict.ts +226 -0
  122. package/src/install/detect.ts +280 -0
  123. package/src/install/paths.ts +90 -0
  124. package/src/install/plan.ts +205 -0
  125. package/src/install/preflight.ts +210 -0
  126. package/src/install/selectedTools.ts +52 -0
  127. package/src/install/toolDetection.ts +120 -0
  128. package/src/install/txlog.ts +161 -0
  129. package/src/install/types.ts +175 -0
  130. package/src/install/wizard-plan.ts +216 -0
  131. package/src/scripts/_cli/cmd_converge.ts +399 -0
  132. package/src/scripts/_cli/cmd_doctor.ts +94 -0
  133. package/src/scripts/_cli/cmd_upgrade.ts +40 -16
  134. package/src/scripts/_dispatch.bash +13 -0
  135. package/src/scripts/condense.ts +67 -36
  136. package/src/scripts/hook_manifest.yaml +16 -7
  137. package/src/scripts/lint_marketplace.ts +54 -56
  138. package/src/scripts/lint_surface_matrix.ts +200 -0
  139. package/src/scripts/prepack-check.mjs +108 -5
  140. package/src/scripts/surface_probe_hook.ts +231 -0
@@ -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": "8.3.0"
12
+ "version": "8.4.1"
13
13
  },
14
14
  "registries": [
15
15
  {
@@ -0,0 +1,103 @@
1
+ # Surface Matrix — per-tool canonical-surface contract
2
+
3
+ > Machine-checked inventory of which install surface is canonical per tool,
4
+ > which alternates are legitimate, what counts as a duplicate surface, and
5
+ > how a duplicate converges. One data file replaces 23 hand-maintained
6
+ > special cases (council Q3, 2026-07-07 — see
7
+ > `agents/settings/contexts/claude-code-single-surface-decision.md` for the
8
+ > parent decision).
9
+
10
+ ## The file
11
+
12
+ `src/config/surface-matrix.yml` — ships in the npm package, resolved at
13
+ runtime relative to the package root (works from both the tsx dev path and
14
+ the bundled CLI).
15
+
16
+ ```yaml
17
+ schema_version: 1
18
+ tools:
19
+ <tool-id>: # must exist in install.ts::USER_SCOPE_PATHS
20
+ surface: projection | plugin | bundles | export-only
21
+ scope_path: "~/..." # must equal USER_SCOPE_PATHS[tool-id]
22
+ hooks: managed-settings-block | settings-hooks-opt-in | plugin | none
23
+ alternates: # optional — legitimate secondary surfaces
24
+ - id: <slug>
25
+ role: <why it is legitimate>
26
+ detail: <prose>
27
+ duplicate: # optional — the duplicate-surface class
28
+ description: <prose>
29
+ detect:
30
+ all_of: [<path>, …] # ~/ = user scope, ./ = package-relative;
31
+ # ALL present ⇒ duplicate surface exists
32
+ # OR, when suspected but unverified:
33
+ pending_evidence: <prose + pointer to the evidence gate>
34
+ converge: # required when detect is defined
35
+ action: <slug>
36
+ command: "<copy-paste fix>"
37
+ reaps: [<path>, …] # package-tagged locations converge may remove
38
+ notes: <provenance>
39
+ ```
40
+
41
+ ## Semantics
42
+
43
+ - **`surface`** — `projection` (installer writes content into the user
44
+ scope path; that projection is canonical), `plugin` (the tool's own
45
+ plugin channel is canonical — e.g. Copilot, where no user-scope
46
+ projection exists), `bundles` (manual import artifacts, e.g.
47
+ Claude Desktop ZIPs), `export-only` (config/marker export, no content
48
+ tree).
49
+ - **`duplicate.detect.all_of`** — existence checks only, never content
50
+ reads. All paths present ⇒ the duplicate class is live. Tools without a
51
+ `detect` block are never flagged.
52
+ - **`pending_evidence`** — documents a *suspected* class that MUST NOT be
53
+ acted on (doctor ignores it, converge refuses it) until an evidence gate
54
+ defines real detect paths. Current example: Augment
55
+ (`agents/settings/contexts/augment-surface-parity.md`).
56
+ - **`converge`** — the consented cleanup. `converge.reaps` is the
57
+ exhaustive allowlist of what `agent-config converge` may remove for this
58
+ tool; anything outside it is refused (hard floor: never user-authored
59
+ files).
60
+
61
+ ## Consumers
62
+
63
+ | Consumer | What it reads | Behavior |
64
+ |---|---|---|
65
+ | `agent-config doctor` (`surface-state` check) | `duplicate.detect.all_of` | Existence-checks every declared duplicate class; `fail` names the tool + the `converge.command` as copy-paste remedy. The legacy `claude-plugin` check stays as the named first consumer with richer version diagnostics. |
66
+ | `agent-config converge` (+ `upgrade --converge`) | `converge.*` | Performs the per-tool cleanup, only on matrix-declared, package-tagged paths (`converge.reaps`), after consent (`install.auto_converge` or interactive y/N). |
67
+ | `dispatch:hook` `surface-probe` concern | `duplicate.detect.all_of` | Once-per-day SessionStart existence probe; one-line nudge naming the duplicate + the converge command. Fail-open, never blocking. |
68
+ | CI (`task lint-surface-matrix` → `src/scripts/lint_surface_matrix.ts`) | whole file | Set equality with `USER_SCOPE_PATHS`, scope-path drift, enum validity, detect-path resolution, converge-command presence, plugin-id drift guard. |
69
+
70
+ ## Onboarding a new tool
71
+
72
+ 1. Add the tool to `install.ts::USER_SCOPE_PATHS` (and `SCOPE_SUPPORT`,
73
+ deploy sources) as usual.
74
+ 2. Add the matrix entry in the same commit — `lint-surface-matrix` fails
75
+ the build on a missing entry (that is the point: no tool ships without
76
+ a declared canonical surface).
77
+ 3. Default shape for a plain projection with no plugin channel:
78
+ `surface: projection`, `hooks: none`, no `duplicate` block.
79
+ 4. Only add a `duplicate` block when a second install channel actually
80
+ exists for the tool — and only add `detect.all_of` once the class has
81
+ been reproduced on a real setup (evidence gate; until then use
82
+ `pending_evidence`).
83
+
84
+ ## Consent model (converge)
85
+
86
+ - First `agent-config converge` run persists `install.auto_converge: true`
87
+ into the **global** settings file
88
+ (`~/.event4u/agent-config/agent-settings.yml`). The key is read directly
89
+ by the CLI (converge/upgrade); it is intentionally NOT part of the
90
+ `MERGEABLE_KEYS` whitelist — it never merges into project-level agent
91
+ settings and never reaches session context.
92
+ - Plain `upgrade` without the key keeps the print-only prompt (no silent
93
+ mutation of a user-owned surface — council Q2, 2026-07-07).
94
+ - `converge --dry-run` prints the exact actions without touching anything.
95
+
96
+ ## See also
97
+
98
+ - `agents/settings/contexts/claude-code-single-surface-decision.md` —
99
+ parent decision (single surface for Claude Code).
100
+ - `agents/settings/contexts/augment-surface-parity.md` — the Augment
101
+ evidence gate that keeps its duplicate class `pending_evidence`.
102
+ - `docs/contracts/skill-distribution-channels.md` — canonical-channel
103
+ contract the matrix operationalizes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "8.3.0",
3
+ "version": "8.4.1",
4
4
  "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -38,6 +38,9 @@
38
38
  "dist/",
39
39
  "docs/",
40
40
  "src/scripts/",
41
+ "src/install/",
42
+ "src/agent-src/scripts/",
43
+ "src/agent-src/templates/scripts/",
41
44
  "src/templates/",
42
45
  "tests/fixtures/hooks/",
43
46
  "AGENTS.md",
@@ -0,0 +1,64 @@
1
+ # Agent Infrastructure
2
+
3
+ > **⚠️ DO NOT edit files in `dist/agent-src/` or `.augment/` directly.**
4
+ > The source of truth is `.agent-src.uncondensed/`. Edit there, then run `task sync`.
5
+ > See `.agent-src.uncondensed/commands/condense.md` for the full workflow.
6
+
7
+ This directory contains the shared agent infrastructure: skills, commands, rules, guidelines,
8
+ templates, and contexts that power the AI coding assistant across all projects.
9
+
10
+ In the package repo, this content lives in `dist/agent-src/` (shipped). Consumer projects install
11
+ it as `.augment/` (local projection).
12
+
13
+ ## Directory Structure
14
+
15
+ ```
16
+ ├── commands/ # Slash commands — executable workflows (/fix-pr-comments, /agent-handoff, ...)
17
+ ├── contexts/ # Background context documents for the agent
18
+ ├── guidelines/ # Coding guidelines by language (reference material)
19
+ ├── rules/ # Always-active behavior rules (auto-loaded every conversation)
20
+ ├── skills/ # On-demand expertise (matched by topic)
21
+ └── templates/ # File templates for roadmaps, features, skills, etc.
22
+ ```
23
+
24
+ ## How It Works
25
+
26
+ | Layer | Always active? | Purpose |
27
+ |---|---|---|
28
+ | **Rules** | ✅ Yes | Behavior constraints — loaded every conversation |
29
+ | **Skills** | 🔄 On demand | Expertise — matched when the topic is relevant |
30
+ | **Commands** | ⚡ On trigger | Workflows — triggered by `/command-name` |
31
+ | **Guidelines** | 📖 Reference | Coding conventions — read when writing code |
32
+ | **Templates** | 📄 Blueprints | File structure templates for new documents |
33
+ | **Contexts** | 🧠 Background | System-level context about the infrastructure |
34
+
35
+ For full details see [augment-infrastructure.md](contexts/augment-infrastructure.md).
36
+
37
+ ## Quick Reference
38
+
39
+ ### Most-used commands
40
+
41
+ | Command | Purpose |
42
+ |---|---|
43
+ | `/agent-handoff` | Generate context summary for a fresh chat |
44
+ | `/fix-pr-comments` | Fix all open PR review comments |
45
+ | `/quality-fix` | Run PHPStan + Rector and fix errors |
46
+ | `/create-pr` | Create a GitHub PR with description |
47
+ | `/bug-investigate` | Investigate a bug from Jira/Sentry |
48
+ | `/tests-create` | Write tests for current changes |
49
+
50
+ ### Key rules
51
+
52
+ | Rule | What it enforces |
53
+ |---|---|
54
+ | `verify-before-complete` | Must run tests/PHPStan before claiming "done" |
55
+ | `scope-control` | Don't commit/push without permission, no unasked changes |
56
+ | `token-efficiency` | Redirect output, don't narrate, trust ECS/Rector |
57
+ | `downstream-changes` | After every edit, update ALL callers, tests, imports |
58
+ | `php-coding` | Strict types, getters/setters, observers |
59
+
60
+ ## Project-Level Overrides
61
+
62
+ Projects can override shared skills, rules, and commands via `agents/overrides/`.
63
+ See [override-system.md](contexts/override-system.md) for details.
64
+
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Archive completed roadmaps — the PR-gate (council 2026-06-14).
4
+ *
5
+ * TypeScript twin of `src/agent-src/scripts/archive_completed_roadmaps.py`
6
+ * (ADR-200, Python→TypeScript migration). The CLI contract is mirrored EXACTLY
7
+ * — the `--all` / `--base` / `--dry-run` flags, the argparse usage / error text
8
+ * (`-h`/`--help` → exit 0, unknown arg → exit 2), the `git`-shell-out semantics
9
+ * (argv / cwd / returncode / stdout / stderr captured via `spawnSync`, matching
10
+ * `subprocess.run(..., capture_output=True, text=True)`), byte-identical stdout
11
+ * (the `ℹ️` / `✅` summary) and stderr (the `⚠️` warnings), the
12
+ * `urp.collect()` completion criterion, and exit code 0. snake_case kept.
13
+ *
14
+ * A roadmap that has reached `count_open == 0` and `count_deferred == 0` is
15
+ * **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites
16
+ * inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`)
17
+ * across tracked files so links never break, and regenerates the dashboard.
18
+ *
19
+ * It replaces the old **merge-gate** (keep one item open + a manual post-merge
20
+ * archival step that got forgotten — leaving finished roadmaps to rot in `main`)
21
+ * with a deterministic **PR-gate**: `/create-pr` runs this before the PR is
22
+ * created, so the roadmap lands already-archived in the PR and merges clean.
23
+ *
24
+ * Default `--changed-only`: only archive roadmaps that appear in this branch's
25
+ * history since it diverged from `origin/main` (`git log origin/main..HEAD`),
26
+ * so a PR archives exactly the roadmaps it completed — never an unrelated complete
27
+ * roadmap. `--all` archives every complete active roadmap. No agent-set
28
+ * annotation is required — completion is detected from the checkbox counts.
29
+ *
30
+ * Usage:
31
+ * ./scripts-run src/scripts/archive_completed_roadmaps # --changed-only (default)
32
+ * ./scripts-run src/scripts/archive_completed_roadmaps --all
33
+ * ./scripts-run src/scripts/archive_completed_roadmaps --base origin/main --dry-run
34
+ *
35
+ * --- Parity notes (ADR-200) ---
36
+ *
37
+ * - `subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)` →
38
+ * `spawnSync(cmd[0], cmd.slice(1), { cwd, encoding: 'utf-8' })`, wrapped in a
39
+ * `CompletedProcess`-shaped object. A spawn that fails to launch surfaces as
40
+ * `returncode = -1` with the error message on `stderr` (the Python path would
41
+ * raise; no test exercises a missing-git environment).
42
+ * - The Python `sys.path.insert(0, …); import update_roadmap_progress as urp`
43
+ * becomes an eager static import of the `.ts` twin — same `collect()`
44
+ * surface, no observable difference.
45
+ * - `_regen_dashboard` runs `[sys.executable, str(script)]`; the twin spawns
46
+ * the dashboard twin via the same `tsx` runtime so the regen + `git add`
47
+ * side effects match. It only fires on a non-dry-run with archived roadmaps.
48
+ * - JSON is not emitted. `set[str]` (touched paths) → `Set<string>`; iteration
49
+ * order is never rendered, so the unordered semantics match.
50
+ * - `process.exitCode` is set; `process.exit()` is never called. argparse
51
+ * usage errors throw `ArgparseExit(2)`; `-h`/`--help` throws `ArgparseExit(0)`.
52
+ */
53
+
54
+ import { spawnSync } from 'node:child_process';
55
+ import * as fs from 'node:fs';
56
+ import * as path from 'node:path';
57
+ import process from 'node:process';
58
+ import { fileURLToPath, pathToFileURL } from 'node:url';
59
+
60
+ import { collect } from './update_roadmap_progress.js';
61
+
62
+ const _HERE = fileURLToPath(import.meta.url);
63
+
64
+ interface CompletedProcess {
65
+ returncode: number;
66
+ stdout: string;
67
+ stderr: string;
68
+ }
69
+
70
+ class ArgparseExit extends Error {
71
+ code: number;
72
+ constructor(code: number) {
73
+ super(`ArgparseExit(${code})`);
74
+ this.name = 'ArgparseExit';
75
+ this.code = code;
76
+ }
77
+ }
78
+
79
+ /** subprocess.run(cmd, cwd=cwd, capture_output=True, text=True). */
80
+ function _run(cmd: string[], cwd: string): CompletedProcess {
81
+ const res = spawnSync(cmd[0] as string, cmd.slice(1), { cwd, encoding: 'utf-8' });
82
+ if (res.error) {
83
+ return { returncode: -1, stdout: res.stdout ?? '', stderr: String(res.error.message ?? '') };
84
+ }
85
+ return {
86
+ returncode: res.status === null ? -1 : res.status,
87
+ stdout: res.stdout ?? '',
88
+ stderr: res.stderr ?? '',
89
+ };
90
+ }
91
+
92
+ function _repo_root(): string {
93
+ const cp = _run(['git', 'rev-parse', '--show-toplevel'], process.cwd());
94
+ return cp.returncode === 0 ? cp.stdout.trim() : process.cwd();
95
+ }
96
+
97
+ /** `str.splitlines()` over LF/CRLF/CR (subprocess text output). */
98
+ function _splitlines(s: string): string[] {
99
+ if (s === '') return [];
100
+ const parts = s.split(/\r\n|\r|\n/);
101
+ if (parts.length && parts[parts.length - 1] === '') {
102
+ parts.pop();
103
+ }
104
+ return parts;
105
+ }
106
+
107
+ /**
108
+ * Repo-relative paths touched in any commit since divergence from base.
109
+ *
110
+ * Returns null when the base ref is unavailable — callers treat null as
111
+ * "cannot scope, fall back to --all".
112
+ */
113
+ function _branch_touched_paths(root: string, base: string): Set<string> | null {
114
+ const cp = _run(
115
+ ['git', 'log', `${base}..HEAD`, '--name-only', '--pretty=format:'],
116
+ root,
117
+ );
118
+ if (cp.returncode !== 0) {
119
+ return null;
120
+ }
121
+ const out = new Set<string>();
122
+ for (const line of _splitlines(cp.stdout)) {
123
+ const t = line.trim();
124
+ if (t) {
125
+ out.add(t);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * Rewrite full-path references `old_rel` → `new_rel` in tracked files.
133
+ *
134
+ * Only the exact repo-relative path is rewritten (bare-filename mentions are
135
+ * left alone). The archived file's own path never matches because the search
136
+ * string is the un-archived path.
137
+ */
138
+ function _inbound_ref_rewrite(
139
+ root: string,
140
+ old_rel: string,
141
+ new_rel: string,
142
+ dry_run: boolean,
143
+ tracked = true,
144
+ ): string[] {
145
+ const changed: string[] = [];
146
+ // Tracked repo → `git grep` (fast, index-aware). Untracked fallback (gap A)
147
+ // → walk the filesystem, since `git grep` only sees committed content and
148
+ // would miss every inbound ref in a pre-first-commit consumer.
149
+ let candidates: string[];
150
+ if (tracked) {
151
+ const grep = _run(['git', 'grep', '-l', '--', old_rel], root);
152
+ if (grep.returncode === 1) {
153
+ return changed; // no matches
154
+ }
155
+ candidates =
156
+ grep.returncode === 0
157
+ ? _splitlines(grep.stdout).map((l) => l.trim())
158
+ : _fs_grep_files(root, old_rel); // git grep unusable → fs walk
159
+ } else {
160
+ candidates = _fs_grep_files(root, old_rel);
161
+ }
162
+ for (const line of candidates) {
163
+ const rel = line.trim();
164
+ if (!rel || rel === old_rel) {
165
+ // skip the roadmap file itself
166
+ continue;
167
+ }
168
+ const fp = path.join(root, rel);
169
+ let text: string;
170
+ try {
171
+ text = fs.readFileSync(fp, { encoding: 'utf-8' });
172
+ } catch {
173
+ continue;
174
+ }
175
+ if (!text.includes(old_rel)) {
176
+ continue;
177
+ }
178
+ if (!dry_run) {
179
+ fs.writeFileSync(fp, _replaceAll(text, old_rel, new_rel), { encoding: 'utf-8' });
180
+ }
181
+ changed.push(rel);
182
+ }
183
+ return changed;
184
+ }
185
+
186
+ /** str.replace(old, new) — replace every occurrence (Python str.replace). */
187
+ function _replaceAll(text: string, oldStr: string, newStr: string): string {
188
+ return text.split(oldStr).join(newStr);
189
+ }
190
+
191
+ interface MoveResult {
192
+ /** the move (or dry-run no-op) succeeded */
193
+ ok: boolean;
194
+ /** the move went through `git mv` (tracked); false → plain filesystem
195
+ * rename was used because the repo has no commits / the file is untracked.
196
+ * The caller rewrites inbound refs on the filesystem (not via `git add`)
197
+ * when this is false. */
198
+ tracked: boolean;
199
+ }
200
+
201
+ /**
202
+ * Move `src_rel` → `dst_rel`, preferring `git mv`.
203
+ *
204
+ * Untracked-safe (road-to-roadmap-archival-robustness, gap A): a consumer that
205
+ * closed a roadmap in a pre-first-commit / untracked working tree cannot use
206
+ * `git mv` (it errors). Instead of printing `git mv failed` and leaving the
207
+ * completed roadmap to rot in the active tree, fall back to a plain
208
+ * `fs.renameSync`. The TS-only enhancement post-dates the deleted Python twin
209
+ * (ADR-200), so there is no byte-parity obligation on this failure path.
210
+ */
211
+ function _git_mv(root: string, src_rel: string, dst_rel: string, dry_run: boolean): MoveResult {
212
+ const dst = path.join(root, dst_rel);
213
+ if (dry_run) {
214
+ return { ok: true, tracked: true };
215
+ }
216
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
217
+ const cp = _run(['git', 'mv', src_rel, dst_rel], root);
218
+ if (cp.returncode === 0) {
219
+ return { ok: true, tracked: true };
220
+ }
221
+ // Fallback: untracked file or a repo with no commits — plain rename.
222
+ try {
223
+ fs.renameSync(path.join(root, src_rel), dst);
224
+ return { ok: true, tracked: false };
225
+ } catch {
226
+ return { ok: false, tracked: false };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Repo-relative paths of files containing `needle`, by walking the filesystem.
232
+ *
233
+ * The untracked fallback for `_inbound_ref_rewrite`: `git grep` only sees
234
+ * tracked content, so in a pre-first-commit consumer it misses every inbound
235
+ * reference. Walk the tree (skipping `.git` / `node_modules` and obvious binary
236
+ * trees), read text files, and match the literal path. Bounded by skipping the
237
+ * heavy dirs; an untracked consumer's tree is small.
238
+ */
239
+ function _fs_grep_files(root: string, needle: string): string[] {
240
+ const SKIP = new Set(['.git', 'node_modules', '.augment', 'dist']);
241
+ const hits: string[] = [];
242
+ const walk = (dirAbs: string): void => {
243
+ let entries: fs.Dirent[];
244
+ try {
245
+ entries = fs.readdirSync(dirAbs, { withFileTypes: true });
246
+ } catch {
247
+ return;
248
+ }
249
+ for (const e of entries) {
250
+ if (e.name.startsWith('.git') || SKIP.has(e.name)) {
251
+ continue;
252
+ }
253
+ const abs = path.join(dirAbs, e.name);
254
+ if (e.isDirectory()) {
255
+ walk(abs);
256
+ } else if (e.isFile() && /\.(md|json|ya?ml|ts|js|txt)$/.test(e.name)) {
257
+ let text: string;
258
+ try {
259
+ text = fs.readFileSync(abs, { encoding: 'utf-8' });
260
+ } catch {
261
+ continue;
262
+ }
263
+ if (text.includes(needle)) {
264
+ hits.push(path.relative(root, abs).split(path.sep).join('/'));
265
+ }
266
+ }
267
+ }
268
+ };
269
+ walk(root);
270
+ return hits;
271
+ }
272
+
273
+ interface ArchiveRecord {
274
+ roadmap: string;
275
+ archived_to: string;
276
+ refs_migrated: string[];
277
+ }
278
+
279
+ /** Archive every complete active roadmap (count_open==0, count_deferred==0). */
280
+ function archive_completed(
281
+ root: string,
282
+ opts: { changed_only: boolean; base: string; dry_run: boolean },
283
+ ): ArchiveRecord[] {
284
+ const { changed_only, base, dry_run } = opts;
285
+ const roadmap_root = path.join(root, 'agents', 'roadmaps');
286
+ if (!_isDir(roadmap_root)) {
287
+ return [];
288
+ }
289
+ const touched = changed_only ? _branch_touched_paths(root, base) : null;
290
+ // changed_only requested but base unavailable → conservative: archive nothing.
291
+ if (changed_only && touched === null) {
292
+ process.stderr.write(
293
+ ` ⚠️ cannot resolve \`${base}\` — skipping the changed-only ` +
294
+ 'archival sweep (run with --all to force).\n',
295
+ );
296
+ return [];
297
+ }
298
+
299
+ const archived: ArchiveRecord[] = [];
300
+ for (const stats of collect(roadmap_root)) {
301
+ if (stats.open_ !== 0 || stats.deferred !== 0) {
302
+ continue; // not complete
303
+ }
304
+ const old_rel = `agents/roadmaps/${stats.rel}`;
305
+ if (changed_only && !(touched as Set<string>).has(old_rel)) {
306
+ continue; // complete, but not this branch's work
307
+ }
308
+ const new_rel = `agents/roadmaps/archive/${stats.rel}`;
309
+ const mv = _git_mv(root, old_rel, new_rel, dry_run);
310
+ if (!mv.ok) {
311
+ process.stderr.write(` ⚠️ could not archive ${old_rel} (mv failed)\n`);
312
+ continue;
313
+ }
314
+ const refs = _inbound_ref_rewrite(root, old_rel, new_rel, dry_run, mv.tracked);
315
+ // Stage rewritten refs only in a tracked repo; an untracked consumer's
316
+ // files are not in the index, so the filesystem rewrite is the whole job.
317
+ if (!dry_run && refs.length && mv.tracked) {
318
+ _run(['git', 'add', '--', ...refs], root);
319
+ }
320
+ archived.push({ roadmap: old_rel, archived_to: new_rel, refs_migrated: refs });
321
+ }
322
+ return archived;
323
+ }
324
+
325
+ function _isDir(p: string): boolean {
326
+ try {
327
+ return fs.statSync(p).isDirectory();
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ function _isFile(p: string): boolean {
334
+ try {
335
+ return fs.statSync(p).isFile();
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+
341
+ function _regen_dashboard(root: string, dry_run: boolean): void {
342
+ if (dry_run) {
343
+ return;
344
+ }
345
+ const script = path.join(path.dirname(_HERE), 'update_roadmap_progress.ts');
346
+ // Python: `_run([sys.executable, str(script)], root)`; the twin spawns the
347
+ // dashboard generator twin through the same `tsx` runtime, cwd=root.
348
+ _runTwin(root, script);
349
+ const dash = path.join(root, 'agents', 'roadmaps-progress.md');
350
+ if (_isFile(dash)) {
351
+ _run(['git', 'add', '--', 'agents/roadmaps-progress.md'], root);
352
+ }
353
+ }
354
+
355
+ /** Spawn the dashboard generator twin (tsx) with cwd=root, mirroring `[sys.executable, script]`. */
356
+ function _runTwin(root: string, script: string): void {
357
+ // Python uses `sys.executable` — the interpreter already running this
358
+ // script, which is always present. The TS analog is the `tsx` that owns
359
+ // THIS twin, found by walking up from the script's own dir (matching the
360
+ // canonical `run.ts` resolver), NOT the `git rev-parse` consumer-repo root
361
+ // (which need not carry a node_modules). Fall back to `npx tsx`.
362
+ const binName = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
363
+ let dir = path.dirname(_HERE);
364
+ for (;;) {
365
+ const candidate = path.join(dir, 'node_modules', '.bin', binName);
366
+ if (fs.existsSync(candidate)) {
367
+ spawnSync(candidate, [script], { cwd: root, encoding: 'utf-8' });
368
+ return;
369
+ }
370
+ const parent = path.dirname(dir);
371
+ if (parent === dir) {
372
+ break;
373
+ }
374
+ dir = parent;
375
+ }
376
+ spawnSync('npx', ['tsx', script], { cwd: root, encoding: 'utf-8' });
377
+ }
378
+
379
+ const _PROG = 'archive_completed_roadmaps.py';
380
+
381
+ function _usage(): string {
382
+ return `usage: ${_PROG} [-h] [--all] [--base BASE] [--dry-run]\n`;
383
+ }
384
+
385
+ interface Args {
386
+ all: boolean;
387
+ base: string;
388
+ dry_run: boolean;
389
+ }
390
+
391
+ function _parseArgs(argv: readonly string[]): Args {
392
+ let all = false;
393
+ let base = 'origin/main';
394
+ let dry_run = false;
395
+ const emitError = (msg: string): never => {
396
+ process.stderr.write(_usage());
397
+ process.stderr.write(`${_PROG}: error: ${msg}\n`);
398
+ throw new ArgparseExit(2);
399
+ };
400
+ let i = 0;
401
+ while (i < argv.length) {
402
+ const tok = argv[i] as string;
403
+ if (tok === '-h' || tok === '--help') {
404
+ process.stdout.write(_usage());
405
+ throw new ArgparseExit(0);
406
+ } else if (tok === '--all') {
407
+ all = true;
408
+ i += 1;
409
+ } else if (tok === '--dry-run') {
410
+ dry_run = true;
411
+ i += 1;
412
+ } else if (tok === '--base') {
413
+ const val = argv[i + 1];
414
+ if (val === undefined) {
415
+ emitError('argument --base: expected one argument');
416
+ }
417
+ base = val as string;
418
+ i += 2;
419
+ } else if (tok.startsWith('--base=')) {
420
+ base = tok.slice('--base='.length);
421
+ i += 1;
422
+ } else {
423
+ emitError(`unrecognized arguments: ${tok}`);
424
+ }
425
+ }
426
+ return { all, base, dry_run };
427
+ }
428
+
429
+ function main(argv?: readonly string[]): number {
430
+ const ns = _parseArgs(argv ?? process.argv.slice(2));
431
+
432
+ const root = _repo_root();
433
+ const archived = archive_completed(root, {
434
+ changed_only: !ns.all,
435
+ base: ns.base,
436
+ dry_run: ns.dry_run,
437
+ });
438
+ if (archived.length === 0) {
439
+ process.stdout.write(' ℹ️ No completed roadmaps to archive.\n');
440
+ return 0;
441
+ }
442
+ _regen_dashboard(root, ns.dry_run);
443
+ const verb = ns.dry_run ? 'Would archive' : 'Archived';
444
+ for (const rec of archived) {
445
+ process.stdout.write(
446
+ ` ✅ ${verb}: ${rec.roadmap} → ${rec.archived_to}` +
447
+ (rec.refs_migrated.length
448
+ ? ` (${rec.refs_migrated.length} ref(s) migrated)`
449
+ : '') +
450
+ '\n',
451
+ );
452
+ }
453
+ return 0;
454
+ }
455
+
456
+ function _isCliEntry(): boolean {
457
+ if (process.argv[1] === undefined) {
458
+ return false;
459
+ }
460
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
461
+ if (import.meta.url === argvUrl) {
462
+ return true;
463
+ }
464
+ // A symlinked invocation (e.g. via `.augment/scripts` → `dist/agent-src/scripts`,
465
+ // or macOS /var → /private/var temp dirs) makes the raw URLs differ:
466
+ // import.meta.url is the resolved real path while argv[1] keeps the symlink
467
+ // path. Compare realpaths so the entry guard still fires (without this the
468
+ // archival sweep silently no-ops when run through the symlink).
469
+ try {
470
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
471
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
472
+ return here === argv;
473
+ } catch {
474
+ return false;
475
+ }
476
+ }
477
+ if (_isCliEntry() || process.argv[1] === _HERE) {
478
+ try {
479
+ process.exitCode = main();
480
+ } catch (exc) {
481
+ if (exc instanceof ArgparseExit) {
482
+ process.exitCode = exc.code;
483
+ } else {
484
+ throw exc;
485
+ }
486
+ }
487
+ }
488
+
489
+ export {
490
+ archive_completed,
491
+ main,
492
+ _branch_touched_paths,
493
+ _inbound_ref_rewrite,
494
+ _git_mv,
495
+ _repo_root,
496
+ };
497
+ export type { ArchiveRecord, CompletedProcess };