@planu/cli 5.3.14 → 5.3.16
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 +20 -0
- package/dist/engine/cascade-hooks/hooks/housekeeping-on-done.hook.js +2 -3
- package/dist/engine/execution/validate-job-executor.js +51 -44
- package/dist/engine/housekeeping/find-stale-branches.d.ts +3 -3
- package/dist/engine/housekeeping/find-stale-branches.js +25 -29
- package/dist/engine/housekeeping/sweep-runner.js +105 -19
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/spec-migrator/planu-root-cleaner.d.ts +2 -2
- package/dist/engine/spec-migrator/planu-root-cleaner.js +3 -2
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +2 -2
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +18 -2
- package/dist/engine/validation/validation-freshness.d.ts +7 -0
- package/dist/engine/validation/validation-freshness.js +46 -5
- package/dist/tools/git/cleanup-ops.d.ts +2 -2
- package/dist/tools/git/cleanup-ops.js +123 -58
- package/dist/tools/github-release-handler.js +3 -12
- package/dist/tools/housekeeping-sweep.d.ts +1 -0
- package/dist/tools/housekeeping-sweep.js +17 -10
- package/dist/tools/init-project/git-setup.d.ts +1 -0
- package/dist/tools/init-project/git-setup.js +7 -23
- package/dist/tools/init-project/handler.js +4 -1
- package/dist/tools/init-project/migration-runner.js +8 -1
- package/dist/tools/init-project/scaffold-writer.d.ts +1 -0
- package/dist/tools/init-project/scaffold-writer.js +1 -0
- package/dist/tools/update-status/batch.js +1 -1
- package/dist/tools/update-status/index.js +1 -0
- package/dist/tools/update-status-actions.d.ts +2 -2
- package/dist/tools/update-status-actions.js +20 -10
- package/dist/types/git.d.ts +19 -0
- package/dist/types/housekeeping.d.ts +9 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec-format.d.ts +3 -0
- package/package.json +11 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [5.3.16] - 2026-08-11
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(spec-1477): bound publication-window wait per implementation-review arbitration
|
|
5
|
+
- fix(spec-1477): publication window blocks same-identity lease supersession mid-receipt
|
|
6
|
+
|
|
7
|
+
### Chores
|
|
8
|
+
- chore(specs): record SPEC-1477 done transition and file SPEC-1487 dogfood bug
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [5.3.15] - 2026-08-11
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(spec-1403): apply implementation-review arbitration fixes
|
|
15
|
+
- fix(spec-1403): make destructive housekeeping report-only under typed authority
|
|
16
|
+
|
|
17
|
+
### Chores
|
|
18
|
+
- chore(spec-1403): record done transition with arbitration evidence
|
|
19
|
+
|
|
20
|
+
|
|
1
21
|
## [5.3.14] - 2026-08-10
|
|
2
22
|
|
|
3
23
|
### Bug Fixes
|
|
@@ -9,8 +9,7 @@ async function handler(ctx) {
|
|
|
9
9
|
try {
|
|
10
10
|
// Dynamic import keeps module graph lean — housekeeping-sweep is only needed on 'done'.
|
|
11
11
|
const { runHousekeepingSweep } = await import('../../housekeeping/index.js');
|
|
12
|
-
|
|
13
|
-
void runHousekeepingSweep({ projectPath, dryRun: false });
|
|
12
|
+
void runHousekeepingSweep({ projectPath });
|
|
14
13
|
}
|
|
15
14
|
catch (err) {
|
|
16
15
|
const error = err instanceof Error ? err.message : String(err);
|
|
@@ -29,7 +28,7 @@ export const housekeepingOnDoneHook = {
|
|
|
29
28
|
id: 'housekeeping-on-done',
|
|
30
29
|
triggers: ['done'],
|
|
31
30
|
requiresProjectPath: true,
|
|
32
|
-
label: 'Auto-run housekeeping_sweep on spec done
|
|
31
|
+
label: 'Auto-run report-only housekeeping_sweep on spec done (SPEC-782)',
|
|
33
32
|
handler,
|
|
34
33
|
};
|
|
35
34
|
//# sourceMappingURL=housekeeping-on-done.hook.js.map
|
|
@@ -11,7 +11,7 @@ import { resolveStorageLayout } from '../../storage/storage-layout.js';
|
|
|
11
11
|
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
12
12
|
import { createValidationReceiptAuthority, ensureValidationIssuerKey, issueValidationReceipt, LOCAL_VALIDATION_RECEIPT_ISSUER, preflightValidationReceiptSigner, } from '../validation/validation-receipt.js';
|
|
13
13
|
import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
|
|
14
|
-
import { assertValidationFreshnessBarrier, beginValidationFreshnessBarrier, bindValidationFreshnessLease, closeValidationFreshnessLease, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
|
|
14
|
+
import { assertValidationFreshnessBarrier, beginValidationFreshnessBarrier, beginValidationFreshnessPublicationWindow, bindValidationFreshnessLease, closeValidationFreshnessLease, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
|
|
15
15
|
import { recoverValidateJobs } from './validate-job-recovery.js';
|
|
16
16
|
import { getRuntimePolicy } from '../runtime-policy.js';
|
|
17
17
|
import { isValidateJob, parseValidatePayload, rehydrateValidationPayload, } from './validate-job-payload.js';
|
|
@@ -664,62 +664,69 @@ export class ValidateJobExecutor {
|
|
|
664
664
|
policy.receiptCheckpointResultMaxBytes,
|
|
665
665
|
};
|
|
666
666
|
}
|
|
667
|
+
// eslint-disable-next-line max-lines-per-function -- publication window and the double freshness assert share one auditable receipt-issuance boundary
|
|
667
668
|
async issueReceipt(identity, payload, checkpoint, publicationLease, fencingToken, signal) {
|
|
668
669
|
const receiptBindings = checkpoint.receiptInput.bindings;
|
|
669
|
-
|
|
670
|
-
signal.throwIfAborted();
|
|
671
|
-
}
|
|
672
|
-
if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
|
|
673
|
-
throw new Error('[Planu] Validation inputs changed before receipt publication');
|
|
674
|
-
}
|
|
675
|
-
const database = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
|
|
670
|
+
const endPublicationWindow = beginValidationFreshnessPublicationWindow(publicationLease);
|
|
676
671
|
try {
|
|
677
|
-
const signingKeyReference = await ensureValidationIssuerKey({
|
|
678
|
-
projectId: identity.projectId,
|
|
679
|
-
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
680
|
-
});
|
|
681
|
-
signal.throwIfAborted();
|
|
682
|
-
const { store } = createValidationReceiptAuthority({
|
|
683
|
-
database,
|
|
684
|
-
workspaceId: identity.workspaceId,
|
|
685
|
-
projectId: identity.projectId,
|
|
686
|
-
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
687
|
-
authorizePersistence: (persistenceDatabase) => {
|
|
688
|
-
persistenceDatabase.assertActiveJobLease({
|
|
689
|
-
id: identity.operationId,
|
|
690
|
-
projectId: identity.projectId,
|
|
691
|
-
workspaceId: identity.workspaceId,
|
|
692
|
-
workerId: this.workerId,
|
|
693
|
-
fencingToken,
|
|
694
|
-
leaseMs: this.workerTimingPolicy().workerLeaseMs,
|
|
695
|
-
now: new Date().toISOString(),
|
|
696
|
-
});
|
|
697
|
-
},
|
|
698
|
-
});
|
|
699
|
-
const receipt = await issueValidationReceipt(checkpoint.receiptInput, {
|
|
700
|
-
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
701
|
-
signingKeyReference,
|
|
702
|
-
store,
|
|
703
|
-
});
|
|
704
672
|
if (executionAborted(signal)) {
|
|
705
673
|
signal.throwIfAborted();
|
|
706
674
|
}
|
|
707
675
|
if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
|
|
708
|
-
|
|
709
|
-
throw new Error('[Planu] Validation inputs changed during receipt publication');
|
|
676
|
+
throw new Error('[Planu] Validation inputs changed before receipt publication');
|
|
710
677
|
}
|
|
711
|
-
const
|
|
712
|
-
let enrichedReportSha;
|
|
678
|
+
const database = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
|
|
713
679
|
try {
|
|
714
|
-
|
|
680
|
+
const signingKeyReference = await ensureValidationIssuerKey({
|
|
681
|
+
projectId: identity.projectId,
|
|
682
|
+
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
683
|
+
});
|
|
684
|
+
signal.throwIfAborted();
|
|
685
|
+
const { store } = createValidationReceiptAuthority({
|
|
686
|
+
database,
|
|
687
|
+
workspaceId: identity.workspaceId,
|
|
688
|
+
projectId: identity.projectId,
|
|
689
|
+
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
690
|
+
authorizePersistence: (persistenceDatabase) => {
|
|
691
|
+
persistenceDatabase.assertActiveJobLease({
|
|
692
|
+
id: identity.operationId,
|
|
693
|
+
projectId: identity.projectId,
|
|
694
|
+
workspaceId: identity.workspaceId,
|
|
695
|
+
workerId: this.workerId,
|
|
696
|
+
fencingToken,
|
|
697
|
+
leaseMs: this.workerTimingPolicy().workerLeaseMs,
|
|
698
|
+
now: new Date().toISOString(),
|
|
699
|
+
});
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
const receipt = await issueValidationReceipt(checkpoint.receiptInput, {
|
|
703
|
+
issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
|
|
704
|
+
signingKeyReference,
|
|
705
|
+
store,
|
|
706
|
+
});
|
|
707
|
+
if (executionAborted(signal)) {
|
|
708
|
+
signal.throwIfAborted();
|
|
709
|
+
}
|
|
710
|
+
if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
|
|
711
|
+
await store.invalidateCurrentBindings(identity.projectId, payload.bindings.specId);
|
|
712
|
+
throw new Error('[Planu] Validation inputs changed during receipt publication');
|
|
713
|
+
}
|
|
714
|
+
const receiptKey = `validation-receipt:${receipt.receiptId.slice('sha256:'.length)}`;
|
|
715
|
+
let enrichedReportSha;
|
|
716
|
+
try {
|
|
717
|
+
enrichedReportSha = await this.persistReceiptArtifacts(identity, payload, receipt.receiptId, receiptKey);
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
throw new ReceiptPublicationInterruptedError({ cause: error });
|
|
721
|
+
}
|
|
722
|
+
return buildReceiptResult(checkpoint.result, receipt.receiptId, receiptKey, enrichedReportSha);
|
|
715
723
|
}
|
|
716
|
-
|
|
717
|
-
|
|
724
|
+
finally {
|
|
725
|
+
database.close();
|
|
718
726
|
}
|
|
719
|
-
return buildReceiptResult(checkpoint.result, receipt.receiptId, receiptKey, enrichedReportSha);
|
|
720
727
|
}
|
|
721
728
|
finally {
|
|
722
|
-
|
|
729
|
+
endPublicationWindow();
|
|
723
730
|
}
|
|
724
731
|
}
|
|
725
732
|
async preparePublicationLease(payload, expectedBindings) {
|
|
@@ -27,9 +27,9 @@ export declare function extractSpecIdFromCommit(projectPath: string, sha: string
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function lookupSpecStatus(projectPath: string, specId: string): Promise<string | null>;
|
|
29
29
|
/**
|
|
30
|
-
* Pass 2:
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Pass 2: a `not_merged` branch whose spec is done/discarded becomes `stale`
|
|
31
|
+
* only when `git merge-base --is-ancestor` proves every commit is reachable
|
|
32
|
+
* from main, develop, or master. Spec status alone never promotes a branch.
|
|
33
33
|
* Mutates both arrays in-place.
|
|
34
34
|
*/
|
|
35
35
|
export declare function applySpecDonePass(projectPath: string, skipped: SkippedBranchInfo[], stale: StaleBranchInfo[], branchShas: Map<string, string>): Promise<void>;
|
|
@@ -148,8 +148,6 @@ export async function extractSpecIdFromCommit(projectPath, sha) {
|
|
|
148
148
|
return null;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
/** Terminal statuses that mean the branch work is complete. */
|
|
152
|
-
const DONE_STATUSES = new Set(['done', 'discarded']);
|
|
153
151
|
/**
|
|
154
152
|
* Look up the status of a spec by reading 'planu/specs/<specId>-star/spec.md' frontmatter.
|
|
155
153
|
* Returns the raw status string, or null when not found.
|
|
@@ -176,43 +174,41 @@ export async function lookupSpecStatus(projectPath, specId) {
|
|
|
176
174
|
return null;
|
|
177
175
|
}
|
|
178
176
|
}
|
|
177
|
+
async function isProvenIntegrated(projectPath, branch) {
|
|
178
|
+
for (const base of ['main', 'develop', 'master']) {
|
|
179
|
+
try {
|
|
180
|
+
await execFile('git', ['merge-base', '--is-ancestor', branch, base], { cwd: projectPath });
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
179
189
|
/**
|
|
180
|
-
* Pass 2:
|
|
181
|
-
*
|
|
182
|
-
*
|
|
190
|
+
* Pass 2: a `not_merged` branch whose spec is done/discarded becomes `stale`
|
|
191
|
+
* only when `git merge-base --is-ancestor` proves every commit is reachable
|
|
192
|
+
* from main, develop, or master. Spec status alone never promotes a branch.
|
|
183
193
|
* Mutates both arrays in-place.
|
|
184
194
|
*/
|
|
185
195
|
export async function applySpecDonePass(projectPath, skipped, stale, branchShas) {
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const toRemove = new Set();
|
|
192
|
-
for (const branch of notMerged) {
|
|
193
|
-
// Try branch name first (zero git calls)
|
|
194
|
-
let specId = extractSpecIdFromName(branch.name);
|
|
195
|
-
// Fall back to commit message if name has no spec id (one git call per branch)
|
|
196
|
-
if (!specId) {
|
|
197
|
-
const sha = branchShas.get(branch.name) ?? '';
|
|
198
|
-
specId = await extractSpecIdFromCommit(projectPath, sha);
|
|
199
|
-
}
|
|
196
|
+
const candidates = skipped.filter((entry) => entry.skipReason === 'not_merged');
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
const sha = branchShas.get(candidate.name) ?? '';
|
|
199
|
+
const specId = extractSpecIdFromName(candidate.name) ?? (await extractSpecIdFromCommit(projectPath, sha));
|
|
200
200
|
if (!specId) {
|
|
201
201
|
continue;
|
|
202
202
|
}
|
|
203
203
|
const status = await lookupSpecStatus(projectPath, specId);
|
|
204
|
-
if (status !==
|
|
205
|
-
|
|
206
|
-
stale.push({ name: branch.name, sha, reason: 'spec-done' });
|
|
207
|
-
toRemove.add(branch.name);
|
|
204
|
+
if (status !== 'done' && status !== 'discarded') {
|
|
205
|
+
continue;
|
|
208
206
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
for (let i = skipped.length - 1; i >= 0; i--) {
|
|
212
|
-
const entry = skipped[i];
|
|
213
|
-
if (entry && toRemove.has(entry.name)) {
|
|
214
|
-
skipped.splice(i, 1);
|
|
207
|
+
if (!(await isProvenIntegrated(projectPath, candidate.name))) {
|
|
208
|
+
continue;
|
|
215
209
|
}
|
|
210
|
+
skipped.splice(skipped.indexOf(candidate), 1);
|
|
211
|
+
stale.push({ name: candidate.name, sha, reason: 'spec-done' });
|
|
216
212
|
}
|
|
217
213
|
}
|
|
218
214
|
export async function findStaleBranches(input) {
|
|
@@ -19,9 +19,43 @@ async function deleteBranch(projectPath, branchName) {
|
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
async function remoteBranchRefExists(projectPath, branchName) {
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execFile('git', ['ls-remote', '--heads', 'origin', branchName], {
|
|
25
|
+
cwd: projectPath,
|
|
26
|
+
});
|
|
27
|
+
return stdout.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function isRemoteBranchProvenMerged(projectPath, branchName) {
|
|
34
|
+
for (const base of ['main', 'develop', 'master']) {
|
|
35
|
+
try {
|
|
36
|
+
await execFile('git', ['merge-base', '--is-ancestor', `origin/${branchName}`, base], {
|
|
37
|
+
cwd: projectPath,
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function deleteRemoteBranch(projectPath, branchName) {
|
|
48
|
+
try {
|
|
49
|
+
await execFile('git', ['push', 'origin', '--delete', branchName], { cwd: projectPath });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
22
56
|
async function removeWorktree(projectPath, worktreePath) {
|
|
23
57
|
try {
|
|
24
|
-
await execFile('git', ['worktree', 'remove',
|
|
58
|
+
await execFile('git', ['worktree', 'remove', worktreePath], { cwd: projectPath });
|
|
25
59
|
return true;
|
|
26
60
|
}
|
|
27
61
|
catch {
|
|
@@ -41,7 +75,6 @@ async function dropStash(projectPath, stashRef) {
|
|
|
41
75
|
// Backup helpers
|
|
42
76
|
// ---------------------------------------------------------------------------
|
|
43
77
|
async function backupAndDeleteBranch(projectPath, branch) {
|
|
44
|
-
// Write to history BEFORE deleting (for recovery)
|
|
45
78
|
try {
|
|
46
79
|
await appendHousekeepingEntry(projectPath, {
|
|
47
80
|
projectPath,
|
|
@@ -50,10 +83,30 @@ async function backupAndDeleteBranch(projectPath, branch) {
|
|
|
50
83
|
sha: branch.sha,
|
|
51
84
|
});
|
|
52
85
|
}
|
|
53
|
-
catch {
|
|
54
|
-
|
|
86
|
+
catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `branch ${branch.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
90
|
+
};
|
|
55
91
|
}
|
|
56
|
-
return deleteBranch(projectPath, branch.name);
|
|
92
|
+
return { ok: await deleteBranch(projectPath, branch.name) };
|
|
93
|
+
}
|
|
94
|
+
async function backupAndDeleteRemoteBranch(projectPath, branch) {
|
|
95
|
+
try {
|
|
96
|
+
await appendHousekeepingEntry(projectPath, {
|
|
97
|
+
projectPath,
|
|
98
|
+
kind: 'branch',
|
|
99
|
+
ref: `origin/${branch.name}`,
|
|
100
|
+
sha: branch.sha,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: `remote branch origin/${branch.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: await deleteRemoteBranch(projectPath, branch.name) };
|
|
57
110
|
}
|
|
58
111
|
async function backupAndRemoveWorktree(projectPath, wt) {
|
|
59
112
|
try {
|
|
@@ -64,10 +117,13 @@ async function backupAndRemoveWorktree(projectPath, wt) {
|
|
|
64
117
|
sha: wt.branch ?? '',
|
|
65
118
|
});
|
|
66
119
|
}
|
|
67
|
-
catch {
|
|
68
|
-
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: `worktree ${wt.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
|
+
};
|
|
69
125
|
}
|
|
70
|
-
return removeWorktree(projectPath, wt.path);
|
|
126
|
+
return { ok: await removeWorktree(projectPath, wt.path) };
|
|
71
127
|
}
|
|
72
128
|
async function backupAndDropStash(projectPath, stash) {
|
|
73
129
|
try {
|
|
@@ -78,16 +134,19 @@ async function backupAndDropStash(projectPath, stash) {
|
|
|
78
134
|
sha: String(stash.createdAt),
|
|
79
135
|
});
|
|
80
136
|
}
|
|
81
|
-
catch {
|
|
82
|
-
|
|
137
|
+
catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: `stash ${stash.ref}: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
|
+
};
|
|
83
142
|
}
|
|
84
|
-
return dropStash(projectPath, stash.ref);
|
|
143
|
+
return { ok: await dropStash(projectPath, stash.ref) };
|
|
85
144
|
}
|
|
86
145
|
// ---------------------------------------------------------------------------
|
|
87
146
|
// Main orchestrator
|
|
88
147
|
// ---------------------------------------------------------------------------
|
|
89
148
|
export async function runHousekeepingSweep(input) {
|
|
90
|
-
const { projectPath,
|
|
149
|
+
const { projectPath, authority = { mode: 'report' }, aggressive = false, includeStashes = false, } = input;
|
|
91
150
|
const sweepAt = new Date().toISOString();
|
|
92
151
|
// Find stale items in parallel
|
|
93
152
|
const [{ stale: staleBranches, skipped }, staleWorktrees, staleStashes] = await Promise.all([
|
|
@@ -96,6 +155,7 @@ export async function runHousekeepingSweep(input) {
|
|
|
96
155
|
includeStashes ? findStaleStashes({ projectPath }) : Promise.resolve([]),
|
|
97
156
|
]);
|
|
98
157
|
const report = {
|
|
158
|
+
mode: authority.mode,
|
|
99
159
|
sweepAt,
|
|
100
160
|
projectPath,
|
|
101
161
|
wouldDelete: {
|
|
@@ -108,34 +168,60 @@ export async function runHousekeepingSweep(input) {
|
|
|
108
168
|
},
|
|
109
169
|
executed: false,
|
|
110
170
|
deleted: { branches: 0, worktrees: 0, stashes: 0, backups: 0 },
|
|
171
|
+
errors: [],
|
|
111
172
|
};
|
|
112
|
-
if (
|
|
173
|
+
if (authority.mode === 'report') {
|
|
113
174
|
return report;
|
|
114
175
|
}
|
|
115
176
|
// Execute deletions
|
|
116
177
|
report.executed = true;
|
|
117
178
|
// Delete branches (sequential — each needs a backup entry first)
|
|
118
179
|
for (const branch of staleBranches) {
|
|
119
|
-
const
|
|
120
|
-
if (ok) {
|
|
180
|
+
const result = await backupAndDeleteBranch(projectPath, branch);
|
|
181
|
+
if (result.ok) {
|
|
121
182
|
report.deleted.branches++;
|
|
122
183
|
}
|
|
184
|
+
else if (result.error) {
|
|
185
|
+
report.errors.push(result.error);
|
|
186
|
+
}
|
|
187
|
+
if (!authority.deleteRemoteRefs) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!(await remoteBranchRefExists(projectPath, branch.name))) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!(await isRemoteBranchProvenMerged(projectPath, branch.name))) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const remoteResult = await backupAndDeleteRemoteBranch(projectPath, branch);
|
|
197
|
+
if (remoteResult.ok) {
|
|
198
|
+
report.deleted.branches++;
|
|
199
|
+
}
|
|
200
|
+
else if (remoteResult.error) {
|
|
201
|
+
report.errors.push(remoteResult.error);
|
|
202
|
+
}
|
|
123
203
|
}
|
|
124
204
|
// Remove worktrees (sequential)
|
|
125
205
|
for (const wt of staleWorktrees) {
|
|
126
|
-
const
|
|
127
|
-
if (ok) {
|
|
206
|
+
const result = await backupAndRemoveWorktree(projectPath, wt);
|
|
207
|
+
if (result.ok) {
|
|
128
208
|
report.deleted.worktrees++;
|
|
129
209
|
}
|
|
210
|
+
else if (result.error) {
|
|
211
|
+
report.errors.push(result.error);
|
|
212
|
+
}
|
|
130
213
|
}
|
|
131
214
|
// Drop stashes (sequential — must handle index shifting)
|
|
132
215
|
// Drop in reverse order so indices stay valid
|
|
133
216
|
const reversedStashes = [...staleStashes].reverse();
|
|
134
217
|
for (const stash of reversedStashes) {
|
|
135
|
-
const
|
|
136
|
-
if (ok) {
|
|
218
|
+
const result = await backupAndDropStash(projectPath, stash);
|
|
219
|
+
if (result.ok) {
|
|
137
220
|
report.deleted.stashes++;
|
|
138
221
|
}
|
|
222
|
+
else if (result.error) {
|
|
223
|
+
report.errors.push(result.error);
|
|
224
|
+
}
|
|
139
225
|
}
|
|
140
226
|
// SPEC-771: clean stale .bak.* backup files from planu/specs/ (best-effort)
|
|
141
227
|
report.deleted.backups = await cleanStaleBackups(projectPath);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.16",
|
|
4
|
+
"engineVersion": "5.3.16",
|
|
5
|
+
"buildId": "1-5.3.16-aarch64-apple-darwin",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"platform": "darwin",
|
|
18
18
|
"arch": "arm64",
|
|
19
19
|
"libc": null,
|
|
20
|
-
"artifactSha256": "
|
|
20
|
+
"artifactSha256": "2a6db4c53e8d2e492a44dc8c276b3f1ec9e00f20056225221aa142752a9dfb4b",
|
|
21
21
|
"sbom": {
|
|
22
22
|
"format": "CycloneDX-1.5",
|
|
23
23
|
"path": "planu-core.darwin-arm64.node.sbom.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "6bfb518a006ae80f0a94328c6fc0be1fa7d86b9a5bab2d7476a80fc8171dfd71"
|
|
25
25
|
},
|
|
26
26
|
"provenance": {
|
|
27
27
|
"builder": "scripts/build-rust-local.sh",
|
|
28
|
-
"sourceCommit": "
|
|
28
|
+
"sourceCommit": "0cde0ff71b6b07e6bb7c059c7f03a7a58910e221",
|
|
29
29
|
"sourceTreeDirty": false,
|
|
30
30
|
"sourceDateEpoch": 1,
|
|
31
31
|
"rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"signature": {
|
|
35
35
|
"algorithm": "Ed25519",
|
|
36
36
|
"keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
|
|
37
|
-
"value": "
|
|
37
|
+
"value": "6QDQPYqy6xJ4RHk+SGv1DdPTxgQDQCG63NMTaK2Kk72npIDR7g6h/mCKNLhFDXPALcS7kjqKRijf/PzRqnmCBQ=="
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.3.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.16",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.16",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "2a6db4c53e8d2e492a44dc8c276b3f1ec9e00f20056225221aa142752a9dfb4b"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.3.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.16",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.16",
|
|
4
|
+
"engineVersion": "5.3.16",
|
|
5
|
+
"buildId": "1-5.3.16-x86_64-apple-darwin",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"platform": "darwin",
|
|
18
18
|
"arch": "x64",
|
|
19
19
|
"libc": null,
|
|
20
|
-
"artifactSha256": "
|
|
20
|
+
"artifactSha256": "be5de1bbcd0f9b4989aa82c5b51f0a7425c3eb1fa9dfd772cc362f476db433f5",
|
|
21
21
|
"sbom": {
|
|
22
22
|
"format": "CycloneDX-1.5",
|
|
23
23
|
"path": "planu-core.darwin-x64.node.sbom.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "38a7cc4b3d77c4414c58fd23af446914e84db52dd937e13b763da35b1962ab1e"
|
|
25
25
|
},
|
|
26
26
|
"provenance": {
|
|
27
27
|
"builder": "scripts/build-rust-local.sh",
|
|
28
|
-
"sourceCommit": "
|
|
28
|
+
"sourceCommit": "0cde0ff71b6b07e6bb7c059c7f03a7a58910e221",
|
|
29
29
|
"sourceTreeDirty": false,
|
|
30
30
|
"sourceDateEpoch": 1,
|
|
31
31
|
"rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"signature": {
|
|
35
35
|
"algorithm": "Ed25519",
|
|
36
36
|
"keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
|
|
37
|
-
"value": "
|
|
37
|
+
"value": "7X8ph1LL1pB4hXlWcXD109sTeHzyRTa94ozW52TdCg6y1LIH1kNwuz4rU0g0KK4CLpnanRpeLpu7NODRGiP2AQ=="
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.3.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.16",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.16",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "be5de1bbcd0f9b4989aa82c5b51f0a7425c3eb1fa9dfd772cc362f476db433f5"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.3.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.16",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.16",
|
|
4
|
+
"engineVersion": "5.3.16",
|
|
5
|
+
"buildId": "1-5.3.16-aarch64-unknown-linux-gnu",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "linux",
|
|
17
17
|
"arch": "arm64",
|
|
18
18
|
"libc": "glibc",
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "01d8ce2cb696744dc58095e8ff7fb2be6afcbccf467b0cd0eb538b6be530e210",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-arm64-gnu.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "5eb69bed870789d764c02f63f0371c51872314e22a888c60af9842c35f536a4b"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "0cde0ff71b6b07e6bb7c059c7f03a7a58910e221",
|
|
28
28
|
"sourceTreeDirty": false,
|
|
29
29
|
"sourceDateEpoch": 1,
|
|
30
30
|
"rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"signature": {
|
|
34
34
|
"algorithm": "Ed25519",
|
|
35
35
|
"keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
|
|
36
|
-
"value": "
|
|
36
|
+
"value": "EJcdFkXbSIapzhu002fZ/X4QRUCpbJjwj4AYsg0G2BiERBQjMavAeiXqVNLr75Jt84NRmcOqXhidCvaEV0snCQ=="
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.3.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.16",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.16",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "01d8ce2cb696744dc58095e8ff7fb2be6afcbccf467b0cd0eb538b6be530e210"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.3.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.16",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|