artshelf 0.13.0 → 0.14.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Added approval-gated `artshelf ledgers prune` registry maintenance: dry-run
6
+ writes or reuses a reviewed plan for missing registered ledger files, `--agent`
7
+ emits the exact registry-prune approval target, execute binds to one registry
8
+ and plan id, writes a rollback copy and receipt, and `doctor`/`status`/`review`
9
+ agent guidance routes stale registrations to this flow instead of manual JSON
10
+ edits.
11
+ - Hardened `cleanup --execute` with durable resumability: a `started` receipt is
12
+ written before the first filesystem move so an interrupted run is detectable,
13
+ terminal receipt evidence preserves an artifact's original
14
+ `executedAt`/`cleanedAt`, an artifact already moved into the plan's trash
15
+ directory without terminal receipt evidence is recorded as `trashed` at resume
16
+ time without moving it again, a missing original path with no trash target and no
17
+ receipt evidence stays a skipped missing path rather than a success, and a
18
+ completed receipt replays idempotently without duplicating the Artshelf-owned
19
+ receipt record (NGX-427).
5
20
  - Renamed the published package and CLI binary from `shelf` to `artshelf`,
6
21
  moved project URLs to `calvinnwq/artshelf`, and prepared public npm publishing.
7
22
  - Added a user-level ledger registry plus `--all` review commands so Artshelf can
@@ -117,6 +132,24 @@
117
132
  - Moved `artshelf put` registry-warning output from stdout to stderr in human
118
133
  mode; `--json` output is unchanged (NGX-429).
119
134
 
