artshelf 0.7.0 → 0.8.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/dist/src/cli.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
3
6
  import { appendPreparedRecord, createCleanupPlan, createTrashPurgePlan, dueEntries, executeCleanupPlan, executeTrashPurgePlan, filterRecordsByStatus, findRecords, getRecord, listTrashedRecords, normalizeLedgerPath, prepareRecord, previewCleanupPlan, readLedger, resolveRecord, validateLedger } from "./ledger.js";
4
7
  import { listRegisteredLedgers, normalizeRegistryPath, registerLedger } from "./registry.js";
5
8
  const VERSION = readPackageVersion();
9
+ const PACKAGE_NAME = "artshelf";
10
+ const NPM_REGISTRY_URL = process.env.ARTSHELF_NPM_REGISTRY_URL ?? `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
11
+ const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
6
12
  const BOOLEAN_FLAGS = new Set(["all", "json", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
7
13
  const VALUE_FLAGS = new Set([
8
14
  "cleanup",
@@ -29,56 +35,83 @@ function readPackageVersion() {
29
35
  }
30
36
  return packageJson.version;
31
37
  }
32
- function main(argv) {
38
+ async function main(argv) {
33
39
  try {
34
40
  const parsed = parseArgs(argv);
41
+ let status = 0;
42
+ let shouldCheckForUpdate = true;
35
43
  if (parsed.command === "--version" || parsed.command === "-v" || boolFlag(parsed, "version")) {
36
44
  process.stdout.write(`artshelf ${VERSION}\n`);
37
- return 0;
45
+ return maybeNotifyUpdateAndReturn(0, parsed);
38
46
  }
39
47
  if (parsed.command === "help" || parsed.command === "--help" || parsed.command === "-h" || boolFlag(parsed, "help")) {
40
48
  printHelp(parsed.command === "help" ? parsed.positionals[0] : parsed.command);
41
- return 0;
49
+ return maybeNotifyUpdateAndReturn(0, parsed);
42
50
  }
43
51
  switch (parsed.command) {
44
52
  case "put":
45
- return handlePut(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
53
+ status = handlePut(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
54
+ break;
46
55
  case "ledgers":
47
- return handleLedgers(parsed, boolFlag(parsed, "json"));
56
+ status = handleLedgers(parsed, boolFlag(parsed, "json"));
57
+ break;
48
58
  case "list":
49
- return handleList(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
59
+ status = handleList(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
60
+ break;
50
61
  case "find":
51
- return handleFind(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
62
+ status = handleFind(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
63
+ break;
52
64
  case "get":
53
- return handleGet(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
65
+ status = handleGet(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
66
+ break;
54
67
  case "due":
55
- return handleDue(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
68
+ status = handleDue(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
69
+ break;
56
70
  case "validate":
57
- return handleValidate(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
71
+ status = handleValidate(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
72
+ break;
58
73
  case "cleanup":
59
- return handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
74
+ status = handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
75
+ break;
60
76
  case "trash":
61
- return handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
77
+ status = handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
78
+ break;
62
79
  case "review":
63
- return handleReview(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
80
+ status = handleReview(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
81
+ break;
64
82
  case "doctor":
65
- return handleDoctor(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
83
+ status = handleDoctor(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
84
+ break;
66
85
  case "status":
67
- return handleStatus(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
86
+ status = handleStatus(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
87
+ break;
68
88
  case "resolve":
69
- return handleResolve(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
89
+ status = handleResolve(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
90
+ break;
91
+ case "update":
92
+ shouldCheckForUpdate = false;
93
+ status = await handleUpdate(parsed, boolFlag(parsed, "json"));
94
+ break;
70
95
  case undefined:
71
96
  printHelp();
72
- return 0;
97
+ status = 0;
98
+ break;
73
99
  default:
74
100
  throw new Error(`Unknown command: ${parsed.command}`);
75
101
  }
102
+ if (!shouldCheckForUpdate)
103
+ return status;
104
+ return maybeNotifyUpdateAndReturn(status, parsed);
76
105
  }
77
106
  catch (error) {
78
107
  process.stderr.write(`artshelf: ${error.message}\nRun \`artshelf help\` for usage.\n`);
