artshelf 0.3.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 +103 -0
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/SPEC.md +675 -0
- package/dist/src/cli.js +1149 -0
- package/dist/src/ledger.js +846 -0
- package/dist/src/registry.js +137 -0
- package/dist/src/time.js +71 -0
- package/dist/src/types.js +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/agent-usage.html +325 -0
- package/docs/agent-usage.md +392 -0
- package/docs/index.html +172 -0
- package/docs/install.html +137 -0
- package/docs/quickstart.html +142 -0
- package/docs/reference.html +170 -0
- package/docs/site.css +639 -0
- package/docs/theme.js +42 -0
- package/package.json +56 -0
- package/skills/artshelf/SKILL.md +373 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { now, toIso } from "./time.js";
|
|
5
|
+
export function defaultRegistryPath() {
|
|
6
|
+
return process.env.ARTSHELF_REGISTRY ?? process.env.SHELF_REGISTRY ?? join(homedir(), ".shelf", "ledgers.json");
|
|
7
|
+
}
|
|
8
|
+
export function normalizeRegistryPath(path) {
|
|
9
|
+
return resolve(path ?? defaultRegistryPath());
|
|
10
|
+
}
|
|
11
|
+
export function readRegistry(registryPath = normalizeRegistryPath()) {
|
|
12
|
+
if (!existsSync(registryPath))
|
|
13
|
+
return { version: 1, ledgers: [] };
|
|
14
|
+
const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
15
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.ledgers)) {
|
|
16
|
+
throw new Error(`Invalid Artshelf ledger registry: ${registryPath}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
version: 1,
|
|
20
|
+
ledgers: parsed.ledgers.map((entry) => normalizeEntry(entry))
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function listRegisteredLedgers(registryPath = normalizeRegistryPath()) {
|
|
24
|
+
return readRegistry(registryPath).ledgers;
|
|
25
|
+
}
|
|
26
|
+
export function registerLedger(input) {
|
|
27
|
+
const registryPath = normalizeRegistryPath(input.registryPath);
|
|
28
|
+
const ledgerPath = resolve(input.ledgerPath);
|
|
29
|
+
return withRegistryLock(registryPath, () => {
|
|
30
|
+
const registry = readRegistry(registryPath);
|
|
31
|
+
const timestamp = toIso(now());
|
|
32
|
+
const existingIndex = registry.ledgers.findIndex((entry) => entry.path === ledgerPath);
|
|
33
|
+
const existing = existingIndex >= 0 ? registry.ledgers[existingIndex] : undefined;
|
|
34
|
+
const name = normalizeName(input.name);
|
|
35
|
+
const entry = {
|
|
36
|
+
name: name ?? existing?.name ?? inferLedgerName(ledgerPath),
|
|
37
|
+
path: ledgerPath,
|
|
38
|
+
scope: input.scope ? assertScope(input.scope) : existing?.scope ?? inferLedgerScope(ledgerPath),
|
|
39
|
+
createdAt: existing?.createdAt ?? timestamp,
|
|
40
|
+
updatedAt: timestamp
|
|
41
|
+
};
|
|
42
|
+
if (existingIndex >= 0) {
|
|
43
|
+
registry.ledgers[existingIndex] = entry;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
registry.ledgers.push(entry);
|
|
47
|
+
}
|
|
48
|
+
registry.ledgers.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
49
|
+
writeRegistry(registryPath, registry);
|
|
50
|
+
return entry;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function writeRegistry(registryPath, registry) {
|
|
54
|
+
mkdirSync(dirname(registryPath), { recursive: true });
|
|
55
|
+
const tmpPath = `${registryPath}.${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.tmp`;
|
|
56
|
+
writeFileSync(tmpPath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
57
|
+
renameSync(tmpPath, registryPath);
|
|
58
|
+
}
|
|
59
|
+
function withRegistryLock(registryPath, fn) {
|
|
60
|
+
mkdirSync(dirname(registryPath), { recursive: true });
|
|
61
|
+
const lockPath = `${registryPath}.lock`;
|
|
62
|
+
const deadline = Date.now() + 5000;
|
|
63
|
+
const staleAfterMs = 30_000;
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(lockPath);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code !== "EEXIST")
|
|
71
|
+
throw error;
|
|
72
|
+
if (isStaleLock(lockPath, staleAfterMs)) {
|
|
73
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (Date.now() > deadline)
|
|
77
|
+
throw new Error(`Timed out waiting for Artshelf ledger registry lock: ${registryPath}`);
|
|
78
|
+
sleep(25);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return fn();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
90
|
+
}
|
|
91
|
+
function isStaleLock(lockPath, staleAfterMs) {
|
|
92
|
+
try {
|
|
93
|
+
return Date.now() - statSync(lockPath).mtimeMs > staleAfterMs;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code === "ENOENT")
|
|
97
|
+
return false;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function normalizeEntry(entry) {
|
|
102
|
+
if (!entry.name || !entry.path || !entry.scope || !entry.createdAt || !entry.updatedAt) {
|
|
103
|
+
throw new Error("Invalid Artshelf ledger registry entry");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
name: entry.name,
|
|
107
|
+
path: resolve(entry.path),
|
|
108
|
+
scope: assertScope(entry.scope),
|
|
109
|
+
createdAt: entry.createdAt,
|
|
110
|
+
updatedAt: entry.updatedAt
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizeName(name) {
|
|
114
|
+
const trimmed = name?.trim();
|
|
115
|
+
return trimmed ? trimmed : undefined;
|
|
116
|
+
}
|
|
117
|
+
function inferLedgerName(ledgerPath) {
|
|
118
|
+
const normalized = resolve(ledgerPath);
|
|
119
|
+
if (normalized === join(homedir(), ".shelf", "ledger.jsonl"))
|
|
120
|
+
return "global";
|
|
121
|
+
if (basename(dirname(normalized)) === ".shelf")
|
|
122
|
+
return basename(dirname(dirname(normalized))) || "repo";
|
|
123
|
+
return basename(dirname(normalized)) || "ledger";
|
|
124
|
+
}
|
|
125
|
+
function inferLedgerScope(ledgerPath) {
|
|
126
|
+
const normalized = resolve(ledgerPath);
|
|
127
|
+
if (normalized.startsWith(join(homedir(), ".shelf")))
|
|
128
|
+
return "user";
|
|
129
|
+
if (basename(dirname(normalized)) === ".shelf")
|
|
130
|
+
return "repo";
|
|
131
|
+
return "other";
|
|
132
|
+
}
|
|
133
|
+
function assertScope(scope) {
|
|
134
|
+
if (scope === "repo" || scope === "user" || scope === "other")
|
|
135
|
+
return scope;
|
|
136
|
+
throw new Error(`Unknown ledger scope: ${scope}`);
|
|
137
|
+
}
|
package/dist/src/time.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const TTL_PATTERN = /^(\d+)([dhm])$/;
|
|
2
|
+
const TTL_MULTIPLIERS = {
|
|
3
|
+
m: 60 * 1000,
|
|
4
|
+
h: 60 * 60 * 1000,
|
|
5
|
+
d: 24 * 60 * 60 * 1000
|
|
6
|
+
};
|
|
7
|
+
export function now() {
|
|
8
|
+
const forced = process.env.ARTSHELF_NOW ?? process.env.SHELF_NOW;
|
|
9
|
+
if (!forced)
|
|
10
|
+
return new Date();
|
|
11
|
+
const parsed = new Date(forced);
|
|
12
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
13
|
+
throw new Error(`Invalid ARTSHELF_NOW value: ${forced}`);
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
export function toIso(date) {
|
|
18
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19
|
+
}
|
|
20
|
+
export function addTtl(start, ttl) {
|
|
21
|
+
const match = TTL_PATTERN.exec(ttl);
|
|
22
|
+
if (!match) {
|
|
23
|
+
throw new Error("TTL must look like 30m, 12h, or 7d");
|
|
24
|
+
}
|
|
25
|
+
const amount = Number(match[1]);
|
|
26
|
+
const unit = match[2];
|
|
27
|
+
if (!unit || !(unit in TTL_MULTIPLIERS)) {
|
|
28
|
+
throw new Error("TTL must look like 30m, 12h, or 7d");
|
|
29
|
+
}
|
|
30
|
+
return new Date(start.getTime() + amount * TTL_MULTIPLIERS[unit]);
|
|
31
|
+
}
|
|
32
|
+
export function ttlToMs(ttl) {
|
|
33
|
+
const match = TTL_PATTERN.exec(ttl);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error("TTL must look like 30m, 12h, or 7d");
|
|
36
|
+
}
|
|
37
|
+
const amount = Number(match[1]);
|
|
38
|
+
const unit = match[2];
|
|
39
|
+
if (!unit || !(unit in TTL_MULTIPLIERS)) {
|
|
40
|
+
throw new Error("TTL must look like 30m, 12h, or 7d");
|
|
41
|
+
}
|
|
42
|
+
return amount * TTL_MULTIPLIERS[unit];
|
|
43
|
+
}
|
|
44
|
+
export function ageOf(nowDate, pastIso) {
|
|
45
|
+
const past = new Date(pastIso);
|
|
46
|
+
if (Number.isNaN(past.getTime())) {
|
|
47
|
+
throw new Error(`Invalid timestamp: ${pastIso}`);
|
|
48
|
+
}
|
|
49
|
+
const ageMs = Math.max(0, nowDate.getTime() - past.getTime());
|
|
50
|
+
const totalMinutes = Math.floor(ageMs / (60 * 1000));
|
|
51
|
+
if (totalMinutes === 0)
|
|
52
|
+
return "0m";
|
|
53
|
+
const days = Math.floor(totalMinutes / (24 * 60));
|
|
54
|
+
const hours = Math.floor(totalMinutes % (24 * 60) / 60);
|
|
55
|
+
const minutes = totalMinutes % 60;
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (days > 0)
|
|
58
|
+
parts.push(`${days}d`);
|
|
59
|
+
if (hours > 0)
|
|
60
|
+
parts.push(`${hours}h`);
|
|
61
|
+
if (minutes > 0 || parts.length === 0)
|
|
62
|
+
parts.push(`${minutes}m`);
|
|
63
|
+
return parts.join(" ");
|
|
64
|
+
}
|
|
65
|
+
export function assertIsoDate(value, label) {
|
|
66
|
+
const parsed = new Date(value);
|
|
67
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
68
|
+
throw new Error(`${label} must be a valid date`);
|
|
69
|
+
}
|
|
70
|
+
return toIso(parsed);
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/.nojekyll
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
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>Artshelf Agent Usage</title>
|
|
7
|
+
<meta name="description" content="How agents should use Artshelf safely.">
|
|
8
|
+
<link rel="stylesheet" href="site.css">
|
|
9
|
+
<script src="theme.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div class="docs-shell">
|
|
13
|
+
<nav class="global-nav" aria-label="Documentation">
|
|
14
|
+
<a class="site-mark" href="index.html"><strong>Artshelf</strong><span>Artifact retention CLI</span></a>
|
|
15
|
+
<button class="theme-toggle" type="button" data-theme-toggle aria-label="Toggle color theme" aria-pressed="false">Dark</button>
|
|
16
|
+
<div class="nav-scroll" aria-label="Documentation sections">
|
|
17
|
+
<div class="nav-section">
|
|
18
|
+
<p class="nav-section-title">Start</p>
|
|
19
|
+
<a href="index.html">Overview</a>
|
|
20
|
+
<a href="install.html">Install</a>
|
|
21
|
+
<a href="quickstart.html">Quickstart</a>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="nav-section">
|
|
24
|
+
<p class="nav-section-title">Agents</p>
|
|
25
|
+
<a href="agent-usage.html" aria-current="page">Agent usage</a>
|
|
26
|
+
<a href="https://github.com/calvinnwq/artshelf/blob/main/skills/artshelf/SKILL.md">Agent skill</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="nav-section">
|
|
29
|
+
<p class="nav-section-title">Reference</p>
|
|
30
|
+
<a href="reference.html">CLI reference</a>
|
|
31
|
+
<a href="https://github.com/calvinnwq/artshelf">GitHub</a>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div class="docs-content">
|
|
37
|
+
<header class="page-top">
|
|
38
|
+
<div class="wrap">
|
|
39
|
+
<nav class="breadcrumbs" aria-label="Breadcrumbs"><a href="index.html">Docs</a><span>/</span><span>Agent usage</span></nav>
|
|
40
|
+
<div class="hero">
|
|
41
|
+
<div>
|
|
42
|
+
<p class="eyebrow">For local agents and automation</p>
|
|
43
|
+
<h1>Register artifacts while intent is fresh.</h1>
|
|
44
|
+
<p class="lede">
|
|
45
|
+
Agents should use Artshelf when temporary files need accountability, restart
|
|
46
|
+
context, or later human review. Source files and cheap build outputs stay out.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="terminal">
|
|
50
|
+
<div class="terminal-head"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span>agent contract</span></div>
|
|
51
|
+
<pre><code>$ artshelf put <path> \
|
|
52
|
+
--reason "<why this exists>" \
|
|
53
|
+
--ttl 3d \
|
|
54
|
+
--kind run-artifact \
|
|
55
|
+
--cleanup review \
|
|
56
|
+
--owner agent \
|
|
57
|
+
--label <project> \
|
|
58
|
+
--json</code></pre>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<main class="wrap">
|
|
65
|
+
<article>
|
|
66
|
+
<section>
|
|
67
|
+
<h2>Registration Trigger</h2>
|
|
68
|
+
<p>
|
|
69
|
+
Treat Artshelf as a finalization check, not an optional cleanup habit. Before an agent reports a task as done,
|
|
70
|
+
it should check whether the task created, copied,
|
|
71
|
+
exported, quarantined, backed up, or preserved any non-source file or directory
|
|
72
|
+
that may outlive the current command.
|
|
73
|
+
</p>
|
|
74
|
+
<p>
|
|
75
|
+
Register eligible artifacts immediately. If an eligible artifact is skipped,
|
|
76
|
+
state the reason: source-controlled, regeneratable, secret-bearing, already
|
|
77
|
+
tracked by another durable ledger, or explicitly not retained by request.
|
|
78
|
+
</p>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section>
|
|
82
|
+
<h2>Install Behavior</h2>
|
|
83
|
+
<p>
|
|
84
|
+
If <code>artshelf</code> is not installed, prefer the package-manager install
|
|
85
|
+
when available and verify the CLI before registering artifacts.
|
|
86
|
+
</p>
|
|
87
|
+
<pre><code>npm install -g artshelf
|
|
88
|
+
artshelf --version
|
|
89
|
+
artshelf doctor</code></pre>
|
|
90
|
+
<pre><code>pnpm add -g artshelf
|
|
91
|
+
artshelf --version
|
|
92
|
+
artshelf doctor</code></pre>
|
|
93
|
+
<p>
|
|
94
|
+
For source installs, ask where the user wants the repo cloned before setup.
|
|
95
|
+
Use the local clone, build, and <code>npm link</code> path. Do not create a
|
|
96
|
+
custom shim.
|
|
97
|
+
</p>
|
|
98
|
+
<pre><code>git clone https://github.com/calvinnwq/artshelf.git "$ARTSHELF_REPO"
|
|
99
|
+
cd "$ARTSHELF_REPO"
|
|
100
|
+
corepack enable
|
|
101
|
+
pnpm install --frozen-lockfile
|
|
102
|
+
pnpm run build
|
|
103
|
+
npm link
|
|
104
|
+
artshelf --version
|
|
105
|
+
artshelf doctor</code></pre>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<section>
|
|
109
|
+
<h2>Use Artshelf For</h2>
|
|
110
|
+
<div class="grid">
|
|
111
|
+
<div class="card"><h3>Backups</h3><p>Rollback copies, pre-edit snapshots, migration backups, and quarantine folders.</p></div>
|
|
112
|
+
<div class="card"><h3>Evidence</h3><p>Debug output directories, generated reports, logs, and live-smoke artifacts.</p></div>
|
|
113
|
+
<div class="card"><h3>Long runs</h3><p>Workflow artifacts a future agent might need to resume, review, or clean up.</p></div>
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<section>
|
|
118
|
+
<h2>Skip Artshelf For</h2>
|
|
119
|
+
<ul>
|
|
120
|
+
<li>Source files that belong in git.</li>
|
|
121
|
+
<li>Build outputs and dependency caches that can be regenerated cheaply.</li>
|
|
122
|
+
<li>Secrets, credential dumps, or private tokens.</li>
|
|
123
|
+
<li>Artifacts already owned by a more specific durable workflow ledger.</li>
|
|
124
|
+
</ul>
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
<section>
|
|
128
|
+
<h2>Idempotent Lookup</h2>
|
|
129
|
+
<p>
|
|
130
|
+
Integrations should query the ledger before creating another record for the same
|
|
131
|
+
artifact. <code>find</code> and <code>get</code> are read-only; they never move,
|
|
132
|
+
resolve, or delete files.
|
|
133
|
+
</p>
|
|
134
|
+
<pre><code>artshelf find --path <path> --owner <agent-or-runtime> --label <task-or-run-id> --json
|
|
135
|
+
artshelf get <id> --json</code></pre>
|
|
136
|
+
<p>
|
|
137
|
+
<code>find</code> requires at least one selector. Multiple labels are an all-label
|
|
138
|
+
match. If it returns a record, reuse that Artshelf id; otherwise call
|
|
139
|
+
<code>artshelf put</code> and record the new id.
|
|
140
|
+
</p>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section>
|
|
144
|
+
<h2>Ledger Registry</h2>
|
|
145
|
+
<p>
|
|
146
|
+
Artshelf keeps a user-level registry at `~/.shelf/ledgers.json` so one CLI can
|
|
147
|
+
review all known ledgers without moving project records into one global file.
|
|
148
|
+
<code>put</code> registers the ledger it writes to.
|
|
149
|
+
</p>
|
|
150
|
+
<pre><code>artshelf ledgers add --ledger <repo>/.shelf/ledger.jsonl --name <project> --scope repo
|
|
151
|
+
artshelf ledgers list --json
|
|
152
|
+
artshelf review --all --json
|
|
153
|
+
artshelf status --all --json
|
|
154
|
+
artshelf find --all --owner <agent-or-runtime> --json
|
|
155
|
+
artshelf trash list --all --json</code></pre>
|
|
156
|
+
<p>
|
|
157
|
+
<code>artshelf ledgers list --json</code> validates each registered ledger and reports
|
|
158
|
+
ok/missing/invalid status with entry and warning/error counts, so agents can detect
|
|
159
|
+
stale registry entries without a separate validate pass; add <code>--plain</code> for a
|
|
160
|
+
fast listing that skips validation. <code>artshelf review --all --json</code> returns an
|
|
161
|
+
aggregate triage summary and the next safe action next to the per-ledger detail.
|
|
162
|
+
</p>
|
|
163
|
+
<p>Use global cleanup dry-run when you want Artshelf to write cleanup plans for registered ledgers with cleanup entries, without moving files.</p>
|
|
164
|
+
<pre><code>artshelf cleanup --dry-run --all --json</code></pre>
|
|
165
|
+
<div class="note">
|
|
166
|
+
<code>--all</code> is for discovery and review. Cleanup execution remains
|
|
167
|
+
ledger-specific and requires a reviewed plan id for that ledger.
|
|
168
|
+
</div>
|
|
169
|
+
<p>If the executable cleanup entries have not changed, dry-run reuses the existing plan id and refreshes the same plan file instead of creating duplicate plans.</p>
|
|
170
|
+
</section>
|
|
171
|
+
|
|
172
|
+
<section>
|
|
173
|
+
<h2>Daily Review Workflow</h2>
|
|
174
|
+
<p>
|
|
175
|
+
Use this flow when a scheduled review, recurring task, or user request reports
|
|
176
|
+
Artshelf cleanup attention.
|
|
177
|
+
</p>
|
|
178
|
+
<ol>
|
|
179
|
+
<li>Register artifacts early during work, or state why an eligible artifact was skipped.</li>
|
|
180
|
+
<li>Review state with read-only commands first: <code>artshelf ledgers list --json</code>, <code>artshelf review --all --json</code>, and <code>artshelf trash list --all --json</code>; for old trash on a selected ledger, run <code>artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json</code>.</li>
|
|
181
|
+
<li>Present a decision packet instead of raw counts. Include registry health, affected ledgers, due/manual-review/missing-path counts, executable entries, skipped entries, refused entries, trashed record counts and ages, purge dry-run plan ids/skipped entries, and the next safe action.</li>
|
|
182
|
+
<li>Classify each candidate as <code>trash-safe</code>, <code>needs-human-review</code>, <code>resolve-candidate</code>, or <code>registry-problem</code>.</li>
|
|
183
|
+
<li>If cleanup execution is appropriate, generate or reuse a dry-run plan, then ask for explicit approval naming the ledger path and reviewed plan id.</li>
|
|
184
|
+
<li>For trashed records, require a separate reviewed purge plan before physical deletion.</li>
|
|
185
|
+
<li>After approved cleanup execute, trash purge, or resolve, verify quiet with <code>artshelf review --all --json</code>, plus <code>artshelf trash list --ledger <ledger-path> --json</code> and purge receipt evidence after purge, or explain what remains.</li>
|
|
186
|
+
</ol>
|
|
187
|
+
<pre><code>artshelf trash list --ledger <ledger-path>
|
|
188
|
+
artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json
|
|
189
|
+
artshelf trash purge --execute --plan-id <purge-plan-id> --ledger <ledger-path> --json</code></pre>
|
|
190
|
+
<pre><code>approve artshelf cleanup ledger <ledger-path> plan <plan-id>
|
|
191
|
+
approve artshelf trash purge ledger <ledger-path> plan <purge-plan-id></code></pre>
|
|
192
|
+
<div class="note">
|
|
193
|
+
Never execute from a read-only preview id. Never generate a fresh plan and execute
|
|
194
|
+
it in the same step. <code>trash</code> moves artifacts into Artshelf trash; physical
|
|
195
|
+
delete requires a separate reviewed trash purge plan.
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
<section>
|
|
200
|
+
<h2>Report The ID</h2>
|
|
201
|
+
<pre><code>Artshelf artifacts:
|
|
202
|
+
- shf_20260601_182800_ab12: /tmp/parser-output, debug evidence for issue-123,
|
|
203
|
+
retain until 2026-06-04, cleanup=review</code></pre>
|
|
204
|
+
<p>
|
|
205
|
+
Put the id in handoffs, PR comments, issue comments, memory, or task run
|
|
206
|
+
summaries when the artifact matters for restart.
|
|
207
|
+
</p>
|
|
208
|
+
<p>
|
|
209
|
+
If there are no eligible artifacts, say nothing. If eligible artifacts were
|
|
210
|
+
skipped instead of registered, include the brief skip reason from the
|
|
211
|
+
completion checklist.
|
|
212
|
+
</p>
|
|
213
|
+
</section>
|
|
214
|
+
|
|
215
|
+
<section>
|
|
216
|
+
<h2>Cleanup Boundary</h2>
|
|
217
|
+
<pre><code>artshelf validate --json
|
|
218
|
+
artshelf validate --all --json
|
|
219
|
+
artshelf due --json
|
|
220
|
+
artshelf due --all --json
|
|
221
|
+
artshelf review --all --json</code></pre>
|
|
222
|
+
<p>Cleanup dry-run is safe to run. It writes plan files for later review only when there are executable cleanup entries; no-op dry-runs report <code>not-created</code> and write no plan file. Matching dry-runs reuse the existing plan id and refresh the plan timestamp.</p>
|
|
223
|
+
<pre><code>artshelf cleanup --dry-run --json
|
|
224
|
+
artshelf cleanup --dry-run --all --json</code></pre>
|
|
225
|
+
<pre><code>artshelf cleanup --execute --plan-id <id></code></pre>
|
|
226
|
+
<pre><code>artshelf trash list --ledger <ledger-path> --json
|
|
227
|
+
artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json
|
|
228
|
+
artshelf trash purge --execute --plan-id <purge-plan-id> --ledger <ledger-path> --json</code></pre>
|
|
229
|
+
<div class="note">
|
|
230
|
+
Execution is approval-only: no daemon, no auto-execute, no global execute, and no
|
|
231
|
+
fresh-plan-then-execute shortcut. Cleanup execution needs explicit human approval
|
|
232
|
+
for the reviewed plan id. Trash list and purge dry-run are review steps; trash
|
|
233
|
+
purge execution needs separate approval naming the ledger and reviewed purge plan
|
|
234
|
+
id. Execution writes a receipt and updates touched ledger records to `trashed`,
|
|
235
|
+
`review-required`, or `cleanup-refused`. <code>cleanup=delete</code> stays refused
|
|
236
|
+
instead of silently deleting files; physical deletion requires a separate reviewed
|
|
237
|
+
trash purge plan. Artshelf records generated plans and receipts as `owner=artshelf`
|
|
238
|
+
artifacts.
|
|
239
|
+
</div>
|
|
240
|
+
<p>
|
|
241
|
+
Agents may mark a ledger record manually resolved when the user confirms the
|
|
242
|
+
artifact was inspected, is already missing, or is no longer needed.
|
|
243
|
+
</p>
|
|
244
|
+
<pre><code>artshelf resolve <id> --status resolved --reason <text></code></pre>
|
|
245
|
+
<p>
|
|
246
|
+
Use a specific reason. <code>resolve</code> only updates the ledger; it does not
|
|
247
|
+
move or delete files. Resolved records stop reappearing in future due and
|
|
248
|
+
dry-run cleanup output while remaining visible through
|
|
249
|
+
<code>artshelf list --status resolved</code>.
|
|
250
|
+
</p>
|
|
251
|
+
</section>
|
|
252
|
+
|
|
253
|
+
<section>
|
|
254
|
+
<h2>Scheduled Review</h2>
|
|
255
|
+
<p>
|
|
256
|
+
Agents may schedule routine Artshelf checks for stale artifacts through their host
|
|
257
|
+
runtime, such as an agent cron, CI job, or recurring task. Keep scheduled jobs
|
|
258
|
+
non-destructive.
|
|
259
|
+
</p>
|
|
260
|
+
<pre><code>artshelf validate --json
|
|
261
|
+
artshelf validate --all --json
|
|
262
|
+
artshelf due --json
|
|
263
|
+
artshelf due --all --json
|
|
264
|
+
artshelf review --all --json</code></pre>
|
|
265
|
+
<p>
|
|
266
|
+
Read-only health and dashboard checks are also safe to schedule. Run
|
|
267
|
+
<code>artshelf review --all --json</code> for aggregate triage (<code>summary</code>
|
|
268
|
+
and <code>nextAction</code>), <code>artshelf doctor --json</code> to catch a broken
|
|
269
|
+
or stale registry before relying on cleanup planning, and
|
|
270
|
+
<code>artshelf status --all --json</code> for a compact cron summary.
|
|
271
|
+
</p>
|
|
272
|
+
<pre><code>artshelf doctor --json
|
|
273
|
+
artshelf status --all --json</code></pre>
|
|
274
|
+
<p>
|
|
275
|
+
Scheduled cleanup and trash purge dry-runs may write plan files for later review
|
|
276
|
+
when entries exist, but must not move or delete files. Matching cleanup dry-runs
|
|
277
|
+
reuse the existing plan id and refresh the plan timestamp.
|
|
278
|
+
</p>
|
|
279
|
+
<pre><code>artshelf cleanup --dry-run --json
|
|
280
|
+
artshelf cleanup --dry-run --all --json
|
|
281
|
+
artshelf trash list --ledger <ledger-path> --json
|
|
282
|
+
artshelf trash list --all --json
|
|
283
|
+
artshelf trash purge --older-than 7d --dry-run --ledger <ledger-path> --json</code></pre>
|
|
284
|
+
<p>
|
|
285
|
+
Reports should include the ledger path, due/manual-review/missing-path counts,
|
|
286
|
+
cleanup dry-run plan id, executable entries, skipped entries, and refused entries.
|
|
287
|
+
Trash reports may use <code>artshelf trash list --all --json</code> to discover
|
|
288
|
+
trashed records across registered ledgers, then include trashed record counts
|
|
289
|
+
and target ages. Run purge dry-runs only for an explicit ledger and report
|
|
290
|
+
any plan id, matching entries, and skipped entries.
|
|
291
|
+
Stay quiet when nothing needs attention unless a regular summary was requested.
|
|
292
|
+
</p>
|
|
293
|
+
<div class="note">
|
|
294
|
+
Use explicit ledger paths for scheduled checks. Do not scan arbitrary filesystem
|
|
295
|
+
locations for ledgers unless the user opted into that discovery scope. Never
|
|
296
|
+
schedule cleanup execution or trash purge execution; scheduled jobs may only
|
|
297
|
+
dry-run and report plans for later human review.
|
|
298
|
+
</div>
|
|
299
|
+
</section>
|
|
300
|
+
|
|
301
|
+
<section>
|
|
302
|
+
<h2>Completion Checklist</h2>
|
|
303
|
+
<ol>
|
|
304
|
+
<li>Did you create, copy, export, quarantine, back up, or preserve any non-source file or directory?</li>
|
|
305
|
+
<li>Will any of those paths outlive this command?</li>
|
|
306
|
+
<li>If yes, did you register them with Artshelf or state why Artshelf is not appropriate?</li>
|
|
307
|
+
</ol>
|
|
308
|
+
<div class="note">Do not call work done while known eligible artifacts are neither registered nor explicitly skipped.</div>
|
|
309
|
+
</section>
|
|
310
|
+
|
|
311
|
+
<section>
|
|
312
|
+
<h2>Portable Skill</h2>
|
|
313
|
+
<p>
|
|
314
|
+
The repo ships a portable skill at
|
|
315
|
+
<a href="https://github.com/calvinnwq/artshelf/blob/main/skills/artshelf/SKILL.md">skills/artshelf/SKILL.md</a>.
|
|
316
|
+
Agents that support local skills can copy or reference it directly.
|
|
317
|
+
</p>
|
|
318
|
+
</section>
|
|
319
|
+
</article>
|
|
320
|
+
</main>
|
|
321
|
+
<footer class="site-footer"><div class="wrap">Artshelf docs · <a href="reference.html">Next: CLI reference</a></div></footer>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</body>
|
|
325
|
+
</html>
|