@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.
- package/.claude-plugin/marketplace.json +4 -426
- package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
- package/CHANGELOG.md +25 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/contracts/surface-matrix.md +103 -0
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/config/surface-matrix.yml +213 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/_cli/cmd_converge.ts +399 -0
- package/src/scripts/_cli/cmd_doctor.ts +94 -0
- package/src/scripts/_cli/cmd_upgrade.ts +40 -16
- package/src/scripts/_dispatch.bash +13 -0
- package/src/scripts/condense.ts +67 -36
- package/src/scripts/hook_manifest.yaml +16 -7
- package/src/scripts/lint_marketplace.ts +54 -56
- package/src/scripts/lint_surface_matrix.ts +200 -0
- package/src/scripts/prepack-check.mjs +108 -5
- package/src/scripts/surface_probe_hook.ts +231 -0
|
@@ -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
|
+
"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 };
|