79
108
  return 1;
80
109
  }
81
110
  }
111
+ async function maybeNotifyUpdateAndReturn(status, parsed) {
112
+ await maybeNotifyAvailableUpdate(parsed);
113
+ return status;
114
+ }
82
115
  function handlePut(parsed, ledgerPath, json) {
83
116
  const path = parsed.positionals[0];
84
117
  if (!path)
@@ -726,6 +759,189 @@ function printStatusSingle(ledger) {
726
759
  process.stdout.write(`error: ${message}\n`);
727
760
  }
728
761
  }
762
+ async function handleUpdate(parsed, json) {
763
+ if (parsed.positionals.length > 0)
764
+ throw new Error("update does not accept positional arguments");
765
+ const info = await getUpdateInfo({ force: true });
766
+ if (!info)
767
+ throw new Error("Could not check npm for the latest Artshelf version");
768
+ if (!info.updateAvailable) {
769
+ if (json)
770
+ return printJson({ ok: true, updated: false, current: info.current, latest: info.latest });
771
+ process.stdout.write(`artshelf is already up to date: v${info.current}\n`);
772
+ return 0;
773
+ }
774
+ if (process.env.ARTSHELF_UPDATE_DRY_RUN === "1") {
775
+ if (json) {
776
+ return printJson({
777
+ ok: true,
778
+ updated: false,
779
+ dryRun: true,
780
+ current: info.current,
781
+ latest: info.latest,
782
+ command: ["npm", "install", "-g", `${PACKAGE_NAME}@latest`]
783
+ });
784
+ }
785
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
786
+ process.stdout.write(`Dry run: would run "npm install -g ${PACKAGE_NAME}@latest"\n`);
787
+ return 0;
788
+ }
789
+ if (!json) {
790
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
791
+ process.stdout.write(`Updating with "npm install -g ${PACKAGE_NAME}@latest"...\n`);
792
+ }
793
+ const result = json
794
+ ? spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], { encoding: "utf8" })
795
+ : spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], { stdio: "inherit" });
796
+ const status = result.status ?? 1;
797
+ const spawnError = result.error instanceof Error ? result.error.message : "";
798
+ if (json) {
799
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
800
+ printJson({
801
+ ok: status === 0,
802
+ updated: status === 0,
803
+ current: info.current,
804
+ latest: info.latest,
805
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
806
+ stderr: appendOutputMessage(stderr, spawnError)
807
+ });
808
+ return status;
809
+ }
810
+ if (spawnError)
811
+ process.stderr.write(`Update failed: ${spawnError}\n`);
812
+ if (status === 0)
813
+ process.stdout.write(`artshelf updated to v${info.latest}\n`);
814
+ return status;
815
+ }
816
+ function appendOutputMessage(output, message) {
817
+ if (!message)
818
+ return output;
819
+ if (!output)
820
+ return message;
821
+ return `${output}${output.endsWith("\n") ? "" : "\n"}${message}`;
822
+ }
823
+ async function maybeNotifyAvailableUpdate(parsed) {
824
+ if (process.env.ARTSHELF_NO_UPDATE_CHECK === "1")
825
+ return;
826
+ if (parsed.command === "update")
827
+ return;
828
+ const info = await getUpdateInfo({ force: false });
829
+ if (!info?.updateAvailable)
830
+ return;
831
+ process.stderr.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
832
+ process.stderr.write(`Run "artshelf update" to update npm installs\n`);
833
+ }
834
+ async function getUpdateInfo(options) {
835
+ const latest = await getLatestVersion(options);
836
+ if (!latest)
837
+ return null;
838
+ return {
839
+ current: VERSION,
840
+ latest,
841
+ updateAvailable: compareVersions(latest, VERSION) > 0
842
+ };
843
+ }
844
+ async function getLatestVersion(options) {
845
+ const override = process.env.ARTSHELF_LATEST_VERSION;
846
+ if (override)
847
+ return normalizeVersion(override);
848
+ if (!options.force) {
849
+ const cached = readUpdateCache();
850
+ if (cached)
851
+ return cached.latest;
852
+ }
853
+ const latest = await fetchLatestNpmVersion();
854
+ writeUpdateCache(latest);
855
+ return latest;
856
+ }
857
+ function readUpdateCache() {
858
+ const ttl = Number(process.env.ARTSHELF_UPDATE_CHECK_TTL_MS ?? UPDATE_CHECK_TTL_MS);
859
+ if (ttl < 0)
860
+ return null;
861
+ const cachePath = updateCachePath();
862
+ if (!existsSync(cachePath))
863
+ return null;
864
+ try {
865
+ const cache = JSON.parse(readFileSync(cachePath, "utf8"));
866
+ if (cache.latest !== null && typeof cache.latest !== "string")
867
+ return null;
868
+ if (typeof cache.checkedAt !== "number")
869
+ return null;
870
+ if (Date.now() - cache.checkedAt > ttl)
871
+ return null;
872
+ return { latest: cache.latest === null ? null : normalizeVersion(cache.latest) };
873
+ }
874
+ catch {
875
+ return null;
876
+ }
877
+ }
878
+ function writeUpdateCache(latest) {
879
+ try {
880
+ const cachePath = updateCachePath();
881
+ const dir = dirname(cachePath);
882
+ if (dir) {
883
+ mkdirSync(dir, { recursive: true });
884
+ writeFileSync(cachePath, `${JSON.stringify({ latest, checkedAt: Date.now() }, null, 2)}\n`);
885
+ }
886
+ }
887
+ catch {
888
+ // Update checks should never affect normal CLI behavior.
889
+ }
890
+ }
891
+ async function fetchLatestNpmVersion() {
892
+ const controller = new AbortController();
893
+ const timeout = setTimeout(() => controller.abort(), 750);
894
+ try {
895
+ const response = await fetch(NPM_REGISTRY_URL, {
896
+ signal: controller.signal,
897
+ headers: { accept: "application/json", "user-agent": `artshelf/${VERSION}` }
898
+ });
899
+ if (!response.ok)
900
+ return null;
901
+ const body = await response.json();
902
+ if (!body || typeof body !== "object" || typeof body.version !== "string")
903
+ return null;
904
+ return normalizeVersion(body.version);
905
+ }
906
+ catch {
907
+ return null;
908
+ }
909
+ finally {
910
+ clearTimeout(timeout);
911
+ }
912
+ }
913
+ function updateCachePath() {
914
+ return process.env.ARTSHELF_UPDATE_CACHE ?? join(homedir(), ".artshelf", "update-check.json");
915
+ }
916
+ function normalizeVersion(version) {
917
+ return version.trim().replace(/^v/i, "");
918
+ }
919
+ function compareVersions(left, right) {
920
+ const a = parseVersion(left);
921
+ const b = parseVersion(right);
922
+ for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index += 1) {
923
+ const diff = (a.numbers[index] ?? 0) - (b.numbers[index] ?? 0);
924
+ if (diff !== 0)
925
+ return diff;
926
+ }
927
+ if (a.prerelease === b.prerelease)
928
+ return 0;
929
+ if (!a.prerelease)
930
+ return 1;
931
+ if (!b.prerelease)
932
+ return -1;
933
+ return a.prerelease.localeCompare(b.prerelease);
934
+ }
935
+ function parseVersion(version) {
936
+ const [main = "", prerelease = ""] = normalizeVersion(version).split("-", 2);
937
+ return {
938
+ numbers: main.split(".").map((part) => {
939
+ const parsed = Number.parseInt(part, 10);
940
+ return Number.isFinite(parsed) ? parsed : 0;
941
+ }),
942
+ prerelease
943
+ };
944
+ }
729
945
  function parseArgs(argv) {
730
946
  const [command, ...rest] = argv;
731
947
  const flags = new Map();
@@ -1107,6 +1323,21 @@ Human output is short enough to paste into a chat; \`artshelf status --all --jso
1107
1323
  is suitable for cron and reporting. Status is read-only: it never creates plans
1108
1324
  or receipts and never mutates records. A healthy selected ledger exits 0; with
1109
1325
  --all, a broken registry or any stale or invalid registered ledger exits non-zero.
1326
+ `);
1327
+ return;
1328
+ }
1329
+ if (command === "update") {
1330
+ process.stdout.write(`Usage:
1331
+ artshelf update [--json]
1332
+
1333
+ Update checks compare the current CLI version with the latest published npm
1334
+ version. Normal commands may print a non-blocking update notice to stderr when a
1335
+ newer version is available. Run update to upgrade npm global installs only:
1336
+
1337
+ npm install -g artshelf@latest
1338
+
1339
+ pnpm global installs should update with pnpm add -g artshelf@latest; source
1340
+ installs should update by pulling, rebuilding, and linking the checkout.
1110
1341
  `);
1111
1342
  return;
1112
1343
  }
