@planu/cli 5.3.14 → 5.3.15
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 +10 -0
- package/dist/engine/cascade-hooks/hooks/housekeeping-on-done.hook.js +2 -3
- 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/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
|
@@ -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.15",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.15",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "ef2493926703d865ea8673d8357a238058a32a64325050d3880518d8884f25f2"
|
|
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.15",
|
|
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.15",
|
|
4
|
+
"engineVersion": "5.3.15",
|
|
5
|
+
"buildId": "1-5.3.15-x86_64-unknown-linux-musl",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "linux",
|
|
17
17
|
"arch": "x64",
|
|
18
18
|
"libc": "musl",
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "be770ad8fff0c70d1adf1599af7b60a9e0055e133890476e08e04c88c3f28c69",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.linux-x64-musl.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "efd3cf900ecd753f0bee1eba0c6ecc8bd21d4f405e5d346f0fbe58972cf65da4"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "14948c630bfb7a355391c10563934cc79e8bb167",
|
|
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": "tiDSdVl5hTKNRg88FbcmEwUh4rXFh7OGfMzb9fV3BqwBAwSA/XktD4rXe4YZyBy75Kebo11zW+gp3DMnf+KECA=="
|
|
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.15",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.15",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "be770ad8fff0c70d1adf1599af7b60a9e0055e133890476e08e04c88c3f28c69"
|
|
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.15",
|
|
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.15",
|
|
4
|
+
"engineVersion": "5.3.15",
|
|
5
|
+
"buildId": "1-5.3.15-aarch64-pc-windows-msvc",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "win32",
|
|
17
17
|
"arch": "arm64",
|
|
18
18
|
"libc": null,
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "583b397fb748d8ce083c4b7d412800789e8b08d6cfcdf51dbb00ca8f16b790c3",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-arm64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "ed94ce39f49489d682bc80cce95eb3ef04bafd64ed7fd7d8ea6def8b12c9a458"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "14948c630bfb7a355391c10563934cc79e8bb167",
|
|
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": "gaOwCaYMRwh0TEujBu7Rabl255fJETQEAo4i+NFVpamCPo5YWa2sknnxsbm/ZM4iX9YLD1W5S1MeYFUcwVrIAg=="
|
|
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.15",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.15",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "583b397fb748d8ce083c4b7d412800789e8b08d6cfcdf51dbb00ca8f16b790c3"
|
|
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.15",
|
|
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.15",
|
|
4
|
+
"engineVersion": "5.3.15",
|
|
5
|
+
"buildId": "1-5.3.15-x86_64-pc-windows-msvc",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "win32",
|
|
17
17
|
"arch": "x64",
|
|
18
18
|
"libc": null,
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "612e2d929b12f1011da69ddcc598cbd251123294b52cfe7c04cd921e18d46ad7",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-x64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "b6f3af124aeadba537605434bc406ac76f0bd798244edfbe31566b5011989207"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "14948c630bfb7a355391c10563934cc79e8bb167",
|
|
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": "2W3TfOtk1zUPWqx2o2a9DmJPT1rSEBL0kLVOrZfggV/B/hEWC5KgLZs0Cy9w6kE5QeWTeGQZ1spl12XVCdAZDQ=="
|
|
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.15",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.15",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "612e2d929b12f1011da69ddcc598cbd251123294b52cfe7c04cd921e18d46ad7"
|
|
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.15",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CleanupResult } from '../../types/index.js';
|
|
1
|
+
import type { CleanupResult, HousekeepingAuthority } from '../../types/index.js';
|
|
2
2
|
export type { CleanupResult };
|
|
3
3
|
/** Remove all non-canonical files from planu/ root and per-spec directories. */
|
|
4
|
-
export declare function cleanPlanuRoot(planuDir: string, projectPath?: string): Promise<CleanupResult>;
|
|
4
|
+
export declare function cleanPlanuRoot(planuDir: string, projectPath?: string, authority?: HousekeepingAuthority): Promise<CleanupResult>;
|
|
5
5
|
//# sourceMappingURL=planu-root-cleaner.d.ts.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { runStrictPlanuCleanup } from './strict-planu-cleanup.js';
|
|
4
4
|
/** Remove all non-canonical files from planu/ root and per-spec directories. */
|
|
5
|
-
export async function cleanPlanuRoot(planuDir, projectPath) {
|
|
5
|
+
export async function cleanPlanuRoot(planuDir, projectPath, authority = { mode: 'report' }) {
|
|
6
6
|
const root = projectPath ?? dirname(planuDir);
|
|
7
|
-
const strict = await runStrictPlanuCleanup(root);
|
|
7
|
+
const strict = await runStrictPlanuCleanup(root, authority);
|
|
8
8
|
const deletedRootFiles = [];
|
|
9
9
|
const deletedSpecFiles = [];
|
|
10
10
|
for (const path of [...strict.deleted, ...strict.merged]) {
|
|
@@ -20,6 +20,7 @@ export async function cleanPlanuRoot(planuDir, projectPath) {
|
|
|
20
20
|
deletedRootFiles,
|
|
21
21
|
deletedSpecFiles,
|
|
22
22
|
totalDeleted: strict.deleted.length + strict.merged.length,
|
|
23
|
+
proposed: strict.proposed,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
//# sourceMappingURL=planu-root-cleaner.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { StrictPlanuCleanupResult, StrictPlanuValidationOptions, StrictPlanuValidationResult } from '../../types/index.js';
|
|
1
|
+
import type { StrictPlanuCleanupResult, StrictPlanuValidationOptions, StrictPlanuValidationResult, HousekeepingAuthority } from '../../types/index.js';
|
|
2
2
|
import { PLANU_CANONICAL_POLICY } from './planu-canonical-policy.js';
|
|
3
|
-
export declare function runStrictPlanuCleanup(projectPath: string): Promise<StrictPlanuCleanupResult>;
|
|
3
|
+
export declare function runStrictPlanuCleanup(projectPath: string, authority?: HousekeepingAuthority): Promise<StrictPlanuCleanupResult>;
|
|
4
4
|
export declare function validateStrictPlanuLayout(projectPath: string, options?: StrictPlanuValidationOptions): Promise<StrictPlanuValidationResult>;
|
|
5
5
|
export { PLANU_CANONICAL_POLICY };
|
|
6
6
|
//# sourceMappingURL=strict-planu-cleanup.d.ts.map
|
|
@@ -139,8 +139,24 @@ async function shouldDeleteOrphanSpecDir(specDir) {
|
|
|
139
139
|
// Keeping them without spec.md leaves a permanently non-canonical orphan behind.
|
|
140
140
|
return !entries.includes('spec.md');
|
|
141
141
|
}
|
|
142
|
-
export async function runStrictPlanuCleanup(projectPath) {
|
|
143
|
-
|
|
142
|
+
export async function runStrictPlanuCleanup(projectPath, authority = { mode: 'report' }) {
|
|
143
|
+
if (authority.mode === 'report') {
|
|
144
|
+
const validation = await validateStrictPlanuLayout(projectPath);
|
|
145
|
+
return {
|
|
146
|
+
mode: 'report',
|
|
147
|
+
deleted: [],
|
|
148
|
+
merged: [],
|
|
149
|
+
proposed: validation.offenders,
|
|
150
|
+
gitignoreUpdated: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const result = {
|
|
154
|
+
mode: 'execute',
|
|
155
|
+
deleted: [],
|
|
156
|
+
merged: [],
|
|
157
|
+
proposed: [],
|
|
158
|
+
gitignoreUpdated: false,
|
|
159
|
+
};
|
|
144
160
|
const planuDir = join(projectPath, 'planu');
|
|
145
161
|
if (!existsSync(planuDir)) {
|
|
146
162
|
return result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolResult, GitConfig, SpecDoneCleanupResult } from '../../types/index.js';
|
|
1
|
+
import type { ToolResult, GitConfig, HousekeepingAuthority, SpecDoneCleanupResult } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* AC-02: Eliminate gone branches + remote prune.
|
|
4
4
|
* AC-03: List stale stashes (>7d); drop if force=true.
|
|
@@ -11,5 +11,5 @@ export declare function handleCleanup(projectId: string, config?: GitConfig): Pr
|
|
|
11
11
|
* AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
|
|
12
12
|
* Best-effort — never throws. Caller should fire-and-forget or log result.
|
|
13
13
|
*/
|
|
14
|
-
export declare function cleanupSpecOnDone(projectPath: string, specId: string, gitBranch: string | undefined): Promise<SpecDoneCleanupResult>;
|
|
14
|
+
export declare function cleanupSpecOnDone(projectPath: string, specId: string, gitBranch: string | undefined, authority?: HousekeepingAuthority): Promise<SpecDoneCleanupResult>;
|
|
15
15
|
//# sourceMappingURL=cleanup-ops.d.ts.map
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { git, resolveProjectPath } from './git-helpers.js';
|
|
6
|
+
import { runAbortableProcess } from '../../engine/abortable-process-runner.js';
|
|
6
7
|
import { compactResult, compactError } from '../output-formatter.js';
|
|
7
8
|
const WORKTREES_DIR = '.claude/worktrees';
|
|
8
9
|
const STALE_DAYS_THRESHOLD = 7;
|
|
@@ -48,9 +49,9 @@ async function getMergedBranches(projectPath) {
|
|
|
48
49
|
return new Set();
|
|
49
50
|
}
|
|
50
51
|
/** Remove a worktree — idempotent (ignores "not a worktree" errors). */
|
|
51
|
-
async function removeWorktree(projectPath, worktreePath) {
|
|
52
|
+
async function removeWorktree(projectPath, worktreePath, force) {
|
|
52
53
|
try {
|
|
53
|
-
await git(projectPath, ['worktree', 'remove', '--force', worktreePath]);
|
|
54
|
+
await git(projectPath, ['worktree', 'remove', ...(force ? ['--force'] : []), worktreePath]);
|
|
54
55
|
}
|
|
55
56
|
catch (err) {
|
|
56
57
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -97,7 +98,7 @@ async function cleanupOrphanWorktrees(projectPath, mergedBranches) {
|
|
|
97
98
|
continue;
|
|
98
99
|
}
|
|
99
100
|
try {
|
|
100
|
-
await removeWorktree(projectPath, wt.path);
|
|
101
|
+
await removeWorktree(projectPath, wt.path, true);
|
|
101
102
|
removed.push(wt.path);
|
|
102
103
|
}
|
|
103
104
|
catch (err) {
|
|
@@ -337,85 +338,149 @@ async function findSpecWorktree(projectPath, specId) {
|
|
|
337
338
|
* AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
|
|
338
339
|
* Best-effort — never throws. Caller should fire-and-forget or log result.
|
|
339
340
|
*/
|
|
340
|
-
export async function cleanupSpecOnDone(projectPath, specId, gitBranch) {
|
|
341
|
+
export async function cleanupSpecOnDone(projectPath, specId, gitBranch, authority = { mode: 'report' }) {
|
|
341
342
|
const result = {
|
|
343
|
+
mode: authority.mode,
|
|
344
|
+
proposals: [],
|
|
342
345
|
worktreeRemoved: null,
|
|
343
346
|
localBranchRemoved: null,
|
|
344
347
|
remoteBranchRemoved: null,
|
|
345
348
|
errors: [],
|
|
346
349
|
};
|
|
347
|
-
// 1. Remove worktree if it exists
|
|
348
350
|
const worktreePath = await findSpecWorktree(projectPath, specId);
|
|
351
|
+
const mergedState = gitBranch ? await getBranchMergedState(projectPath, gitBranch) : 'unknown';
|
|
352
|
+
if (worktreePath) {
|
|
353
|
+
const worktreeClean = await isWorktreeClean(worktreePath);
|
|
354
|
+
result.proposals.push({
|
|
355
|
+
kind: 'worktree',
|
|
356
|
+
ref: worktreePath,
|
|
357
|
+
worktreeClean,
|
|
358
|
+
mergedState,
|
|
359
|
+
reason: `spec ${specId} is done`,
|
|
360
|
+
suggestedAction: worktreeClean ? 'delete' : 'inspect',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (gitBranch) {
|
|
364
|
+
const currentBranch = await getCurrentBranch(projectPath);
|
|
365
|
+
const suggestedAction = mergedState === 'merged' && currentBranch !== gitBranch ? 'delete' : 'keep';
|
|
366
|
+
result.proposals.push({
|
|
367
|
+
kind: 'local-branch',
|
|
368
|
+
ref: gitBranch,
|
|
369
|
+
worktreeClean: null,
|
|
370
|
+
mergedState,
|
|
371
|
+
reason: `spec ${specId} is done`,
|
|
372
|
+
suggestedAction,
|
|
373
|
+
});
|
|
374
|
+
if (await remoteBranchExists(projectPath, gitBranch)) {
|
|
375
|
+
result.proposals.push({
|
|
376
|
+
kind: 'remote-branch',
|
|
377
|
+
ref: `origin/${gitBranch}`,
|
|
378
|
+
worktreeClean: null,
|
|
379
|
+
mergedState,
|
|
380
|
+
reason: `spec ${specId} is done`,
|
|
381
|
+
suggestedAction: mergedState === 'merged' ? 'delete' : 'keep',
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (authority.mode === 'report') {
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
349
388
|
if (worktreePath) {
|
|
350
389
|
try {
|
|
351
|
-
await removeWorktree(projectPath, worktreePath);
|
|
390
|
+
await removeWorktree(projectPath, worktreePath, false);
|
|
352
391
|
result.worktreeRemoved = worktreePath;
|
|
353
392
|
}
|
|
354
393
|
catch (err) {
|
|
355
394
|
result.errors.push(`worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
356
395
|
}
|
|
357
396
|
}
|
|
358
|
-
if (!gitBranch) {
|
|
397
|
+
if (!gitBranch || mergedState !== 'merged') {
|
|
359
398
|
return result;
|
|
360
399
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.split('\n')
|
|
366
|
-
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
367
|
-
.filter(Boolean));
|
|
368
|
-
const isMerged = merged.has(gitBranch);
|
|
369
|
-
// Also check main as fallback
|
|
370
|
-
let isMergedMain = false;
|
|
371
|
-
if (!isMerged) {
|
|
372
|
-
try {
|
|
373
|
-
const { stdout: mainOut } = await git(projectPath, ['branch', '--merged', 'main']);
|
|
374
|
-
const mergedMain = new Set(mainOut
|
|
375
|
-
.split('\n')
|
|
376
|
-
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
377
|
-
.filter(Boolean));
|
|
378
|
-
isMergedMain = mergedMain.has(gitBranch);
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
// ignore
|
|
382
|
-
}
|
|
400
|
+
if ((await getCurrentBranch(projectPath)) !== gitBranch) {
|
|
401
|
+
try {
|
|
402
|
+
await git(projectPath, ['branch', '-d', gitBranch]);
|
|
403
|
+
result.localBranchRemoved = gitBranch;
|
|
383
404
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const { stdout: headOut } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
387
|
-
if (headOut.trim() !== gitBranch) {
|
|
388
|
-
try {
|
|
389
|
-
await git(projectPath, ['branch', '-d', gitBranch]);
|
|
390
|
-
result.localBranchRemoved = gitBranch;
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
// Force-delete if needed
|
|
394
|
-
try {
|
|
395
|
-
await git(projectPath, ['branch', '-D', gitBranch]);
|
|
396
|
-
result.localBranchRemoved = gitBranch;
|
|
397
|
-
}
|
|
398
|
-
catch (err) {
|
|
399
|
-
result.errors.push(`local branch: ${err instanceof Error ? err.message : String(err)}`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
result.errors.push(`local branch: ${err instanceof Error ? err.message : String(err)}`);
|
|
403
407
|
}
|
|
404
408
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
// 3. Remove remote branch if it exists
|
|
409
|
-
try {
|
|
410
|
-
const { stdout } = await git(projectPath, ['ls-remote', '--heads', 'origin', gitBranch]);
|
|
411
|
-
if (stdout.trim()) {
|
|
409
|
+
if (authority.deleteRemoteRefs && (await remoteBranchExists(projectPath, gitBranch))) {
|
|
410
|
+
try {
|
|
412
411
|
await git(projectPath, ['push', 'origin', '--delete', gitBranch]);
|
|
413
412
|
result.remoteBranchRemoved = `origin/${gitBranch}`;
|
|
414
413
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
catch (err) {
|
|
415
|
+
result.errors.push(`remote branch: ${err instanceof Error ? err.message : String(err)}`);
|
|
416
|
+
}
|
|
418
417
|
}
|
|
419
418
|
return result;
|
|
420
419
|
}
|
|
420
|
+
async function isAncestorOf(projectPath, branch, base) {
|
|
421
|
+
try {
|
|
422
|
+
const result = await runAbortableProcess('git', ['merge-base', '--is-ancestor', branch, base], {
|
|
423
|
+
cwd: projectPath,
|
|
424
|
+
timeoutMs: 30_000,
|
|
425
|
+
maxBufferBytes: 1024 * 1024,
|
|
426
|
+
});
|
|
427
|
+
if (result.status === 0) {
|
|
428
|
+
return 'yes';
|
|
429
|
+
}
|
|
430
|
+
return result.status === 1 && !result.error ? 'no' : 'indeterminate';
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return 'indeterminate';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function getBranchMergedState(projectPath, branch) {
|
|
437
|
+
let sawNotMerged = false;
|
|
438
|
+
let indeterminate = false;
|
|
439
|
+
for (const base of ['main', 'develop', 'master']) {
|
|
440
|
+
try {
|
|
441
|
+
await git(projectPath, ['rev-parse', '--verify', base]);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const ancestry = await isAncestorOf(projectPath, branch, base);
|
|
447
|
+
if (ancestry === 'yes') {
|
|
448
|
+
return 'merged';
|
|
449
|
+
}
|
|
450
|
+
if (ancestry === 'no') {
|
|
451
|
+
sawNotMerged = true;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
indeterminate = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return sawNotMerged && !indeterminate ? 'not_merged' : 'unknown';
|
|
458
|
+
}
|
|
459
|
+
async function isWorktreeClean(worktreePath) {
|
|
460
|
+
try {
|
|
461
|
+
const { stdout } = await git(worktreePath, ['status', '--porcelain']);
|
|
462
|
+
return stdout.trim() === '';
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function getCurrentBranch(projectPath) {
|
|
469
|
+
try {
|
|
470
|
+
const { stdout } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
471
|
+
return stdout.trim() || null;
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function remoteBranchExists(projectPath, branch) {
|
|
478
|
+
try {
|
|
479
|
+
const { stdout } = await git(projectPath, ['ls-remote', '--heads', 'origin', branch]);
|
|
480
|
+
return stdout.trim().length > 0;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
421
486
|
//# sourceMappingURL=cleanup-ops.js.map
|
|
@@ -209,13 +209,12 @@ export async function handleCreateRelease(args, exec = defaultExec) {
|
|
|
209
209
|
const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
|
|
210
210
|
const sweepReport = await runHousekeepingSweep({
|
|
211
211
|
projectPath: releasePath,
|
|
212
|
-
dryRun: false,
|
|
213
212
|
aggressive: false,
|
|
214
213
|
includeStashes: false,
|
|
215
214
|
});
|
|
216
|
-
if (sweepReport.
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
if (sweepReport.wouldDelete.branches.length > 0 ||
|
|
216
|
+
sweepReport.wouldDelete.worktrees.length > 0) {
|
|
217
|
+
outputLines.push(` Housekeeping: ${String(sweepReport.wouldDelete.branches.length)} branch(es), ${String(sweepReport.wouldDelete.worktrees.length)} worktree(s) proposed — run housekeeping_sweep`);
|
|
219
218
|
}
|
|
220
219
|
}
|
|
221
220
|
catch {
|
|
@@ -247,14 +246,6 @@ export async function handleCreateRelease(args, exec = defaultExec) {
|
|
|
247
246
|
validateScore: null,
|
|
248
247
|
allSpecs,
|
|
249
248
|
}, { disabledByConfig: new Set(), disabledByEnv: new Set() });
|
|
250
|
-
// SPEC-790: Auto-trigger housekeeping_sweep after release to clean branches/worktrees
|
|
251
|
-
try {
|
|
252
|
-
const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
|
|
253
|
-
void runHousekeepingSweep({ projectPath: releasePath, dryRun: false });
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
/* best-effort — housekeeping never blocks the release */
|
|
257
|
-
}
|
|
258
249
|
}
|
|
259
250
|
catch {
|
|
260
251
|
/* best-effort — release_completed cascade never blocks the release response */
|
|
@@ -3,6 +3,7 @@ import type { ToolResult } from '../types/index.js';
|
|
|
3
3
|
export declare const HousekeepingSweepInputSchema: z.ZodObject<{
|
|
4
4
|
projectPath: z.ZodOptional<z.ZodString>;
|
|
5
5
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
6
|
+
deleteRemoteRefs: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
6
7
|
aggressive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
8
|
includeStashes: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
9
|
}, z.core.$strip>;
|
|
@@ -19,6 +19,11 @@ export const HousekeepingSweepInputSchema = z.object({
|
|
|
19
19
|
.optional()
|
|
20
20
|
.default(true)
|
|
21
21
|
.describe('When true (default), only report — no deletions performed'),
|
|
22
|
+
deleteRemoteRefs: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe('When true with dryRun false, allow deletion of merged origin branches'),
|
|
22
27
|
aggressive: z
|
|
23
28
|
.boolean()
|
|
24
29
|
.optional()
|
|
@@ -107,9 +112,13 @@ function formatReportWithEphemeral(report, orphan, ephemeral) {
|
|
|
107
112
|
export async function handleHousekeepingSweep(args, resolvedProjectPath) {
|
|
108
113
|
const projectPath = args.projectPath ?? resolvedProjectPath;
|
|
109
114
|
const dryRun = args.dryRun ?? true;
|
|
115
|
+
const deleteRemoteRefs = args.deleteRemoteRefs ?? false;
|
|
116
|
+
const authority = dryRun
|
|
117
|
+
? { mode: 'report' }
|
|
118
|
+
: { mode: 'execute', deleteRemoteRefs };
|
|
110
119
|
const report = await runHousekeepingSweep({
|
|
111
120
|
projectPath,
|
|
112
|
-
|
|
121
|
+
authority,
|
|
113
122
|
aggressive: args.aggressive ?? false,
|
|
114
123
|
includeStashes: args.includeStashes ?? false,
|
|
115
124
|
});
|
|
@@ -145,15 +154,13 @@ export async function handleHousekeepingSweep(args, resolvedProjectPath) {
|
|
|
145
154
|
}
|
|
146
155
|
let strictPlanuCleanup = null;
|
|
147
156
|
try {
|
|
148
|
-
const { runStrictPlanuCleanup
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
strictPlanuCleanup = { deleted: [], merged: [], offenders: validation.offenders };
|
|
156
|
-
}
|
|
157
|
+
const { runStrictPlanuCleanup } = await import('../engine/spec-migrator/index.js');
|
|
158
|
+
const cleanup = await runStrictPlanuCleanup(projectPath, authority);
|
|
159
|
+
strictPlanuCleanup = {
|
|
160
|
+
deleted: cleanup.deleted,
|
|
161
|
+
merged: cleanup.merged,
|
|
162
|
+
offenders: cleanup.proposed,
|
|
163
|
+
};
|
|
157
164
|
}
|
|
158
165
|
catch {
|
|
159
166
|
/* best-effort — never block housekeeping_sweep */
|
|
@@ -3,6 +3,7 @@ export interface GitSetupResult {
|
|
|
3
3
|
gitFlowType: string;
|
|
4
4
|
gitignoreUpdated: boolean;
|
|
5
5
|
gitRepoDetected: boolean;
|
|
6
|
+
layoutOffenders: string[];
|
|
6
7
|
}
|
|
7
8
|
/** SPEC-001 + SPEC-539: Setup git hooks, detect branching model, and configure .gitignore.
|
|
8
9
|
* SPEC-539: Only installs hooks if a git repository is detected — never creates .git. */
|