artshelf 0.13.1 → 0.15.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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Added read-only `artshelf get <id> --inspect` decision cards with human,
6
+ `--json`, and `--agent` render modes, including registry-wide `--all` lookup,
7
+ metadata-only existence/size reporting, recommendation buckets, and exact
8
+ next-safe actions without reading file contents or mutating ledgers.
9
+ - Added approval-gated `artshelf ledgers prune` registry maintenance: dry-run
10
+ writes or reuses a reviewed plan for missing registered ledger files, `--agent`
11
+ emits the exact registry-prune approval target, execute binds to one registry
12
+ and plan id, writes a rollback copy and receipt, and `doctor`/`status`/`review`
13
+ agent guidance routes stale registrations to this flow instead of manual JSON
14
+ edits.
5
15
  - Hardened `cleanup --execute` with durable resumability: a `started` receipt is
6
16
  written before the first filesystem move so an interrupted run is detectable,
7
17
  terminal receipt evidence preserves an artifact's original
@@ -126,6 +136,25 @@
126
136
  - Moved `artshelf put` registry-warning output from stdout to stderr in human
127
137
  mode; `--json` output is unchanged (NGX-429).
128
138
 
139
+ ## [0.15.0](https://github.com/calvinnwq/artshelf/compare/v0.14.0...v0.15.0) (2026-06-20)
140
+
141
+
142
+ ### Features
143
+
144
+ * **inspect:** add read-only Artshelf review cards ([08478a8](https://github.com/calvinnwq/artshelf/commit/08478a871f624b048173a986511cc57cf2a2e609))
145
+ * **inspect:** add read-only inspect decision model ([334ea7d](https://github.com/calvinnwq/artshelf/commit/334ea7de99326ed0b0c3129228beaa0d33f378b9))
146
+ * **inspect:** wire get inspect output modes ([4a7270d](https://github.com/calvinnwq/artshelf/commit/4a7270df29f1895efe9032b2cfb7a2c1d47e971f))
147
+
148
+ ## [0.14.0](https://github.com/calvinnwq/artshelf/compare/v0.13.1...v0.14.0) (2026-06-19)
149
+
150
+
151
+ ### Features
152
+
153
+ * **commands:** add approval-gated registry pruning ([beaaca8](https://github.com/calvinnwq/artshelf/commit/beaaca84b6d0c62e66f4438ecf9323f482fcdba4))
154
+ * **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))
155
+ * **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))
156
+ * **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))
157
+
129
158
  ## [0.13.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.13.0...artshelf-v0.13.1) (2026-06-15)
130
159
 
131
160
 
package/README.md CHANGED
@@ -118,8 +118,9 @@ destructive deletion.
118
118
  registry mutations take a cross-process lock so overlapping commands never
119
119
  lose records or leave a half-written ledger.
120
120
  - **`--json` on every command**, so agents can act on structured output.
121
- - **`--agent` on `review`/`status`/`doctor`**, a compact, token-efficient
122
- decision packet for agents, while the default render stays human-scannable.
121
+ - **`--agent` on `review`/`status`/`doctor`, `ledgers prune --dry-run`, and
122
+ `get --inspect`**, a compact, token-efficient decision packet for agents,
123
+ while the default render stays human-scannable.
123
124
 
124
125
  ## Reference
125
126
 
@@ -130,10 +131,14 @@ destructive deletion.
130
131
  artshelf put <path> --reason "debug parser output" --ttl 3d --kind scratch
131
132
  artshelf ledgers list [--plain] [--json]
132
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]
133
136
  artshelf list [--all] [--status active]
134
137
  artshelf find --path <path> --owner <agent-or-runtime> --label <task-or-run-id>
135
138
  artshelf find --all --owner <agent-or-runtime>
136
139
  artshelf get <id> [--all]
140
+ artshelf get <id> --inspect [--ledger <path>] [--json|--agent]
141
+ artshelf get <id> --inspect --all [--registry <path>] [--json|--agent]
137
142
  artshelf due [--all]
138
143
  artshelf validate [--all]
139
144
  artshelf review [--all]
@@ -152,10 +157,11 @@ artshelf resolve <id> --status resolved --reason "inspected and no longer needed
152
157
 
153
158
  Use `artshelf help` for a grouped command list, then `artshelf <command> --help`
154
159
  or `artshelf help <command>` for focused details. Nested commands such as
155
- `artshelf trash purge --help` and `artshelf ledgers add --help` show only that
156
- subcommand. All core commands support `--json`; `review`, `status`, and `doctor`
157
- also take `--agent` for a compact decision packet; `--ledger`, `--registry`, and
158
- `--all` are scope flags only on commands that list them.
160
+ `artshelf trash purge --help`, `artshelf ledgers add --help`, and
161
+ `artshelf ledgers prune --help` show only that subcommand. All core commands
162
+ support `--json`; `review`, `status`, `doctor`, `ledgers prune --dry-run`, and
163
+ `get --inspect` also take `--agent` for a compact decision packet; `--ledger`,
164
+ `--registry`, and `--all` are scope flags only on commands that list them.
159
165
  </details>
160
166
 
161
167
  <details>
@@ -176,7 +182,11 @@ Artshelf keeps a small global registry of known ledgers at
176
182
  existing one with `artshelf ledgers add --ledger <path> --name <project> --json`.
177
183
  `artshelf ledgers list` validates each registered ledger by default (ok/missing/invalid
178
184
  status with counts, non-zero exit when broken), so it doubles as a stale-entry
179
- check; add `--plain` to skip validation.
185
+ check; add `--plain` to skip validation. When registered ledger files are
186
+ missing, use `artshelf ledgers prune --dry-run --registry <path>` to write a
187
+ reviewed registry-prune plan, approve `approve artshelf ledgers prune registry
188
+ <registry-path> plan <plan-id>`, then execute that exact plan id; duplicate paths
189
+ are blocked for manual repair and are never pruned automatically.
180
190
 
181
191
  Use `--all` for one read-only discovery entry point across registered ledgers
182
192
  (`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.
@@ -183,10 +199,56 @@ Read-only lookup of a single ledger record by Artshelf id.
183
199
  artshelf get <id>
184
200
  artshelf get <id> --json
185
201
  artshelf get <id> --all --json
202
+ artshelf get <id> --inspect
203
+ artshelf get <id> --inspect --json
204
+ artshelf get <id> --inspect --agent
205
+ artshelf get <id> --inspect --all --registry <path> --agent
186
206
  ```
187
207
 
188
208
  `get` is for audit and handoff follow-up. Missing ids are an error. `--all`
189
- searches registered ledgers until the id is found.
209
+ searches registered ledgers until the id is found. With `--inspect --all`, the
210
+ registry is only used for lookup; the decision card reports the concrete ledger
211
+ that owns the matching record.
212
+
213
+ `--inspect` turns a record into a review decision card. It never moves files or
214
+ mutates the ledger; it only reports existence, node kind, size, age, retention/due and
215
+ manual-review state, cleanup mode, reason, and a recommendation bucket with the
216
+ exact next-safe action. It never reads or previews arbitrary file contents:
217
+
218
+ - `keep` — held for manual review, already resolved, already trashed, or due
219
+ with `cleanup=review`; it needs your judgment but nothing auto-runs.
220
+ - `snooze` — retention has not expired yet; re-inspect after it is due.
221
+ - `trash-safe` — due with `cleanup=trash`; safe to plan a reviewed cleanup.
222
+ - `resolve-only` — the recorded path is gone; resolve the record (ledger-only)
223
+ rather than cleaning a file.
224
+ - `blocked` — needs a human decision first: `cleanup=delete` (refused at
225
+ execute), a review-required flag, or a prior cleanup refusal.
226
+
227
+ File-content previews are intentionally outside Artshelf core; an acting agent or
228
+ host runtime may inspect file contents separately when appropriate.
229
+ `--inspect --json` returns `{ inspect: <report> }`; `--inspect --agent` returns a compact single-line
230
+ decision packet with a read-only safety block, the next-safe action, and a
231
+ reproducer command, and takes precedence over `--json`. Both shapes are
232
+ deterministic so portable agent skills can act without re-deriving anything.
233
+
234
+ Example pattern — the dogfooding case that motivated this surface was an old
235
+ rollback `backup` registered with `cleanup=review` (the kind of stale record
236
+ `ledgers prune` and `review --all` surface). Inspect it before deciding, using
237
+ the record id and ledger path rather than hardcoding either:
238
+
239
+ ```bash
240
+ artshelf get <id> --inspect --ledger <ledger-path>
241
+ ```
242
+
243
+ ```text
244
+ ✓ <id> [backup] — keep
245
+ path: <backup-path>
246
+ status: active · cleanup: review · owner: agent · labels: registry-prune
247
+ existence: present (directory, 49 B) · age: 14d · retention: manual-review · due: manual-review
248
+ reason: rollback backup before registry prune
249
+ next: Held for manual review — keep it, change its retention, or resolve it explicitly; no cleanup is scheduled.
250
+ ledger: <ledger-path>
251
+ ```
190
252
 
191
253
  ### `artshelf due`
192
254
 
@@ -259,22 +321,28 @@ counts plus the preview plan ids; JSON also includes the next safe action. The
259
321
  per-ledger human detail appends a `reconcile` count when a ledger has reconcile
260
322
  drift. Human output adds a one-line triage count with the same reconcile counts
261
323
  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,
324
+ registry-prune dry-run for missing registered ledgers, dry-run reconcile for
325
+ missing-path or reconcile drift, or nothing to do). Review never writes a plan,
326
+ so the next action always points at an explicit follow-up command.
327
+
328
+ `review`, `status`, `doctor`, `ledgers prune --dry-run`, and `get --inspect`
329
+ expose agent-oriented render modes. For review/status/doctor, the default human render
330
+ leads each ledger and summary line with a `✓`/`⚠` attention glyph. `--json` stays
331
+ the full, backward-compatible public audit report; and `--agent` emits a compact,
269
332
  deterministic single-line JSON decision packet for agents, taking precedence over
270
- `--json` when both are passed. For `review`, the packet sorts records into
333
+ `--json` when both are passed. For `get --inspect`, `--agent` returns the
334
+ per-record decision packet and requires `--inspect`. For `review`, the packet sorts records into
271
335
  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
336
+ read-only and never mints a cleanup or registry-prune plan, the exact approval
337
+ targets it emits are `resolve missing` and `reconcile`; the `reconcile` target
338
+ appears only when a prior reviewed reconcile plan still matches the live drift.
339
+ Cleanup-eligible records and reconcile drift without a reviewed plan stay
340
+ needs-review-first and point at `cleanup --dry-run` or `reconcile --dry-run`,
341
+ which mint the reviewed plan id to approve. Missing registered ledger files in
342
+ `--all` mode surface as blocked registry fixes that point at `ledgers prune
343
+ --dry-run --registry <path>`; the prune dry-run produces the registry-prune
344
+ approval target. Invalid-but-present ledger files still point at manual
345
+ re-register/fix work. Blocked or ambiguous reconcile findings surface in the
278
346
  blocked group with no approval target.
279
347
 
280
348
  ### `artshelf doctor`
@@ -302,10 +370,13 @@ Doctor reports:
302
370
  physical trash purge requires a separate reviewed purge plan.
303
371
 
304
372
  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`
373
+ registered ledger exits non-zero with actionable errors. When stale/missing
374
+ registrations exist, the agent next action points at `artshelf ledgers prune
375
+ --dry-run --registry <path>` before re-running doctor; invalid ledger files still
376
+ need manual repair. Humans should run `artshelf doctor` after install or when
377
+ `--all` commands behave unexpectedly; agents may run it on a schedule to catch
378
+ stale registry entries before relying on cleanup planning. Doctor never creates
379
+ plans, receipts, or records. Like `review`
309
380
  and `status`, `doctor` accepts `--agent` for a compact single-line JSON decision
310
381
  packet (health, registry and registered-ledger health, blockers, cleanup-safety
311
382
  posture, next action, and a verify command); `--agent` takes precedence over
@@ -337,9 +408,12 @@ Status reports:
337
408
  output is short enough to paste into a chat. Status is strictly read-only: it
338
409
  never creates plans or receipts and never mutates records. A healthy machine
339
410
  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
411
+ ledger exits non-zero. When stale/missing registrations exist, `--all --agent`
412
+ points at `artshelf ledgers prune --dry-run --registry <path>` before re-running
413
+ status; invalid ledgers are still manual repair. Due entries are normal
414
+ operational state and do not change the exit code. With single `--ledger`, a
415
+ not-yet-created ledger reports empty counts. Like `review` and `doctor`,
416
+ `status` accepts `--agent` for a compact
343
417
  single-line JSON decision packet (health, counts, attention categories, blockers,
344
418
  next action, and a verify command); `--agent` takes precedence over `--json`.
345
419
 
@@ -662,8 +736,11 @@ V1 also supports a user-level registry of known ledgers:
662
736
  - `--all` reads registered ledgers as one review surface.
663
737
  - `trash list --all` reads trashed records across registered ledgers after
664
738
  registry validation.
665
- - `cleanup --execute --all` and `trash purge --all` are refused; execution stays
666
- scoped to one explicit ledger and one reviewed plan id.
739
+ - Registry-prune artifacts live next to the registry: `registry-prune-plans/`,
740
+ `registry-prune-rollbacks/`, and `registry-prune-receipts/`.
741
+ - `cleanup --execute --all`, `reconcile --execute --all`, and `trash purge --all`
742
+ are refused; execution stays scoped to one explicit ledger or registry and one
743
+ reviewed plan id.
667
744
 
668
745
  ## Ledger Registry Schema
669
746
 
@@ -929,9 +1006,9 @@ installs. The report groups decisions into ready-for-approval,
929
1006
  needs-review-first, and blocked sections, and must still include exact approval
930
1007
  targets in the message body.
931
1008
 
932
- Scheduled jobs must never run `artshelf cleanup --execute` or
933
- `artshelf trash purge --execute`; they may only dry-run and report plans for later
934
- human review.
1009
+ Scheduled jobs must never run `artshelf cleanup --execute`,
1010
+ `artshelf ledgers prune --execute`, or `artshelf trash purge --execute`; they may
1011
+ only dry-run and report plans for later human review.
935
1012
 
936
1013
  ## Dogfood Scenarios
937
1014
 
@@ -949,6 +1026,10 @@ human review.
949
1026
  by default, or a `--plain` fast path that skips validation.
950
1027
  - CLI can review registered ledgers through `--all` read-only entry points,
951
1028
  emitting an aggregate triage summary and the next safe action.
1029
+ - CLI can prune missing/stale ledger registrations through an approval-gated
1030
+ `artshelf ledgers prune` dry-run/execute workflow that writes a reviewed plan,
1031
+ rollback copy, and receipt; duplicate registry paths are blocked for manual
1032
+ repair.
952
1033
  - CLI refuses records without a reason.
953
1034
  - CLI requires TTL, retain-until, or manual-review.
954
1035
  - CLI can list, filter by status, and show due entries.
@@ -990,13 +1071,14 @@ human review.
990
1071
  - Package includes the deterministic `ArtshelfReviewReport` schema, canonical
991
1072
  example, and portable renderer script for agent-rendered review reports.
992
1073
  - All core commands support `--json`.
993
- - `review`, `status`, and `doctor` also support `--agent`, a compact single-line
994
- JSON decision packet for agents that takes precedence over `--json`.
1074
+ - `review`, `status`, `doctor`, `ledgers prune --dry-run`, and `get --inspect`
1075
+ also support `--agent`, a compact single-line JSON decision packet for agents
1076
+ that takes precedence over `--json`.
995
1077
  - Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
996
1078
  `artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
997
1079
  dry-run, global-dry-run, execute-plan, cleanup plan-id validation, concurrent
998
- ledger writes, trash list/purge, path provenance validation, and reconcile
999
- dry-run/execute behavior.
1080
+ ledger writes, trash list/purge, path provenance validation, registry-prune,
1081
+ and reconcile dry-run/execute behavior.
1000
1082
 
1001
1083
  ## Deferred
1002
1084
 
package/dist/src/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { maybeNotifyAvailableUpdate, runCommand } from "./commands/index.js";
3
3
  import { VERSION } from "./config/package.js";
4
4
  import { formatCliError } from "./shared/errors.js";
5
- import { BOOLEAN_FLAGS, boolFlag, VALUE_FLAGS } from "./shared/flags.js";
5
+ import { boolFlag, isBooleanFlag, VALUE_FLAGS } from "./shared/flags.js";
6
6
  import { renderHelp, resolveHelpKey } from "./shared/help-text.js";
7
7
  async function main(argv) {
8
8
  try {
@@ -53,7 +53,7 @@ function parseArgs(argv) {
53
53
  }
54
54
  if (token.startsWith("--")) {
55
55
  const name = token.slice(2);
56
- if (BOOLEAN_FLAGS.has(name)) {
56
+ if (isBooleanFlag(name, command)) {
57
57
  flags.set(name, true);
58
58
  continue;
59
59
  }
@@ -1,20 +1,28 @@
1
+ import { buildInspectReport } from "../inspect.js";
1
2
  import { getRecord, readLedger } from "../ledger.js";
2
3
  import { normalizeRegistryPath } from "../registry.js";
3
- import { printJson } from "../renderers/json.js";
4
+ import { buildInspectAgentPacket, printInspect } from "../renderers/inspect.js";
5
+ import { printCompactJson, printJson } from "../renderers/json.js";
4
6
  import { boolFlag, stringFlag } from "../shared/flags.js";
5
7
  import { printRegisteredLedgerValidation, validateRegisteredLedgersOrThrow } from "./shared.js";
6
8
  export function handleGet(parsed, ledgerPath, json) {
7
9
  const id = parsed.positionals[0];
8
10
  if (!id)
9
11
  throw new Error("get requires <id>");
12
+ const inspect = boolFlag(parsed, "inspect");
13
+ const agent = boolFlag(parsed, "agent");
14
+ if (agent && !inspect)
15
+ throw new Error("--agent requires --inspect for get");
10
16
  if (boolFlag(parsed, "all")) {
11
17
  const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
12
18
  const validation = validateRegisteredLedgersOrThrow(registryPath);
13
19
  if (!validation.ok)
14
- return printRegisteredLedgerValidation(registryPath, validation.results, json);
20
+ return printRegisteredLedgerValidation(registryPath, validation.results, json || agent);
15
21
  for (const { ledger } of validation.results) {
16
22
  const record = readLedger(ledger.path).find((entry) => entry.id === id);
17
23
  if (record) {
24
+ if (inspect)
25
+ return renderInspect(record, ledger.path, json, agent);
18
26
  if (json)
19
27
  return printJson({ ok: true, registryPath, ledger, record });
20
28
  process.stdout.write(`${record.id} ${record.kind} ${record.status} ${record.cleanup} ${record.path}\nreason: ${record.reason}\nledger: ${ledger.path}\nregistry: ${registryPath}\n`);
@@ -24,8 +32,22 @@ export function handleGet(parsed, ledgerPath, json) {
24
32
  throw new Error(`Artshelf record not found: ${id}`);
25
33
  }
26
34
  const record = getRecord(readLedger(ledgerPath), id);
35
+ if (inspect)
36
+ return renderInspect(record, ledgerPath, json, agent);
27
37
  if (json)
28
38
  return printJson({ ok: true, ledgerPath, record });
29
39
  process.stdout.write(`${record.id} ${record.kind} ${record.status} ${record.cleanup} ${record.path}\nreason: ${record.reason}\nledger: ${ledgerPath}\n`);
30
40
  return 0;
31
41
  }
42
+ // Read-only inspect surface (NGX-482): builds a deterministic decision report and
43
+ // renders it as a human card, full JSON, or a compact agent packet. --agent wins
44
+ // over --json, matching `review`. Never mutates the ledger or the filesystem.
45
+ function renderInspect(record, ledgerPath, json, agent) {
46
+ const report = buildInspectReport(record, { ledgerPath });
47
+ if (agent)
48
+ return printCompactJson(buildInspectAgentPacket(report, ledgerPath));
49
+ if (json)
50
+ return printJson({ ok: true, ledgerPath, inspect: report });
51
+ printInspect(report, ledgerPath);
52
+ return 0;
53
+ }
@@ -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;