artshelf 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -64,6 +64,45 @@
64
64
  and `artshelf ledgers help` aliases, advertised short `-h`/`-v` flags, and
65
65
  reclassified `--ledger`, `--registry`, and `--all` as command-specific scope
66
66
  flags instead of global options.
67
+ - Added an `--agent` render mode to `artshelf review`, `status`, and `doctor`: a
68
+ compact, deterministic single-line JSON decision packet (health, counts,
69
+ attention categories or classified decision groups, blockers, the next safe
70
+ action, and a verification command) tuned for agents acting on results.
71
+ `--agent` takes precedence over `--json`, while `--json` stays the full,
72
+ backward-compatible audit report. The default human renders of these three
73
+ commands now lead each ledger and summary line with a `✓`/`⚠` attention glyph
74
+ (plain Unicode, no color) so redirected output stays clean.
75
+ - Shortened the automatic update-check cache so no-update, failed, missing, or
76
+ null results expire after 1 hour while update-available results keep the
77
+ 24-hour TTL, letting newly published releases surface sooner. `artshelf update`
78
+ forces a fresh latest-version check instead of trusting a stale no-update
79
+ cache, `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed TTL
80
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility), and a
81
+ non-numeric TTL value falls back to the default instead of disabling expiry.
82
+
83
+ ## [0.10.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.0...artshelf-v0.10.1) (2026-06-12)
84
+
85
+
86
+ ### Bug Fixes
87
+
88
+ * **cli:** shorten no-update cache TTL for update checks ([d41e49e](https://github.com/calvinnwq/artshelf/commit/d41e49e7d5da02dfaa86fb70eaa7d5e7fb3d543e))
89
+ * **cli:** split update-check cache TTL so new releases surface sooner ([5afcfaa](https://github.com/calvinnwq/artshelf/commit/5afcfaafac4941b71f6a84c694139a64774a1d59))
90
+
91
+ ## [0.10.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.9.0...artshelf-v0.10.0) (2026-06-12)
92
+
93
+
94
+ ### Features
95
+
96
+ * **cli:** add --agent decision-packet render mode for review, status, and doctor ([4d2dee0](https://github.com/calvinnwq/artshelf/commit/4d2dee099569803b887ae49438b0747d1330ec5d))
97
+ * **cli:** add --agent render mode and implement status --agent ([36f8e78](https://github.com/calvinnwq/artshelf/commit/36f8e7839d535fcabddadfc616ba518a9b444114))
98
+ * **cli:** add ✓/⚠ attention glyphs to human renders of status/doctor/review ([6f6cbe8](https://github.com/calvinnwq/artshelf/commit/6f6cbe85d54886cfd137791863e1b3554ca908f0))
99
+ * **cli:** implement artshelf doctor --agent compact decision packet ([d9abd4e](https://github.com/calvinnwq/artshelf/commit/d9abd4e75a7f4b2898eeacc3b3404221f4456bd4))
100
+ * **cli:** implement artshelf review --agent compact decision packet ([6f5476c](https://github.com/calvinnwq/artshelf/commit/6f5476ca987de3190f7a8760c6bb9c1efa8b9fce))
101
+
102
+
103
+ ### Bug Fixes
104
+
105
+ * **cli:** preserve ledger scope in agent next actions ([a583683](https://github.com/calvinnwq/artshelf/commit/a583683064cdd16dd929766dc01f23fc31fa50e7))
67
106
 
68
107
  ## [0.9.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.8.0...artshelf-v0.9.0) (2026-06-11)
69
108
 
package/README.md CHANGED
@@ -62,12 +62,14 @@ install with `npm unlink -g artshelf`.
62
62
  </details>
63
63
 
64
64
  Artshelf checks npm occasionally and prints a non-blocking notice to stderr when
65
- a newer published version is available. Run `artshelf update` only for npm
66
- global installs; it upgrades with `npm install -g artshelf@latest`. pnpm global
67
- installs should update with `pnpm add -g artshelf@latest`, and source installs
68
- still update by pulling, rebuilding, and linking the checkout. Set
69
- `ARTSHELF_NO_UPDATE_CHECK=1` for scheduled jobs that must avoid network and
70
- update-cache writes.
65
+ a newer published version is available. Available-update results are cached for
66
+ 24 hours by default; failed, missing, or no-update results are cached for 1 hour
67
+ so a newly published release is noticed sooner. Run `artshelf update` only for
68
+ npm global installs; it forces a fresh latest-version check before upgrading
69
+ with `npm install -g artshelf@latest`. pnpm global installs should update with
70
+ `pnpm add -g artshelf@latest`, and source installs still update by pulling,
71
+ rebuilding, and linking the checkout. Set `ARTSHELF_NO_UPDATE_CHECK=1` for
72
+ scheduled jobs that must avoid network and update-cache writes.
71
73
 
72
74
  ### Recommended agent setup
73
75
 
@@ -112,6 +114,8 @@ destructive deletion.
112
114
  - **Trash before delete** — `cleanup=delete` stays refused; physical deletion
113
115
  needs its own reviewed trash purge. No silent deletion, ever.
114
116
  - **`--json` on every command**, so agents can act on structured output.
117
+ - **`--agent` on `review`/`status`/`doctor`**, a compact, token-efficient
118
+ decision packet for agents, while the default render stays human-scannable.
115
119
 
116
120
  ## Reference
117
121
 
@@ -143,7 +147,8 @@ artshelf resolve <id> --status resolved --reason "inspected and no longer needed
143
147
  Use `artshelf help` for a grouped command list, then `artshelf <command> --help`
144
148
  or `artshelf help <command>` for focused details. Nested commands such as
145
149
  `artshelf trash purge --help` and `artshelf ledgers add --help` show only that
146
- subcommand. All core commands support `--json`; `--ledger`, `--registry`, and
150
+ subcommand. All core commands support `--json`; `review`, `status`, and `doctor`
151
+ also take `--agent` for a compact decision packet; `--ledger`, `--registry`, and
147
152
  `--all` are scope flags only on commands that list them.
148
153
  </details>
149
154
 
package/SPEC.md CHANGED
@@ -242,7 +242,9 @@ files or writing a plan.
242
242
 
243
243
  ```bash
244
244
  artshelf review --json
245
+ artshelf review --agent
245
246
  artshelf review --all --json
247
+ artshelf review --all --agent
246
248
  ```
247
249
 
248
250
  `review` is the compact report surface for scheduled checks. `--all` reads every
@@ -257,6 +259,16 @@ triage count and states the same next safe action (repair broken ledgers, dry-ru
257
259
  cleanup, inspect missing paths, or nothing to do). Review never writes a plan, so
258
260
  the next action always points at an explicit follow-up command.
259
261
 
262
+ `review`, `status`, and `doctor` share three render modes. The default human
263
+ render leads each ledger and summary line with a `✓`/`⚠` attention glyph; `--json`
264
+ stays the full, backward-compatible audit report; and `--agent` emits a compact,
265
+ deterministic single-line JSON decision packet for agents, taking precedence over
266
+ `--json` when both are passed. For `review`, the packet sorts records into
267
+ ready-for-approval, needs-review-first, and blocked groups. Because review is
268
+ read-only and never mints a cleanup plan, the only exact approval target it emits
269
+ is `resolve missing`; cleanup-eligible records stay needs-review-first and point
270
+ at `cleanup --dry-run`, which mints the reviewed plan id to approve.
271
+
260
272
  ### `artshelf doctor`
261
273
 
262
274
  Reports whether Artshelf is healthy on the current machine without mutating
@@ -265,6 +277,7 @@ anything.
265
277
  ```bash
266
278
  artshelf doctor
267
279
  artshelf doctor --json
280
+ artshelf doctor --agent
268
281
  artshelf doctor --ledger <path>
269
282
  artshelf doctor --registry <path>
