artshelf 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -110,6 +110,26 @@
110
110
  state drifted since review, stamps the reconcile audit trail (`previousPath`,
111
111
  `reconcilePlanId`, `reconcileReceiptPath`, `reconciledAt`, `reconcileReason`) on
112
112
  every touched row, and writes an Artshelf-owned reconcile receipt.
113
+ - Integrated reconcile findings into `review --agent`, `status --agent`, and
114
+ `doctor --agent` triage: missing-path warnings now route to reconcile dry-run
115
+ guidance before approval, reconciled plans escalate to ready-for-approval, and
116
+ the `ArtshelfReviewReport` schema adds the `reconcile` action type (NGX-438).
117
+ - Moved `artshelf put` registry-warning output from stdout to stderr in human
118
+ mode; `--json` output is unchanged (NGX-429).
119
+
120
+ ## [0.13.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.12.0...artshelf-v0.13.0) (2026-06-15)
121
+
122
+
123
+ ### Features
124
+
125
+ * **review:** integrate reconcile findings into agent review packets ([878785e](https://github.com/calvinnwq/artshelf/commit/878785e72c4e65bd8e09572525b05cc020d2f1e1))
126
+ * **review:** integrate reconcile findings into agent triage; move put registry-warning to stderr (NGX-438, NGX-429) ([2573470](https://github.com/calvinnwq/artshelf/commit/25734701b439f617a33609ac98c3fae895199640))
127
+
128
+
129
+ ### Bug Fixes
130
+
131
+ * **review:** include reconcile counts in all-ledger triage ([2eeb2fe](https://github.com/calvinnwq/artshelf/commit/2eeb2fea6eae58bfd652be959f2bb6e28d7cb90f))
132
+ * **review:** keep reconcile approval schema and blocked triage consistent ([0c8925a](https://github.com/calvinnwq/artshelf/commit/0c8925a851023622796f2b8d847fcc89cab3c5f0))
113
133
 
114
134
  ## [0.12.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.11.0...artshelf-v0.12.0) (2026-06-15)
115
135
 
package/SPEC.md CHANGED
@@ -99,8 +99,9 @@ Defaults:
99
99
  `put` should refuse to record a path that does not exist unless a future flag
100
100
  explicitly supports planned artifacts. After appending the record, `put`
101
101
  registers the ledger in the ledger registry. Registry registration is
102
- best-effort: if it fails, the record remains appended and output includes a
103
- registry warning or `registryError`.
102
+ best-effort: if it fails, the record remains appended and a registry warning is
103
+ printed to stderr in human mode, or surfaced as a `registryError` field in
104
+ `--json` output, so stdout stays machine-clean.
104
105
 
105
106
  ### `artshelf ledgers`
106
107
 
@@ -253,10 +254,13 @@ included with a `not-created` plan instead of writing a plan file.
253
254
 
254
255
  In `--all` mode, review emits an aggregate triage summary on top of the
255
256
  per-ledger detail. JSON includes a `summary` block with affected-ledger, due,
256
- manual-review, missing-path, executable, and skipped counts plus the preview
257
- plan ids; JSON also includes the next safe action. Human output adds a one-line
258
- triage count and states the same next safe action (repair broken ledgers, dry-run
259
- cleanup, inspect missing paths, or nothing to do). Review never writes a plan, so
257
+ manual-review, missing-path, executable, skipped, and reconcile entry/blocked
258
+ counts plus the preview plan ids; JSON also includes the next safe action. The
259
+ per-ledger human detail appends a `reconcile` count when a ledger has reconcile
260
+ drift. Human output adds a one-line triage count with the same reconcile counts
261
+ and states the same next safe action (repair broken ledgers, dry-run cleanup,
262
+ dry-run reconcile for missing-path or reconcile drift, or nothing to do). Review
263
+ never writes a plan, so
260
264
  the next action always points at an explicit follow-up command.
261
265
 
262
266
  `review`, `status`, and `doctor` share three render modes. The default human
@@ -265,9 +269,13 @@ stays the full, backward-compatible public audit report; and `--agent` emits a c
265
269
  deterministic single-line JSON decision packet for agents, taking precedence over
266
270
  `--json` when both are passed. For `review`, the packet sorts records into
267
271
  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.
272
+ read-only and never mints a cleanup plan, the exact approval targets it emits are
273
+ `resolve missing` and `reconcile`; the `reconcile` target appears only when a
274
+ prior reviewed reconcile plan still matches the live drift. Cleanup-eligible
275
+ records and reconcile drift without a reviewed plan stay needs-review-first and
276
+ point at `cleanup --dry-run` or `reconcile --dry-run`, which mint the reviewed
277
+ plan id to approve. Blocked or ambiguous reconcile findings surface in the
278
+ blocked group with no approval target.
271
279
 
272
280
  ### `artshelf doctor`
273
281
 
@@ -31,6 +31,6 @@ export function handlePut(parsed, ledgerPath, json) {
31
31
  return printJson({ ok: true, record, ledgerPath, registryPath, ...(ledger ? { ledger } : {}), ...(registryError ? { registryError } : {}) });
32
32
  process.stdout.write(`recorded ${record.id}\npath: ${record.path}\nretains until: ${record.retainUntil ?? "manual review"}\nledger: ${ledgerPath}\n`);
33
33
  if (registryError)
34
- process.stdout.write(`registry warning: ${registryError}\n`);
34
+ process.stderr.write(`registry warning: ${registryError}\n`);
35
35
  return 0;
36
36
  }
@@ -15,7 +15,7 @@ export function handleReview(parsed, ledgerPath, json) {
15
15
  printCompactJson(buildReviewAgentPacketAll(results, summary, { path: registryPath, exists: existsSync(registryPath) }));
16
16
  return ok ? 0 : 1;
17
17
  }
18
- const nextAction = reviewNextAction(summary, "all");
18
+ const nextAction = reviewNextAction(summary, "all", undefined, registryPath);
19
19
  if (json) {
20
20
  printJson({ ok, registryPath, summary, nextAction, ledgers: results.map(reviewJsonResult) });
21
21
  return ok ? 0 : 1;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dueEntries, previewCleanupPlan, readLedger, validateLedger } from "../ledger.js";
3
3
  import { listRegisteredLedgers } from "../registry.js";
4
+ import { matchingReconcilePlan, previewReconcilePlan } from "../reconcile.js";
4
5
  import { printJson } from "../renderers/json.js";
5
6
  export function registeredLedgersOrThrow(registryPath) {
6
7
  const ledgers = listRegisteredLedgers(registryPath);
@@ -43,15 +44,19 @@ export function reviewLedger(ledger, registered = true) {
43
44
  ledgerExists,
44
45
  validate,
45
46
  due: [],
46
- plan: emptyReviewPlan(ledger.path)
47
+ plan: emptyReviewPlan(ledger.path),
48
+ reconcile: null
47
49
  };
48
50
  }
51
+ const reconcilePlan = previewReconcilePlan(ledger.path);
52
+ const reviewedReconcilePlan = reconcilePlan.entries.length > 0 || reconcilePlan.blocked.length > 0 ? matchingReconcilePlan(ledger.path, reconcilePlan) : null;
49
53
  return {
50
54
  ledger,
51
55
  ledgerExists,
52
56
  validate,
53
57
  due: dueEntries(readLedger(ledger.path)),
54
- plan: previewCleanupPlan(ledger.path)
58
+ plan: previewCleanupPlan(ledger.path),
59
+ reconcile: { plan: reconcilePlan, reviewedPlan: reviewedReconcilePlan }
55
60
  };
56
61
  }
57
62
  export function reviewJsonResult(result) {
@@ -147,6 +152,8 @@ export function summarizeReview(results) {
147
152
  missingPath: 0,
148
153
  executable: 0,
149
154
  skipped: 0,
155
+ reconcileEntries: 0,
156
+ reconcileBlocked: 0,
150
157
  previewPlanIds: []
151
158
  };
152
159
  for (const result of results) {
@@ -162,14 +169,18 @@ export function summarizeReview(results) {
162
169
  const due = result.due.filter((entry) => entry.dueStatus === "due").length;
163
170
  const manualReview = result.due.filter((entry) => entry.dueStatus === "manual-review").length;
164
171
  const missingPath = result.due.filter((entry) => entry.dueStatus === "missing-path").length;
172
+ const reconcileEntries = result.reconcile?.plan.entries.length ?? 0;
173
+ const reconcileBlocked = result.reconcile?.plan.blocked.length ?? 0;
165
174
  summary.due += due;
166
175
  summary.manualReview += manualReview;
167
176
  summary.missingPath += missingPath;
168
177
  summary.executable += result.plan.entries.length;
169
178
  summary.skipped += result.plan.skipped.length;
179
+ summary.reconcileEntries += reconcileEntries;
180
+ summary.reconcileBlocked += reconcileBlocked;
170
181
  if (result.plan.planId !== "not-created")
171
182
  summary.previewPlanIds.push(result.plan.planId);
172
- if (!result.validate.ok || due + manualReview + missingPath > 0 || result.plan.entries.length > 0) {
183
+ if (!result.validate.ok || due + manualReview + missingPath + reconcileEntries + reconcileBlocked > 0 || result.plan.entries.length > 0) {
173
184
  summary.affected += 1;
174
185
  }
175
186
  }
@@ -64,6 +64,9 @@ export function createReconcilePlan(ledgerPath) {
64
64
  });
65
65
  return reviewed;
66
66
  }
67
+ export function matchingReconcilePlan(ledgerPath, plan) {
68
+ return matchingExistingReconcilePlan(ledgerPath, plan);
69
+ }
67
70
  // Apply a reviewed reconcile plan (NGX-437 `reconcile --execute`). This is the only
68
71
  // mutating reconcile entrypoint and it is deliberately conservative:
69
72
  // * It refuses up front when the plan id is missing, the plan file is absent, or the
@@ -3,12 +3,12 @@ const DOCTOR_ATTENTION_CATEGORIES = ["stale", "invalid", "warnings"];
3
3
  function doctorAttention(summary) {
4
4
  return DOCTOR_ATTENTION_CATEGORIES.filter((key) => summary[key] > 0);
5
5
  }
6
- function doctorNextAction(blockers, summary) {
6
+ function doctorNextAction(blockers, summary, registryPath) {
7
7
  if (blockers.length > 0) {
8
8
  return `repair ${blockers.length} registry/ledger issue(s) above, then re-run \`artshelf doctor\``;
9
9
  }
10
10
  if (summary.warnings > 0) {
11
- return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf validate --all\` to inspect; nothing is auto-executed`;
11
+ return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf reconcile --dry-run --all --registry ${registryPath}\` to prepare reconcile-ready approvals, then run \`artshelf review --all --registry ${registryPath}\`; nothing is auto-executed`;
12
12
  }
13
13
  return "artshelf is healthy on this machine — cleanup safety enforced; no action needed";
14
14
  }
@@ -39,7 +39,7 @@ export function buildDoctorAgentPacket(report) {
39
39
  attention: doctorAttention(report.summary),
40
40
  blockers,
41
41
  cleanupSafety: report.cleanupSafety,
42
- nextAction: doctorNextAction(blockers, report.summary),
42
+ nextAction: doctorNextAction(blockers, report.summary, report.registryPath),
43
43
  verification: `artshelf doctor --agent --registry ${report.registryPath}`
44
44
  };
45
45
  }
@@ -7,7 +7,7 @@ const REVIEW_SAFETY = {
7
7
  noResolveRan: true,
8
8
  noDeleteRan: true
9
9
  };
10
- export function reviewNextAction(summary, scope, ledgerPath) {
10
+ export function reviewNextAction(summary, scope, ledgerPath, registryPath) {
11
11
  const broken = summary.invalid + summary.stale;
12
12
  const review = statusCommand(scope, "review", ledgerPath);
13
13
  if (broken > 0) {
@@ -18,25 +18,30 @@ export function reviewNextAction(summary, scope, ledgerPath) {
18
18
  const dryRun = scope === "all" ? "artshelf cleanup --dry-run --all" : `artshelf cleanup --dry-run${ledgerPath ? ` --ledger ${ledgerPath}` : ""}`;
19
19
  return `run \`${dryRun}\` to generate plans, then \`artshelf cleanup --execute --plan-id <id> --ledger <path>\` for each reviewed plan`;
20
20
  }
21
- if (summary.missingPath > 0) {
22
- return "inspect missing-path entries and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
21
+ if (summary.missingPath > 0 || summary.reconcileEntries > 0 || summary.reconcileBlocked > 0) {
22
+ const reconcile = scope === "all" ? `artshelf reconcile --dry-run --all${registryPath ? ` --registry ${registryPath}` : ""}` : `artshelf reconcile --dry-run --ledger ${ledgerPath}`;
23
+ return `run \`${reconcile} --json\` and then \`${review}\` to surface reconcile-ready approvals; nothing is auto-executable`;
23
24
  }
24
25
  return "nothing to do — no broken ledgers and no due, manual-review, missing-path, or executable cleanup entries";
25
26
  }
26
27
  export function printReviewAll(results, summary, nextAction, registryPath) {
27
- const needsAttention = summary.invalid + summary.stale + summary.executable + summary.due + summary.manualReview + summary.missingPath > 0;
28
+ const needsAttention = summary.invalid + summary.stale + summary.executable + summary.due + summary.manualReview + summary.missingPath + summary.reconcileEntries + summary.reconcileBlocked > 0;
28
29
  process.stdout.write(`${attentionGlyph(needsAttention)} artshelf review --all: ${needsAttention ? "needs attention" : "all clear"}\n`);
29
30
  process.stdout.write(`registry: ${registryPath} — ${summary.ledgers} ledgers (${summary.ok} ok, ${summary.invalid} invalid, ${summary.stale} stale)\n`);
30
31
  printReview(results);
31
- process.stdout.write(`triage: due ${summary.due} · manual-review ${summary.manualReview} · missing ${summary.missingPath} · executable ${summary.executable} · skipped ${summary.skipped}\n`);
32
+ process.stdout.write(`triage: due ${summary.due} · manual-review ${summary.manualReview} · missing ${summary.missingPath} · executable ${summary.executable} · skipped ${summary.skipped} · reconcile ${summary.reconcileEntries} · blocked ${summary.reconcileBlocked}\n`);
32
33
  process.stdout.write(`next: ${nextAction}\n`);
33
34
  }
34
35
  export function printReview(results) {
35
36
  for (const result of results) {
36
37
  const visibleDue = result.due.filter((entry) => entry.dueStatus !== "kept");
37
- const needsAttention = !result.validate.ok || visibleDue.length > 0 || result.plan.entries.length > 0;
38
+ const reconcileEntries = result.reconcile?.plan.entries.length ?? 0;
39
+ const reconcileBlocked = result.reconcile?.plan.blocked.length ?? 0;
40
+ const reconcileDrift = reconcileEntries + reconcileBlocked;
41
+ const needsAttention = !result.validate.ok || visibleDue.length > 0 || result.plan.entries.length > 0 || reconcileDrift > 0;
42
+ const reconcileDetail = reconcileDrift > 0 ? `; reconcile: ${reconcileEntries} entries, ${reconcileBlocked} blocked` : "";
38
43
  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`);
39
- process.stdout.write(`due/manual/missing: ${visibleDue.length}; plan ${result.plan.planId}: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped\n`);
44
+ process.stdout.write(`due/manual/missing: ${visibleDue.length}; plan ${result.plan.planId}: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped${reconcileDetail}\n`);
40
45
  process.stdout.write(`ledger: ${result.ledger.path}\n`);
41
46
  }
42
47
  }
@@ -60,7 +65,15 @@ function buildReviewDecisions(results, scope) {
60
65
  });
61
66
  continue;
62
67
  }
63
- const missingPath = due.filter((entry) => entry.dueStatus === "missing-path");
68
+ const handledReconcileIds = new Set([
69
+ ...(result.reconcile?.plan.entries.map((entry) => entry.id) ?? []),
70
+ ...(result.reconcile?.plan.blocked.map((entry) => entry.id) ?? [])
71
+ ]);
72
+ const reconcileActions = buildReconcileDecisions(result, scope);
73
+ readyForApproval.push(...reconcileActions.readyForApproval);
74
+ needsReviewFirst.push(...reconcileActions.needsReviewFirst);
75
+ blocked.push(...reconcileActions.blocked);
76
+ const missingPath = due.filter((entry) => entry.dueStatus === "missing-path" && !handledReconcileIds.has(entry.id));
64
77
  const trashSafe = due.filter((entry) => entry.dueStatus === "due" && entry.cleanup === "trash");
65
78
  const inspectItems = due.filter((entry) => entry.dueStatus === "manual-review" ||
66
79
  (entry.dueStatus === "due" && (entry.cleanup === "review" || entry.cleanup === "delete")));
@@ -103,6 +116,57 @@ function buildReviewDecisions(results, scope) {
103
116
  }
104
117
  return { readyForApproval, needsReviewFirst, blocked };
105
118
  }
119
+ function buildReconcileDecisions(result, _scope) {
120
+ if (!result.reconcile)
121
+ return { readyForApproval: [], needsReviewFirst: [], blocked: [] };
122
+ const readyForApproval = [];
123
+ const needsReviewFirst = [];
124
+ const blocked = [];
125
+ const hasReviewedPlan = Boolean(result.reconcile.reviewedPlan && result.reconcile.reviewedPlan.planId !== "not-created");
126
+ const reviewedPlanId = result.reconcile.reviewedPlan?.planId ?? null;
127
+ const byCategory = {
128
+ remap: [],
129
+ "resolve-missing": [],
130
+ "resolve-stale-trash": [],
131
+ "registry-remap": [],
132
+ blocked: []
133
+ };
134
+ for (const finding of result.reconcile.plan.entries.concat(result.reconcile.plan.blocked)) {
135
+ byCategory[finding.category].push(finding);
136
+ }
137
+ const reconcileActionCategories = ["remap", "resolve-missing", "resolve-stale-trash", "registry-remap"];
138
+ for (const category of reconcileActionCategories) {
139
+ const entries = byCategory[category];
140
+ if (entries.length === 0)
141
+ continue;
142
+ const ids = entries.map((entry) => entry.id).sort();
143
+ const label = `Review ${entries.length} reconcile ${category} finding(s) in ${result.ledger.name}`;
144
+ const reason = `recorded paths are ${category === "remap" ? "safe to remap" : "stale and require manual review before execution"}`;
145
+ const decision = {
146
+ label,
147
+ itemIds: ids,
148
+ actionType: "reconcile",
149
+ approvalTarget: hasReviewedPlan ? `approve artshelf reconcile ledger ${result.ledger.path} plan ${reviewedPlanId}` : null,
150
+ reason,
151
+ nextStep: hasReviewedPlan
152
+ ? `run \`artshelf reconcile --execute --plan-id ${reviewedPlanId} --ledger ${result.ledger.path}\``
153
+ : `run \`artshelf reconcile --dry-run --ledger ${result.ledger.path} --json\`, then approve with \`approve artshelf reconcile ledger ${result.ledger.path} plan <plan-id>\``
154
+ };
155
+ (hasReviewedPlan ? readyForApproval : needsReviewFirst).push(decision);
156
+ }
157
+ if (byCategory.blocked.length > 0) {
158
+ const entries = byCategory.blocked;
159
+ blocked.push({
160
+ label: `Review ${entries.length} blocked reconcile finding(s) in ${result.ledger.name}`,
161
+ itemIds: entries.map((entry) => entry.id).sort(),
162
+ actionType: "reconcile",
163
+ approvalTarget: null,
164
+ reason: "path drift is ambiguous or unsafe and needs manual investigation",
165
+ nextStep: `run \`artshelf reconcile --dry-run --ledger ${result.ledger.path} --json\`, then handle each item manually`
166
+ });
167
+ }
168
+ return { readyForApproval, needsReviewFirst, blocked };
169
+ }
106
170
  function reviewCounts(summary) {
107
171
  return {
108
172
  due: summary.due,
@@ -131,7 +195,7 @@ export function buildReviewAgentPacketAll(results, summary, registry) {
131
195
  needsReviewFirst: groups.needsReviewFirst,
132
196
  blocked: groups.blocked,
133
197
  safety: REVIEW_SAFETY,
134
- nextAction: reviewNextAction(summary, "all"),
198
+ nextAction: reviewNextAction(summary, "all", undefined, registry.path),
135
199
  verification: `artshelf review --all --agent --registry ${registry.path}`
136
200
  };
137
201
  }
@@ -14,7 +14,7 @@ export function statusCommand(scope, command, ledgerPath) {
14
14
  return `artshelf ${command} --all`;
15
15
  return ledgerPath ? `artshelf ${command} --ledger ${ledgerPath}` : `artshelf ${command}`;
16
16
  }
17
- function statusNextAction(blockers, counts, scope, ledgerPath) {
17
+ function statusNextAction(blockers, counts, scope, ledgerPath, registryPath) {
18
18
  if (blockers.length > 0) {
19
19
  const verify = statusCommand(scope, "status", ledgerPath);
20
20
  return `repair ${blockers.length} broken ledger(s) above, then re-run \`${verify}\``;
@@ -27,7 +27,8 @@ function statusNextAction(blockers, counts, scope, ledgerPath) {
27
27
  return `run \`${review}\` to inspect manual-review records; nothing is auto-executed`;
28
28
  }
29
29
  if (counts.missingPath > 0) {
30
- return "inspect missing-path records and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
30
+ const reconcile = scope === "all" ? `artshelf reconcile --dry-run --all${registryPath ? ` --registry ${registryPath}` : ""}` : `artshelf reconcile --dry-run --ledger ${ledgerPath}`;
31
+ return `run \`${reconcile} --json\` and then \`${review}\` to surface reconcile-ready approvals; nothing is auto-executable`;
31
32
  }
32
33
  return "nothing due — no broken ledgers and no due, manual-review, missing-path, or pending cleanup entries";
33
34
  }
@@ -58,7 +59,7 @@ export function buildStatusAgentPacketAll(report) {
58
59
  counts,
59
60
  attention: statusAttention(counts),
60
61
  blockers,
61
- nextAction: statusNextAction(blockers, counts, "all"),
62
+ nextAction: statusNextAction(blockers, counts, "all", undefined, report.registryPath),
62
63
  verification: `artshelf status --all --agent --registry ${report.registryPath}`
63
64
  };
64
65
  }
@@ -188,7 +188,8 @@ Resolved records stay in the audit trail but no longer participate in due or cle
188
188
 
189
189
  Review runs validate, due, and cleanup plan preview without moving files or
190
190
  writing a plan. With --all, review adds aggregate triage counts and the next
191
- safe action.
191
+ safe action, including reconcile entry and blocked counts when path drift is
192
+ detected.
192
193
 
193
194
  Render modes:
194
195
  (default) Human summary of validation, triage counts, and the next safe action.
@@ -111,6 +111,13 @@ artshelf doctor --agent
111
111
  <span class="c"># lightweight counts, cron-friendly</span>
112
112
  artshelf status --all --json
113
113
  artshelf status --all --agent</code></pre>
114
+ <p>
115
+ If any ledger is stale or missing-path, run the safe sequence in order:
116
+ <code>review --all --json</code> for affected classifications,
117
+ <code>validate --all --json</code> for provenance signals, then
118
+ <code>reconcile --dry-run --all --json --registry &lt;registry-path&gt;</code>
119
+ before any approve/execute step.
120
+ </p>
114
121
  <p>
115
122
  Use <code>--agent</code> for concise monitor decisions and exact next
116
123
  actions. Use <code>--json</code> instead when the job needs full
@@ -44,10 +44,15 @@
44
44
  <section>
45
45
  <h2>Daily review workflow</h2>
46
46
  <ol>
47
- <li>Read <code>ledgers list --json</code>, <code>review --all --agent</code>, and <code>trash list --all --json</code>.</li>
47
+ <li>Start with health and registry-level context: <code>status --all --agent</code>,
48
+ then read raw pressure points from <code>review --all --agent</code>
49
+ and <code>trash list --all --json</code>.</li>
50
+ <li>For stale or missing path warnings, run <code>validate --all --json</code>
51
+ and then <code>reconcile --dry-run --all --json --registry &lt;registry-path&gt;</code>
52
+ to produce reviewed-safe plans.</li>
48
53
  <li>Run explicit-ledger purge dry-runs only when old trash needs review.</li>
49
54
  <li>Classify each candidate: <code>trash-safe</code>, <code>needs-human-review</code>, <code>resolve-candidate</code>, or <code>registry-problem</code>.</li>
50
- <li>Ask only with exact ledger path, reviewed plan id, or ids.</li>
55
+ <li>Execute the exact approval path only after review packet verification: exact ledger path and reviewed plan id or ids.</li>
51
56
  </ol>
52
57
  <p>
53
58
  Prefer the built-in <code>--agent</code> packet when an acting agent
@@ -104,7 +109,8 @@ Dry-run only. No execute, resolve, or delete ran.</code></pre>
104
109
  </p>
105
110
  <pre><code>approve artshelf cleanup ledger &lt;ledger-path&gt; plan &lt;plan-id&gt;
106
111
  approve artshelf trash purge ledger &lt;ledger-path&gt; plan &lt;purge-plan-id&gt;
107
- approve artshelf resolve missing ledger &lt;ledger-path&gt; ids &lt;id...&gt;</code></pre>
112
+ approve artshelf resolve missing ledger &lt;ledger-path&gt; ids &lt;id...&gt;
113
+ approve artshelf reconcile ledger &lt;ledger-path&gt; plan &lt;plan-id&gt;</code></pre>
108
114
  <div class="callout" data-kind="boundary">
109
115
  <span class="callout-label">Hard boundary</span>
110
116
  <p>
@@ -27,27 +27,41 @@
27
27
  "skipped": 2,
28
28
  "refused": 0,
29
29
  "manualReview": 1,
30
- "missingPath": 1,
30
+ "missingPath": 2,
31
31
  "trashed": 0
32
32
  },
33
33
  "decisionSummary": {
34
- "readyForApproval": 2,
35
- "needsReviewFirst": 1,
36
- "blocked": 0
34
+ "readyForApproval": 3,
35
+ "needsReviewFirst": 2,
36
+ "blocked": 1
37
37
  },
38
38
  "decisionGroups": {
39
39
  "readyForApproval": [
40
40
  {
41
41
  "label": "Clean up temp debug output",
42
- "itemIds": ["shf_20260606_120000_ab12"],
42
+ "itemIds": [
43
+ "shf_20260606_120000_ab12"
44
+ ],
43
45
  "actionType": "cleanup",
44
46
  "approvalTarget": "approve artshelf cleanup ledger /path/to/example-project/.artshelf/ledger.jsonl plan plan_20260606_120000_ab12",
45
47
  "reason": "Disposable temp artifact has a reviewed cleanup plan.",
46
48
  "nextStep": "Approve the exact cleanup plan to move the artifact into Artshelf trash."
47
49
  },
50
+ {
51
+ "label": "Review 1 reconcile remap finding(s) in example-project",
52
+ "itemIds": [
53
+ "shf_20260606_121500_gh78"
54
+ ],
55
+ "actionType": "reconcile",
56
+ "approvalTarget": "approve artshelf reconcile ledger /path/to/example-project/.artshelf/ledger.jsonl plan plan_20260606_122000_repl",
57
+ "reason": "Parent folder drift looks safe to reconcile after inspection and review.",
58
+ "nextStep": "run `artshelf reconcile --execute --plan-id plan_20260606_122000_repl --ledger /path/to/example-project/.artshelf/ledger.jsonl` after approval."
59
+ },
48
60
  {
49
61
  "label": "Resolve missing report record",
50
- "itemIds": ["shf_20260606_120500_cd34"],
62
+ "itemIds": [
63
+ "shf_20260606_120500_cd34"
64
+ ],
51
65
  "actionType": "resolve-missing",
52
66
  "approvalTarget": "approve artshelf resolve missing ledger /path/to/example-project/.artshelf/ledger.jsonl ids shf_20260606_120500_cd34",
53
67
  "reason": "The report path is already missing.",
@@ -56,17 +70,40 @@
56
70
  ],
57
71
  "needsReviewFirst": [
58
72
  {
59
- "label": "Inspect lifecycle smoke report",
60
- "itemIds": ["shf_20260606_121000_ef56"],
61
- "actionType": "inspect",
73
+ "label": "Review 1 reconcile resolve-stale-trash finding(s) in example-project",
74
+ "itemIds": [
75
+ "shf_20260606_121800_jk90"
76
+ ],
77
+ "actionType": "reconcile",
78
+ "approvalTarget": null,
79
+ "reason": "Trash references point to trashed artifacts whose originals moved or got deleted.",
80
+ "nextStep": "run `artshelf reconcile --dry-run --ledger /path/to/example-project/.artshelf/ledger.jsonl --json`, then approve with `approve artshelf reconcile ledger /path/to/example-project/.artshelf/ledger.jsonl plan <plan-id>`"
81
+ },
82
+ {
83
+ "label": "Review 1 reconcile registry-remap finding(s) in example-project",
84
+ "itemIds": [
85
+ "shf_20260606_121900_lm01"
86
+ ],
87
+ "actionType": "reconcile",
62
88
  "approvalTarget": null,
63
- "reason": "cleanup=review means the artifact should be inspected before closing.",
64
- "nextStep": "Inspect the path, then choose keep, change retention, resolve, or clean up later."
89
+ "reason": "Ledger name or registry path drift needs explicit review before remapping records.",
90
+ "nextStep": "run `artshelf reconcile --dry-run --ledger /path/to/example-project/.artshelf/ledger.jsonl --json`, then approve with `approve artshelf reconcile ledger /path/to/example-project/.artshelf/ledger.jsonl plan <plan-id>`"
65
91
  }
66
92
  ],
67
- "blocked": []
93
+ "blocked": [
94
+ {
95
+ "label": "Review 1 reconcile blocked finding(s) in example-project",
96
+ "itemIds": [
97
+ "shf_20260606_122200_qr11"
98
+ ],
99
+ "actionType": "reconcile",
100
+ "approvalTarget": null,
101
+ "reason": "Path drift is ambiguous or unsafe and needs manual investigation.",
102
+ "nextStep": "Run a manual artifact-by-artifact review and decide whether to keep, resolve, or move the ledger entry."
103
+ }
104
+ ]
68
105
  },
69
- "recommendation": "Approve the reviewed cleanup plan for the disposable temp directory, then resolve the missing record after confirming it is no longer needed.",
106
+ "recommendation": "Run reconcile dry-run for stale/missing path entries, then follow the exact approvals returned in the review packet.",
70
107
  "items": [
71
108
  {
72
109
  "id": "shf_20260606_120000_ab12",
@@ -87,13 +124,31 @@
87
124
  "note": "Resolution updates only the ledger and does not move or delete files."
88
125
  },
89
126
  {
90
- "id": "shf_20260606_121000_ef56",
91
- "path": "/tmp/lifecycle-smoke-report.json",
92
- "classification": "needs-human-review",
93
- "proposedAction": "inspect before choosing cleanup or retention",
127
+ "id": "shf_20260606_121800_jk90",
128
+ "path": "/tmp/stale-trash-record.json",
129
+ "classification": "resolve-candidate",
130
+ "proposedAction": "reconcile stale trashed entry after review",
131
+ "dueStatus": "trashed",
132
+ "reason": "trash path is stale after source artifact delete",
133
+ "note": "Run reconcile to confirm whether to rewrite the artifact path or resolve manually."
134
+ },
135
+ {
136
+ "id": "shf_20260606_121900_lm01",
137
+ "path": "/path/to/example-project/src/legacy/file.txt",
138
+ "classification": "registry-problem",
139
+ "proposedAction": "inspect registry-backed path drift",
140
+ "dueStatus": "missing-path",
141
+ "reason": "Artifact moved by repository migration from .shelf to .artshelf",
142
+ "note": "Review registry identity and run reconcile dry-run before executing any path updates."
143
+ },
144
+ {
145
+ "id": "shf_20260606_122200_qr11",
146
+ "path": "/tmp/blocked-dupe-path",
147
+ "classification": "registry-problem",
148
+ "proposedAction": "manual investigation required",
94
149
  "dueStatus": "manual-review",
95
- "reason": "cleanup=review artifact retained for manual inspection",
96
- "note": "No approval target is shown until the review decision is known."
150
+ "reason": "reconstructed paths conflicted with another active artifact",
151
+ "note": "Blocked finding intentionally requires human disambiguation."
97
152
  }
98
153
  ],
99
154
  "alternatives": [
@@ -130,10 +130,11 @@ 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
- summary plus the next safe action. <code>status</code> is the lightweight dashboard of
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. All three also accept
136
- <code>--agent</code> for a deterministic, token-efficient decision packet (see Output mode).
133
+ summary, including reconcile entry and blocked counts, plus the next safe action.
134
+ <code>status</code> is the lightweight dashboard of counts; <code>--all --json</code> is
135
+ cron-friendly. <code>doctor</code> reports CLI version, resolved paths, registry health,
136
+ and the cleanup safety posture. All three also accept <code>--agent</code> for a
137
+ deterministic, token-efficient decision packet (see Output mode).
137
138
  </p>
138
139
  </section>
139
140