@@ -1132,6 +1363,7 @@ Usage:
1132
1363
  artshelf doctor [--json]
1133
1364
  artshelf status [--json]
1134
1365
  artshelf status --all [--json]
1366
+ artshelf update [--json]
1135
1367
  artshelf cleanup --dry-run [--json]
1136
1368
  artshelf cleanup --dry-run --all [--json]
1137
1369
  artshelf cleanup --execute --plan-id <id> [--json]
@@ -1154,4 +1386,11 @@ Examples:
1154
1386
  artshelf cleanup --execute --plan-id plan_20260601_120000_ab12
1155
1387
  `);
1156
1388
  }
1157
- process.exitCode = main(process.argv.slice(2));
1389
+ main(process.argv.slice(2))
1390
+ .then((status) => {
1391
+ process.exitCode = status;
1392
+ })
1393
+ .catch((error) => {
1394
+ process.stderr.write(`artshelf: ${error.message}\nRun \`artshelf help\` for usage.\n`);
1395
+ process.exitCode = 1;
1396
+ });
@@ -47,11 +47,11 @@
47
47
  <ul class="boundary-list">
48
48
  <li>
49
49
  <span class="stamp readonly">Allowed freely</span>
50
- <span><code>validate</code>, <code>review</code>, <code>cleanup --dry-run</code>, <code>trash list</code>, and <code>trash purge --dry-run</code>.</span>
50
+ <span><code>validate</code>, <code>review</code>, and <code>cleanup --dry-run</code>.</span>
51
51
  </li>