270
283
  ```
@@ -284,7 +297,11 @@ A healthy machine exits 0. A broken registry file or any stale or invalid
284
297
  registered ledger exits non-zero with actionable errors. Humans should run
285
298
  `artshelf doctor` after install or when `--all` commands behave unexpectedly; agents
286
299
  may run it on a schedule to catch stale registry entries before relying on
287
- cleanup planning. Doctor never creates plans, receipts, or records.
300
+ cleanup planning. Doctor never creates plans, receipts, or records. Like `review`
301
+ and `status`, `doctor` accepts `--agent` for a compact single-line JSON decision
302
+ packet (health, registry and registered-ledger health, blockers, cleanup-safety
303
+ posture, next action, and a verify command); `--agent` takes precedence over
304
+ `--json`.
288
305
 
289
306
  ### `artshelf status`
290
307
 
@@ -293,7 +310,9 @@ The lightweight daily "what is going on?" view across ledgers.
293
310
  ```bash
294
311
  artshelf status
295
312
  artshelf status --json
313
+ artshelf status --agent
296
314
  artshelf status --all --json
315
+ artshelf status --all --agent
297
316
  artshelf status --all --registry <path> --json
298
317
  ```
299
318
 
@@ -312,7 +331,9 @@ never creates plans or receipts and never mutates records. A healthy machine
312
331
  exits 0. In `--all` mode, a broken registry or any stale or invalid registered
313
332
  ledger exits non-zero. Due entries are normal operational state and do not change
314
333
  the exit code. With single `--ledger`, a not-yet-created ledger reports empty
315
- counts.
334
+ counts. Like `review` and `doctor`, `status` accepts `--agent` for a compact
335
+ single-line JSON decision packet (health, counts, attention categories, blockers,
336
+ next action, and a verify command); `--agent` takes precedence over `--json`.
316
337
 
317
338
  ### `artshelf update`
318
339
 
@@ -332,12 +353,17 @@ Rules:
332
353
  - Read-only command guarantees refer to ledger and artifact mutation; automatic
333
354
  update-check cache writes are separate and can be disabled.
334
355
  - Update notices must never pollute JSON stdout.
335
- - Automatic checks cache successful and failed latest-version lookups at
336
- `~/.artshelf/update-check.json` by default, with a 24-hour TTL.
356
+ - Automatic checks cache latest-version lookups at
357
+ `~/.artshelf/update-check.json` by default. Cached update-available results
358
+ (`latest > current`) keep the long 24-hour TTL; cached no-update, failed,
359
+ missing, or null results use a shorter 1-hour TTL so newly published releases
360
+ are noticed sooner.
337
361
  - `ARTSHELF_NO_UPDATE_CHECK=1` disables automatic checks for scheduled jobs,
338
362
  tests, and no-network environments.
339
363
  - `ARTSHELF_UPDATE_CACHE` overrides the update-cache path,
340
- `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the cache TTL, and
364
+ `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the update-available cache TTL,
365
+ `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed cache TTL
366
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility), and
341
367
  `ARTSHELF_NPM_REGISTRY_URL` overrides the npm latest-version endpoint.
342
368
  - `ARTSHELF_LATEST_VERSION` overrides the discovered latest version for tests.
343
369
  - `ARTSHELF_UPDATE_DRY_RUN=1` makes `artshelf update` report the npm command it
@@ -520,9 +546,13 @@ V1 also supports a user-level registry of known ledgers:
520
546
  overrides it for tests and controlled runs; legacy `SHELF_NOW` is read only
521
547
  when `ARTSHELF_NOW` is unset.
522
548
  - Automatic npm update checks cache their latest-version result at
523
- `~/.artshelf/update-check.json` by default. `ARTSHELF_NO_UPDATE_CHECK=1`
524
- disables automatic checks, `ARTSHELF_UPDATE_CACHE` overrides the cache path,
525
- and `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the cache TTL.
549
+ `~/.artshelf/update-check.json` by default. Cached update-available results
550
+ use the long 24-hour TTL; cached no-update, failed, missing, or null results
551
+ use a shorter 1-hour TTL. `ARTSHELF_NO_UPDATE_CHECK=1` disables automatic
552
+ checks, `ARTSHELF_UPDATE_CACHE` overrides the cache path,
553
+ `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the update-available TTL, and
554
+ `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed TTL
555
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility).
526
556
  - `put` registers the ledger it writes to.
527
557
  - `ledgers add` registers an existing ledger explicitly.
528
558
  - `--all` reads registered ledgers as one review surface.
@@ -773,6 +803,8 @@ human review.
773
803
  - Package includes the deterministic `ArtshelfReviewReport` schema, canonical
774
804
  example, and portable renderer script for agent-rendered review reports.
775
805
  - All core commands support `--json`.
806
+ - `review`, `status`, and `doctor` also support `--agent`, a compact single-line
807
+ JSON decision packet for agents that takes precedence over `--json`.
776
808
  - Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
777
809
  `artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
778
810
  dry-run, global-dry-run, execute-plan, and trash list/purge behavior.
package/dist/src/cli.js CHANGED
@@ -9,7 +9,8 @@ const VERSION = readPackageVersion();
9
9
  const PACKAGE_NAME = "artshelf";
