@planu/cli 5.7.2 → 5.7.4
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 +18 -0
- package/dist/.planu-build.json +1 -1
- package/dist/engine/spec-migrator/drift-detector.js +2 -2
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +6 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +34 -14
- package/dist/types/spec-format.d.ts +1 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [5.7.4] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(cleanup): run runtime hygiene in report mode and exempt .runtime from offender scans
|
|
5
|
+
- fix(cleanup): ignore planu/.runtime and untrack leaked runtime files in strict cleanup
|
|
6
|
+
|
|
7
|
+
### Chores
|
|
8
|
+
- chore(runtime): untrack leaked planu runtime state and pin repo-local ignores
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [5.7.3] - 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(release): validate ledger shape before abort version derivation
|
|
15
|
+
- fix(release): fail-closed recovery ledger writer and tolerant abort for corrupted releaseVersion
|
|
16
|
+
- fix(build): report real git failure in provenance stamp and retry transient errors
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
## [5.7.2] - 2026-08-31
|
|
2
20
|
|
|
3
21
|
### Bug Fixes
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"bae55ef85d50f56c6406a9a52291bdff9fcca4e3"}
|
|
@@ -3,7 +3,7 @@ import { readdir, stat, access, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isOldFormat } from './lean-migration.js';
|
|
5
5
|
import { fastScanSpecsAsync } from '../core-bridge.js';
|
|
6
|
-
import { isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalSpecFile, } from './planu-canonical-policy.js';
|
|
6
|
+
import { isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalSpecFile, isRuntimeHomeRootDir, } from './planu-canonical-policy.js';
|
|
7
7
|
async function scanPlanuRoot(projectPath) {
|
|
8
8
|
const rootFiles = [];
|
|
9
9
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -14,7 +14,7 @@ async function scanPlanuRoot(projectPath) {
|
|
|
14
14
|
try {
|
|
15
15
|
const info = await stat(fullPath);
|
|
16
16
|
if (info.isDirectory()) {
|
|
17
|
-
if (!isCanonicalPlanuRootDir(entry)) {
|
|
17
|
+
if (!isCanonicalPlanuRootDir(entry) && !isRuntimeHomeRootDir(entry)) {
|
|
18
18
|
rootFiles.push(entry);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -2,6 +2,7 @@ import type { PlanuCanonicalPathPolicy } from '../../types/index.js';
|
|
|
2
2
|
export declare const PLANU_CANONICAL_POLICY: PlanuCanonicalPathPolicy;
|
|
3
3
|
export declare function isCanonicalPlanuRootFile(name: string): boolean;
|
|
4
4
|
export declare function isCanonicalPlanuRootDir(name: string): boolean;
|
|
5
|
+
export declare function isRuntimeHomeRootDir(name: string): boolean;
|
|
5
6
|
export declare function isCanonicalSpecFile(name: string): boolean;
|
|
6
7
|
export declare function isCanonicalSpecDir(name: string): boolean;
|
|
7
8
|
export declare function mustMergeBeforeDeleteSpecFile(name: string): boolean;
|
|
@@ -48,12 +48,18 @@ const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
|
48
48
|
const SPEC_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecFiles);
|
|
49
49
|
const SPEC_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecDirs);
|
|
50
50
|
const LEGACY_MERGE_SET = new Set(PLANU_CANONICAL_POLICY.legacyMergeBeforeDeleteFiles);
|
|
51
|
+
const RUNTIME_HOME_TOP_DIR = PLANU_CANONICAL_POLICY.runtimeHomeDir
|
|
52
|
+
.replace(/^planu\//, '')
|
|
53
|
+
.replace(/\/$/, '');
|
|
51
54
|
export function isCanonicalPlanuRootFile(name) {
|
|
52
55
|
return ROOT_FILE_SET.has(name);
|
|
53
56
|
}
|
|
54
57
|
export function isCanonicalPlanuRootDir(name) {
|
|
55
58
|
return ROOT_DIR_SET.has(name);
|
|
56
59
|
}
|
|
60
|
+
export function isRuntimeHomeRootDir(name) {
|
|
61
|
+
return name === RUNTIME_HOME_TOP_DIR;
|
|
62
|
+
}
|
|
57
63
|
export function isCanonicalSpecFile(name) {
|
|
58
64
|
return SPEC_FILE_SET.has(name);
|
|
59
65
|
}
|
|
@@ -8,7 +8,7 @@ import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
|
8
8
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
9
9
|
import { safeUnlink } from './git-aware-fs.js';
|
|
10
10
|
import { scanLegacyArtifacts } from './legacy-classifier.js';
|
|
11
|
-
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isLegacyReleasePendingFile, isCanonicalSpecDir, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
11
|
+
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isLegacyReleasePendingFile, isCanonicalSpecDir, isCanonicalSpecFile, isRuntimeHomeRootDir, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
// SPEC-1699: legacy planu/ root files folded into project.json by init_project's
|
|
14
14
|
// migration. Destructive cleanup must never delete them outright — only the
|
|
@@ -44,7 +44,6 @@ async function gitRmCached(projectPath, relPath) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
// This helper is only called for directories from removePath.
|
|
48
47
|
const args = ['rm', '--cached', '--quiet', '--ignore-unmatch', '-r', relPath];
|
|
49
48
|
await execFileAsync('git', args, {
|
|
50
49
|
cwd: projectPath,
|
|
@@ -82,6 +81,7 @@ function buildPlanuIgnoreBlock() {
|
|
|
82
81
|
'planu/data/',
|
|
83
82
|
'planu/state/',
|
|
84
83
|
'planu/.locks/',
|
|
84
|
+
'planu/.runtime/',
|
|
85
85
|
'planu/specs/**',
|
|
86
86
|
'!planu/specs/**/',
|
|
87
87
|
'!planu/specs/**/spec.md',
|
|
@@ -163,16 +163,23 @@ async function shouldDeleteOrphanSpecDir(specDir) {
|
|
|
163
163
|
// Keeping them without spec.md leaves a permanently non-canonical orphan behind.
|
|
164
164
|
return !entries.includes('spec.md');
|
|
165
165
|
}
|
|
166
|
+
async function runReportModeCleanup(projectPath) {
|
|
167
|
+
const validation = await validateStrictPlanuLayout(projectPath);
|
|
168
|
+
const planuExists = existsSync(join(projectPath, 'planu'));
|
|
169
|
+
const gitignoreUpdated = planuExists ? await updateGitignore(projectPath) : false;
|
|
170
|
+
const untracked = planuExists ? await untrackRuntimeFiles(projectPath) : [];
|
|
171
|
+
return {
|
|
172
|
+
mode: 'report',
|
|
173
|
+
deleted: [],
|
|
174
|
+
merged: [],
|
|
175
|
+
proposed: validation.offenders,
|
|
176
|
+
gitignoreUpdated,
|
|
177
|
+
untracked,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
166
180
|
export async function runStrictPlanuCleanup(projectPath, authority = { mode: 'report' }) {
|
|
167
181
|
if (authority.mode === 'report') {
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
mode: 'report',
|
|
171
|
-
deleted: [],
|
|
172
|
-
merged: [],
|
|
173
|
-
proposed: validation.offenders,
|
|
174
|
-
gitignoreUpdated: false,
|
|
175
|
-
};
|
|
182
|
+
return runReportModeCleanup(projectPath);
|
|
176
183
|
}
|
|
177
184
|
const result = {
|
|
178
185
|
mode: 'execute',
|
|
@@ -180,6 +187,7 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
180
187
|
merged: [],
|
|
181
188
|
proposed: [],
|
|
182
189
|
gitignoreUpdated: false,
|
|
190
|
+
untracked: [],
|
|
183
191
|
};
|
|
184
192
|
const planuDir = join(projectPath, 'planu');
|
|
185
193
|
if (!existsSync(planuDir)) {
|
|
@@ -189,10 +197,7 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
189
197
|
for (const entry of entries) {
|
|
190
198
|
const full = join(planuDir, entry);
|
|
191
199
|
const isDir = await pathIsDirectory(full);
|
|
192
|
-
if (isDir && entry === 'releases') {
|
|
193
|
-
// SPEC-1699: 'releases' is no longer a canonical root dir, but its
|
|
194
|
-
// pending.json still needs the dedicated fold-into-project.json
|
|
195
|
-
// migration below before its directory can be removed.
|
|
200
|
+
if (isDir && (entry === 'releases' || isRuntimeHomeRootDir(entry))) {
|
|
196
201
|
continue;
|
|
197
202
|
}
|
|
198
203
|
if (isDir && !isCanonicalPlanuRootDir(entry)) {
|
|
@@ -236,8 +241,20 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
236
241
|
await walkSpecDirectory(projectPath, full, result);
|
|
237
242
|
}
|
|
238
243
|
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
244
|
+
result.untracked = await untrackRuntimeFiles(projectPath);
|
|
239
245
|
return result;
|
|
240
246
|
}
|
|
247
|
+
async function untrackRuntimeFiles(projectPath) {
|
|
248
|
+
const trackedRuntimeFiles = await listTrackedFiles(projectPath, 'planu/.runtime');
|
|
249
|
+
if (trackedRuntimeFiles.length === 0) {
|
|
250
|
+
return trackedRuntimeFiles;
|
|
251
|
+
}
|
|
252
|
+
for (const relPath of trackedRuntimeFiles) {
|
|
253
|
+
await gitRmCached(projectPath, relPath);
|
|
254
|
+
}
|
|
255
|
+
const stillTracked = new Set(await listTrackedFiles(projectPath, 'planu/.runtime'));
|
|
256
|
+
return trackedRuntimeFiles.filter((relPath) => !stillTracked.has(relPath));
|
|
257
|
+
}
|
|
241
258
|
function resolveSpecDirsForValidation(projectPath, specPath) {
|
|
242
259
|
if (!specPath?.trim()) {
|
|
243
260
|
return null;
|
|
@@ -253,6 +270,9 @@ export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
|
253
270
|
for (const entry of entries) {
|
|
254
271
|
const full = join(planuDir, entry);
|
|
255
272
|
const isDir = await pathIsDirectory(full);
|
|
273
|
+
if (isDir && isRuntimeHomeRootDir(entry)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
256
276
|
if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
|
|
257
277
|
(!isDir && !isCanonicalPlanuRootFile(entry))) {
|
|
258
278
|
offenders.push(relative(projectPath, full));
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.7.
|
|
5
|
+
"version": "5.7.4",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|