52
52
  <li>
53
53
  <span class="stamp approval">Needs approval</span>
54
- <span><code>cleanup --execute --plan-id</code> and <code>trash purge --execute --plan-id</code>, each for one reviewed plan.</span>
54
+ <span><code>cleanup --execute --plan-id</code> for one reviewed plan.</span>
55
55
  </li>
56
56
  <li>
57
57
  <span class="stamp refused">Refused</span>
@@ -70,31 +70,13 @@ artshelf cleanup --execute --plan-id &lt;id&gt;</code></pre>
70
70
  <span class="callout-label">Hard boundary</span>
71
71
  <p>
72
72
  Cleanup execution needs explicit human approval for the reviewed plan id.
73
- <code>cleanup=delete</code> stays refused instead of silently deleting files.
73
+ <code>cleanup=trash</code> quarantines files into Artshelf trash recoverable,
74
+ not deleted — and <code>cleanup=delete</code> stays refused. Emptying the trash
75
+ for good is a separate stage: see <a href="agent-purge.html">Purge</a>.
74
76
  </p>
75
77
  </div>
76
78
  </section>
77
79
 
78
- <section>
79
- <h2>Clear the trash</h2>
80
- <p>
81
- <code>cleanup=trash</code> moves artifacts into Artshelf trash, so Clean is
82
- not finished until the trash is cleared. Purging runs the loop one more
83
- time: list what is in trash, preview an age-based purge plan, and execute
84
- only after a human approves that purge plan id. Physical deletion never
85
- piggybacks on the cleanup plan that trashed the file; the purge plan is
86
- always separate and separately reviewed.
87
- </p>
88
- <pre><code><span class="c"># what is in trash for this ledger</span>
89
- artshelf trash list --ledger &lt;ledger-path&gt; --json
90
-
91
- <span class="c"># preview an age-based purge and get a purge plan id</span>
92
- artshelf trash purge --older-than 7d --dry-run --ledger &lt;ledger-path&gt; --json
93
-
94
- <span class="c"># delete for real, only with the reviewed purge plan id</span>
95
- artshelf trash purge --execute --plan-id &lt;purge-plan-id&gt; --ledger &lt;ledger-path&gt; --json</code></pre>
96
- </section>
97
-
98
80
  <section>