10
10
  const NPM_REGISTRY_URL = process.env.ARTSHELF_NPM_REGISTRY_URL ?? `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
11
11
  const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
12
- const BOOLEAN_FLAGS = new Set(["all", "json", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
12
+ const NO_UPDATE_CHECK_TTL_MS = 60 * 60 * 1000;
13
+ const BOOLEAN_FLAGS = new Set(["all", "json", "agent", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
13
14
  const VALUE_FLAGS = new Set([
14
15
  "cleanup",
15
16
  "kind",
@@ -535,12 +536,17 @@ function handleTrashPurge(parsed, ledgerPath, json) {
535
536
  return 0;
536
537
  }
537
538
  function handleReview(parsed, ledgerPath, json) {
539
+ const agent = boolFlag(parsed, "agent");
538
540
  if (boolFlag(parsed, "all")) {
539
541
  const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
540
542
  const results = registeredLedgersOrThrow(registryPath).map((ledger) => reviewLedger(ledger));
541
543
  const ok = results.every((entry) => entry.validate.ok);
542
544
  const summary = summarizeReview(results);
543
- const nextAction = reviewNextAction(summary);
545
+ if (agent) {
546
+ printCompactJson(buildReviewAgentPacketAll(results, summary, registryPath));
547
+ return ok ? 0 : 1;
548
+ }
549
+ const nextAction = reviewNextAction(summary, "all");
544
550
  if (json) {
545
551
  printJson({ ok, registryPath, summary, nextAction, ledgers: results });
546
552
  return ok ? 0 : 1;
@@ -549,6 +555,10 @@ function handleReview(parsed, ledgerPath, json) {
549
555
  return ok ? 0 : 1;
550
556
  }
551
557
  const result = reviewLedger({ name: "current", path: ledgerPath, scope: "other", createdAt: "", updatedAt: "" }, false);
558
+ if (agent) {
559
+ printCompactJson(buildReviewAgentPacketSingle(result, ledgerPath));
560
+ return result.validate.ok ? 0 : 1;
561
+ }
552
562
  if (json) {
553
563
  printJson({ ok: result.validate.ok, ledger: result });
554
564
  return result.validate.ok ? 0 : 1;
@@ -558,6 +568,10 @@ function handleReview(parsed, ledgerPath, json) {
558
568
  }
559
569
  function handleDoctor(parsed, ledgerPath, json) {
560
570
  const report = buildDoctorReport(ledgerPath, normalizeRegistryPath(stringFlag(parsed, "registry")));
571
+ if (boolFlag(parsed, "agent")) {
572
+ printCompactJson(buildDoctorAgentPacket(report));
573
+ return report.ok ? 0 : 1;
574
+ }
561
575
  if (json) {
562
576
  printJson(report);
563
577
  return report.ok ? 0 : 1;
@@ -624,16 +638,72 @@ function buildDoctorReport(ledgerPath, registryPath) {
624
638
  errors
625
639
  };
626
640
  }
641
+ // Actionable categories only — ok ledgers are healthy states, never attention.
642
+ // Order is fixed so the packet is byte-for-byte deterministic. Warnings surface
643
+ // even when health is ok (they never fail the machine), mirroring status attention.
644
+ const DOCTOR_ATTENTION_CATEGORIES = ["stale", "invalid", "warnings"];
645
+ function doctorAttention(summary) {
646
+ return DOCTOR_ATTENTION_CATEGORIES.filter((key) => summary[key] > 0);
647
+ }
648
+ function doctorNextAction(blockers, summary) {
649
+ if (blockers.length > 0) {
650
+ return `repair ${blockers.length} registry/ledger issue(s) above, then re-run \`artshelf doctor\``;
651
+ }
652
+ if (summary.warnings > 0) {
653
+ return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf validate --all\` to inspect; nothing is auto-executed`;
654
+ }
655
+ return "artshelf is healthy on this machine — cleanup safety enforced; no action needed";
656
+ }
657
+ function buildDoctorAgentPacket(report) {
658
+ const blockers = [];
659
+ if (report.registryError)
660
+ blockers.push(`registry unreadable: ${report.registryError}`);
661
+ for (const ledger of report.ledgers) {
662
+ if (ledger.status !== "ok") {
663
+ blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
664
+ }
665
+ }
666
+ return {
667
+ schemaVersion: 1,
668
+ command: "doctor",
669
+ health: report.ok ? "ok" : "attention",
670
+ version: report.version,
671
+ node: report.node,
672
+ ledgerPath: report.ledgerPath,
673
+ registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
674
+ ledgers: {
675
+ total: report.summary.ledgers,
676
+ ok: report.summary.ok,
677
+ stale: report.summary.stale,
678
+ invalid: report.summary.invalid,
679
+ warnings: report.summary.warnings
680
+ },
681
+ attention: doctorAttention(report.summary),
682
+ blockers,
683
+ cleanupSafety: report.cleanupSafety,
684
+ nextAction: doctorNextAction(blockers, report.summary),
685
+ verification: `artshelf doctor --agent --registry ${report.registryPath}`
686
+ };
687
+ }
688
+ // Human render (NGX-396): a scannable left-column glyph so attention state is
689
+ // obvious at a glance — ✓ clear, ⚠ needs attention. Plain Unicode (no ANSI
690
+ // color) keeps redirected/piped human output clean, and the `--agent`/`--json`
691
+ // renders never carry glyphs (those stay machine contracts).
692
+ const HUMAN_OK_GLYPH = "✓";
693
+ const HUMAN_ATTENTION_GLYPH = "⚠";
694
+ function attentionGlyph(needsAttention) {
695
+ return needsAttention ? HUMAN_ATTENTION_GLYPH : HUMAN_OK_GLYPH;
696
+ }
627
697
  function printDoctor(report) {
628
698
  process.stdout.write(`artshelf ${report.version} (node ${report.node})\n`);
629
- process.stdout.write(`health: ${report.ok ? "ok" : "needs attention"}\n`);
699
+ process.stdout.write(`${attentionGlyph(!report.ok)} health: ${report.ok ? "ok" : "needs attention"}\n`);
630
700
  process.stdout.write(`ledger: ${report.ledgerPath}${report.ledgerExists ? "" : " (absent)"}\n`);
631
701
  process.stdout.write(`registry: ${report.registryPath}${report.registryExists ? "" : " (absent)"}\n`);
632
702
  if (report.registryError)
633
703
  process.stdout.write(`registry error: ${report.registryError}\n`);
634
704
  process.stdout.write(`registered ledgers: ${report.summary.ledgers} (${report.summary.ok} ok, ${report.summary.stale} stale, ${report.summary.invalid} invalid)\n`);
635
705
  for (const ledger of report.ledgers) {
636
- process.stdout.write(` ${ledger.status} ${ledger.name} ${ledger.path}\n`);
706
+ process.stdout.write(` ${attentionGlyph(ledger.status !== "ok")} ${ledger.status} ${ledger.name} ${ledger.path}\n`);
637
707
  for (const message of ledger.errors)
638
708
  process.stdout.write(` error: ${message}\n`);
639
709
  }
@@ -644,8 +714,13 @@ function printDoctor(report) {
644
714
  }
645
715
  }
