artshelf 0.13.1 → 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 +16 -0
- package/README.md +13 -5
- package/SPEC.md +67 -32
- package/dist/src/commands/ledgers.js +88 -1
- package/dist/src/registry-prune.js +259 -0
- package/dist/src/registry.js +27 -0
- package/dist/src/renderers/doctor.js +11 -1
- package/dist/src/renderers/review.js +24 -2
- package/dist/src/renderers/status.js +10 -2
- package/dist/src/shared/help-text.js +30 -1
- package/docs/agent-monitor.html +16 -8
- package/docs/agent-review.html +8 -3
- package/docs/agent-usage.html +3 -3
- package/docs/agent-usage.md +5 -4
- package/docs/install.html +11 -2
- package/docs/reference.html +38 -9
- package/package.json +1 -1
- package/skills/artshelf/SKILL.md +18 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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.
|
|
5
11
|
- Hardened `cleanup --execute` with durable resumability: a `started` receipt is
|
|
6
12
|
written before the first filesystem move so an interrupted run is detectable,
|
|
7
13
|
terminal receipt evidence preserves an artifact's original
|
|
@@ -126,6 +132,16 @@
|
|
|
126
132
|
- Moved `artshelf put` registry-warning output from stdout to stderr in human
|
|
127
133
|
mode; `--json` output is unchanged (NGX-429).
|
|
128
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
|
+
|
|
129
145
|
## [0.13.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.13.0...artshelf-v0.13.1) (2026-06-15)
|
|
130
146
|
|
|
131
147
|
|
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
|
|
122
|
-
decision packet for agents, while the default render
|
|
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.
|
|
123
124
|
|
|
124
125
|
## Reference
|
|
125
126
|
|
|
@@ -130,6 +131,8 @@ 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>
|
|
@@ -152,8 +155,9 @@ artshelf resolve <id> --status resolved --reason "inspected and no longer needed
|
|
|
152
155
|
|
|
153
156
|
Use `artshelf help` for a grouped command list, then `artshelf <command> --help`
|
|
154
157
|
or `artshelf help <command>` for focused details. Nested commands such as
|
|
155
|
-
`artshelf trash purge --help
|
|
156
|
-
|
|
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`
|
|
157
161
|
also take `--agent` for a compact decision packet; `--ledger`, `--registry`, and
|
|
158
162
|
`--all` are scope flags only on commands that list them.
|
|
159
163
|
</details>
|
|
@@ -176,7 +180,11 @@ Artshelf keeps a small global registry of known ledgers at
|
|
|
176
180
|
existing one with `artshelf ledgers add --ledger <path> --name <project> --json`.
|
|
177
181
|
`artshelf ledgers list` validates each registered ledger by default (ok/missing/invalid
|
|
178
182
|
status with counts, non-zero exit when broken), so it doubles as a stale-entry
|
|
179
|
-
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.
|
|
180
188
|
|
|
181
189
|
Use `--all` for one read-only discovery entry point across registered ledgers
|
|
182
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`,
|
|
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
|
|
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
|
|
263
|
-
never writes a plan,
|
|
264
|
-
the next action always points at an explicit follow-up command.
|
|
265
|
-
|
|
266
|
-
`review`, `status`,
|
|
267
|
-
render
|
|
268
|
-
|
|
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
|
|
273
|
-
`resolve missing` and `reconcile`; the `reconcile` target
|
|
274
|
-
prior reviewed reconcile plan still matches the live drift.
|
|
275
|
-
records and reconcile drift without a reviewed plan stay
|
|
276
|
-
point at `cleanup --dry-run` or `reconcile --dry-run`,
|
|
277
|
-
plan id to approve.
|
|
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.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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.
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
|
@@ -662,8 +689,11 @@ V1 also supports a user-level registry of known ledgers:
|
|
|
662
689
|
- `--all` reads registered ledgers as one review surface.
|
|
663
690
|
- `trash list --all` reads trashed records across registered ledgers after
|
|
664
691
|
registry validation.
|
|
665
|
-
-
|
|
666
|
-
|
|
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.
|
|
667
697
|
|
|
668
698
|
## Ledger Registry Schema
|
|
669
699
|
|
|
@@ -929,9 +959,9 @@ installs. The report groups decisions into ready-for-approval,
|
|
|
929
959
|
needs-review-first, and blocked sections, and must still include exact approval
|
|
930
960
|
targets in the message body.
|
|
931
961
|
|
|
932
|
-
Scheduled jobs must never run `artshelf cleanup --execute
|
|
933
|
-
`artshelf trash purge --execute`; they may
|
|
934
|
-
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.
|
|
935
965
|
|
|
936
966
|
## Dogfood Scenarios
|
|
937
967
|
|
|
@@ -949,6 +979,10 @@ human review.
|
|
|
949
979
|
by default, or a `--plain` fast path that skips validation.
|
|
950
980
|
- CLI can review registered ledgers through `--all` read-only entry points,
|
|
951
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.
|
|
952
986
|
- CLI refuses records without a reason.
|
|
953
987
|
- CLI requires TTL, retain-until, or manual-review.
|
|
954
988
|
- CLI can list, filter by status, and show due entries.
|
|
@@ -990,13 +1024,14 @@ human review.
|
|
|
990
1024
|
- Package includes the deterministic `ArtshelfReviewReport` schema, canonical
|
|
991
1025
|
example, and portable renderer script for agent-rendered review reports.
|
|
992
1026
|
- All core commands support `--json`.
|
|
993
|
-
- `review`, `status`, and `
|
|
994
|
-
JSON decision packet for agents that takes
|
|
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`.
|
|
995
1030
|
- Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
|
|
996
1031
|
`artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
|
|
997
1032
|
dry-run, global-dry-run, execute-plan, cleanup plan-id validation, concurrent
|
|
998
|
-
ledger writes, trash list/purge, path provenance validation,
|
|
999
|
-
dry-run/execute behavior.
|
|
1033
|
+
ledger writes, trash list/purge, path provenance validation, registry-prune,
|
|
1034
|
+
and reconcile dry-run/execute behavior.
|
|
1000
1035
|
|
|
1001
1036
|
## Deferred
|
|
1002
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 {
|
|
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;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { assertSafeGeneratedId } from "./ledger.js";
|
|
5
|
+
import { withPathLock } from "./locks.js";
|
|
6
|
+
import { listRegisteredLedgers, normalizeRegistryPath, removeRegisteredLedgers } from "./registry.js";
|
|
7
|
+
import { now, toIso } from "./time.js";
|
|
8
|
+
// Classify the registry into prune findings (read-only). A registration is prunable
|
|
9
|
+
// when its ledger file is missing — the same "missing/stale" signal `ledgers list`
|
|
10
|
+
// reports via existence of the ledger path. Registrations whose resolved path appears
|
|
11
|
+
// more than once are ambiguous: pruning one would silently drop a sibling, so they are
|
|
12
|
+
// blocked for manual resolution instead of pruned. Present ledger files yield nothing.
|
|
13
|
+
export function classifyRegistryPruneFindings(registryPath) {
|
|
14
|
+
const entries = listRegisteredLedgers(normalizeRegistryPath(registryPath));
|
|
15
|
+
const pathCounts = new Map();
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
pathCounts.set(entry.path, (pathCounts.get(entry.path) ?? 0) + 1);
|
|
18
|
+
}
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if ((pathCounts.get(entry.path) ?? 0) > 1) {
|
|
22
|
+
findings.push(finding(entry, "blocked", "ambiguous duplicate registry path; resolve manually before pruning"));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (existsSync(entry.path))
|
|
26
|
+
continue;
|
|
27
|
+
findings.push(finding(entry, "prune", "registered ledger file is missing"));
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
}
|
|
31
|
+
// Build the registry-prune plan without persisting anything (dry-run preview). Fully
|
|
32
|
+
// read-only: it classifies the registry and returns the plan a `--dry-run` would
|
|
33
|
+
// create, but never writes a plan file or mutates the registry. A plan with no
|
|
34
|
+
// actionable entries collapses to the not-created shape so callers can render
|
|
35
|
+
// "nothing to prune" the same way cleanup and reconcile do.
|
|
36
|
+
export function previewRegistryPrunePlan(registryPath) {
|
|
37
|
+
const plan = buildRegistryPrunePlan(normalizeRegistryPath(registryPath));
|
|
38
|
+
return plan.entries.length === 0 ? noCreatedRegistryPrunePlan(plan) : plan;
|
|
39
|
+
}
|
|
40
|
+
// Create (or reuse) a reviewed registry-prune plan (dry-run). This is the only part of
|
|
41
|
+
// dry-run that writes, and it only writes the plan file — never the registry. When an
|
|
42
|
+
// earlier plan already covers the same prunable entries it is reused verbatim (stable
|
|
43
|
+
// plan id), and when nothing is actionable no plan artifact is created at all, keeping
|
|
44
|
+
// dry-run side-effect-free in that case. The plan file lives next to the registry under
|
|
45
|
+
// `registry-prune-plans/` so a later `--execute` can discover it by exact plan id.
|
|
46
|
+
export function createRegistryPrunePlan(registryPath) {
|
|
47
|
+
const normalized = normalizeRegistryPath(registryPath);
|
|
48
|
+
const plan = buildRegistryPrunePlan(normalized);
|
|
49
|
+
if (plan.entries.length === 0)
|
|
50
|
+
return noCreatedRegistryPrunePlan(plan);
|
|
51
|
+
const existing = matchingExistingRegistryPrunePlan(normalized, plan);
|
|
52
|
+
const reviewed = existing ? { ...plan, planId: existing.planId, planPath: existing.planPath } : plan;
|
|
53
|
+
if (!reviewed.planPath)
|
|
54
|
+
throw new Error("registry prune plan path was not created");
|
|
55
|
+
writeRegistryPrunePlanFile(reviewed.planPath, reviewed);
|
|
56
|
+
return reviewed;
|
|
57
|
+
}
|
|
58
|
+
// Apply a reviewed registry-prune plan (NGX-481 `ledgers prune --execute`). This is the
|
|
59
|
+
// only mutating registry-prune entrypoint and it is deliberately conservative:
|
|
60
|
+
// * It refuses up front when the plan id is missing, the registry is absent, the plan
|
|
61
|
+
// file is absent, or the plan file's declared id/registry does not match the scoped
|
|
62
|
+
// request (no fresh plan, no `--all`; it binds to one exact reviewed plan id against
|
|
63
|
+
// one exact registry path).
|
|
64
|
+
// * Inside one registry lock it re-classifies the live registry and only removes a
|
|
65
|
+
// planned entry that still classifies as prunable; entries whose ledger file
|
|
66
|
+
// reappeared or whose path became an ambiguous duplicate are skipped, not removed.
|
|
67
|
+
// * It writes a rollback copy of the registry before mutating and a receipt after,
|
|
68
|
+
// then verifies the removed registrations are actually gone.
|
|
69
|
+
export function executeRegistryPrunePlan(registryPath, planId) {
|
|
70
|
+
if (!planId)
|
|
71
|
+
throw new Error("ledgers prune --execute requires --plan-id");
|
|
72
|
+
const normalized = normalizeRegistryPath(registryPath);
|
|
73
|
+
if (!existsSync(normalized))
|
|
74
|
+
throw new Error(`Registry not found: ${normalized}`);
|
|
75
|
+
const planPath = registryPrunePlanPath(normalized, planId);
|
|
76
|
+
if (!existsSync(planPath))
|
|
77
|
+
throw new Error(`Registry prune plan not found: ${planId}`);
|
|
78
|
+
const plan = JSON.parse(readFileSync(planPath, "utf8"));
|
|
79
|
+
assertRegistryPrunePlanExecutable(plan, planId, normalized);
|
|
80
|
+
const receiptPath = registryPruneReceiptPath(normalized, planId);
|
|
81
|
+
const rollbackPath = registryPruneRollbackPath(normalized, planId);
|
|
82
|
+
return withPathLock(normalized, () => {
|
|
83
|
+
const existingReceipt = readExistingRegistryPruneReceipt(receiptPath, planId, normalized);
|
|
84
|
+
if (existingReceipt)
|
|
85
|
+
return existingReceipt;
|
|
86
|
+
const liveByKey = new Map(classifyRegistryPruneFindings(normalized).map((item) => [pruneKey(item.name, item.path), item]));
|
|
87
|
+
const removable = [];
|
|
88
|
+
const skipped = [];
|
|
89
|
+
for (const entry of plan.entries) {
|
|
90
|
+
if (liveByKey.get(pruneKey(entry.name, entry.path))?.status === "prune")
|
|
91
|
+
removable.push(entry);
|
|
92
|
+
else
|
|
93
|
+
skipped.push(removal(entry.name, entry.path, entry.scope));
|
|
94
|
+
}
|
|
95
|
+
let removedEntries = [];
|
|
96
|
+
const receiptRollbackPath = removable.length > 0 ? rollbackPath : null;
|
|
97
|
+
if (removable.length > 0) {
|
|
98
|
+
copyRegistrySnapshot(normalized, rollbackPath);
|
|
99
|
+
removedEntries = removeRegisteredLedgers(normalized, removable.map((entry) => ({ name: entry.name, path: entry.path })));
|
|
100
|
+
}
|
|
101
|
+
const removed = removedEntries.map((entry) => removal(entry.name, entry.path, entry.scope));
|
|
102
|
+
const verification = verifyRegistryPrune(normalized, removed);
|
|
103
|
+
const receipt = {
|
|
104
|
+
planId,
|
|
105
|
+
registryPath: normalized,
|
|
106
|
+
executedAt: toIso(now()),
|
|
107
|
+
rollbackPath: receiptRollbackPath,
|
|
108
|
+
removed,
|
|
109
|
+
skipped,
|
|
110
|
+
verification,
|
|
111
|
+
receiptPath
|
|
112
|
+
};
|
|
113
|
+
writeRegistryPruneReceiptFile(receiptPath, receipt);
|
|
114
|
+
return receipt;
|
|
115
|
+
}, "Artshelf ledger registry");
|
|
116
|
+
}
|
|
117
|
+
function finding(entry, status, reason) {
|
|
118
|
+
return { name: entry.name, path: entry.path, scope: entry.scope, status, reason };
|
|
119
|
+
}
|
|
120
|
+
function buildRegistryPrunePlan(registryPath) {
|
|
121
|
+
const generatedAt = now();
|
|
122
|
+
const findings = classifyRegistryPruneFindings(registryPath);
|
|
123
|
+
const entries = findings.filter((item) => item.status === "prune").map(planEntry);
|
|
124
|
+
const skipped = findings.filter((item) => item.status === "blocked").map(planEntry);
|
|
125
|
+
const planId = makeRegistryPrunePlanId(generatedAt);
|
|
126
|
+
return {
|
|
127
|
+
planId,
|
|
128
|
+
generatedAt: toIso(generatedAt),
|
|
129
|
+
registryPath,
|
|
130
|
+
entries,
|
|
131
|
+
skipped,
|
|
132
|
+
planPath: registryPrunePlanPath(registryPath, planId)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function planEntry(item) {
|
|
136
|
+
return { name: item.name, path: item.path, scope: item.scope, reason: item.reason };
|
|
137
|
+
}
|
|
138
|
+
function noCreatedRegistryPrunePlan(plan) {
|
|
139
|
+
return { ...plan, planId: "not-created", planPath: null };
|
|
140
|
+
}
|
|
141
|
+
// Reuse an unexecuted earlier plan whose prunable entries match this one's, so repeated
|
|
142
|
+
// dry-runs converge on a single stable plan id (mirrors cleanup/reconcile plan reuse).
|
|
143
|
+
// Only the structural entry fields are fingerprinted; volatile fields (generatedAt) and
|
|
144
|
+
// the review-only skipped list do not affect reuse.
|
|
145
|
+
function matchingExistingRegistryPrunePlan(registryPath, plan) {
|
|
146
|
+
const plansDir = join(dirname(registryPath), "registry-prune-plans");
|
|
147
|
+
if (!existsSync(plansDir))
|
|
148
|
+
return null;
|
|
149
|
+
const filenames = readdirSync(plansDir).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
150
|
+
for (const filename of filenames) {
|
|
151
|
+
const planPath = join(plansDir, filename);
|
|
152
|
+
try {
|
|
153
|
+
const candidate = JSON.parse(readFileSync(planPath, "utf8"));
|
|
154
|
+
if (candidate.registryPath !== registryPath)
|
|
155
|
+
continue;
|
|
156
|
+
if (registryPrunePlanFingerprint(candidate) !== registryPrunePlanFingerprint(plan))
|
|
157
|
+
continue;
|
|
158
|
+
if (existsSync(registryPruneReceiptPath(registryPath, candidate.planId)))
|
|
159
|
+
continue;
|
|
160
|
+
return { ...candidate, planPath };
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function registryPrunePlanFingerprint(plan) {
|
|
169
|
+
return JSON.stringify(plan.entries.map((entry) => ({ name: entry.name, path: entry.path, scope: entry.scope })));
|
|
170
|
+
}
|
|
171
|
+
function writeRegistryPrunePlanFile(planPath, plan) {
|
|
172
|
+
mkdirSync(dirname(planPath), { recursive: true });
|
|
173
|
+
writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
174
|
+
}
|
|
175
|
+
function makeRegistryPrunePlanId(date) {
|
|
176
|
+
return `registry-prune_${toIso(date).replace(/[-:]/g, "").replace("T", "_").replace("Z", "")}_${randomBytes(2).toString("hex")}`;
|
|
177
|
+
}
|
|
178
|
+
function registryPrunePlanPath(registryPath, planId) {
|
|
179
|
+
assertSafeGeneratedId(planId, "registry prune plan id");
|
|
180
|
+
return join(dirname(registryPath), "registry-prune-plans", `${planId}.json`);
|
|
181
|
+
}
|
|
182
|
+
// Bind a loaded registry-prune plan to the request before any registry mutation,
|
|
183
|
+
// mirroring reconcile's assertReconcilePlanExecutable: the plan must declare the
|
|
184
|
+
// requested id, belong to the executing registry, and carry well-formed entries.
|
|
185
|
+
function assertRegistryPrunePlanExecutable(plan, planId, registryPath) {
|
|
186
|
+
if (plan.planId !== planId) {
|
|
187
|
+
throw new Error(`Registry prune plan id mismatch: plan file declares ${plan.planId}, requested ${planId}`);
|
|
188
|
+
}
|
|
189
|
+
if (plan.registryPath !== registryPath) {
|
|
190
|
+
throw new Error(`Registry prune plan registry mismatch: plan was created for ${plan.registryPath}, executing ${registryPath}`);
|
|
191
|
+
}
|
|
192
|
+
if (!Array.isArray(plan.entries)) {
|
|
193
|
+
throw new Error(`Registry prune plan entries are malformed: ${planId}`);
|
|
194
|
+
}
|
|
195
|
+
for (const entry of plan.entries) {
|
|
196
|
+
if (!entry || typeof entry.name !== "string" || typeof entry.path !== "string") {
|
|
197
|
+
throw new Error(`Registry prune plan entries are malformed: ${planId}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Re-scan the registry after mutation and confirm every removed registration is gone.
|
|
202
|
+
// `ok` stays true only when none of them resurface; `remainingPrunable` counts any
|
|
203
|
+
// prunable registrations left registry-wide so the receipt reflects whether the
|
|
204
|
+
// registry is fully clean or other plans still have work.
|
|
205
|
+
function verifyRegistryPrune(registryPath, removed) {
|
|
206
|
+
const live = classifyRegistryPruneFindings(registryPath);
|
|
207
|
+
const stillPresent = removed.filter((entry) => live.some((item) => item.name === entry.name && item.path === entry.path));
|
|
208
|
+
const remainingPrunable = live.filter((item) => item.status === "prune").length;
|
|
209
|
+
return {
|
|
210
|
+
ok: stillPresent.length === 0,
|
|
211
|
+
remainingPrunable,
|
|
212
|
+
detail: stillPresent.length === 0
|
|
213
|
+
? "removed registrations are gone; registry re-scan is clean of them"
|
|
214
|
+
: `still registered after prune: ${stillPresent.map((entry) => entry.name).join(", ")}`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Snapshot the registry verbatim before mutation so the receipt's rollbackPath points
|
|
218
|
+
// at a restorable copy. The registry is always UTF-8 JSON, so a read/write round-trip
|
|
219
|
+
// reproduces it byte-for-byte and keeps file I/O consistent with the rest of the code.
|
|
220
|
+
function copyRegistrySnapshot(registryPath, rollbackPath) {
|
|
221
|
+
mkdirSync(dirname(rollbackPath), { recursive: true });
|
|
222
|
+
writeFileSync(rollbackPath, readFileSync(registryPath, "utf8"));
|
|
223
|
+
}
|
|
224
|
+
function readExistingRegistryPruneReceipt(receiptPath, planId, registryPath) {
|
|
225
|
+
if (!existsSync(receiptPath))
|
|
226
|
+
return null;
|
|
227
|
+
let receipt;
|
|
228
|
+
try {
|
|
229
|
+
receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
throw new Error(`Registry prune receipt already exists but is unreadable: ${receiptPath}`);
|
|
233
|
+
}
|
|
234
|
+
if (receipt.planId !== planId || receipt.registryPath !== registryPath || receipt.receiptPath !== receiptPath) {
|
|
235
|
+
throw new Error(`Registry prune receipt already exists for a different execution: ${receiptPath}`);
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(receipt.removed) || !Array.isArray(receipt.skipped) || !receipt.verification) {
|
|
238
|
+
throw new Error(`Registry prune receipt already exists but is malformed: ${receiptPath}`);
|
|
239
|
+
}
|
|
240
|
+
return receipt;
|
|
241
|
+
}
|
|
242
|
+
function removal(name, path, scope) {
|
|
243
|
+
return { name, path, scope };
|
|
244
|
+
}
|
|
245
|
+
function pruneKey(name, path) {
|
|
246
|
+
return JSON.stringify([name, path]);
|
|
247
|
+
}
|
|
248
|
+
function registryPruneReceiptPath(registryPath, planId) {
|
|
249
|
+
assertSafeGeneratedId(planId, "registry prune plan id");
|
|
250
|
+
return join(dirname(registryPath), "registry-prune-receipts", `${planId}.json`);
|
|
251
|
+
}
|
|
252
|
+
function registryPruneRollbackPath(registryPath, planId) {
|
|
253
|
+
assertSafeGeneratedId(planId, "registry prune plan id");
|
|
254
|
+
return join(dirname(registryPath), "registry-prune-rollbacks", `${planId}.json`);
|
|
255
|
+
}
|
|
256
|
+
function writeRegistryPruneReceiptFile(receiptPath, receipt) {
|
|
257
|
+
mkdirSync(dirname(receiptPath), { recursive: true });
|
|
258
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
259
|
+
}
|
package/dist/src/registry.js
CHANGED
|
@@ -51,6 +51,33 @@ export function registerLedger(input) {
|
|
|
51
51
|
return entry;
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
// Remove registrations matching the given (name, path) targets, returning the entries
|
|
55
|
+
// actually removed. The approval-gated registry prune execute composes this under its
|
|
56
|
+
// own registry lock (re-entrant), so classification, rollback copy, mutation, and
|
|
57
|
+
// verification all stay inside one critical section. Matching on both name and path
|
|
58
|
+
// keeps removal precise when two registrations happen to share a path. The registry is
|
|
59
|
+
// only rewritten when something is actually removed, so a no-op target list is inert.
|
|
60
|
+
export function removeRegisteredLedgers(registryPath, targets) {
|
|
61
|
+
const normalized = normalizeRegistryPath(registryPath);
|
|
62
|
+
return withRegistryLock(normalized, () => {
|
|
63
|
+
const registry = readRegistry(normalized);
|
|
64
|
+
const wanted = new Set(targets.map((target) => removalKey(target.name, resolve(target.path))));
|
|
65
|
+
const removed = [];
|
|
66
|
+
const kept = [];
|
|
67
|
+
for (const entry of registry.ledgers) {
|
|
68
|
+
if (wanted.has(removalKey(entry.name, entry.path)))
|
|
69
|
+
removed.push(entry);
|
|
70
|
+
else
|
|
71
|
+
kept.push(entry);
|
|
72
|
+
}
|
|
73
|
+
if (removed.length > 0)
|
|
74
|
+
writeRegistry(normalized, { version: 1, ledgers: kept });
|
|
75
|
+
return removed;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function removalKey(name, path) {
|
|
79
|
+
return JSON.stringify([name, path]);
|
|
80
|
+
}
|
|
54
81
|
function writeRegistry(registryPath, registry) {
|
|
55
82
|
mkdirSync(dirname(registryPath), { recursive: true });
|
|
56
83
|
const tmpPath = `${registryPath}.${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -5,7 +5,17 @@ function doctorAttention(summary) {
|
|
|
5
5
|
}
|
|
6
6
|
function doctorNextAction(blockers, summary, registryPath) {
|
|
7
7
|
if (blockers.length > 0) {
|
|
8
|
-
|
|
8
|
+
const fixes = [];
|
|
9
|
+
if (summary.stale > 0) {
|
|
10
|
+
fixes.push(`run \`artshelf ledgers prune --dry-run --registry ${registryPath}\` to review removing ${summary.stale} missing/stale registration(s)`);
|
|
11
|
+
}
|
|
12
|
+
if (summary.invalid > 0) {
|
|
13
|
+
fixes.push(`repair ${summary.invalid} invalid ledger file(s) above`);
|
|
14
|
+
}
|
|
15
|
+
if (fixes.length === 0) {
|
|
16
|
+
return `repair ${blockers.length} registry/ledger issue(s) above, then re-run \`artshelf doctor\``;
|
|
17
|
+
}
|
|
18
|
+
return `${fixes.join("; ")}, then re-run \`artshelf doctor\``;
|
|
9
19
|
}
|
|
10
20
|
if (summary.warnings > 0) {
|
|
11
21
|
return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf reconcile --dry-run --all --registry ${registryPath}\` to prepare reconcile-ready approvals, then run \`artshelf review --all --registry ${registryPath}\`; nothing is auto-executed`;
|
|
@@ -11,6 +11,14 @@ export function reviewNextAction(summary, scope, ledgerPath, registryPath) {
|
|
|
11
11
|
const broken = summary.invalid + summary.stale;
|
|
12
12
|
const review = statusCommand(scope, "review", ledgerPath);
|
|
13
13
|
if (broken > 0) {
|
|
14
|
+
if (scope === "all" && summary.stale > 0 && registryPath) {
|
|
15
|
+
const fixes = [
|
|
16
|
+
`run \`artshelf ledgers prune --dry-run --registry ${registryPath}\` to review removing ${summary.stale} missing/stale registration(s)`
|
|
17
|
+
];
|
|
18
|
+
if (summary.invalid > 0)
|
|
19
|
+
fixes.push(`repair ${summary.invalid} invalid ledger(s) above (re-register or fix the file)`);
|
|
20
|
+
return `${fixes.join("; ")}, then re-run \`${review}\``;
|
|
21
|
+
}
|
|
14
22
|
const repair = scope === "all" ? "re-register or fix the file" : "fix the file";
|
|
15
23
|
return `repair ${broken} broken ledger(s) above (${repair}), then re-run \`${review}\``;
|
|
16
24
|
}
|
|
@@ -45,7 +53,7 @@ export function printReview(results) {
|
|
|
45
53
|
process.stdout.write(`ledger: ${result.ledger.path}\n`);
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
|
-
function buildReviewDecisions(results, scope) {
|
|
56
|
+
function buildReviewDecisions(results, scope, registryPath) {
|
|
49
57
|
const readyForApproval = [];
|
|
50
58
|
const needsReviewFirst = [];
|
|
51
59
|
const blocked = [];
|
|
@@ -54,6 +62,20 @@ function buildReviewDecisions(results, scope) {
|
|
|
54
62
|
const { ledger, validate, due } = result;
|
|
55
63
|
if (!validate.ok) {
|
|
56
64
|
const status = result.ledgerExists ? "invalid" : "missing";
|
|
65
|
+
// A missing registered ledger is repaired through the approval-gated registry-prune
|
|
66
|
+
// flow rather than hand-editing the registry; an invalid-but-present file still needs
|
|
67
|
+
// a manual re-register/fix.
|
|
68
|
+
if (scope === "all" && status === "missing" && registryPath) {
|
|
69
|
+
blocked.push({
|
|
70
|
+
label: `Prune ${ledger.name} registration (missing)`,
|
|
71
|
+
itemIds: [],
|
|
72
|
+
actionType: "fix-registry",
|
|
73
|
+
approvalTarget: null,
|
|
74
|
+
reason: validate.errors[0] ?? "the registered ledger file is missing",
|
|
75
|
+
nextStep: `run \`artshelf ledgers prune --dry-run --registry ${registryPath} --json\` to review removing it, then approve \`approve artshelf ledgers prune registry ${registryPath} plan <plan-id>\``
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
57
79
|
const repair = scope === "all" ? `re-register or fix ${ledger.path}` : `fix ${ledger.path}`;
|
|
58
80
|
blocked.push({
|
|
59
81
|
label: `Repair ${ledger.name} ledger (${status})`,
|
|
@@ -177,7 +199,7 @@ function reviewCounts(summary) {
|
|
|
177
199
|
};
|
|
178
200
|
}
|
|
179
201
|
export function buildReviewAgentPacketAll(results, summary, registry) {
|
|
180
|
-
const groups = buildReviewDecisions(results, "all");
|
|
202
|
+
const groups = buildReviewDecisions(results, "all", registry.path);
|
|
181
203
|
return {
|
|
182
204
|
schemaVersion: 1,
|
|
183
205
|
command: "review",
|
|
@@ -14,9 +14,17 @@ export function statusCommand(scope, command, ledgerPath) {
|
|
|
14
14
|
return `artshelf ${command} --all`;
|
|
15
15
|
return ledgerPath ? `artshelf ${command} --ledger ${ledgerPath}` : `artshelf ${command}`;
|
|
16
16
|
}
|
|
17
|
-
function statusNextAction(blockers, counts, scope, ledgerPath, registryPath) {
|
|
17
|
+
function statusNextAction(blockers, counts, scope, ledgerPath, registryPath, stale = 0, invalid = 0) {
|
|
18
18
|
if (blockers.length > 0) {
|
|
19
19
|
const verify = statusCommand(scope, "status", ledgerPath);
|
|
20
|
+
if (scope === "all" && stale > 0 && registryPath) {
|
|
21
|
+
const fixes = [
|
|
22
|
+
`run \`artshelf ledgers prune --dry-run --registry ${registryPath}\` to review removing ${stale} missing/stale registration(s)`
|
|
23
|
+
];
|
|
24
|
+
if (invalid > 0)
|
|
25
|
+
fixes.push(`repair ${invalid} invalid ledger(s) above`);
|
|
26
|
+
return `${fixes.join("; ")}, then re-run \`${verify}\``;
|
|
27
|
+
}
|
|
20
28
|
return `repair ${blockers.length} broken ledger(s) above, then re-run \`${verify}\``;
|
|
21
29
|
}
|
|
22
30
|
const review = statusCommand(scope, "review", ledgerPath);
|
|
@@ -59,7 +67,7 @@ export function buildStatusAgentPacketAll(report) {
|
|
|
59
67
|
counts,
|
|
60
68
|
attention: statusAttention(counts),
|
|
61
69
|
blockers,
|
|
62
|
-
nextAction: statusNextAction(blockers, counts, "all", undefined, report.registryPath),
|
|
70
|
+
nextAction: statusNextAction(blockers, counts, "all", undefined, report.registryPath, report.totals.stale, report.totals.invalid),
|
|
63
71
|
verification: `artshelf status --all --agent --registry ${report.registryPath}`
|
|
64
72
|
};
|
|
65
73
|
}
|
|
@@ -6,6 +6,7 @@ Usage:
|
|
|
6
6
|
Available Commands:
|
|
7
7
|
list List and validate registered ledgers
|
|
8
8
|
add Register an existing ledger file
|
|
9
|
+
prune Review and remove registrations whose ledger files are missing
|
|
9
10
|
|
|
10
11
|
Flags:
|
|
11
12
|
-h, --help help for ledgers
|
|
@@ -68,7 +69,7 @@ const COMMAND_GROUPS = [
|
|
|
68
69
|
];
|
|
69
70
|
const NESTED_HELP = new Map([
|
|
70
71
|
["trash", new Set(["list", "purge"])],
|
|
71
|
-
["ledgers", new Set(["list", "add"])]
|
|
72
|
+
["ledgers", new Set(["list", "add", "prune"])]
|
|
72
73
|
]);
|
|
73
74
|
export function resolveHelpKey(parsed) {
|
|
74
75
|
if (parsed.command === "help") {
|
|
@@ -347,6 +348,34 @@ Options:
|
|
|
347
348
|
|
|
348
349
|
Ledgers add registers an existing ledger file in the global registry so --all
|
|
349
350
|
commands and the registry index can find it. The ledger file must already exist.
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
if (command === "ledgers prune") {
|
|
354
|
+
return `Usage:
|
|
355
|
+
artshelf ledgers prune --dry-run [--registry <path>] [--json|--agent]
|
|
356
|
+
artshelf ledgers prune --execute --plan-id <id> [--registry <path>] [--json]
|
|
357
|
+
|
|
358
|
+
Options:
|
|
359
|
+
--dry-run Review prunable registrations and write a reviewed plan
|
|
360
|
+
--execute Apply a reviewed plan, removing the missing registrations
|
|
361
|
+
--plan-id <id> Reviewed plan id to execute (required with --execute)
|
|
362
|
+
--registry <path> Registry path to inspect or prune
|
|
363
|
+
--json Emit machine-readable output
|
|
364
|
+
--agent Emit a compact single-line decision packet (dry-run)
|
|
365
|
+
|
|
366
|
+
Ledgers prune is the approval-gated way to remove registry entries whose ledger
|
|
367
|
+
files are missing, so missing temp ledgers no longer need hand-edited registry
|
|
368
|
+
JSON. Dry-run is read-only except for writing a reviewed plan when action is
|
|
369
|
+
needed; it never mutates the registry. Registrations sharing a duplicate path are
|
|
370
|
+
surfaced as blocked, never pruned. Dry-run prints the exact approval target:
|
|
371
|
+
approve artshelf ledgers prune registry <registry-path> plan <plan-id>
|
|
372
|
+
|
|
373
|
+
Execute binds to one exact registry path and reviewed plan id. It re-checks the
|
|
374
|
+
live registry and only removes entries still classified as prunable (entries
|
|
375
|
+
whose ledger file reappeared or whose path became an ambiguous duplicate are
|
|
376
|
+
skipped). It writes a rollback copy of the registry before mutating and a receipt
|
|
377
|
+
after, both discoverable next to the registry under registry-prune-rollbacks/ and
|
|
378
|
+
registry-prune-receipts/.
|
|
350
379
|
`;
|
|
351
380
|
}
|
|
352
381
|
return renderTopLevelHelp(version);
|
package/docs/agent-monitor.html
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
</li>
|
|
55
55
|
<li>
|
|
56
56
|
<span class="stamp refused">Repair</span>
|
|
57
|
-
<span>Registry stale, ledger invalid, or path state unsafe.
|
|
57
|
+
<span>Registry stale, ledger invalid, or path state unsafe. Review missing registrations with registry prune; fix invalid files manually.</span>
|
|
58
58
|
</li>
|
|
59
59
|
</ul>
|
|
60
60
|
</section>
|
|
@@ -72,6 +72,9 @@ artshelf ledgers add --ledger <repo>/.artshelf/ledger.jsonl --name <pro
|
|
|
72
72
|
<span class="c"># list registered ledgers and their health</span>
|
|
73
73
|
artshelf ledgers list --json
|
|
74
74
|
|
|
75
|
+
<span class="c"># review missing/stale registrations without hand-editing JSON</span>
|
|
76
|
+
artshelf ledgers prune --dry-run --registry <registry-path> --agent
|
|
77
|
+
|
|
75
78
|
<span class="c"># review and due-check every registered ledger at once</span>
|
|
76
79
|
artshelf review --all --agent
|
|
77
80
|
artshelf due --all --json
|
|
@@ -112,11 +115,12 @@ artshelf doctor --agent
|
|
|
112
115
|
artshelf status --all --json
|
|
113
116
|
artshelf status --all --agent</code></pre>
|
|
114
117
|
<p>
|
|
115
|
-
If
|
|
116
|
-
|
|
117
|
-
<code>
|
|
118
|
-
<code>
|
|
119
|
-
before any
|
|
118
|
+
If registered ledgers are stale/missing, run <code>ledgers prune --dry-run --registry <registry-path></code>
|
|
119
|
+
to review removing missing registrations, then approve the exact registry/prune plan before execute.
|
|
120
|
+
If valid ledgers report missing-path records, run <code>review --all --json</code> for
|
|
121
|
+
affected classifications, <code>validate --all --json</code> for provenance signals, then
|
|
122
|
+
<code>reconcile --dry-run --all --json --registry <registry-path></code> before any
|
|
123
|
+
reconcile approve/execute step.
|
|
120
124
|
</p>
|
|
121
125
|
<p>
|
|
122
126
|
Use <code>--agent</code> for concise monitor decisions and exact next
|
|
@@ -131,6 +135,9 @@ artshelf status --all --agent</code></pre>
|
|
|
131
135
|
artshelf cleanup --dry-run --json
|
|
132
136
|
artshelf cleanup --dry-run --all --json
|
|
133
137
|
|
|
138
|
+
<span class="c"># preview stale registry registration pruning</span>
|
|
139
|
+
artshelf ledgers prune --dry-run --registry <registry-path> --json
|
|
140
|
+
|
|
134
141
|
<span class="c"># what is sitting in trash, and how old it is</span>
|
|
135
142
|
artshelf trash list --ledger <ledger-path> --json
|
|
136
143
|
artshelf trash list --all --json
|
|
@@ -139,7 +146,7 @@ artshelf trash list --all --json
|
|
|
139
146
|
artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json</code></pre>
|
|
140
147
|
<p>
|
|
141
148
|
Dry-runs may write reusable plan files when entries exist. No-op dry-runs
|
|
142
|
-
report <code>not-created</code>. Matching cleanup dry-runs reuse the existing plan id.
|
|
149
|
+
report <code>not-created</code>. Matching cleanup and registry-prune dry-runs reuse the existing plan id.
|
|
143
150
|
</p>
|
|
144
151
|
</section>
|
|
145
152
|
|
|
@@ -148,10 +155,11 @@ artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --js
|
|
|
148
155
|
<p>Do not scan arbitrary filesystem locations unless the user opted into that discovery scope.</p>
|
|
149
156
|
<div class="callout" data-kind="boundary">
|
|
150
157
|
<span class="callout-label">Never scheduled</span>
|
|
151
|
-
<p>These
|
|
158
|
+
<p>These commands require exact human approval and must never run from a monitor job:</p>
|
|
152
159
|
</div>
|
|
153
160
|
<pre><code><span class="c"># mutating commands: approval only, never from a schedule</span>
|
|
154
161
|
artshelf cleanup --execute --plan-id <id>
|
|
162
|
+
artshelf ledgers prune --execute --plan-id <id>
|
|
155
163
|
artshelf trash purge --execute --plan-id <id></code></pre>
|
|
156
164
|
</section>
|
|
157
165
|
</article>
|
package/docs/agent-review.html
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
<li>Start with health and registry-level context: <code>status --all --agent</code>,
|
|
48
48
|
then read raw pressure points from <code>review --all --agent</code>
|
|
49
49
|
and <code>trash list --all --json</code>.</li>
|
|
50
|
-
<li>For stale
|
|
50
|
+
<li>For stale registered ledgers, run <code>ledgers prune --dry-run --registry <registry-path></code>
|
|
51
|
+
to review removing missing registrations; duplicate paths remain manual
|
|
52
|
+
<code>registry-problem</code> blockers.</li>
|
|
53
|
+
<li>For missing-path records inside valid ledgers, run <code>validate --all --json</code>
|
|
51
54
|
and then <code>reconcile --dry-run --all --json --registry <registry-path></code>
|
|
52
55
|
to produce reviewed-safe plans.</li>
|
|
53
56
|
<li>Run explicit-ledger purge dry-runs only when old trash needs review.</li>
|
|
@@ -110,12 +113,14 @@ Dry-run only. No execute, resolve, or delete ran.</code></pre>
|
|
|
110
113
|
<pre><code>approve artshelf cleanup ledger <ledger-path> plan <plan-id>
|
|
111
114
|
approve artshelf trash purge ledger <ledger-path> plan <purge-plan-id>
|
|
112
115
|
approve artshelf resolve missing ledger <ledger-path> ids <id...>
|
|
113
|
-
approve artshelf reconcile ledger <ledger-path> plan <plan-id>
|
|
116
|
+
approve artshelf reconcile ledger <ledger-path> plan <plan-id>
|
|
117
|
+
approve artshelf ledgers prune registry <registry-path> plan <plan-id></code></pre>
|
|
114
118
|
<div class="callout" data-kind="boundary">
|
|
115
119
|
<span class="callout-label">Hard boundary</span>
|
|
116
120
|
<p>
|
|
117
121
|
Never execute from a read-only preview id. Never generate a fresh plan and
|
|
118
|
-
execute it in the same step. After
|
|
122
|
+
execute it in the same step. After registry-prune approval, verify with
|
|
123
|
+
<code>artshelf ledgers list --json</code>; after any approved ledger action, verify quiet with
|
|
119
124
|
<code>artshelf review --all --json</code>.
|
|
120
125
|
</p>
|
|
121
126
|
</div>
|
package/docs/agent-usage.html
CHANGED
|
@@ -90,13 +90,13 @@
|
|
|
90
90
|
<section>
|
|
91
91
|
<h2>Render modes</h2>
|
|
92
92
|
<p>
|
|
93
|
-
<code>review</code>, <code>status</code>, and <code>
|
|
93
|
+
<code>review</code>, <code>status</code>, <code>doctor</code>, and <code>ledgers prune --dry-run</code> share agent-oriented render modes
|
|
94
94
|
so the same data fits both people and agents. Reach for <code>--agent</code> when an agent
|
|
95
95
|
decides and acts; reach for <code>--json</code> for full record, plan, or health detail.
|
|
96
96
|
</p>
|
|
97
97
|
<dl class="def-rows">
|
|
98
98
|
<div><dt>default</dt><dd>human render: scannable grouped counts, attention states, and a short next action for a person at the terminal</dd></div>
|
|
99
|
-
<div><dt>--agent</dt><dd>a deterministic, token-efficient decision packet (single-line compact JSON) with health, counts, classifications,
|
|
99
|
+
<div><dt>--agent</dt><dd>a deterministic, token-efficient decision packet (single-line compact JSON) with health, counts, classifications, blockers, approval targets where applicable, and a verification command</dd></div>
|
|
100
100
|
<div><dt>--json</dt><dd>the backward-compatible public audit contract: complete machine-readable JSON for debugging and integrations</dd></div>
|
|
101
101
|
</dl>
|
|
102
102
|
</section>
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
</li>
|
|
115
115
|
<li>
|
|
116
116
|
<span class="stamp refused">Blocked</span>
|
|
117
|
-
<span>Registry is stale, a path is suspicious, or the action would delete without a purge plan.</span>
|
|
117
|
+
<span>Registry is stale, a path is suspicious, or the action would delete without a purge plan. Missing registrations use a reviewed registry-prune plan, not hand-edited JSON.</span>
|
|
118
118
|
</li>
|
|
119
119
|
</ul>
|
|
120
120
|
</section>
|
package/docs/agent-usage.md
CHANGED
|
@@ -47,19 +47,20 @@ The browsable docs split the workflow into focused child pages:
|
|
|
47
47
|
- Scheduled checks read and report only; set `ARTSHELF_NO_UPDATE_CHECK=1` when
|
|
48
48
|
they must avoid npm network checks and update-cache writes.
|
|
49
49
|
- Review output is a decision packet, not raw counts.
|
|
50
|
-
-
|
|
50
|
+
- Stale registry entries route through `ledgers prune --dry-run`, not manual JSON edits.
|
|
51
|
+
- Approval names the exact ledger or registry, plan id, or record ids.
|
|
51
52
|
- Every approved action ends with a read-only verification.
|
|
52
53
|
|
|
53
54
|
## Render modes
|
|
54
55
|
|
|
55
|
-
`review`, `status`, and `
|
|
56
|
+
`review`, `status`, `doctor`, and `ledgers prune --dry-run` share agent-oriented render modes so the same data fits
|
|
56
57
|
both people and agents:
|
|
57
58
|
|
|
58
59
|
- **default**: a human render — scannable grouped counts, attention states, and a
|
|
59
60
|
short next action for a person at the terminal.
|
|
60
61
|
- **`--agent`**: a deterministic, token-efficient decision packet (single-line
|
|
61
|
-
compact JSON) with health, counts, classifications,
|
|
62
|
-
|
|
62
|
+
compact JSON) with health, counts, classifications, blockers, approval targets where applicable, and a
|
|
63
|
+
verification command. Use it when an agent acts on the result.
|
|
63
64
|
- **`--json`**: the backward-compatible public audit contract — complete
|
|
64
65
|
machine-readable JSON for debugging and integrations.
|
|
65
66
|
|
package/docs/install.html
CHANGED
|
@@ -197,6 +197,15 @@ artshelf ledgers add --ledger <repo>/.artshelf/ledger.jsonl --name <pro
|
|
|
197
197
|
artshelf ledgers list --json
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
If `ledgers list`, `doctor`, or `status --all --agent` reports stale/missing
|
|
201
|
+
registrations, review a registry-prune plan instead of hand-editing the registry:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
artshelf ledgers prune --dry-run --registry <registry-path> --json
|
|
205
|
+
# after approval: approve artshelf ledgers prune registry <registry-path> plan <plan-id>
|
|
206
|
+
artshelf ledgers prune --execute --plan-id <plan-id> --registry <registry-path> --json
|
|
207
|
+
```
|
|
208
|
+
|
|
200
209
|
## 4. Scheduled review (ask the user first)
|
|
201
210
|
|
|
202
211
|
Ask the user whether they want a scheduled review job before creating one.
|
|
@@ -209,8 +218,8 @@ artshelf review --all --json
|
|
|
209
218
|
```
|
|
210
219
|
|
|
211
220
|
and reports what needs attention. Scheduled jobs are review and report only:
|
|
212
|
-
never schedule `artshelf cleanup --execute
|
|
213
|
-
--execute`.
|
|
221
|
+
never schedule `artshelf cleanup --execute`, `artshelf ledgers prune --execute`,
|
|
222
|
+
or `artshelf trash purge --execute`.
|
|
214
223
|
|
|
215
224
|
## 5. Verify and report
|
|
216
225
|
|
package/docs/reference.html
CHANGED
|
@@ -46,8 +46,9 @@
|
|
|
46
46
|
Run <code>artshelf help</code> for the grouped command list. Use
|
|
47
47
|
<code>artshelf <command> --help</code> or
|
|
48
48
|
<code>artshelf help <command></code> for focused help; nested commands
|
|
49
|
-
such as <code>artshelf trash purge --help</code
|
|
50
|
-
<code>artshelf ledgers add --help</code
|
|
49
|
+
such as <code>artshelf trash purge --help</code>,
|
|
50
|
+
<code>artshelf ledgers add --help</code>, and
|
|
51
|
+
<code>artshelf ledgers prune --help</code> show only that subcommand.
|
|
51
52
|
</p>
|
|
52
53
|
|
|
53
54
|
<section class="cmd">
|
|
@@ -84,6 +85,29 @@ artshelf ledgers add --ledger <path> [--name <project>] [--scope rep
|
|
|
84
85
|
</p>
|
|
85
86
|
</section>
|
|
86
87
|
|
|
88
|
+
<section class="cmd">
|
|
89
|
+
<div class="cmd-head"><h2>artshelf ledgers prune</h2><span class="cmd-flag approval">approval-gated</span></div>
|
|
90
|
+
<pre><code><span class="c"># review stale/missing registrations before touching the registry</span>
|
|
91
|
+
artshelf ledgers prune --dry-run [--registry <path>] [--json|--agent]
|
|
92
|
+
|
|
93
|
+
<span class="c"># execute exactly one reviewed registry-prune plan</span>
|
|
94
|
+
artshelf ledgers prune --execute --plan-id <id> [--registry <path>] [--json]</code></pre>
|
|
95
|
+
<p>
|
|
96
|
+
<code>prune --dry-run</code> is the registry-safe way to remove registrations whose
|
|
97
|
+
ledger files are missing. It writes a reviewed plan only when entries are
|
|
98
|
+
prunable, reuses matching unexecuted plans, prints
|
|
99
|
+
<code>approve artshelf ledgers prune registry <registry-path> plan <plan-id></code>,
|
|
100
|
+
and never mutates the registry during review. Duplicate registry paths are
|
|
101
|
+
blocked for manual repair.
|
|
102
|
+
</p>
|
|
103
|
+
<p>
|
|
104
|
+
<code>prune --execute</code> binds to one registry path and reviewed plan id,
|
|
105
|
+
re-checks the live registry, skips stale plan entries whose files reappeared
|
|
106
|
+
or became ambiguous, writes a rollback copy, removes only still-prunable
|
|
107
|
+
registrations, and writes a receipt.
|
|
108
|
+
</p>
|
|
109
|
+
</section>
|
|
110
|
+
|
|
87
111
|
<section class="cmd">
|
|
88
112
|
<div class="cmd-head"><h2>artshelf list / find / get</h2><span class="cmd-flag readonly">read-only</span></div>
|
|
89
113
|
<pre><code><span class="c"># browse ledger entries by status</span>
|
|
@@ -133,8 +157,10 @@ artshelf doctor [--agent] [--json]</code></pre>
|
|
|
133
157
|
summary, including reconcile entry and blocked counts, plus the next safe action.
|
|
134
158
|
<code>status</code> is the lightweight dashboard of counts; <code>--all --json</code> is
|
|
135
159
|
cron-friendly. <code>doctor</code> reports CLI version, resolved paths, registry health,
|
|
136
|
-
and the cleanup safety posture.
|
|
137
|
-
|
|
160
|
+
and the cleanup safety posture. Missing registered ledger files now point to
|
|
161
|
+
<code>ledgers prune --dry-run</code>; invalid-but-present ledgers still need manual
|
|
162
|
+
repair. All three also accept <code>--agent</code> for a deterministic, token-efficient
|
|
163
|
+
decision packet (see Output mode).
|
|
138
164
|
</p>
|
|
139
165
|
</section>
|
|
140
166
|
|
|
@@ -250,9 +276,9 @@ artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
|
250
276
|
output stays clean.
|
|
251
277
|
</p>
|
|
252
278
|
<p>
|
|
253
|
-
<code>review</code>, <code>status</code>, and <code>
|
|
279
|
+
<code>review</code>, <code>status</code>, <code>doctor</code>, and <code>ledgers prune --dry-run</code> add <code>--agent</code>:
|
|
254
280
|
a deterministic, token-efficient decision packet emitted as a single line of compact JSON.
|
|
255
|
-
It names health
|
|
281
|
+
It names the relevant health or prune counts, classifications, blockers, approval targets where applicable, and the command
|
|
256
282
|
to re-run for verification, so an agent can act without parsing decorative output.
|
|
257
283
|
</p>
|
|
258
284
|
<p>
|
|
@@ -264,7 +290,7 @@ artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
|
264
290
|
<table class="opts">
|
|
265
291
|
<tr><th>option</th><th>meaning</th></tr>
|
|
266
292
|
<tr><td>(default)</td><td>human render: grouped counts, ✓/⚠ attention glyphs, and a short next action</td></tr>
|
|
267
|
-
<tr><td>--agent</td><td>token-efficient decision packet for agents on <code>review</code>, <code>status</code>, <code>doctor</code></td></tr>
|
|
293
|
+
<tr><td>--agent</td><td>token-efficient decision packet for agents on <code>review</code>, <code>status</code>, <code>doctor</code>, and <code>ledgers prune --dry-run</code></td></tr>
|
|
268
294
|
<tr><td>--json</td><td>full machine-readable audit JSON on commands that return data</td></tr>
|
|
269
295
|
</table>
|
|
270
296
|
</section>
|
|
@@ -279,7 +305,7 @@ artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
|
279
305
|
<tr><th>option</th><th>meaning</th></tr>
|
|
280
306
|
<tr><td>--ledger <path></td><td>target an explicit JSONL ledger</td></tr>
|
|
281
307
|
<tr><td>--registry <path></td><td>target an explicit ledger registry</td></tr>
|
|
282
|
-
<tr><td>--all</td><td>read every registered ledger on commands that support discovery (<code>list</code>, <code>find</code>, <code>get</code>, <code>due</code>, <code>validate</code>, <code>review</code>, <code>status</code>, <code>cleanup --dry-run</code>, <code>reconcile --dry-run</code>, <code>trash list</code>)</td></tr>
|
|
308
|
+
<tr><td>--all</td><td>read every registered ledger on commands that support discovery (<code>list</code>, <code>find</code>, <code>get</code>, <code>due</code>, <code>validate</code>, <code>review</code>, <code>status</code>, <code>cleanup --dry-run</code>, <code>reconcile --dry-run</code>, <code>trash list</code>); registry pruning is scoped by <code>--registry</code>, not <code>--all</code></td></tr>
|
|
283
309
|
</table>
|
|
284
310
|
</section>
|
|
285
311
|
|
|
@@ -320,7 +346,10 @@ artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
|
320
346
|
repo it defaults to <code>~/.artshelf/ledger.jsonl</code>. A user-level registry at
|
|
321
347
|
<code>~/.artshelf/ledgers.json</code> is the discovery index for <code>--all</code>
|
|
322
348
|
review, status, cleanup dry-run, reconcile dry-run, and trash-list; project records stay in their own
|
|
323
|
-
repo-local ledgers.
|
|
349
|
+
repo-local ledgers. Registry-prune plans, rollback copies, and receipts live next to the
|
|
350
|
+
selected registry under <code>registry-prune-plans/</code>,
|
|
351
|
+
<code>registry-prune-rollbacks/</code>, and <code>registry-prune-receipts/</code>.
|
|
352
|
+
Automatic update checks cache their last npm result at
|
|
324
353
|
<code>~/.artshelf/update-check.json</code> by default, with a long TTL
|
|
325
354
|
for update-available results and a shorter TTL for no-update or failed
|
|
326
355
|
results.
|
package/package.json
CHANGED
package/skills/artshelf/SKILL.md
CHANGED
|
@@ -96,8 +96,9 @@ artshelf trash list --all --json
|
|
|
96
96
|
|
|
97
97
|
`artshelf ledgers list --json` reports per-ledger validation status. `--plain`
|
|
98
98
|
skips validation. `--all` is for discovery and review, not mutation permission.
|
|
99
|
-
Use `--agent` on `review`, `status`,
|
|
100
|
-
`--json` for full audit/API payloads, custom
|
|
99
|
+
Use `--agent` on `review`, `status`, `doctor`, and `ledgers prune --dry-run`
|
|
100
|
+
for compact decisions; use `--json` for full audit/API payloads, custom
|
|
101
|
+
rendering, or debugging.
|
|
101
102
|
|
|
102
103
|
Register existing project ledgers explicitly:
|
|
103
104
|
|
|
@@ -108,7 +109,7 @@ artshelf ledgers add --ledger <repo>/.artshelf/ledger.jsonl --name <project> --s
|
|
|
108
109
|
### Scheduled Review
|
|
109
110
|
|
|
110
111
|
Scheduled jobs are review/report only. Set `ARTSHELF_NO_UPDATE_CHECK=1` for no
|
|
111
|
-
npm network/cache writes. Reports should name the ledger path and plan ids. They may run:
|
|
112
|
+
npm network/cache writes. Reports should name the ledger path or registry path and plan ids. They may run:
|
|
112
113
|
|
|
113
114
|
```bash
|
|
114
115
|
artshelf validate --json
|
|
@@ -119,6 +120,7 @@ artshelf cleanup --dry-run --all --json
|
|
|
119
120
|
artshelf trash list --all --json
|
|
120
121
|
artshelf doctor --json
|
|
121
122
|
artshelf status --all --json
|
|
123
|
+
artshelf ledgers prune --dry-run --registry <registry-path> --json
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
For old-trash review, dry-run purge only for an explicit ledger:
|
|
@@ -128,10 +130,12 @@ artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json
|
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
Do not scan arbitrary filesystem locations for ledgers unless the user opted
|
|
131
|
-
into that discovery scope.
|
|
133
|
+
into that discovery scope. Scheduled jobs may review registry-prune plans but
|
|
134
|
+
must not execute them. Never schedule cleanup, registry-prune, or purge execution:
|
|
132
135
|
|
|
133
136
|
```bash
|
|
134
137
|
artshelf cleanup --execute --plan-id <id>
|
|
138
|
+
artshelf ledgers prune --execute --plan-id <id>
|
|
135
139
|
artshelf trash purge --execute --plan-id <id>
|
|
136
140
|
```
|
|
137
141
|
|
|
@@ -142,16 +146,17 @@ count dump.
|
|
|
142
146
|
|
|
143
147
|
1. Run read-only review first: `artshelf status --all --agent` for machine health,
|
|
144
148
|
then `artshelf review --all --agent`, and `artshelf trash list --all --json`.
|
|
145
|
-
2. If stale
|
|
146
|
-
3. If
|
|
147
|
-
4.
|
|
149
|
+
2. If stale registered ledgers exist, run `artshelf ledgers prune --dry-run --registry <registry-path> --json` to review removing missing registrations; duplicate paths remain manual registry-problem blockers.
|
|
150
|
+
3. If missing-path warnings exist inside valid ledgers, run `artshelf validate --all --json` then `artshelf reconcile --dry-run --all --json --registry <registry-path>` for renames, moves, deletes, topology after handoff/finalization, and `.shelf`/`.artshelf` migration fallout.
|
|
151
|
+
4. If cleanup attention exists, run `artshelf cleanup --dry-run --all --json`.
|
|
152
|
+
5. Classify candidates as `trash-safe`, `needs-human-review`,
|
|
148
153
|
`resolve-candidate`, or `registry-problem`.
|
|
149
|
-
|
|
154
|
+
6. Use the built-in `--agent` packet when the CLI output is enough to decide,
|
|
150
155
|
because it is deterministic and token-efficient. Use
|
|
151
156
|
`ArtshelfReviewReport` from `schemas/artshelf-review-report.schema.json` and `examples/artshelf-review-report.json` when you need a host-specific card, attachment, or richer audit record.
|
|
152
|
-
|
|
157
|
+
7. Render full packets with `scripts/render-review-report.mjs`; keep
|
|
153
158
|
`decisionSummary` in audit, while `decisionGroups` drive counts. Emojis are encouraged only in host-specific wrappers, not the renderer.
|
|
154
|
-
|
|
159
|
+
8. Always include the exact approval target in the message body as a fallback.
|
|
155
160
|
Do not paste the whole packet into chat unless the user asks for it.
|
|
156
161
|
|
|
157
162
|
### Review Plan Report Schema
|
|
@@ -201,23 +206,15 @@ approve artshelf cleanup ledger <ledger-path> plan <plan-id>
|
|
|
201
206
|
approve artshelf trash purge ledger <ledger-path> plan <purge-plan-id>
|
|
202
207
|
approve artshelf resolve missing ledger <ledger-path> ids <id...>
|
|
203
208
|
approve artshelf reconcile ledger <ledger-path> plan <plan-id>
|
|
209
|
+
approve artshelf ledgers prune registry <registry-path> plan <plan-id>
|
|
204
210
|
```
|
|
205
211
|
|
|
206
212
|
Never execute from a read-only preview id. Never generate a fresh plan and
|
|
207
|
-
execute it in the same step. After cleanup or
|
|
213
|
+
execute it in the same step. After cleanup, resolve, or registry-prune approval, verify with `artshelf review --all --json` and `artshelf ledgers list --json`; after trash purge approval, also run `artshelf trash list --all --json`.
|
|
208
214
|
|
|
209
215
|
## Clean
|
|
210
216
|
|
|
211
|
-
Read-only and dry-run commands are safe
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
artshelf validate --json
|
|
215
|
-
artshelf validate --all --json
|
|
216
|
-
artshelf due --json
|
|
217
|
-
artshelf due --all --json
|
|
218
|
-
artshelf cleanup --dry-run --json
|
|
219
|
-
artshelf cleanup --dry-run --all --json
|
|
220
|
-
```
|
|
217
|
+
Read-only and dry-run commands listed above are safe. Registry-prune execution requires approval naming the reviewed registry and plan id (`artshelf ledgers prune --execute --plan-id <id> --registry <registry-path> --json`); it removes only registrations whose ledger files are still missing, writes a rollback copy and receipt next to the registry, and skips entries that changed after review.
|
|
221
218
|
|
|
222
219
|
Cleanup execution requires approval naming the reviewed ledger and plan id:
|
|
223
220
|
|