99
81
  <h2>Resolve confirmed records</h2>
100
82
  <p>Resolve only updates the ledger; it does not move or delete files.</p>
@@ -106,7 +88,7 @@ artshelf resolve &lt;id&gt; --status resolved --reason &lt;text&gt;</code></pre>
106
88
 
107
89
  <section>
108
90
  <h2>Verify quiet</h2>
109
- <p>After cleanup execute, trash purge, or resolve, verify with <code>artshelf review --all --json</code>.</p>
91
+ <p>After cleanup execute or resolve, verify with <code>artshelf review --all --json</code>.</p>
110
92
  <p>
111
93
  Execution writes a receipt and updates touched ledger records to
112
94
  <code>trashed</code>, <code>review-required</code>, or
@@ -89,6 +89,7 @@ artshelf find --all --owner &lt;agent-or-runtime&gt; --json</code></pre>
89
89
  <ul>
90
90
  <li><strong>Read-only.</strong> Validate, status, due, review, doctor, and trash list are fine.</li>
91
91
  <li><strong>Quiet by default.</strong> Send nothing when the review is clean unless a summary was requested.</li>
92
+ <li><strong>No network mode.</strong> Set <code>ARTSHELF_NO_UPDATE_CHECK=1</code> when jobs must avoid npm update checks and cache writes.</li>
92
93
  <li><strong>Never schedule execution.</strong> Scheduled jobs must not run cleanup execute or trash purge execute.</li>
93
94
  </ul>
94
95
  <pre><code><span class="c"># ledger health, current ledger or all registered ledgers</span>