135
+ ## [0.14.0](https://github.com/calvinnwq/artshelf/compare/v0.13.1...v0.14.0) (2026-06-19)
136
+
137
+
138
+ ### Features
139
+
140
+ * **commands:** add approval-gated registry pruning ([beaaca8](https://github.com/calvinnwq/artshelf/commit/beaaca84b6d0c62e66f4438ecf9323f482fcdba4))
141
+ * **ledgers:** Implemented the approval-gated `artshelf ledgers prune --dry-run` registry-prune planning slice of NGX-481 — new domain module, command wiring with human/JSON/agent output carrying the exact approval target, help text, and 12 focused tests — with all verification gates passing. ([6d2de1f](https://github.com/calvinnwq/artshelf/commit/6d2de1fe2390785aa9e0ffa2b009e318e350e06e))
142
+ * **ledgers:** Implemented the approval-gated `artshelf ledgers prune --execute --plan-id` slice of NGX-481 — plan-id-bound registry mutation with a pre-mutation rollback copy, post-mutation receipt with verification, and stale/duplicate/mismatch refusals — with 10 new tests and all five verification gates passing. ([11e03db](https://github.com/calvinnwq/artshelf/commit/11e03dbde7c5e4e933f241548bd8f88d316ae5a4))
143
+ * **ledgers:** Wired doctor, status --all, and review --all to point users at the approval-gated `artshelf ledgers prune --dry-run` flow when the registry has stale (missing-file) registrations, completing the last NGX-481 scope bullet with 4 focused tests and all five verification gates passing. ([66cd791](https://github.com/calvinnwq/artshelf/commit/66cd791dae2a6f5b60ab506a4f4e2be8358369d6))
144
+
145
+ ## [0.13.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.13.0...artshelf-v0.13.1) (2026-06-15)
146
+
147
+
148
+ ### Bug Fixes
149
+
150
+ * **cleanup:** make cleanup --execute resumable after interruption ([d0188f7](https://github.com/calvinnwq/artshelf/commit/d0188f73a62b1ff2d173e26c61c826a67bbc9542))
151
+ * **cleanup:** make cleanup execution resumable ([7ec0ebe](https://github.com/calvinnwq/artshelf/commit/7ec0ebe113f589ccd00ed0fdd1a54034afc242ec))
152
+
120
153
  ## [0.13.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.12.0...artshelf-v0.13.0) (2026-06-15)
121
154
 
122
155
 
package/README.md CHANGED
@@ -113,12 +113,14 @@ destructive deletion.
113
113
  - **No fresh-plan-then-execute shortcut** — review the plan, then run that plan.
114
114
  - **Trash before delete** — `cleanup=delete` stays refused; physical deletion
115
115
  needs its own reviewed trash purge. No silent deletion, ever.
116
- - **Durable, concurrency-safe writes** — ledger and registry mutations take a
117
- cross-process lock and commit atomically, so overlapping commands never lose
118
- records or leave a half-written ledger.
116
+ - **Durable, resumable cleanup** — execution writes a started receipt before
117
+ moving files, can replay the same plan id after interruption, and ledger and
118
+ registry mutations take a cross-process lock so overlapping commands never
119
+ lose records or leave a half-written ledger.
119
120
  - **`--json` on every command**, so agents can act on structured output.
120
- - **`--agent` on `review`/`status`/`doctor`**, a compact, token-efficient
121
- decision packet for agents, while the default render stays human-scannable.
121
+ - **`--agent` on `review`/`status`/`doctor` and `ledgers prune --dry-run`**, a
122
+ compact, token-efficient decision packet for agents, while the default render
123
+ stays human-scannable.
122
124
 
123
125
  ## Reference
124
126
 
@@ -129,6 +131,8 @@ destructive deletion.
129
131
  artshelf put <path> --reason "debug parser output" --ttl 3d --kind scratch
130
132
  artshelf ledgers list [--plain] [--json]
131
133
  artshelf ledgers add --ledger <path> [--name <project>] [--scope repo|user|other] [--json]
134
+ artshelf ledgers prune --dry-run [--registry <path>] [--json|--agent]
135
+ artshelf ledgers prune --execute --plan-id <id> [--registry <path>] [--json]
132
136
  artshelf list [--all] [--status active]
133
137
  artshelf find --path <path> --owner <agent-or-runtime> --label <task-or-run-id>
134
138
  artshelf find --all --owner <agent-or-runtime>
@@ -151,8 +155,9 @@ artshelf resolve <id> --status resolved --reason "inspected and no longer needed
151
155
 
152
156
  Use `artshelf help` for a grouped command list, then `artshelf <command> --help`
153
157
  or `artshelf help <command>` for focused details. Nested commands such as
154
- `artshelf trash purge --help` and `artshelf ledgers add --help` show only that
155
- subcommand. All core commands support `--json`; `review`, `status`, and `doctor`
158
+ `artshelf trash purge --help`, `artshelf ledgers add --help`, and
159
+ `artshelf ledgers prune --help` show only that subcommand. All core commands
160
+ support `--json`; `review`, `status`, `doctor`, and `ledgers prune --dry-run`
156
161
  also take `--agent` for a compact decision packet; `--ledger`, `--registry`, and
157
162
  `--all` are scope flags only on commands that list them.
158
163
  </details>
@@ -175,7 +180,11 @@ Artshelf keeps a small global registry of known ledgers at
175
180
  existing one with `artshelf ledgers add --ledger <path> --name <project> --json`.
176
181
  `artshelf ledgers list` validates each registered ledger by default (ok/missing/invalid
177
182
  status with counts, non-zero exit when broken), so it doubles as a stale-entry
178
- check; add `--plain` to skip validation.
183
+ check; add `--plain` to skip validation. When registered ledger files are
184
+ missing, use `artshelf ledgers prune --dry-run --registry <path>` to write a
185
+ reviewed registry-prune plan, approve `approve artshelf ledgers prune registry
186
+ <registry-path> plan <plan-id>`, then execute that exact plan id; duplicate paths
187
+ are blocked for manual repair and are never pruned automatically.
179
188
 
180
189
  Use `--all` for one read-only discovery entry point across registered ledgers
181
190
  (`review`, `status`, `due`, `trash list`, `find`). `artshelf cleanup --dry-run --all`
package/SPEC.md CHANGED
@@ -56,8 +56,8 @@ Rules:
56
56
  - Command groups are `Create`, `Inspect`, `Review`, `Clean`, and `System`.
57
57
  - `artshelf <command> --help` and `artshelf help <command>` show focused help
58
58
  for that command.
59
- - Nested help is supported for `trash list`, `trash purge`, `ledgers list`, and
60
- `ledgers add`.
59
+ - Nested help is supported for `trash list`, `trash purge`, `ledgers list`,
60
+ `ledgers add`, and `ledgers prune`.
61
61
  - `artshelf trash help` and `artshelf ledgers help` are aliases for the focused
62
62
  help of those commands, matching `artshelf help trash` and `artshelf help ledgers`.
63
63
  - Top-level help presents `-h, --help` and `-v, --version` as global options,
@@ -105,13 +105,16 @@ printed to stderr in human mode, or surfaced as a `registryError` field in
105
105
 
106
106
  ### `artshelf ledgers`
107
107
 
108
- Lists or registers known Artshelf ledgers.
108
+ Lists, registers, or prunes known Artshelf ledger registrations.
109
109
 
110
110
  ```bash
111
111
  artshelf ledgers list
112
112
  artshelf ledgers list --json
113
113
  artshelf ledgers list --plain
114
114
  artshelf ledgers add --ledger <path> --name <project> --scope repo --json
115
+ artshelf ledgers prune --dry-run --registry <path> --json
116
+ artshelf ledgers prune --dry-run --registry <path> --agent
117
+ artshelf ledgers prune --execute --plan-id <id> --registry <path> --json
115
118
  ```
116
119
 
117
120
  Rules:
@@ -125,6 +128,19 @@ Rules:
125
128
  them; it does not validate and exits zero whenever the registry itself is
126
129
  readable.
127
130
  - `add` requires an existing ledger path.
131
+ - `prune --dry-run` classifies registry entries whose ledger files are missing,
132
+ writes a reviewed registry-prune plan only when prunable entries exist, and
133
+ never mutates the registry. Repeated matching dry-runs reuse the same
134
+ unexecuted plan id. Duplicate registry paths are ambiguous and are reported as
135
+ blocked for manual repair, never pruned automatically.
136
+ - `prune --dry-run --agent` emits a compact single-line packet with the prunable
137
+ count, blocked count, plan id, and exact approval target:
138
+ `approve artshelf ledgers prune registry <registry-path> plan <plan-id>`.
139
+ - `prune --execute --plan-id <id>` binds to one exact registry path and reviewed
140
+ plan id. It re-checks the live registry, removes only entries still classified
141
+ as prunable, skips stale plan entries whose file reappeared or became
142
+ ambiguous, writes a rollback copy before mutation, writes a receipt after, and
143
+ exits non-zero if verification fails.
128
144
  - `--name` defaults from the ledger path when omitted.
129
145
  - `--scope` is optional; when omitted, Artshelf infers `repo`, `user`, or
130
146
  `other` from the ledger path.
@@ -259,22 +275,27 @@ counts plus the preview plan ids; JSON also includes the next safe action. The
259
275
  per-ledger human detail appends a `reconcile` count when a ledger has reconcile
260
276
  drift. Human output adds a one-line triage count with the same reconcile counts
261
277
  and states the same next safe action (repair broken ledgers, dry-run cleanup,
262
- dry-run reconcile for missing-path or reconcile drift, or nothing to do). Review
263
- never writes a plan, so
264
- the next action always points at an explicit follow-up command.
265
-
266
- `review`, `status`, and `doctor` share three render modes. The default human
267
- render leads each ledger and summary line with a `✓`/`⚠` attention glyph; `--json`
268
- stays the full, backward-compatible public audit report; and `--agent` emits a compact,
278
+ registry-prune dry-run for missing registered ledgers, dry-run reconcile for
279
+ missing-path or reconcile drift, or nothing to do). Review never writes a plan,
280
+ so the next action always points at an explicit follow-up command.
281
+
282
+ `review`, `status`, `doctor`, and `ledgers prune --dry-run` expose
283
+ agent-oriented render modes. For review/status/doctor, the default human render
284
+ leads each ledger and summary line with a `✓`/`⚠` attention glyph. `--json` stays
285
+ the full, backward-compatible public audit report; and `--agent` emits a compact,
269
286
  deterministic single-line JSON decision packet for agents, taking precedence over
270
287
  `--json` when both are passed. For `review`, the packet sorts records into
271
288
  ready-for-approval, needs-review-first, and blocked groups. Because review is
272
- read-only and never mints a cleanup plan, the exact approval targets it emits are
273
- `resolve missing` and `reconcile`; the `reconcile` target appears only when a
274
- prior reviewed reconcile plan still matches the live drift. Cleanup-eligible
275
- records and reconcile drift without a reviewed plan stay needs-review-first and
276
- point at `cleanup --dry-run` or `reconcile --dry-run`, which mint the reviewed
277
- plan id to approve. Blocked or ambiguous reconcile findings surface in the
289
+ read-only and never mints a cleanup or registry-prune plan, the exact approval
290
+ targets it emits are `resolve missing` and `reconcile`; the `reconcile` target
291
+ appears only when a prior reviewed reconcile plan still matches the live drift.
292
+ Cleanup-eligible records and reconcile drift without a reviewed plan stay
293
+ needs-review-first and point at `cleanup --dry-run` or `reconcile --dry-run`,
294
+ which mint the reviewed plan id to approve. Missing registered ledger files in
295
+ `--all` mode surface as blocked registry fixes that point at `ledgers prune
296
+ --dry-run --registry <path>`; the prune dry-run produces the registry-prune
297
+ approval target. Invalid-but-present ledger files still point at manual
298
+ re-register/fix work. Blocked or ambiguous reconcile findings surface in the
278
299
  blocked group with no approval target.
279
300
 
280
301
  ### `artshelf doctor`
@@ -302,10 +323,13 @@ Doctor reports:
302
323
  physical trash purge requires a separate reviewed purge plan.
303
324
 
304
325
  A healthy machine exits 0. A broken registry file or any stale or invalid
305
- registered ledger exits non-zero with actionable errors. Humans should run
306
- `artshelf doctor` after install or when `--all` commands behave unexpectedly; agents
307
- may run it on a schedule to catch stale registry entries before relying on
308
- cleanup planning. Doctor never creates plans, receipts, or records. Like `review`
326
+ registered ledger exits non-zero with actionable errors. When stale/missing
327
+ registrations exist, the agent next action points at `artshelf ledgers prune
328
+ --dry-run --registry <path>` before re-running doctor; invalid ledger files still
329
+ need manual repair. Humans should run `artshelf doctor` after install or when
330
+ `--all` commands behave unexpectedly; agents may run it on a schedule to catch
331
+ stale registry entries before relying on cleanup planning. Doctor never creates
332
+ plans, receipts, or records. Like `review`
309
333
  and `status`, `doctor` accepts `--agent` for a compact single-line JSON decision
310
334
  packet (health, registry and registered-ledger health, blockers, cleanup-safety
311
335
  posture, next action, and a verify command); `--agent` takes precedence over
@@ -337,9 +361,12 @@ Status reports:
337
361
  output is short enough to paste into a chat. Status is strictly read-only: it
338
362
  never creates plans or receipts and never mutates records. A healthy machine
339
363
  exits 0. In `--all` mode, a broken registry or any stale or invalid registered
340
- ledger exits non-zero. Due entries are normal operational state and do not change
341
- the exit code. With single `--ledger`, a not-yet-created ledger reports empty
342
- counts. Like `review` and `doctor`, `status` accepts `--agent` for a compact
364
+ ledger exits non-zero. When stale/missing registrations exist, `--all --agent`
365
+ points at `artshelf ledgers prune --dry-run --registry <path>` before re-running
366
+ status; invalid ledgers are still manual repair. Due entries are normal
367
+ operational state and do not change the exit code. With single `--ledger`, a
368
+ not-yet-created ledger reports empty counts. Like `review` and `doctor`,
369
+ `status` accepts `--agent` for a compact
343
370
  single-line JSON decision packet (health, counts, attention categories, blockers,
344
371
  next action, and a verify command); `--agent` takes precedence over `--json`.
345
372
 
@@ -440,10 +467,19 @@ Rules:
440
467
  ledger, and its entries must be well-formed. A mismatched or malformed plan is
441
468
  refused without moving files or writing a receipt, mirroring the live-record
442
469
  re-checks `trash purge --execute` performs.
443
- - Writes a cleanup receipt and appends or refreshes an Artshelf-owned ledger record
444
- for that receipt with `owner=artshelf`, `kind=run-artifact`, `ttl=30d`,
445
- `cleanup=review`, and labels including `artshelf`, `cleanup-receipt`, and the
446
- plan id.
470
+ - Writes a `started` cleanup receipt to `<ledger-dir>/receipts/<plan-id>.json` before
471
+ the first filesystem move, then completes the receipt with `completedAt` and the
472
+ per-entry `trashed`, `review-required`, `refused`, or `skipped` results.
473
+ - Appends or refreshes an Artshelf-owned ledger record for the completed receipt with
474
+ `owner=artshelf`, `kind=run-artifact`, `ttl=30d`, `cleanup=review`, and labels
475
+ including `artshelf`, `cleanup-receipt`, and the plan id.
476
+ - Resumes an interrupted run on rerun of the same plan id: terminal receipt evidence
477
+ for an artifact keeps its original `executedAt`/`cleanedAt`, an artifact already
478
+ moved into the plan's trash directory without terminal receipt evidence is recorded
479
+ as `trashed` at resume time without moving it again, a missing original path with no
480
+ trash target and no receipt evidence stays a skipped missing path rather than a
481
+ success, and a completed receipt replays idempotently without duplicating the
482
+ Artshelf-owned receipt record.
447
483
  - Updates touched ledger records so handled artifacts stop appearing as active
448
484
  cleanup candidates.
449
485
  - Uses trash/review behavior by default.
@@ -653,8 +689,11 @@ V1 also supports a user-level registry of known ledgers:
653
689
  - `--all` reads registered ledgers as one review surface.
654
690
  - `trash list --all` reads trashed records across registered ledgers after
655
691
  registry validation.
656
- - `cleanup --execute --all` and `trash purge --all` are refused; execution stays
657
- scoped to one explicit ledger and one reviewed plan id.
692
+ - Registry-prune artifacts live next to the registry: `registry-prune-plans/`,
693
+ `registry-prune-rollbacks/`, and `registry-prune-receipts/`.
694
+ - `cleanup --execute --all`, `reconcile --execute --all`, and `trash purge --all`
695
+ are refused; execution stays scoped to one explicit ledger or registry and one
696
+ reviewed plan id.
658
697
 
659
698
  ## Ledger Registry Schema
660
699
 
@@ -829,11 +868,15 @@ Operational rules that back those boundaries:
829
868
  - Dry-run first.
830
869
  - Execute only by plan id.
831
870
  - Trash/review before delete.
832
- - Execute updates ledger state after writing the cleanup receipt. A trashed,
833
- review-required, or refused record no longer participates in future `due` or
834
- cleanup dry-run output by default.
835
- - Missing paths update the report; they are not treated as a successful cleanup
836
- unless the user explicitly repairs the ledger later.
871
+ - Execute writes a `started` cleanup receipt before the first filesystem move,
872
+ updates ledger state after recording per-entry outcomes, and completes the
873
+ receipt with `completedAt`. A trashed, review-required, or refused record no
874
+ longer participates in future `due` or cleanup dry-run output by default.
875
+ - Rerunning the same plan id resumes or replays durable receipt/trash evidence:
876
+ terminal receipt evidence keeps its original cleanup timestamp, existing
877
+ plan-trash targets are not moved again, completed receipts are idempotent,
878
+ and missing paths without receipt or trash evidence stay skipped rather than
879
+ successful.
837
880
  - Cleanup never scans arbitrary filesystem paths for deletion in v1.
838
881
  - Cleanup only acts on ledger entries.
839
882
  - Trash purge is scoped to one ledger, requires a reviewed purge plan id, and
@@ -916,9 +959,9 @@ installs. The report groups decisions into ready-for-approval,
916
959
  needs-review-first, and blocked sections, and must still include exact approval
917
960
  targets in the message body.
918
961
 
919
- Scheduled jobs must never run `artshelf cleanup --execute` or
920
- `artshelf trash purge --execute`; they may only dry-run and report plans for later
921
- human review.
962
+ Scheduled jobs must never run `artshelf cleanup --execute`,
963
+ `artshelf ledgers prune --execute`, or `artshelf trash purge --execute`; they may
964
+ only dry-run and report plans for later human review.
922
965
 
923
966
  ## Dogfood Scenarios
924
967
 
@@ -936,6 +979,10 @@ human review.
936
979
  by default, or a `--plain` fast path that skips validation.
937
980
  - CLI can review registered ledgers through `--all` read-only entry points,
938
981
  emitting an aggregate triage summary and the next safe action.
982
+ - CLI can prune missing/stale ledger registrations through an approval-gated
983
+ `artshelf ledgers prune` dry-run/execute workflow that writes a reviewed plan,
984
+ rollback copy, and receipt; duplicate registry paths are blocked for manual
985
+ repair.
939
986
  - CLI refuses records without a reason.
940
987
  - CLI requires TTL, retain-until, or manual-review.
941
988
  - CLI can list, filter by status, and show due entries.
@@ -957,7 +1004,9 @@ human review.
957
1004
  creates.
958
1005
  - Cleanup execute refuses to run without a plan id, and refuses an unsafe,
959
1006
  mismatched, or malformed plan before moving files or writing a receipt.
960
- - Cleanup execute writes a receipt.
1007
+ - Cleanup execute writes a started receipt before moving files, resumes or
1008
+ replays the same plan id from receipt/trash evidence, and completes the
1009
+ receipt idempotently.
961
1010
  - CLI can list trashed records (single ledger or `--all`) and purge them through
962
1011
  an approval-first, ledger-scoped dry-run/execute boundary that writes a purge
963
1012
  receipt; purge refuses `--all` and never deletes without a reviewed plan id.
@@ -975,13 +1024,14 @@ human review.
975
1024
  - Package includes the deterministic `ArtshelfReviewReport` schema, canonical
976
1025
  example, and portable renderer script for agent-rendered review reports.
977
1026
  - All core commands support `--json`.
978
- - `review`, `status`, and `doctor` also support `--agent`, a compact single-line
979
- JSON decision packet for agents that takes precedence over `--json`.
1027
+ - `review`, `status`, `doctor`, and `ledgers prune --dry-run` also support
1028
+ `--agent`, a compact single-line JSON decision packet for agents that takes
1029
+ precedence over `--json`.
980
1030
  - Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
981
1031
  `artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
982
1032
  dry-run, global-dry-run, execute-plan, cleanup plan-id validation, concurrent
983
- ledger writes, trash list/purge, path provenance validation, and reconcile
984
- dry-run/execute behavior.
1033
+ ledger writes, trash list/purge, path provenance validation, registry-prune,
1034
+ and reconcile dry-run/execute behavior.
985
1035
 
986
1036
  ## Deferred
987
1037
 
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { normalizeLedgerPath } from "../ledger.js";
3
3
  import { listRegisteredLedgers, normalizeRegistryPath, registerLedger } from "../registry.js";
4
- import { printJson } from "../renderers/json.js";
4
+ import { createRegistryPrunePlan, executeRegistryPrunePlan } from "../registry-prune.js";
5
+ import { printCompactJson, printJson } from "../renderers/json.js";
5
6
  import { boolFlag, requiredStringFlag, stringFlag } from "../shared/flags.js";
6
7
  import { LEDGERS_HELP } from "../shared/help-text.js";
7
8
  import { validateRegisteredLedger } from "./shared.js";
@@ -49,8 +50,94 @@ export function handleLedgers(parsed, json) {
49
50
  printLedgersList(report);
50
51
  return report.ok ? 0 : 1;
51
52
  }
53
+ if (action === "prune") {
54
+ return handleLedgersPrune(parsed, registryPath, json);
55
+ }
52
56
  throw new Error(`Unknown ledgers action: ${action}`);
53
57
  }
58
+ // Approval-gated registry prune (NGX-481). Dry-run is read-only except for writing a
59
+ // reviewed plan when missing registrations are detected; it never mutates the registry.
60
+ // Execute binds to one exact registry path and reviewed plan id, copies a rollback
61
+ // snapshot before mutating, and writes a receipt after.
62
+ function handleLedgersPrune(parsed, registryPath, json) {
63
+ const dryRun = boolFlag(parsed, "dry-run");
64
+ const execute = boolFlag(parsed, "execute");
65
+ if (dryRun && execute)
66
+ throw new Error("ledgers prune accepts either --dry-run or --execute, not both");
67
+ if (execute)
68
+ return handleLedgersPruneExecute(parsed, registryPath, json);
69
+ if (!dryRun)
70
+ throw new Error("ledgers prune requires --dry-run or --execute");
71
+ const plan = createRegistryPrunePlan(registryPath);
72
+ const approve = plan.planId === "not-created" ? null : pruneApprovalTarget(registryPath, plan.planId);
73
+ if (boolFlag(parsed, "agent")) {
74
+ return printCompactJson({
75
+ ok: true,
76
+ command: "ledgers-prune",
77
+ registryPath,
78
+ prunable: plan.entries.length,
79
+ blocked: plan.skipped.length,
80
+ planId: plan.planId === "not-created" ? null : plan.planId,
81
+ approve
82
+ });
83
+ }
84
+ if (json)
85
+ return printJson({ ok: true, registryPath, plan, approve });
86
+ printRegistryPrunePlan(plan, registryPath, approve);
87
+ return 0;
88
+ }
89
+ // Execute one reviewed registry-prune plan. The plan id is required up front (refusing
90
+ // `--execute` without it), then the domain layer re-checks the live registry, takes a
91
+ // rollback copy, removes only entries still classified as prunable, and writes a
92
+ // receipt. Exit is non-zero when post-mutation verification fails.
93
+ function handleLedgersPruneExecute(parsed, registryPath, json) {
94
+ const planId = stringFlag(parsed, "plan-id");
95
+ if (!planId) {
96
+ throw new Error("ledgers prune --execute requires --plan-id <id>; run `artshelf ledgers prune --dry-run` first to review a plan");
97
+ }
98
+ const receipt = executeRegistryPrunePlan(registryPath, planId);
99
+ if (json) {
100
+ printJson({ ok: receipt.verification.ok, registryPath, receipt });
101
+ return receipt.verification.ok ? 0 : 1;
102
+ }
103
+ printRegistryPruneReceipt(receipt);
104
+ return receipt.verification.ok ? 0 : 1;
105
+ }
106
+ function pruneApprovalTarget(registryPath, planId) {
107
+ return `approve artshelf ledgers prune registry ${registryPath} plan ${planId}`;
108
+ }
109
+ function printRegistryPruneReceipt(receipt) {
110
+ process.stdout.write(`artshelf ledgers prune --execute: removed ${receipt.removed.length}, skipped ${receipt.skipped.length}\nregistry: ${receipt.registryPath}\n`);
111
+ for (const entry of receipt.removed) {
112
+ process.stdout.write(`[${entry.name}] removed ${entry.scope} — ${entry.path}\n`);
113
+ }
114
+ for (const entry of receipt.skipped) {
115
+ process.stdout.write(`[${entry.name}] skipped ${entry.scope}: live registry no longer matches the reviewed plan — ${entry.path}\n`);
116
+ }
117
+ if (receipt.rollbackPath)
118
+ process.stdout.write(`rollback: ${receipt.rollbackPath}\n`);
119
+ process.stdout.write(`receipt: ${receipt.receiptPath}\n`);
120
+ process.stdout.write(`verification: ${receipt.verification.ok ? "ok" : "failed"} — ${receipt.verification.detail}\n`);
121
+ }
122
+ function printRegistryPrunePlan(plan, registryPath, approve) {
123
+ if (plan.entries.length === 0) {
124
+ process.stdout.write(`artshelf ledgers prune: nothing to prune\nregistry: ${registryPath}\n`);
125
+ for (const entry of plan.skipped) {
126
+ process.stdout.write(`[${entry.name}] blocked ${entry.scope}: ${entry.reason} — ${entry.path}\n`);
127
+ }
128
+ return;
129
+ }
130
+ process.stdout.write(`artshelf ledgers prune: ${plan.entries.length} prunable, ${plan.skipped.length} blocked\nregistry: ${registryPath}\n`);
131
+ for (const entry of plan.entries) {
132
+ process.stdout.write(`[${entry.name}] prune ${entry.scope}: ${entry.reason} — ${entry.path}\n`);
133
+ }
134
+ for (const entry of plan.skipped) {
135
+ process.stdout.write(`[${entry.name}] blocked ${entry.scope}: ${entry.reason} — ${entry.path}\n`);
136
+ }
137
+ process.stdout.write(`plan: ${plan.planPath ?? "not created"}\n`);
138
+ if (approve)
139
+ process.stdout.write(`approve: ${approve}\n`);
140
+ }
54
141
  function buildLedgersReport(registryPath) {
55
142
  let registryOk = true;
56
143
  let registryError = null;