artshelf 0.13.0 → 0.13.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 +17 -0
- package/README.md +4 -3
- package/SPEC.md +25 -10
- package/dist/src/ledger.js +141 -22
- package/docs/agent-clean.html +7 -6
- package/docs/reference.html +3 -1
- package/package.json +1 -1
- package/skills/artshelf/SKILL.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Hardened `cleanup --execute` with durable resumability: a `started` receipt is
|
|
6
|
+
written before the first filesystem move so an interrupted run is detectable,
|
|
7
|
+
terminal receipt evidence preserves an artifact's original
|
|
8
|
+
`executedAt`/`cleanedAt`, an artifact already moved into the plan's trash
|
|
9
|
+
directory without terminal receipt evidence is recorded as `trashed` at resume
|
|
10
|
+
time without moving it again, a missing original path with no trash target and no
|
|
11
|
+
receipt evidence stays a skipped missing path rather than a success, and a
|
|
12
|
+
completed receipt replays idempotently without duplicating the Artshelf-owned
|
|
13
|
+
receipt record (NGX-427).
|
|
5
14
|
- Renamed the published package and CLI binary from `shelf` to `artshelf`,
|
|
6
15
|
moved project URLs to `calvinnwq/artshelf`, and prepared public npm publishing.
|
|
7
16
|
- Added a user-level ledger registry plus `--all` review commands so Artshelf can
|
|
@@ -117,6 +126,14 @@
|
|
|
117
126
|
- Moved `artshelf put` registry-warning output from stdout to stderr in human
|
|
118
127
|
mode; `--json` output is unchanged (NGX-429).
|
|
119
128
|
|
|
129
|
+
## [0.13.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.13.0...artshelf-v0.13.1) (2026-06-15)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### Bug Fixes
|
|
133
|
+
|
|
134
|
+
* **cleanup:** make cleanup --execute resumable after interruption ([d0188f7](https://github.com/calvinnwq/artshelf/commit/d0188f73a62b1ff2d173e26c61c826a67bbc9542))
|
|
135
|
+
* **cleanup:** make cleanup execution resumable ([7ec0ebe](https://github.com/calvinnwq/artshelf/commit/7ec0ebe113f589ccd00ed0fdd1a54034afc242ec))
|
|
136
|
+
|
|
120
137
|
## [0.13.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.12.0...artshelf-v0.13.0) (2026-06-15)
|
|
121
138
|
|
|
122
139
|
|
package/README.md
CHANGED
|
@@ -113,9 +113,10 @@ destructive deletion.
|
|
|
113
113
|
- **No fresh-plan-then-execute shortcut** — review the plan, then run that plan.
|
|
114
114
|
- **Trash before delete** — `cleanup=delete` stays refused; physical deletion
|
|
115
115
|
needs its own reviewed trash purge. No silent deletion, ever.
|
|
116
|
-
- **Durable,
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
- **Durable, resumable cleanup** — execution writes a started receipt before
|
|
117
|
+
moving files, can replay the same plan id after interruption, and ledger and
|
|
118
|
+
registry mutations take a cross-process lock so overlapping commands never
|
|
119
|
+
lose records or leave a half-written ledger.
|
|
119
120
|
- **`--json` on every command**, so agents can act on structured output.
|
|
120
121
|
- **`--agent` on `review`/`status`/`doctor`**, a compact, token-efficient
|
|
121
122
|
decision packet for agents, while the default render stays human-scannable.
|
package/SPEC.md
CHANGED
|
@@ -440,10 +440,19 @@ Rules:
|
|
|
440
440
|
ledger, and its entries must be well-formed. A mismatched or malformed plan is
|
|
441
441
|
refused without moving files or writing a receipt, mirroring the live-record
|
|
442
442
|
re-checks `trash purge --execute` performs.
|
|
443
|
-
- Writes a cleanup receipt
|
|
444
|
-
|
|
445
|
-
`
|
|
446
|
-
|
|
443
|
+
- Writes a `started` cleanup receipt to `<ledger-dir>/receipts/<plan-id>.json` before
|
|
444
|
+
the first filesystem move, then completes the receipt with `completedAt` and the
|
|
445
|
+
per-entry `trashed`, `review-required`, `refused`, or `skipped` results.
|
|
446
|
+
- Appends or refreshes an Artshelf-owned ledger record for the completed receipt with
|
|
447
|
+
`owner=artshelf`, `kind=run-artifact`, `ttl=30d`, `cleanup=review`, and labels
|
|
448
|
+
including `artshelf`, `cleanup-receipt`, and the plan id.
|
|
449
|
+
- Resumes an interrupted run on rerun of the same plan id: terminal receipt evidence
|
|
450
|
+
for an artifact keeps its original `executedAt`/`cleanedAt`, an artifact already
|
|
451
|
+
moved into the plan's trash directory without terminal receipt evidence is recorded
|
|
452
|
+
as `trashed` at resume time without moving it again, a missing original path with no
|
|
453
|
+
trash target and no receipt evidence stays a skipped missing path rather than a
|
|
454
|
+
success, and a completed receipt replays idempotently without duplicating the
|
|
455
|
+
Artshelf-owned receipt record.
|
|
447
456
|
- Updates touched ledger records so handled artifacts stop appearing as active
|
|
448
457
|
cleanup candidates.
|
|
449
458
|
- Uses trash/review behavior by default.
|
|
@@ -829,11 +838,15 @@ Operational rules that back those boundaries:
|
|
|
829
838
|
- Dry-run first.
|
|
830
839
|
- Execute only by plan id.
|
|
831
840
|
- Trash/review before delete.
|
|
832
|
-
- Execute
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
841
|
+
- Execute writes a `started` cleanup receipt before the first filesystem move,
|
|
842
|
+
updates ledger state after recording per-entry outcomes, and completes the
|
|
843
|
+
receipt with `completedAt`. A trashed, review-required, or refused record no
|
|
844
|
+
longer participates in future `due` or cleanup dry-run output by default.
|
|
845
|
+
- Rerunning the same plan id resumes or replays durable receipt/trash evidence:
|
|
846
|
+
terminal receipt evidence keeps its original cleanup timestamp, existing
|
|
847
|
+
plan-trash targets are not moved again, completed receipts are idempotent,
|
|
848
|
+
and missing paths without receipt or trash evidence stay skipped rather than
|
|
849
|
+
successful.
|
|
837
850
|
- Cleanup never scans arbitrary filesystem paths for deletion in v1.
|
|
838
851
|
- Cleanup only acts on ledger entries.
|
|
839
852
|
- Trash purge is scoped to one ledger, requires a reviewed purge plan id, and
|
|
@@ -957,7 +970,9 @@ human review.
|
|
|
957
970
|
creates.
|
|
958
971
|
- Cleanup execute refuses to run without a plan id, and refuses an unsafe,
|
|
959
972
|
mismatched, or malformed plan before moving files or writing a receipt.
|
|
960
|
-
- Cleanup execute writes a receipt
|
|
973
|
+
- Cleanup execute writes a started receipt before moving files, resumes or
|
|
974
|
+
replays the same plan id from receipt/trash evidence, and completes the
|
|
975
|
+
receipt idempotently.
|
|
961
976
|
- CLI can list trashed records (single ledger or `--all`) and purge them through
|
|
962
977
|
an approval-first, ledger-scoped dry-run/execute boundary that writes a purge
|
|
963
978
|
receipt; purge refuses `--all` and never deletes without a reviewed plan id.
|
package/dist/src/ledger.js
CHANGED
|
@@ -553,41 +553,94 @@ export function executeCleanupPlan(ledgerPath, planId) {
|
|
|
553
553
|
const plan = JSON.parse(readFileSync(planPath, "utf8"));
|
|
554
554
|
assertCleanupPlanExecutable(plan, planId, ledgerPath);
|
|
555
555
|
const trashRoot = join(dirname(ledgerPath), "trash", planId);
|
|
556
|
+
const receiptPath = receiptPathFor(ledgerPath, planId);
|
|
556
557
|
return withLedgerLock(ledgerPath, () => {
|
|
558
|
+
const existingReceipt = existsSync(receiptPath) ? readCleanupReceiptIfValid(receiptPath) : null;
|
|
559
|
+
const planReceipt = existingReceipt?.planId === planId ? existingReceipt : null;
|
|
560
|
+
const priorResultById = new Map((planReceipt?.results ?? []).map((result) => [result.id, result]));
|
|
557
561
|
const records = readLedger(ledgerPath);
|
|
562
|
+
if (planReceipt?.completedAt) {
|
|
563
|
+
if (!records.some((record) => record.status === "active" && isMatchingArtshelfArtifact(record, receiptPath, ["artshelf", "cleanup-receipt", planId]))) {
|
|
564
|
+
registerArtshelfArtifact(ledgerPath, receiptPath, {
|
|
565
|
+
reason: `Artshelf cleanup receipt for plan ${planId}`,
|
|
566
|
+
ttl: "30d",
|
|
567
|
+
kind: "run-artifact",
|
|
568
|
+
cleanup: "review",
|
|
569
|
+
labels: ["artshelf", "cleanup-receipt", planId]
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return { planId, receiptPath, results: planReceipt.results };
|
|
573
|
+
}
|
|
558
574
|
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
559
|
-
const
|
|
560
|
-
|
|
575
|
+
const executedAt = toIso(now());
|
|
576
|
+
const results = plan.entries.map((entry) => {
|
|
577
|
+
const prior = priorResultById.get(entry.id);
|
|
578
|
+
if (prior && isTerminalCleanupResult(prior)) {
|
|
579
|
+
return withCleanupResultExecutedAt(prior, cleanupResultExecutedAt(prior, planReceipt, executedAt));
|
|
580
|
+
}
|
|
581
|
+
return { id: entry.id, action: entry.action, status: "pending", path: entry.path };
|
|
582
|
+
});
|
|
583
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
584
|
+
for (const [index, entry] of plan.entries.entries()) {
|
|
561
585
|
const record = recordsById.get(entry.id);
|
|
586
|
+
const prior = priorResultById.get(entry.id);
|
|
587
|
+
const priorExecutedAt = prior && isTerminalCleanupResult(prior)
|
|
588
|
+
? cleanupResultExecutedAt(prior, planReceipt, executedAt)
|
|
589
|
+
: executedAt;
|
|
562
590
|
if (!record) {
|
|
563
|
-
results
|
|
591
|
+
results[index] = { id: entry.id, action: entry.action, status: "skipped", path: entry.path, reason: "record is missing from ledger" };
|
|
592
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
564
593
|
continue;
|
|
565
594
|
}
|
|
566
595
|
if (record.status !== "active") {
|
|
567
|
-
|
|
596
|
+
if (prior && priorCleanupResultMatchesLedger(record, prior, { planId, receiptPath, executedAt: priorExecutedAt })) {
|
|
597
|
+
results[index] = withCleanupResultExecutedAt(prior, priorExecutedAt);
|
|
598
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
results[index] = { id: entry.id, action: entry.action, status: "skipped", path: entry.path, reason: `record is ${record.status}` };
|
|
602
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
568
603
|
continue;
|
|
569
604
|
}
|
|
570
|
-
if (
|
|
571
|
-
results
|
|
605
|
+
if (prior && isTerminalCleanupResult(prior)) {
|
|
606
|
+
results[index] = withCleanupResultExecutedAt(prior, priorExecutedAt);
|
|
607
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
572
608
|
continue;
|
|
573
609
|
}
|
|
574
|
-
if (entry.action === "
|
|
575
|
-
|
|
610
|
+
if (entry.action === "trash") {
|
|
611
|
+
const target = join(trashRoot, `${entry.id}-${basename(entry.path)}`);
|
|
612
|
+
if (existsSync(target)) {
|
|
613
|
+
results[index] = { id: entry.id, action: entry.action, status: "trashed", path: entry.path, target, executedAt };
|
|
614
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (existsSync(entry.path)) {
|
|
618
|
+
mkdirSync(trashRoot, { recursive: true });
|
|
619
|
+
renameSync(entry.path, target);
|
|
620
|
+
results[index] = { id: entry.id, action: entry.action, status: "trashed", path: entry.path, target, executedAt };
|
|
621
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
results[index] = { id: entry.id, action: entry.action, status: "skipped", path: entry.path, reason: "path is missing" };
|
|
625
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!existsSync(entry.path)) {
|
|
629
|
+
results[index] = { id: entry.id, action: entry.action, status: "skipped", path: entry.path, reason: "path is missing" };
|
|
630
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
576
631
|
continue;
|
|
577
632
|
}
|
|
578
|
-
if (entry.action === "
|
|
579
|
-
results
|
|
633
|
+
if (entry.action === "delete") {
|
|
634
|
+
results[index] = { id: entry.id, action: entry.action, status: "refused", path: entry.path, reason: "delete is disabled in v1", executedAt };
|
|
635
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
580
636
|
continue;
|
|
581
637
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
renameSync(entry.path, target);
|
|
585
|
-
results.push({ id: entry.id, action: entry.action, status: "trashed", path: entry.path, target });
|
|
638
|
+
results[index] = { id: entry.id, action: entry.action, status: "review-required", path: entry.path, executedAt };
|
|
639
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: cleanupReceiptExecutedAt(results, executedAt), status: "started", results });
|
|
586
640
|
}
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
updateLedgerAfterCleanup(ledgerPath, records, { planId, receiptPath, executedAt, results });
|
|
641
|
+
const receiptExecutedAt = cleanupReceiptExecutedAt(results, executedAt);
|
|
642
|
+
updateLedgerAfterCleanup(ledgerPath, records, { planId, receiptPath, executedAt: receiptExecutedAt, results });
|
|
643
|
+
writeCleanupReceipt(receiptPath, { planId, executedAt: receiptExecutedAt, completedAt: toIso(now()), results });
|
|
591
644
|
registerArtshelfArtifact(ledgerPath, receiptPath, {
|
|
592
645
|
reason: `Artshelf cleanup receipt for plan ${planId}`,
|
|
593
646
|
ttl: "30d",
|
|
@@ -598,6 +651,69 @@ export function executeCleanupPlan(ledgerPath, planId) {
|
|
|
598
651
|
return { planId, receiptPath, results };
|
|
599
652
|
});
|
|
600
653
|
}
|
|
654
|
+
function priorCleanupResultMatchesLedger(record, result, receipt) {
|
|
655
|
+
if (record.cleanupPlanId !== receipt.planId)
|
|
656
|
+
return false;
|
|
657
|
+
if (record.receiptPath !== receipt.receiptPath)
|
|
658
|
+
return false;
|
|
659
|
+
if (record.cleanedAt !== receipt.executedAt)
|
|
660
|
+
return false;
|
|
661
|
+
if (result.status === "trashed") {
|
|
662
|
+
return record.status === "trashed" && !!result.target && record.targetPath === result.target;
|
|
663
|
+
}
|
|
664
|
+
if (result.status === "review-required") {
|
|
665
|
+
return record.status === "review-required";
|
|
666
|
+
}
|
|
667
|
+
if (result.status === "refused") {
|
|
668
|
+
return record.status === "cleanup-refused" && (!result.reason || record.cleanupReason === result.reason);
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
function isTerminalCleanupResult(result) {
|
|
673
|
+
return result.status === "trashed" || result.status === "review-required" || result.status === "refused";
|
|
674
|
+
}
|
|
675
|
+
function cleanupResultExecutedAt(result, receipt, fallback) {
|
|
676
|
+
if (typeof result.executedAt === "string")
|
|
677
|
+
return result.executedAt;
|
|
678
|
+
if (isTerminalCleanupResult(result) && typeof receipt?.executedAt === "string")
|
|
679
|
+
return receipt.executedAt;
|
|
680
|
+
return fallback;
|
|
681
|
+
}
|
|
682
|
+
function withCleanupResultExecutedAt(result, executedAt) {
|
|
683
|
+
return { ...result, executedAt };
|
|
684
|
+
}
|
|
685
|
+
function cleanupReceiptExecutedAt(results, fallback) {
|
|
686
|
+
const terminalExecutedAt = results
|
|
687
|
+
.filter(isTerminalCleanupResult)
|
|
688
|
+
.map((result) => result.executedAt)
|
|
689
|
+
.filter((value) => typeof value === "string");
|
|
690
|
+
const firstExecutedAt = terminalExecutedAt[0];
|
|
691
|
+
if (firstExecutedAt && terminalExecutedAt.every((value) => value === firstExecutedAt)) {
|
|
692
|
+
return firstExecutedAt;
|
|
693
|
+
}
|
|
694
|
+
return fallback;
|
|
695
|
+
}
|
|
696
|
+
function readCleanupReceiptIfValid(receiptPath) {
|
|
697
|
+
try {
|
|
698
|
+
return readCleanupReceipt(receiptPath);
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function readCleanupReceipt(receiptPath) {
|
|
705
|
+
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
706
|
+
return {
|
|
707
|
+
...(typeof receipt.planId === "string" ? { planId: receipt.planId } : {}),
|
|
708
|
+
...(typeof receipt.executedAt === "string" ? { executedAt: receipt.executedAt } : {}),
|
|
709
|
+
...(typeof receipt.completedAt === "string" ? { completedAt: receipt.completedAt } : {}),
|
|
710
|
+
...(typeof receipt.status === "string" ? { status: receipt.status } : {}),
|
|
711
|
+
results: Array.isArray(receipt.results) ? receipt.results : []
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function writeCleanupReceipt(receiptPath, receipt) {
|
|
715
|
+
writeJson(receiptPath, receipt);
|
|
716
|
+
}
|
|
601
717
|
// Exported so the reconcile plan layer (src/reconcile.ts) registers its dry-run plan
|
|
602
718
|
// artifacts through the same upsert-by-path-and-labels path that cleanup plans use,
|
|
603
719
|
// keeping plan files tracked and reused under a stable plan id.
|
|
@@ -712,31 +828,34 @@ function updateLedgerAfterCleanup(ledgerPath, records, receipt) {
|
|
|
712
828
|
if (!result)
|
|
713
829
|
return record;
|
|
714
830
|
if (result.status === "trashed") {
|
|
831
|
+
const cleanedAt = result.executedAt ?? receipt.executedAt;
|
|
715
832
|
return {
|
|
716
833
|
...record,
|
|
717
834
|
status: "trashed",
|
|
718
835
|
cleanupPlanId: receipt.planId,
|
|
719
836
|
receiptPath: receipt.receiptPath,
|
|
720
|
-
cleanedAt
|
|
837
|
+
cleanedAt,
|
|
721
838
|
...(result.target ? { targetPath: result.target } : {})
|
|
722
839
|
};
|
|
723
840
|
}
|
|
724
841
|
if (result.status === "review-required") {
|
|
842
|
+
const cleanedAt = result.executedAt ?? receipt.executedAt;
|
|
725
843
|
return {
|
|
726
844
|
...record,
|
|
727
845
|
status: "review-required",
|
|
728
846
|
cleanupPlanId: receipt.planId,
|
|
729
847
|
receiptPath: receipt.receiptPath,
|
|
730
|
-
cleanedAt
|
|
848
|
+
cleanedAt
|
|
731
849
|
};
|
|
732
850
|
}
|
|
733
851
|
if (result.status === "refused") {
|
|
852
|
+
const cleanedAt = result.executedAt ?? receipt.executedAt;
|
|
734
853
|
return {
|
|
735
854
|
...record,
|
|
736
855
|
status: "cleanup-refused",
|
|
737
856
|
cleanupPlanId: receipt.planId,
|
|
738
857
|
receiptPath: receipt.receiptPath,
|
|
739
|
-
cleanedAt
|
|
858
|
+
cleanedAt,
|
|
740
859
|
...(result.reason ? { cleanupReason: result.reason } : {})
|
|
741
860
|
};
|
|
742
861
|
}
|
|
@@ -909,5 +1028,5 @@ function assertCleanupPlanExecutable(plan, planId, ledgerPath) {
|
|
|
909
1028
|
}
|
|
910
1029
|
function writeJson(path, value) {
|
|
911
1030
|
mkdirSync(dirname(path), { recursive: true });
|
|
912
|
-
|
|
1031
|
+
atomicWriteFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
913
1032
|
}
|
package/docs/agent-clean.html
CHANGED
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
<h1>Execute only what was reviewed and approved.</h1>
|
|
39
39
|
<p class="lede">
|
|
40
40
|
Clean is meant to be boring. The human approves one plan, the agent runs
|
|
41
|
-
exactly that plan,
|
|
42
|
-
comes back quiet.
|
|
41
|
+
exactly that plan, leaves durable receipt evidence before moving files,
|
|
42
|
+
and checks that the next review comes back quiet.
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
45
|
<section>
|
|
@@ -90,10 +90,11 @@ artshelf resolve <id> --status resolved --reason <text></code></pre>
|
|
|
90
90
|
<h2>Verify quiet</h2>
|
|
91
91
|
<p>After cleanup execute or resolve, verify with <code>artshelf review --all --json</code>.</p>
|
|
92
92
|
<p>
|
|
93
|
-
Execution writes a receipt
|
|
94
|
-
<code>trashed</code>,
|
|
95
|
-
<code>cleanup-refused</code>.
|
|
96
|
-
|
|
93
|
+
Execution writes a started receipt before the first move, completes it after
|
|
94
|
+
ledger updates, and updates touched ledger records to <code>trashed</code>,
|
|
95
|
+
<code>review-required</code>, or <code>cleanup-refused</code>. Rerunning the same
|
|
96
|
+
plan id resumes or idempotently replays durable receipt/trash evidence.
|
|
97
|
+
Generated plans and receipts are recorded as <code>owner=artshelf</code> artifacts.
|
|
97
98
|
</p>
|
|
98
99
|
</section>
|
|
99
100
|
</article>
|
package/docs/reference.html
CHANGED
|
@@ -170,7 +170,9 @@ artshelf cleanup --execute --plan-id <id> [--ledger <path>] [--json]
|
|
|
170
170
|
<code>--dry-run</code> creates and registers a cleanup plan without moving files;
|
|
171
171
|
no-op dry-runs report <code>not-created</code>, and matching dry-runs reuse the
|
|
172
172
|
existing plan id. <code>--execute</code> is approval-only for one reviewed plan id:
|
|
173
|
-
it writes a receipt
|
|
173
|
+
it writes a started receipt before the first move, completes and registers the receipt
|
|
174
|
+
artifact, updates touched records in the ledger, and can resume or idempotently replay
|
|
175
|
+
the same plan id from durable receipt/trash evidence.
|
|
174
176
|
</p>
|
|
175
177
|
<div class="callout" data-kind="boundary">
|
|
176
178
|
<span class="callout-label">Hard boundary</span>
|
package/package.json
CHANGED
package/skills/artshelf/SKILL.md
CHANGED
|
@@ -225,8 +225,9 @@ Cleanup execution requires approval naming the reviewed ledger and plan id:
|
|
|
225
225
|
artshelf cleanup --execute --plan-id <id> --ledger <ledger-path> --json
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
If cleanup is interrupted, rerun the same plan id; durable receipt/trash
|
|
229
|
+
evidence resumes or replays without a fresh plan. `cleanup=trash` quarantines
|
|
230
|
+
files into Artshelf trash. Physical deletion belongs to the separate Purge stage.
|
|
230
231
|
|
|
231
232
|
Resolve only after confirmation; it updates the ledger and does not move or
|
|
232
233
|
delete files:
|