@@ -0,0 +1,111 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Purge · Agent usage · Artshelf</title>
7
+ <meta name="description" content="How agents purge Artshelf trash: physical deletion only from a separate, separately reviewed purge plan.">
8
+ <script>(function(){var stored=null;try{stored=localStorage.getItem("artshelf-docs-theme");}catch(e){}var dark=false;try{dark=matchMedia("(prefers-color-scheme: dark)").matches;}catch(e){}document.documentElement.dataset.theme=stored||(dark?"dark":"light");})();</script>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,450..680;1,9..144,450..680&family=Newsreader:ital,opsz,wght@0,6..72,400..650;1,6..72,400..650&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="site.css">
13
+ <script src="site.js" defer></script>
14
+ </head>
15
+ <body data-page="agent-purge.html">
16
+ <a class="skip" href="#content">Skip to content</a>
17
+ <header class="masthead">
18
+ <div class="masthead-inner">
19
+ <button class="menu-btn" type="button" data-menu aria-label="Toggle navigation" aria-expanded="false"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 3.5h14M1 8h14M1 12.5h14" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
20
+ <a class="brand" href="index.html">Artshelf<span class="brand-tag">docs</span></a>
21
+ <button class="search-btn" type="button" data-search-open><span>Search docs</span><kbd>/</kbd></button>
22
+ <div class="masthead-tools">
23
+ <a class="gh" href="https://github.com/calvinnwq/artshelf">GitHub</a>
24
+ <button class="theme-btn" type="button" data-theme-toggle aria-label="Toggle color theme" aria-pressed="false">
25
+ <svg class="icon-moon" viewBox="0 0 20 20" aria-hidden="true"><path d="M14.6 12.1A6.5 6.5 0 0 1 7.4 2.7a6.5 6.5 0 1 0 7.2 9.4z" fill="currentColor"/></svg>
26
+ <svg class="icon-sun" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="10" y1="2" x2="10" y2="4"/><line x1="10" y1="16" x2="10" y2="18"/><line x1="2" y1="10" x2="4" y2="10"/><line x1="16" y1="10" x2="18" y2="10"/><line x1="4.2" y1="4.2" x2="5.6" y2="5.6"/><line x1="14.4" y1="14.4" x2="15.8" y2="15.8"/><line x1="4.2" y1="15.8" x2="5.6" y2="14.4"/><line x1="14.4" y1="5.6" x2="15.8" y2="4.2"/></g></svg>
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </header>
31
+
32
+ <div class="frame">
33
+ <nav id="sidebar" class="sidebar" aria-label="Documentation"></nav>
34
+
35
+ <main id="content" class="article-col">
36
+ <article>
37
+ <p class="kicker"><span class="n">4.5</span>Agents · Purge</p>
38
+ <h1>Delete for real, from its own reviewed plan.</h1>
39
+ <p class="lede">
40
+ Purge is the only stage that physically deletes. Clean trashes; purge
41
+ empties the trash — and only from a separate plan a human reviewed and
42
+ approved. Trashed records stay discoverable until you deliberately
43
+ remove them.
44
+ </p>
45
+
46
+ <section>
47
+ <h2>Purge boundary</h2>
48
+ <ul class="boundary-list">
49
+ <li>
50
+ <span class="stamp readonly">Allowed freely</span>
51
+ <span><code>trash list</code> and <code>trash purge --dry-run</code> — discover and preview, moving nothing.</span>
52
+ </li>
53
+ <li>
54
+ <span class="stamp approval">Needs approval</span>
55
+ <span><code>trash purge --execute --plan-id</code> for one reviewed purge plan, on one ledger.</span>
56
+ </li>
57
+ <li>
58
+ <span class="stamp refused">Refused</span>
59
+ <span>No global purge — <code>--all</code> is not supported for purge — and no piggybacking on the cleanup plan that trashed the file.</span>
60
+ </li>
61
+ </ul>
62
+ <div class="callout" data-kind="boundary">
63
+ <span class="callout-label">Hard boundary</span>
64
+ <p>
65
+ The purge plan is always separate from the cleanup plan and separately
66
+ reviewed. Physical deletion never happens as a side effect of cleanup.
67
+ </p>
68
+ </div>
69
+ </section>
70
+
71
+ <section>
72
+ <h2>Preview, then purge</h2>
73
+ <p>
74
+ Purge runs the loop one more time: list what is in trash, preview an
75
+ age-based purge plan to get a purge plan id, then execute only that id
76
+ after a human approves it.
77
+ </p>
78
+ <pre><code><span class="c"># what is in trash for this ledger</span>
79
+ artshelf trash list --ledger &lt;ledger-path&gt; --json
80
+
81
+ <span class="c"># preview an age-based purge and get a purge plan id</span>
82
+ artshelf trash purge --older-than 7d --dry-run --ledger &lt;ledger-path&gt; --json
83
+
84
+ <span class="c"># delete for real, only with the reviewed purge plan id</span>
85
+ artshelf trash purge --execute --plan-id &lt;purge-plan-id&gt; --ledger &lt;ledger-path&gt; --json</code></pre>
86
+ <p>The approval wording for a purge:</p>
87
+ <pre><code>approve artshelf trash purge ledger &lt;ledger-path&gt; plan &lt;purge-plan-id&gt;</code></pre>
88
+ </section>
89
+
90
+ <section>
91
+ <h2>Verify quiet</h2>
92
+ <p>
93
+ After a purge executes, confirm the trash is empty and the shelf is
94
+ quiet with <code>artshelf trash list --all --json</code> and
95
+ <code>artshelf review --all --json</code>.
96
+ </p>
97
+ <p>
98
+ Purge writes a receipt and records it as an <code>owner=artshelf</code>
99
+ artifact, so the deletion stays auditable even after the files are gone.
100
+ </p>
101
+ </section>
102
+ </article>
103
+ <footer class="pager" id="pager"></footer>
104
+ </main>
105
+
106
+ <aside class="toc-col"><p class="toc-title">On this page</p><nav id="toc" aria-label="On this page"></nav></aside>
107
+ </div>
108
+
109
+ <footer class="colophon"><div class="colophon-inner"><span>Artshelf docs</span><span>MIT</span><a href="https://github.com/calvinnwq/artshelf">GitHub</a></div></footer>
110
+ </body>
111
+ </html>
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Agent usage · Artshelf</title>
7
- <meta name="description" content="How agents use Artshelf safely: Create, Monitor, Review, Clean.">
7
+ <meta name="description" content="How agents use Artshelf safely: Create, Monitor, Review, Clean, Purge.">
8
8
  <script>(function(){var stored=null;try{stored=localStorage.getItem("artshelf-docs-theme");}catch(e){}var dark=false;try{dark=matchMedia("(prefers-color-scheme: dark)").matches;}catch(e){}document.documentElement.dataset.theme=stored||(dark?"dark":"light");})();</script>
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -35,9 +35,9 @@
35
35
  <main id="content" class="article-col">