646
716
  function handleStatus(parsed, ledgerPath, json) {
717
+ const agent = boolFlag(parsed, "agent");
647
718
  if (boolFlag(parsed, "all")) {
648
719
  const report = buildStatusReport(normalizeRegistryPath(stringFlag(parsed, "registry")));
720
+ if (agent) {
721
+ printCompactJson(buildStatusAgentPacketAll(report));
722
+ return report.ok ? 0 : 1;
723
+ }
649
724
  if (json) {
650
725
  printJson(report);
651
726
  return report.ok ? 0 : 1;
@@ -654,6 +729,10 @@ function handleStatus(parsed, ledgerPath, json) {
654
729
  return report.ok ? 0 : 1;
655
730
  }
656
731
  const ledger = statusLedger({ name: "current", path: ledgerPath, scope: "other", createdAt: "", updatedAt: "" }, false);
732
+ if (agent) {
733
+ printCompactJson(buildStatusAgentPacketSingle(ledger, ledgerPath));
734
+ return ledger.ok ? 0 : 1;
735
+ }
657
736
  if (json) {
658
737
  printJson({ ok: ledger.ok, ledger });
659
738
  return ledger.ok ? 0 : 1;
@@ -737,23 +816,101 @@ function sumStatusCounts(ledgers, key) {
737
816
  function formatStatusCounts(counts) {
738
817
  return `active ${counts.active} · due ${counts.due} · manual-review ${counts.manualReview} · missing ${counts.missingPath} · kept ${counts.kept} · pending ${counts.pendingCleanup}`;
739
818
  }
819
+ // Actionable categories only — active and kept are healthy states, never
820
+ // attention. Order is fixed so the packet is byte-for-byte deterministic.
821
+ const STATUS_ATTENTION_CATEGORIES = ["due", "manualReview", "missingPath", "pendingCleanup"];
822
+ function statusAttention(counts) {
823
+ return STATUS_ATTENTION_CATEGORIES.filter((key) => counts[key] > 0);
824
+ }
825
+ function statusCommand(scope, command, ledgerPath) {
826
+ if (scope === "all")
827
+ return `artshelf ${command} --all`;
828
+ return ledgerPath ? `artshelf ${command} --ledger ${ledgerPath}` : `artshelf ${command}`;
829
+ }
830
+ function statusNextAction(blockers, counts, scope, ledgerPath) {
831
+ if (blockers.length > 0) {
832
+ const verify = statusCommand(scope, "status", ledgerPath);
833
+ return `repair ${blockers.length} broken ledger(s) above, then re-run \`${verify}\``;
834
+ }
835
+ const review = statusCommand(scope, "review", ledgerPath);
836
+ if (counts.pendingCleanup > 0 || counts.due > 0) {
837
+ return `run \`${review}\` to preview cleanup plans; nothing is auto-executed`;
838
+ }
839
+ if (counts.manualReview > 0) {
840
+ return `run \`${review}\` to inspect manual-review records; nothing is auto-executed`;
841
+ }
842
+ if (counts.missingPath > 0) {
843
+ return "inspect missing-path records and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
844
+ }
845
+ return "nothing due — no broken ledgers and no due, manual-review, missing-path, or pending cleanup entries";
846
+ }
847
+ function buildStatusAgentPacketAll(report) {
848
+ const blockers = [];
849
+ if (report.registryError)
850
+ blockers.push(`registry unreadable: ${report.registryError}`);
851
+ for (const ledger of report.ledgers) {
852
+ if (ledger.status !== "ok") {
853
+ blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
854
+ }
855
+ }
856
+ const counts = {
857
+ active: report.totals.active,
858
+ due: report.totals.due,
859
+ manualReview: report.totals.manualReview,
860
+ missingPath: report.totals.missingPath,
861
+ kept: report.totals.kept,
862
+ pendingCleanup: report.totals.pendingCleanup
863
+ };
864
+ return {
865
+ schemaVersion: 1,
866
+ command: "status",
867
+ scope: "all",
868
+ health: report.ok ? "ok" : "attention",
869
+ registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
870
+ ledgers: { total: report.totals.ledgers, ok: report.totals.ok, stale: report.totals.stale, invalid: report.totals.invalid },
871
+ counts,
872
+ attention: statusAttention(counts),
873
+ blockers,
874
+ nextAction: statusNextAction(blockers, counts, "all"),
875
+ verification: `artshelf status --all --agent --registry ${report.registryPath}`
876
+ };
877
+ }
878
+ function buildStatusAgentPacketSingle(ledger, ledgerPath) {
879
+ const blockers = ledger.ok
880
+ ? []
881
+ : [`${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`];
882
+ return {
883
+ schemaVersion: 1,
884
+ command: "status",
885
+ scope: "single",
886
+ health: ledger.ok ? "ok" : "attention",
887
+ ledgerPath,
888
+ counts: ledger.counts,
889
+ attention: statusAttention(ledger.counts),
890
+ blockers,
891
+ nextAction: statusNextAction(blockers, ledger.counts, "single", ledgerPath),
892
+ verification: `artshelf status --agent --ledger ${ledgerPath}`
893
+ };
894
+ }
740
895
  function printStatusAll(report) {
741
- process.stdout.write(`artshelf status: ${report.ok ? "ok" : "needs attention"}\n`);
896
+ const anyActionable = report.ledgers.some((ledger) => statusAttention(ledger.counts).length > 0);
897
+ process.stdout.write(`${attentionGlyph(!report.ok || anyActionable)} artshelf status: ${report.ok ? "ok" : "needs attention"}\n`);
742
898
  process.stdout.write(`registry: ${report.registryPath}${report.registryExists ? "" : " (absent)"} — ${report.totals.ledgers} ledgers (${report.totals.ok} ok, ${report.totals.stale} stale, ${report.totals.invalid} invalid)\n`);
743
899
  if (report.registryError)
744
900
  process.stdout.write(`registry error: ${report.registryError}\n`);
745
901
  for (const ledger of report.ledgers) {
746
902
  if (ledger.status === "ok") {
747
- process.stdout.write(`[${ledger.name}] ${formatStatusCounts(ledger.counts)}\n`);
903
+ process.stdout.write(`${attentionGlyph(statusAttention(ledger.counts).length > 0)} [${ledger.name}] ${formatStatusCounts(ledger.counts)}\n`);
748
904
  }
749
905
  else {
750
- process.stdout.write(`[${ledger.name}] ${ledger.status}: ${ledger.errors.join("; ")}\n`);
906
+ process.stdout.write(`${HUMAN_ATTENTION_GLYPH} [${ledger.name}] ${ledger.status}: ${ledger.errors.join("; ")}\n`);
751
907
  }
752
908
  }
753
909
  process.stdout.write(`total: ${formatStatusCounts(report.totals)}\n`);
754
910
  }
755
911
  function printStatusSingle(ledger) {
756
- process.stdout.write(`artshelf status: ${ledger.ok ? "ok" : ledger.status}\n`);
912
+ const needsAttention = !ledger.ok || statusAttention(ledger.counts).length > 0;
913
+ process.stdout.write(`${attentionGlyph(needsAttention)} artshelf status: ${ledger.ok ? "ok" : ledger.status}\n`);
757
914
  process.stdout.write(`ledger: ${ledger.path}\n`);
758
915
  if (ledger.ok) {
759
916
  process.stdout.write(`${formatStatusCounts(ledger.counts)}\n`);
@@ -859,26 +1016,41 @@ async function getLatestVersion(options) {
859
1016
  return latest;
860
1017
  }
861
1018
  function readUpdateCache() {
862
- const ttl = Number(process.env.ARTSHELF_UPDATE_CHECK_TTL_MS ?? UPDATE_CHECK_TTL_MS);
863
- if (ttl < 0)
864
- return null;
865
1019
  const cachePath = updateCachePath();
866
1020
  if (!existsSync(cachePath))
867
1021
  return null;
868
1022
  try {
869
1023
  const cache = JSON.parse(readFileSync(cachePath, "utf8"));
1024
+ if (!("latest" in cache))
1025
+ cache.latest = null;
870
1026
  if (cache.latest !== null && typeof cache.latest !== "string")
871
1027
  return null;
872
1028
  if (typeof cache.checkedAt !== "number")
873
1029
  return null;
1030
+ const latest = cache.latest === null ? null : normalizeVersion(cache.latest);
1031
+ const ttl = updateCacheTtlFor(latest);
1032
+ if (ttl < 0)
1033
+ return null;
874
1034
  if (Date.now() - cache.checkedAt > ttl)
875
1035
  return null;
876
- return { latest: cache.latest === null ? null : normalizeVersion(cache.latest) };
1036
+ return { latest };
877
1037
  }
878
1038
  catch {
879
1039
  return null;
880
1040
  }
881
1041
  }
1042
+ function updateCacheTtlFor(latest) {
1043
+ if (latest && compareVersions(latest, VERSION) > 0) {
1044
+ return resolveTtlMs(process.env.ARTSHELF_UPDATE_CHECK_TTL_MS, UPDATE_CHECK_TTL_MS);
1045
+ }
1046
+ return resolveTtlMs(process.env.ARTSHELF_NO_UPDATE_CHECK_TTL_MS ?? process.env.ARTSHELF_UPDATE_CHECK_TTL_MS, NO_UPDATE_CHECK_TTL_MS);
1047
+ }
1048
+ function resolveTtlMs(value, fallback) {
1049
+ if (value === undefined)
1050
+ return fallback;
1051
+ const parsed = Number(value);
1052
+ return Number.isFinite(parsed) ? parsed : fallback;
1053
+ }
882
1054
  function writeUpdateCache(latest) {
883
1055
  try {
884
1056
  const cachePath = updateCachePath();
@@ -1008,6 +1180,12 @@ function printJson(value) {
1008
1180
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
1009
1181
  return 0;
1010
1182
  }
1183
+ // Agent/compact surface: a single minified JSON line. The default `--json`
1184
+ // stays pretty-printed for audit/debug; agent packets optimize for tokens.
1185
+ function printCompactJson(value) {
1186
+ process.stdout.write(`${JSON.stringify(value)}\n`);
1187
+ return 0;
1188
+ }
1011
1189
  function registeredLedgersOrThrow(registryPath) {
1012
1190
  const ledgers = listRegisteredLedgers(registryPath);
1013
1191
  if (ledgers.length === 0)
@@ -1157,13 +1335,16 @@ function summarizeReview(results) {
1157
1335
  }
1158
1336
  return summary;
1159
1337
  }
1160
- function reviewNextAction(summary) {
1338
+ function reviewNextAction(summary, scope, ledgerPath) {
1161
1339
  const broken = summary.invalid + summary.stale;
1340
+ const review = statusCommand(scope, "review", ledgerPath);
1162
1341
  if (broken > 0) {
1163
- return `repair ${broken} broken ledger(s) above (re-register or fix the file), then re-run \`artshelf review --all\``;
1342
+ const repair = scope === "all" ? "re-register or fix the file" : "fix the file";
1343
+ return `repair ${broken} broken ledger(s) above (${repair}), then re-run \`${review}\``;
1164
1344
  }
1165
1345
  if (summary.executable > 0) {
1166
- return "run `artshelf cleanup --dry-run --all` to generate plans, then `artshelf cleanup --execute --plan-id <id> --ledger <path>` for each reviewed plan";
1346
+ const dryRun = scope === "all" ? "artshelf cleanup --dry-run --all" : `artshelf cleanup --dry-run${ledgerPath ? ` --ledger ${ledgerPath}` : ""}`;
1347
+ return `run \`${dryRun}\` to generate plans, then \`artshelf cleanup --execute --plan-id <id> --ledger <path>\` for each reviewed plan`;
1167
1348
  }
1168
1349
  if (summary.missingPath > 0) {
1169
1350
  return "inspect missing-path entries and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
@@ -1172,7 +1353,7 @@ function reviewNextAction(summary) {
1172
1353
  }
1173
1354
  function printReviewAll(results, summary, nextAction, registryPath) {
1174
1355
  const needsAttention = summary.invalid + summary.stale + summary.executable + summary.due + summary.manualReview + summary.missingPath > 0;
1175
- process.stdout.write(`artshelf review --all: ${needsAttention ? "needs attention" : "all clear"}\n`);
1356
+ process.stdout.write(`${attentionGlyph(needsAttention)} artshelf review --all: ${needsAttention ? "needs attention" : "all clear"}\n`);
1176
1357
  process.stdout.write(`registry: ${registryPath} — ${summary.ledgers} ledgers (${summary.ok} ok, ${summary.invalid} invalid, ${summary.stale} stale)\n`);
1177
1358
  printReview(results);
1178
1359
  process.stdout.write(`triage: due ${summary.due} · manual-review ${summary.manualReview} · missing ${summary.missingPath} · executable ${summary.executable} · skipped ${summary.skipped}\n`);
@@ -1181,11 +1362,149 @@ function printReviewAll(results, summary, nextAction, registryPath) {
1181
1362
  function printReview(results) {
1182
1363
  for (const result of results) {
1183
1364
  const visibleDue = result.due.filter((entry) => entry.dueStatus !== "kept");
1184
- process.stdout.write(`[${result.ledger.name}] ${result.validate.ok ? "ok" : "invalid"}: ${result.validate.entries} entries, ${result.validate.errors.length} errors, ${result.validate.warnings.length} warnings\n`);
1365
+ const needsAttention = !result.validate.ok || visibleDue.length > 0 || result.plan.entries.length > 0;
1366
+ process.stdout.write(`${attentionGlyph(needsAttention)} [${result.ledger.name}] ${result.validate.ok ? "ok" : "invalid"}: ${result.validate.entries} entries, ${result.validate.errors.length} errors, ${result.validate.warnings.length} warnings\n`);
1185
1367
  process.stdout.write(`due/manual/missing: ${visibleDue.length}; plan ${result.plan.planId}: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped\n`);
1186
1368
  process.stdout.write(`ledger: ${result.ledger.path}\n`);
1187
1369
  }
1188
1370
  }
1371
+ // review is read-only, so every safety guarantee holds unconditionally.
1372
+ const REVIEW_SAFETY = {
1373
+ dryRunOnly: true,
1374
+ executeAllRefused: true,
1375
+ noExecuteRan: true,
1376
+ noResolveRan: true,
1377
+ noDeleteRan: true
1378
+ };
1379
+ // Classify each registered ledger's records into decision groups. Order is
1380
+ // fixed (registry order, then a stable per-ledger sub-order) so the packet is
1381
+ // byte-for-byte deterministic.
1382
+ function buildReviewDecisions(results, scope) {
1383
+ const readyForApproval = [];
1384
+ const needsReviewFirst = [];
1385
+ const blocked = [];
1386
+ const review = scope === "all" ? "artshelf review --all" : "artshelf review";
1387
+ for (const result of results) {
1388
+ const { ledger, validate, due } = result;
1389
+ if (!validate.ok) {
1390
+ const status = existsSync(ledger.path) ? "invalid" : "missing";
1391
+ const repair = scope === "all" ? `re-register or fix ${ledger.path}` : `fix ${ledger.path}`;
1392
+ blocked.push({
1393
+ label: `Repair ${ledger.name} ledger (${status})`,
1394
+ itemIds: [],
1395
+ actionType: "fix-registry",
1396
+ approvalTarget: null,
1397
+ reason: validate.errors[0] ?? `${scope === "all" ? "registered ledger" : "ledger"} is ${status}`,
1398
+ nextStep: `${repair}, then re-run \`${review}\``
1399
+ });
1400
+ continue;
1401
+ }
1402
+ const missingPath = due.filter((entry) => entry.dueStatus === "missing-path");
1403
+ const trashSafe = due.filter((entry) => entry.dueStatus === "due" && entry.cleanup === "trash");
1404
+ const inspectItems = due.filter((entry) => entry.dueStatus === "manual-review" ||
1405
+ (entry.dueStatus === "due" && (entry.cleanup === "review" || entry.cleanup === "delete")));
1406
+ // Ready for approval: missing-path records resolve ledger-only with an exact,
1407
+ // plan-less approval target. Resolution updates the ledger and never touches
1408
+ // files, so it is the one action review can hand an agent directly.
1409
+ if (missingPath.length > 0) {
1410
+ const ids = missingPath.map((entry) => entry.id).sort();
1411
+ readyForApproval.push({
1412
+ label: `Resolve ${ids.length} missing-path record(s) in ${ledger.name}`,
1413
+ itemIds: ids,
1414
+ actionType: "resolve-missing",
1415
+ approvalTarget: `approve artshelf resolve missing ledger ${ledger.path} ids ${ids.join(" ")}`,
1416
+ reason: "the recorded path is already missing",
1417
+ nextStep: "confirm the artifact is no longer needed, then approve the ledger-only resolve"
1418
+ });
1419
+ }
1420
+ // Trash-safe records are cleanup-eligible, but review never mints a plan, so
1421
+ // they carry no approval target: the next step is the dry-run that produces
1422
+ // the reviewed plan id to approve.
1423
+ if (trashSafe.length > 0) {
1424
+ const ids = trashSafe.map((entry) => entry.id).sort();
1425
+ needsReviewFirst.push({
1426
+ label: `Plan cleanup for ${ids.length} trash-eligible artifact(s) in ${ledger.name}`,
1427
+ itemIds: ids,
1428
+ actionType: "cleanup",
1429
+ approvalTarget: null,
1430
+ reason: "disposable artifacts are due but no reviewed cleanup plan exists yet",
1431
+ nextStep: `run \`artshelf cleanup --dry-run --ledger ${ledger.path} --json\`, then approve \`approve artshelf cleanup ledger ${ledger.path} plan <plan-id>\``
1432
+ });
1433
+ }
1434
+ // manual-review and cleanup=review records need a human decision before any
1435
+ // cleanup; cleanup=delete is refused outright. None carry an approval target.
1436
+ if (inspectItems.length > 0) {
1437
+ const ids = inspectItems.map((entry) => entry.id).sort();
1438
+ const hasDelete = inspectItems.some((entry) => entry.cleanup === "delete");
1439
+ needsReviewFirst.push({
1440
+ label: `Inspect ${ids.length} record(s) in ${ledger.name} before cleanup`,
1441
+ itemIds: ids,
1442
+ actionType: "inspect",
1443
+ approvalTarget: null,
1444
+ reason: hasDelete
1445
+ ? "records need manual review; cleanup=delete is refused and never deletes files"
1446
+ : "records are held for manual review before any cleanup",
1447
+ nextStep: "inspect each path, then keep, change retention, resolve, or set cleanup=trash and plan a cleanup"
1448
+ });
1449
+ }
1450
+ }
1451
+ return { readyForApproval, needsReviewFirst, blocked };
1452
+ }
1453
+ function reviewCounts(summary) {
1454
+ return {
1455
+ due: summary.due,
1456
+ manualReview: summary.manualReview,
1457
+ missingPath: summary.missingPath,
1458
+ executable: summary.executable,
1459
+ skipped: summary.skipped
1460
+ };
1461
+ }
1462
+ function buildReviewAgentPacketAll(results, summary, registryPath) {
1463
+ const groups = buildReviewDecisions(results, "all");
1464
+ return {
1465
+ schemaVersion: 1,
1466
+ command: "review",
1467
+ scope: "all",
1468
+ health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
1469
+ registry: { path: registryPath, exists: existsSync(registryPath) },
1470
+ ledgers: { total: summary.ledgers, ok: summary.ok, stale: summary.stale, invalid: summary.invalid },
1471
+ counts: reviewCounts(summary),
1472
+ decisionSummary: {
1473
+ readyForApproval: groups.readyForApproval.length,
1474
+ needsReviewFirst: groups.needsReviewFirst.length,
1475
+ blocked: groups.blocked.length
1476
+ },
1477
+ readyForApproval: groups.readyForApproval,
1478
+ needsReviewFirst: groups.needsReviewFirst,
1479
+ blocked: groups.blocked,
1480
+ safety: REVIEW_SAFETY,
1481
+ nextAction: reviewNextAction(summary, "all"),
1482
+ verification: `artshelf review --all --agent --registry ${registryPath}`
1483
+ };
1484
+ }
1485
+ function buildReviewAgentPacketSingle(result, ledgerPath) {
1486
+ const summary = summarizeReview([result]);
1487
+ const groups = buildReviewDecisions([result], "single");
1488
+ return {
1489
+ schemaVersion: 1,
1490
+ command: "review",
1491
+ scope: "single",
1492
+ health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
1493
+ ledgerPath,
1494
+ counts: reviewCounts(summary),
1495
+ decisionSummary: {
1496
+ readyForApproval: groups.readyForApproval.length,
1497
+ needsReviewFirst: groups.needsReviewFirst.length,
1498
+ blocked: groups.blocked.length
1499
+ },
1500
+ readyForApproval: groups.readyForApproval,
1501
+ needsReviewFirst: groups.needsReviewFirst,
1502
+ blocked: groups.blocked,
1503
+ safety: REVIEW_SAFETY,
1504
+ nextAction: reviewNextAction(summary, "single", ledgerPath),
1505
+ verification: `artshelf review --agent --ledger ${ledgerPath}`
1506
+ };
1507
+ }
1189
1508
  const COMMAND_GROUPS = [
1190
1509
  {
1191
1510
  group: "Create",
@@ -1387,17 +1706,28 @@ Resolved records stay in the audit trail but no longer participate in due or cle
1387
1706
  }
1388
1707
  if (command === "review") {
1389
1708
  process.stdout.write(`Usage:
1390
- artshelf review [--ledger <path>] [--json]
1391
- artshelf review --all [--registry <path>] [--json]
1709
+ artshelf review [--ledger <path>] [--json|--agent]
1710
+ artshelf review --all [--registry <path>] [--json|--agent]
1711
+
1712
+ Review runs validate, due, and cleanup plan preview without moving files or
1713
+ writing a plan. With --all, review adds aggregate triage counts and the next
1714
+ safe action.
1392
1715
 
1393
- Review runs validate, due, and cleanup plan preview without moving files or writing a plan.
1394
- With --all, review adds aggregate triage counts and the next safe action.
1716
+ Render modes:
1717
+ (default) Human summary of validation, triage counts, and the next safe action.
1718
+ --json Full read-only audit report (backward-compatible).
1719
+ --agent Compact single-line JSON decision packet for agents: health, triage
1720
+ counts, and classified decision groups (ready for approval, needs
1721
+ review first, blocked) with exact approval targets where they are
1722
+ safe. Review is read-only, so cleanup approval targets are minted by
1723
+ \`cleanup --dry-run\`, never leaked from a preview plan id.
1724
+ Token-efficient; --agent takes precedence over --json.
1395
1725
  `);
1396
1726
  return;
1397
1727
  }
1398
1728
  if (command === "doctor") {
1399
1729
  process.stdout.write(`Usage:
1400
- artshelf doctor [--registry <path>] [--ledger <path>] [--json]
1730
+ artshelf doctor [--registry <path>] [--ledger <path>] [--json|--agent]
1401
1731
 
1402
1732
  Doctor reports whether Artshelf is healthy on this machine: CLI version, selected
1403
1733
  or default ledger path, selected or global registry path, registered ledger health
@@ -1406,6 +1736,14 @@ selected or default ledger and still requires a reviewed plan id; --all execute
1406
1736
  and cleanup=delete are refused, while physical trash purge requires a separate
1407
1737
  reviewed purge plan.
1408
1738
 
1739
+ Render modes:
1740
+ (default) Human summary of machine health and cleanup safety.
1741
+ --json Full audit report (backward-compatible; suitable for cron/reporting).
1742
+ --agent Compact single-line JSON decision packet for agents: health, registry
1743
+ and registered-ledger health, blockers, cleanup-safety posture, next
1744
+ action, and a verify command. Token-efficient; --agent takes
1745
+ precedence over --json.
1746
+
1409
1747
  Run it after install, when --all commands behave unexpectedly, or on a schedule to
1410
1748
  catch stale registry entries. Doctor is read-only. A healthy machine exits 0; a
1411
1749
  broken registry or registered ledger exits non-zero with actionable errors.
@@ -1414,8 +1752,8 @@ broken registry or registered ledger exits non-zero with actionable errors.
1414
1752
  }
1415
1753
  if (command === "status") {
1416
1754
  process.stdout.write(`Usage:
1417
- artshelf status [--ledger <path>] [--json]
1418
- artshelf status --all [--registry <path>] [--json]
1755
+ artshelf status [--ledger <path>] [--json|--agent]
1756
+ artshelf status --all [--registry <path>] [--json|--agent]
1419
1757
 
1420
1758
  Status is the lightweight daily "what is going on?" view. Without --all, it
1421
1759
  reports counts for the selected or default ledger only. With --all, it adds
@@ -1423,10 +1761,16 @@ registry health, total ledgers, and aggregated counts across registered ledgers.
1423
1761
  Counts include active artifacts, kept, due, manual-review, missing-path, and
1424
1762
  pending cleanup entries.
1425
1763
 
1426
- Human output is short enough to paste into a chat; \`artshelf status --all --json\`
1427
- is suitable for cron and reporting. Status is read-only: it never creates plans
1428
- or receipts and never mutates records. A healthy selected ledger exits 0; with
1429
- --all, a broken registry or any stale or invalid registered ledger exits non-zero.
1764
+ Render modes:
1765
+ (default) Human summary, short enough to paste into a chat.
1766
+ --json Full audit report (backward-compatible; suitable for cron/reporting).
1767
+ --agent Compact single-line JSON decision packet for agents: health, counts,
1768
+ attention categories, blockers, next action, and a verify command.
1769
+ Token-efficient; --agent takes precedence over --json.
1770
+
1771
+ Status is read-only: it never creates plans or receipts and never mutates
1772
+ records. A healthy selected ledger exits 0; with --all, a broken registry or any
1773
+ stale or invalid registered ledger exits non-zero.
1430
1774
  `);
1431
1775
  return;
1432
1776
  }
@@ -73,7 +73,7 @@ artshelf ledgers add --ledger &lt;repo&gt;/.artshelf/ledger.jsonl --name &lt;pro
73
73
  artshelf ledgers list --json
74
74
 
75
75
  <span class="c"># review and due-check every registered ledger at once</span>
76
- artshelf review --all --json
76
+ artshelf review --all --agent
77
77
  artshelf due --all --json
78
78
 
79
79
  <span class="c"># find records this agent owns, across ledgers</span>
@@ -102,12 +102,20 @@ artshelf due --all --json
102
102
 
103
103
  <span class="c"># the full read-only pass: validate + due + plan preview</span>
104
104
  artshelf review --all --json
105
+ artshelf review --all --agent
105
106
 
106
107
  <span class="c"># CLI version, paths, registry health, safety posture</span>
107
108
  artshelf doctor --json
109
+ artshelf doctor --agent
108
110
 
109
111
  <span class="c"># lightweight counts, cron-friendly</span>
110
- artshelf status --all --json</code></pre>
112
+ artshelf status --all --json
113
+ artshelf status --all --agent</code></pre>
114
+ <p>
115
+ Use <code>--agent</code> for concise monitor decisions and exact next
116
+ actions. Use <code>--json</code> instead when the job needs full
117
+ audit/API detail for storage, debugging, or a custom report.
118
+ </p>
111
119
  </section>
112
120
 
113
121
  <section>
@@ -44,16 +44,22 @@
44
44
  <section>
45
45
  <h2>Daily review workflow</h2>
46
46
  <ol>
47
- <li>Read <code>ledgers list</code>, <code>review --all</code>, and <code>trash list --all</code>.</li>
47
+ <li>Read <code>ledgers list --json</code>, <code>review --all --agent</code>, and <code>trash list --all --json</code>.</li>
48
48
  <li>Run explicit-ledger purge dry-runs only when old trash needs review.</li>
49
49
  <li>Classify each candidate: <code>trash-safe</code>, <code>needs-human-review</code>, <code>resolve-candidate</code>, or <code>registry-problem</code>.</li>
50
50
  <li>Ask only with exact ledger path, reviewed plan id, or ids.</li>
51
51
  </ol>
52
+ <p>
53
+ Prefer the built-in <code>--agent</code> packet when an acting agent
54
+ needs the compact decision surface. Use full <code>--json</code>
55
+ output when you need every ledger field for audit, debugging, or a
56
+ custom renderer.
57
+ </p>
52
58
  </section>
53
59
 
54
60
  <section>
55
61
  <h2>Review plan report schema</h2>
56
- <p>Construct an <code>ArtshelfReviewReport</code> JSON packet first, then render a compact decision card.</p>
62
+ <p>For richer host cards, attachments, or audit packets, construct an <code>ArtshelfReviewReport</code> JSON packet first, then render a compact decision card.</p>
57
63
  <p>
58
64
  Use <a href="schemas/artshelf-review-report.schema.json">schemas/artshelf-review-report.schema.json</a>
59
65
  and <a href="examples/artshelf-review-report.json">examples/artshelf-review-report.json</a>.
@@ -87,6 +87,20 @@
87
87
  </dl>
88
88
  </section>
89
89
 
90
+ <section>
91
+ <h2>Render modes</h2>
92
+ <p>
93
+ <code>review</code>, <code>status</code>, and <code>doctor</code> share three render modes
94
+ so the same data fits both people and agents. Reach for <code>--agent</code> when an agent
95
+ decides and acts; reach for <code>--json</code> for full record, plan, or health detail.
96
+ </p>
97
+ <dl class="def-rows">
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, exact approval targets, blockers, and a verification command</dd></div>
100
+ <div><dt>--json</dt><dd>the full audit and API contract: complete machine-readable JSON for debugging and existing integrations, unchanged and backward compatible</dd></div>
101
+ </dl>
102
+ </section>
103
+
90
104
  <section>
91
105
  <h2>The mental model</h2>
92
106
  <ul class="boundary-list">
@@ -50,6 +50,23 @@ The browsable docs split the workflow into focused child pages:
50
50
  - Approval names the exact ledger, plan id, or record ids.
51
51
  - Every approved action ends with a read-only verification.
52
52
 
53
+ ## Render modes
54
+
55
+ `review`, `status`, and `doctor` share three render modes so the same data fits
56
+ both people and agents:
57
+
58
+ - **default**: a human render — scannable grouped counts, attention states, and a
59
+ short next action for a person at the terminal.
60
+ - **`--agent`**: a deterministic, token-efficient decision packet (single-line
61
+ compact JSON) with health, counts, classifications, exact approval targets,
62
+ blockers, and a verification command. Use it when an agent acts on the result.
63
+ - **`--json`**: the full audit and API contract — complete machine-readable JSON
64
+ for debugging and existing integrations, unchanged and backward compatible.
65
+
66
+ Reach for `--agent` when an agent needs to decide and act cheaply; reach for
67
+ `--json` when you want the full record, plan, or health detail for audit or
68
+ debugging. `--agent` takes precedence if both flags are passed.
69
+
53
70
  ## Portable Skill
54
71
 
55
72
  The repo ships a portable skill at
@@ -120,19 +120,20 @@ artshelf validate [--all] [--json]</code></pre>
120
120
  <section class="cmd">
121
121
  <div class="cmd-head"><h2>artshelf review / status / doctor</h2><span class="cmd-flag readonly">read-only</span></div>
122
122
  <pre><code><span class="c"># validate + due + plan preview in one pass</span>
123
- artshelf review [--all] [--json]
123
+ artshelf review [--all] [--agent] [--json]
124
124
 
125
125
  <span class="c"># lightweight dashboard of counts</span>
126
- artshelf status [--all] [--json]
126
+ artshelf status [--all] [--agent] [--json]
127
127
 
128
128
  <span class="c"># CLI version, resolved paths, registry health</span>
129
- artshelf doctor [--json]</code></pre>
129
+ artshelf doctor [--agent] [--json]</code></pre>
130
130
  <p>
131
131
  <code>review</code> runs validate, due, and a cleanup plan preview in one pass; no-op
132
132
  previews report <code>not-created</code>, and <code>--all</code> adds an aggregate triage
133
133
  summary plus the next safe action. <code>status</code> is the lightweight dashboard of
134
134
  counts; <code>--all --json</code> is cron-friendly. <code>doctor</code> reports CLI version,
135
- resolved paths, registry health, and the cleanup safety posture.
135
+ resolved paths, registry health, and the cleanup safety posture. All three also accept
136
+ <code>--agent</code> for a deterministic, token-efficient decision packet (see Output mode).
136
137
  </p>
137
138
  </section>
138
139
 
@@ -147,7 +148,10 @@ artshelf update [--json]</code></pre>
147
148
  only. pnpm global installs should update with
148
149
  <code>pnpm add -g artshelf@latest</code>. Source installs should update by
149
150
  pulling, rebuilding, and linking the checkout. Notices are cached in
150
- <code>~/.artshelf/update-check.json</code>; set
151
+ <code>~/.artshelf/update-check.json</code>: update-available results use
152
+ the long 24-hour TTL, while no-update, failed, missing, or null results
153
+ use a shorter 1-hour TTL so newly published releases are noticed sooner.
154
+ <code>artshelf update</code> forces a fresh latest-version check. Set
151
155
  <code>ARTSHELF_NO_UPDATE_CHECK=1</code> to disable automatic checks for
152
156
  no-network scripts and scheduled jobs. Read-only command labels refer
153
157
  to ledger and artifact mutation, not this optional update-check cache.
@@ -212,13 +216,29 @@ artshelf trash purge --execute --plan-id &lt;id&gt; [--ledger &lt;path&gt;] [--j
212
216
  <section>
213
217
  <h2>Output mode</h2>
214
218
  <p>
215
- <code>--json</code> is the agent contract: it switches commands that return data,
216
- records, plans, receipts, or health output to machine-readable JSON on stdout.
217
- Update notices and other diagnostics stay on stderr and never corrupt JSON output.
219
+ Every command has a default human render: compact, scannable terminal output with
220
+ grouped counts, a leading <code>✓</code>/<code>⚠</code> attention glyph on each ledger
221
+ and the summary line, and a short next action meant for a person reading the screen,
222
+ not a wall of raw JSON. The glyphs are plain Unicode (no ANSI color), so redirected
223
+ output stays clean.
224
+ </p>
225
+ <p>
226
+ <code>review</code>, <code>status</code>, and <code>doctor</code> add <code>--agent</code>:
227
+ a deterministic, token-efficient decision packet emitted as a single line of compact JSON.
228
+ It names health, counts, classifications, exact approval targets, blockers, and the command
229
+ to re-run for verification, so an agent can act without parsing decorative output.
230
+ </p>
231
+ <p>
232
+ <code>--json</code> stays the audit and API contract: the full, pretty-printed report for
233
+ debugging, audit trails, and existing integrations. It is unchanged and backward compatible,
234
+ and <code>--agent</code> takes precedence when both are passed. Update notices and other
235
+ diagnostics stay on stderr and never corrupt JSON or packet output.
218
236
  </p>
219
237
  <table class="opts">
220
238
  <tr><th>option</th><th>meaning</th></tr>
221
- <tr><td>--json</td><td>emit machine-readable JSON on commands that return data</td></tr>
239
+ <tr><td>(default)</td><td>human render: grouped counts, ✓/⚠ attention glyphs, and a short next action</td></tr>
240
+ <tr><td>--agent</td><td>token-efficient decision packet for agents on <code>review</code>, <code>status</code>, <code>doctor</code></td></tr>
241
+ <tr><td>--json</td><td>full machine-readable audit JSON on commands that return data</td></tr>
222
242
  </table>
223
243
  </section>
224
244
 
@@ -244,7 +264,8 @@ artshelf trash purge --execute --plan-id &lt;id&gt; [--ledger &lt;path&gt;] [--j
244
264
  <tr><td>ARTSHELF_NOW</td><td>override current time for retention and due calculations</td></tr>
245
265
  <tr><td>ARTSHELF_NO_UPDATE_CHECK=1</td><td>disable automatic npm update checks</td></tr>
246
266
  <tr><td>ARTSHELF_UPDATE_CACHE</td><td>override the update-check cache path</td></tr>
247
- <tr><td>ARTSHELF_UPDATE_CHECK_TTL_MS</td><td>override the update-check cache TTL</td></tr>
267
+ <tr><td>ARTSHELF_UPDATE_CHECK_TTL_MS</td><td>override the update-available cache TTL; also acts as the no-update TTL fallback for compatibility</td></tr>
268
+ <tr><td>ARTSHELF_NO_UPDATE_CHECK_TTL_MS</td><td>override the no-update/failed cache TTL specifically</td></tr>
248
269
  <tr><td>ARTSHELF_NPM_REGISTRY_URL</td><td>override the npm latest-version endpoint</td></tr>
249
270
  <tr><td>ARTSHELF_UPDATE_DRY_RUN=1</td><td>print the npm update command without running it</td></tr>
250
271
  <tr><td>ARTSHELF_LATEST_VERSION</td><td>override the latest-version value for tests</td></tr>
@@ -273,7 +294,9 @@ artshelf trash purge --execute --plan-id &lt;id&gt; [--ledger &lt;path&gt;] [--j
273
294
  <code>~/.artshelf/ledgers.json</code> is the discovery index for <code>--all</code>
274
295
  review, status, cleanup dry-run, and trash-list; project records stay in their own
275
296
  repo-local ledgers. Automatic update checks cache their last npm result at
276
- <code>~/.artshelf/update-check.json</code> by default.
297
+ <code>~/.artshelf/update-check.json</code> by default, with a long TTL
298
+ for update-available results and a shorter TTL for no-update or failed
299
+ results.
277
300
  </p>
278
301
  <div class="callout" data-kind="boundary">
279
302
  <span class="callout-label">Hard boundary</span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artshelf",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Tiny CLI for accountable temporary artifact retention.",
5
5
  "type": "module",
6
6
  "author": "Calvin",
@@ -59,8 +59,7 @@ npm link
59
59
  artshelf doctor
60
60
  ```
61
61
 
62
- Install, copy, or reference this portable skill only after the user chooses the
63
- integration path. Offer to schedule read-only review job delivery in the host runtime.
62
+ Install, copy, or reference this portable skill only after the user chooses the integration path. Offer to schedule read-only review job delivery in the host runtime.
64
63
 
65
64
  ## Create
66
65
 
@@ -72,15 +71,11 @@ artshelf put <path> --reason "<why this exists>" --ttl 3d --kind run-artifact --
72
71
  artshelf get <id> --json
73
72
  ```
74
73
 
75
- Register backups, quarantine folders, debug output, generated reports, long-run
76
- evidence, and copied files kept for review. Skip source files, cheap regenerated
77
- build output, dependency caches, secrets, credential dumps, and artifacts already
78
- owned by another durable ledger.
74
+ Register backups, quarantine folders, debug output, generated reports, long-run evidence, and copied files kept for review. Skip source files, cheap regenerated
75
+ build output, dependency caches, secrets, credential dumps, and artifacts already owned by another durable ledger.
79
76
 
80
- Defaults: `kind=scratch` for temp dirs, `backup` for rollback copies,
81
- `run-artifact` for logs/reports/evidence, `quarantine` for isolated questionable
82
- files. Use `cleanup=review` when judgment is needed and `cleanup=trash` only when
83
- later disposal is clearly safe.
77
+ Defaults: `kind=scratch` for temp dirs, `backup` for rollback copies, `run-artifact` for logs/reports/evidence, `quarantine` for isolated questionable
78
+ files. Use `cleanup=review` when judgment is needed and `cleanup=trash` only when later disposal is clearly safe.
84
79
 
85
80
  When JSON registration succeeds, include this deterministic Artshelf footnote:
86
81
 
@@ -94,13 +89,15 @@ Use the ledger registry for whole-machine review:
94
89
 
95
90
  ```bash
96
91
  artshelf ledgers list --json
97
- artshelf status --all --json
98
- artshelf review --all --json
92
+ artshelf status --all --agent
93
+ artshelf review --all --agent
99
94
  artshelf trash list --all --json
100
95
  ```
101
96
 
102
97
  `artshelf ledgers list --json` reports per-ledger validation status. `--plain`
103
98
  skips validation. `--all` is for discovery and review, not mutation permission.
99
+ Use `--agent` on `review`, `status`, and `doctor` for compact decisions; use
100
+ `--json` for full audit/API payloads, custom rendering, or debugging.
104
101
 
105
102
  Register existing project ledgers explicitly:
106
103
 
@@ -144,22 +141,23 @@ Daily Review Workflow: turn raw Artshelf output into a decision packet, not a
144
141
  count dump.
145
142
 
146
143
  1. Run read-only review first: `artshelf ledgers list --json`,
147
- `artshelf review --all --json`, and `artshelf trash list --all --json`.
144
+ `artshelf review --all --agent`, and `artshelf trash list --all --json`.
148
145
  2. If cleanup attention exists, run `artshelf cleanup --dry-run --all --json`.
149
146
  3. Classify candidates as `trash-safe`, `needs-human-review`,
150
147
  `resolve-candidate`, or `registry-problem`.
151
- 4. Use `ArtshelfReviewReport` from
152
- `schemas/artshelf-review-report.schema.json`; use
153
- `examples/artshelf-review-report.json` as the canonical packet.
154
- 5. Render the compact decision card with `scripts/render-review-report.mjs`;
155
- keep `decisionSummary` in audit, while `decisionGroups` drive counts.
156
- Emojis are encouraged only in host-specific wrappers, not the renderer.
148
+ 4. Use the built-in `--agent` packet when the CLI output is enough to decide,
149
+ because it is deterministic and token-efficient. Use
150
+ `ArtshelfReviewReport` from `schemas/artshelf-review-report.schema.json` and
151
+ `examples/artshelf-review-report.json` when you need a host-specific card,
152
+ attachment, or richer audit record.
153
+ 5. Render full packets with `scripts/render-review-report.mjs`; keep
154
+ `decisionSummary` in audit, while `decisionGroups` drive counts. Emojis are encouraged only in host-specific wrappers, not the renderer.
157
155
  6. Always include the exact approval target in the message body as a fallback.
158
156
  Do not paste the whole packet into chat unless the user asks for it.
159
157
 
160
158
  ### Review Plan Report Schema
161
159
 
162
- Deterministic renderer:
160
+ Deterministic compact decision card renderer:
163
161
 
164
162
  ```bash
165
163
  cd /path/to/skills/artshelf