36
36
  <article>
37
37
  <p class="kicker"><span class="n">04</span>Agents · Overview</p>
38
- <h1>Create, monitor, review, clean.</h1>
38
+ <h1>Create, monitor, review, clean, purge.</h1>
39
39
  <p class="lede">
40
- An agent's whole relationship with Artshelf fits in four stages. Each stage
40
+ An agent's whole relationship with Artshelf fits in five stages. Each stage
41
41
  has its own page with the exact commands; this page is the contract that
42
42
  ties them together.
43
43
  </p>
@@ -63,14 +63,18 @@
63
63
  <a class="ledger-row" href="agent-clean.html">
64
64
  <span class="n">4.4</span>
65
65
  <span class="stage">Clean</span>
66
- <span class="what"><span class="cmdline">artshelf cleanup --execute --plan-id &lt;id&gt;</span>Run the one plan the human approved, clear old trash with its own reviewed purge plan, resolve confirmed ids, and verify the next review is quiet.</span>
66
+ <span class="what"><span class="cmdline">artshelf cleanup --execute --plan-id &lt;id&gt;</span>Run the one plan the human approved, resolve confirmed ids, and verify the next review is quiet.</span>
67
+ </a>
68
+ <a class="ledger-row" href="agent-purge.html">
69
+ <span class="n">4.5</span>
70
+ <span class="stage">Purge</span>
71
+ <span class="what"><span class="cmdline">artshelf trash purge --execute --plan-id &lt;id&gt;</span>Clear old trash through its own separately reviewed purge plan. Physical deletion never piggybacks on the cleanup plan that trashed the file.</span>
67
72
  </a>
68
73
  </div>
69
74
  <p>
70
- Trash rides the same loop. Monitor lists what sits in Artshelf trash,
71
- Review previews an age-based purge plan, and Clean executes only the
72
- reviewed purge plan id. Clearing trash never piggybacks on the cleanup
73
- plan that trashed the file.
75
+ Clean trashes; Purge deletes. Cleanup quarantines artifacts into Artshelf
76
+ trash, and only a separate, separately reviewed purge plan removes them for
77
+ good a second approval boundary before destructive deletion.
74
78
  </p>
75
79
  </section>
76
80
 
@@ -78,7 +82,7 @@
78
82
  <h2>Operating principles</h2>
79
83
  <dl class="def-rows">
80
84
  <div><dt>agents remember</dt><dd>use the portable skill so final, status, and handoff turns check artifacts</dd></div>
81
- <div><dt>crons only read</dt><dd>scheduled jobs may validate, review, dry-run, and report, but never execute</dd></div>
85
+ <div><dt>crons only read</dt><dd>scheduled jobs may validate, review, dry-run, and report, but never execute; set <code>ARTSHELF_NO_UPDATE_CHECK=1</code> when they must avoid npm network checks and update-cache writes</dd></div>
82
86
  <div><dt>humans approve</dt><dd>mutation needs exact approval targets: ledger path, reviewed plan id, or record id list</dd></div>
83
87
  </dl>
84
88
  </section>
@@ -11,7 +11,7 @@ mutation.
11
11
 
12
12
  ## Workflow Summary
13
13
 
14
- Use Artshelf as a four-stage loop around agent work:
14
+ Use Artshelf as a five-stage loop around agent work:
15
15
 
16
16
  1. **Create**: register durable temp artifacts with lookup-before-put and
17
17
  `artshelf put`, or state the skip reason.
@@ -19,10 +19,12 @@ Use Artshelf as a four-stage loop around agent work:
19
19
  paths, and trash state.
20
20
  3. **Review**: turn raw output into an `ArtshelfReviewReport` decision packet
21
21
  with exact approval targets.
22
- 4. **Clean**: execute approved plans, clear trash only from a separate
23
- reviewed purge plan, resolve confirmed ids, then verify quiet.
22
+ 4. **Clean**: execute approved cleanup plans (which trash, never delete),
23
+ resolve confirmed ids, then verify quiet.
24
+ 5. **Purge**: clear old trash only from a separate, separately reviewed purge
25
+ plan; physical deletion never piggybacks on the cleanup plan.
24
26
 
25
- This maps to the product loop: **Create -> Monitor -> Review -> Clean**.
27
+ This maps to the product loop: **Create -> Monitor -> Review -> Clean -> Purge**.
26
28
 
27
29
  ## Child Pages
28
30
 
@@ -34,13 +36,16 @@ The browsable docs split the workflow into focused child pages:
34
36
  and preview plans.
35
37
  - [Review](agent-review.html): decision packet schema, classifications, and
36
38
  exact approval wording.
37
- - [Clean](agent-clean.html): approval-only cleanup, trash purge, resolve,
38
- receipts, and verify-quiet checks.
39
+ - [Clean](agent-clean.html): approval-only cleanup, resolve, receipts, and
40
+ verify-quiet checks.
41
+ - [Purge](agent-purge.html): separately reviewed trash purge that physically
42
+ deletes, with its own approval target and receipts.
39
43
 
40
44
  ## Operating Principles
41
45
 
42
46
  - Agents remember with the portable skill.
43
- - Scheduled checks read and report only.
47
+ - Scheduled checks read and report only; set `ARTSHELF_NO_UPDATE_CHECK=1` when
48
+ they must avoid npm network checks and update-cache writes.
44
49
  - Review output is a decision packet, not raw counts.
45
50
  - Approval names the exact ledger, plan id, or record ids.
46
51
  - Every approved action ends with a read-only verification.