@planu/cli 3.9.13 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/cli/commands/spec.js +20 -1
- package/dist/cli/commands/status.js +18 -1
- package/dist/config/license-plans.json +1 -0
- package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
- package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
- package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
- package/dist/engine/autopilot/action-registry.js +5 -14
- package/dist/engine/autopilot/state-updater.js +13 -10
- package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
- package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
- package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
- package/dist/engine/git/planu-autocommit.d.ts +1 -0
- package/dist/engine/git/planu-autocommit.js +6 -0
- package/dist/engine/git-hook-injector.js +3 -3
- package/dist/engine/handoff-artifacts/io.js +3 -2
- package/dist/engine/handoff-packager.js +2 -1
- package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
- package/dist/engine/hooks/full-spectrum-generator.js +5 -3
- package/dist/engine/release/postmortem-generator.d.ts +1 -1
- package/dist/engine/release/postmortem-generator.js +3 -2
- package/dist/engine/safety/cross-process-lock.js +2 -2
- package/dist/engine/session/checkpoint-writer.js +0 -1
- package/dist/engine/session-context-generator.js +4 -1
- package/dist/engine/spec-audit/index.js +2 -2
- package/dist/engine/spec-audit/report-writer.d.ts +1 -1
- package/dist/engine/spec-audit/report-writer.js +5 -4
- package/dist/engine/spec-migrator/index.d.ts +1 -0
- package/dist/engine/spec-migrator/index.js +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
- package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
- package/dist/engine/spec-summary-html.d.ts +5 -5
- package/dist/engine/spec-summary-html.js +7 -32
- package/dist/storage/gaps-log.js +4 -4
- package/dist/storage/transition-log.js +3 -2
- package/dist/tools/audit-specs-drift.js +3 -3
- package/dist/tools/create-spec/post-creation.d.ts +2 -1
- package/dist/tools/create-spec/post-creation.js +9 -11
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/create-spec.js +42 -18
- package/dist/tools/flag-spec-gap.d.ts +1 -1
- package/dist/tools/flag-spec-gap.js +1 -1
- package/dist/tools/generate-dashboard.js +3 -3
- package/dist/tools/housekeeping-sweep.js +16 -0
- package/dist/tools/init-project/git-setup.js +11 -2
- package/dist/tools/init-project/handler.js +1 -27
- package/dist/tools/init-project/migration-runner.js +8 -0
- package/dist/tools/license-gate.d.ts +1 -0
- package/dist/tools/license-gate.js +5 -1
- package/dist/tools/list-specs.js +13 -0
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +1 -0
- package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
- package/dist/tools/spec-lock-handler.js +1 -1
- package/dist/tools/sync-spec-state-handler.js +7 -0
- package/dist/tools/tool-registry/group-misc.js +4 -4
- package/dist/tools/update-status/batch.d.ts +3 -0
- package/dist/tools/update-status/batch.js +96 -0
- package/dist/tools/update-status/dod-gates.js +1 -1
- package/dist/tools/update-status/file-sync.js +3 -1
- package/dist/tools/update-status/index.js +15 -2
- package/dist/tools/update-status-actions.js +2 -6
- package/dist/tools/validate.js +27 -0
- package/dist/tools/workspace-dashboard-handler.js +6 -9
- package/dist/types/git.d.ts +1 -1
- package/dist/types/spec-format.d.ts +26 -0
- package/package.json +7 -7
|
@@ -13,4 +13,5 @@ export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
|
13
13
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
14
14
|
export { foldProgressIntoSpec, isBoilerplateProgress } from './fold-progress.js';
|
|
15
15
|
export { runSsrBackMigration } from './ssr-back-migration.js';
|
|
16
|
+
export { PLANU_CANONICAL_POLICY, runStrictPlanuCleanup, validateStrictPlanuLayout, } from './strict-planu-cleanup.js';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -26,4 +26,5 @@ export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
|
26
26
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
27
27
|
export { foldProgressIntoSpec, isBoilerplateProgress } from './fold-progress.js';
|
|
28
28
|
export { runSsrBackMigration } from './ssr-back-migration.js';
|
|
29
|
+
export { PLANU_CANONICAL_POLICY, runStrictPlanuCleanup, validateStrictPlanuLayout, } from './strict-planu-cleanup.js';
|
|
29
30
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlanuCanonicalPathPolicy } from '../../types/index.js';
|
|
2
|
+
export declare const PLANU_CANONICAL_POLICY: PlanuCanonicalPathPolicy;
|
|
3
|
+
export declare function isCanonicalPlanuRootFile(name: string): boolean;
|
|
4
|
+
export declare function isCanonicalPlanuRootDir(name: string): boolean;
|
|
5
|
+
export declare function isCanonicalSpecFile(name: string): boolean;
|
|
6
|
+
export declare function mustMergeBeforeDeleteSpecFile(name: string): boolean;
|
|
7
|
+
export declare function isCanonicalReleaseFile(relativeToPlanu: string): boolean;
|
|
8
|
+
export declare function canonicalContractText(): string;
|
|
9
|
+
//# sourceMappingURL=planu-canonical-policy.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// engine/spec-migrator/planu-canonical-policy.ts — SPEC-1017
|
|
2
|
+
// Single source of truth for the strict Planu managed directory contract.
|
|
3
|
+
export const PLANU_CANONICAL_POLICY = {
|
|
4
|
+
canonicalRootFiles: ['conventions.json', 'context.md', 'session-context.md', 'session.json'],
|
|
5
|
+
canonicalRootDirs: ['releases', 'specs'],
|
|
6
|
+
canonicalSpecFiles: ['spec.md'],
|
|
7
|
+
generatedRuntimePatterns: [
|
|
8
|
+
'planu/index.html',
|
|
9
|
+
'planu/roadmap.html',
|
|
10
|
+
'planu/status.json',
|
|
11
|
+
'planu/CHANGELOG.md',
|
|
12
|
+
'planu/.housekeeping-history.jsonl',
|
|
13
|
+
'planu/audits/',
|
|
14
|
+
'planu/handoffs/',
|
|
15
|
+
'planu/data/',
|
|
16
|
+
'planu/state/',
|
|
17
|
+
'planu/.locks/',
|
|
18
|
+
'planu/specs/data/',
|
|
19
|
+
'planu/specs/*/technical-report.html',
|
|
20
|
+
'planu/specs/*/reference/',
|
|
21
|
+
'planu/specs/*/test-stubs.ts',
|
|
22
|
+
'planu/specs/*/.analysis.json',
|
|
23
|
+
'planu/specs/*/prompt.md',
|
|
24
|
+
'planu/specs/*/implementation-brief.md',
|
|
25
|
+
'planu/specs/*/risk-register.md',
|
|
26
|
+
],
|
|
27
|
+
legacyMergeBeforeDeleteFiles: ['technical.md', 'plan.md', 'PLAN.md', 'progress.md'],
|
|
28
|
+
};
|
|
29
|
+
const ROOT_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootFiles);
|
|
30
|
+
const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
31
|
+
const SPEC_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecFiles);
|
|
32
|
+
const LEGACY_MERGE_SET = new Set(PLANU_CANONICAL_POLICY.legacyMergeBeforeDeleteFiles);
|
|
33
|
+
export function isCanonicalPlanuRootFile(name) {
|
|
34
|
+
return ROOT_FILE_SET.has(name);
|
|
35
|
+
}
|
|
36
|
+
export function isCanonicalPlanuRootDir(name) {
|
|
37
|
+
return ROOT_DIR_SET.has(name);
|
|
38
|
+
}
|
|
39
|
+
export function isCanonicalSpecFile(name) {
|
|
40
|
+
return SPEC_FILE_SET.has(name);
|
|
41
|
+
}
|
|
42
|
+
export function mustMergeBeforeDeleteSpecFile(name) {
|
|
43
|
+
return LEGACY_MERGE_SET.has(name);
|
|
44
|
+
}
|
|
45
|
+
export function isCanonicalReleaseFile(relativeToPlanu) {
|
|
46
|
+
return relativeToPlanu === 'releases/pending.json';
|
|
47
|
+
}
|
|
48
|
+
export function canonicalContractText() {
|
|
49
|
+
return [
|
|
50
|
+
'planu/',
|
|
51
|
+
' conventions.json',
|
|
52
|
+
' context.md',
|
|
53
|
+
' session-context.md',
|
|
54
|
+
' session.json',
|
|
55
|
+
' releases/',
|
|
56
|
+
' pending.json',
|
|
57
|
+
' specs/',
|
|
58
|
+
' SPEC-XXX-slug/',
|
|
59
|
+
' spec.md',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=planu-canonical-policy.js.map
|
|
@@ -1,101 +1,25 @@
|
|
|
1
|
-
// engine/spec-migrator/planu-root-cleaner.ts — SPEC-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
6
|
-
import { safeUnlink } from './git-aware-fs.js';
|
|
7
|
-
/** Files allowed in planu/ root. Everything else gets deleted. */
|
|
8
|
-
const CANONICAL_ROOT_FILES = new Set([
|
|
9
|
-
'status.json',
|
|
10
|
-
'conventions.json',
|
|
11
|
-
'index.html',
|
|
12
|
-
'roadmap.html',
|
|
13
|
-
]);
|
|
14
|
-
/** Directories allowed in planu/ root. */
|
|
15
|
-
const CANONICAL_ROOT_DIRS = new Set(['specs']);
|
|
16
|
-
/** Files allowed inside each planu/specs/SPEC-XXX/ directory. */
|
|
17
|
-
const CANONICAL_SPEC_FILES = new Set(['spec.md']);
|
|
18
|
-
/** Remove a file from git tracking (best-effort, silent if not a git repo or file not tracked). */
|
|
19
|
-
function gitRmCached(absolutePath, cwd) {
|
|
20
|
-
try {
|
|
21
|
-
const rel = relative(cwd, absolutePath);
|
|
22
|
-
execFileSync('git', ['rm', '--cached', '--quiet', '--force', rel], {
|
|
23
|
-
cwd,
|
|
24
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
-
timeout: 3_000,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Not a git repo, file not tracked, or git not available — ignore
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
// engine/spec-migrator/planu-root-cleaner.ts — SPEC-1017 strict wrapper
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { runStrictPlanuCleanup } from './strict-planu-cleanup.js';
|
|
32
4
|
/** Remove all non-canonical files from planu/ root and per-spec directories. */
|
|
33
5
|
export async function cleanPlanuRoot(planuDir, projectPath) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
entries = await readdir(planuDir);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return result; // Dir doesn't exist — nothing to clean
|
|
46
|
-
}
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
const fullPath = join(planuDir, entry);
|
|
49
|
-
try {
|
|
50
|
-
const info = await stat(fullPath);
|
|
51
|
-
if (info.isDirectory()) {
|
|
52
|
-
if (!CANONICAL_ROOT_DIRS.has(entry)) {
|
|
53
|
-
// Non-canonical directory — skip (don't delete dirs recursively for safety)
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else if (!CANONICAL_ROOT_FILES.has(entry)) {
|
|
58
|
-
gitRmCached(fullPath, planuDir);
|
|
59
|
-
await safeUnlink(projectPath ?? planuDir, fullPath);
|
|
60
|
-
result.deletedRootFiles.push(entry);
|
|
61
|
-
}
|
|
6
|
+
const root = projectPath ?? dirname(planuDir);
|
|
7
|
+
const strict = await runStrictPlanuCleanup(root);
|
|
8
|
+
const deletedRootFiles = [];
|
|
9
|
+
const deletedSpecFiles = [];
|
|
10
|
+
for (const path of [...strict.deleted, ...strict.merged]) {
|
|
11
|
+
const normalized = path.replace(/^planu\//, '');
|
|
12
|
+
if (normalized.startsWith('specs/')) {
|
|
13
|
+
deletedSpecFiles.push(normalized.replace(/^specs\//, ''));
|
|
62
14
|
}
|
|
63
|
-
|
|
64
|
-
|
|
15
|
+
else {
|
|
16
|
+
deletedRootFiles.push(normalized);
|
|
65
17
|
}
|
|
66
18
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
result.totalDeleted = result.deletedRootFiles.length;
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
for (const specDir of specDirs) {
|
|
78
|
-
const specPath = join(specsDir, specDir);
|
|
79
|
-
try {
|
|
80
|
-
const info = await stat(specPath);
|
|
81
|
-
if (!info.isDirectory()) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const files = await readdir(specPath);
|
|
85
|
-
for (const file of files) {
|
|
86
|
-
if (!CANONICAL_SPEC_FILES.has(file)) {
|
|
87
|
-
const filePath = join(specPath, file);
|
|
88
|
-
gitRmCached(filePath, planuDir);
|
|
89
|
-
await safeUnlink(projectPath ?? planuDir, filePath);
|
|
90
|
-
result.deletedSpecFiles.push(`${specDir}/${file}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Spec dir issue — skip
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
result.totalDeleted = result.deletedRootFiles.length + result.deletedSpecFiles.length;
|
|
99
|
-
return result;
|
|
19
|
+
return {
|
|
20
|
+
deletedRootFiles,
|
|
21
|
+
deletedSpecFiles,
|
|
22
|
+
totalDeleted: strict.deleted.length + strict.merged.length,
|
|
23
|
+
};
|
|
100
24
|
}
|
|
101
25
|
//# sourceMappingURL=planu-root-cleaner.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StrictPlanuCleanupResult, StrictPlanuValidationResult } from '../../types/index.js';
|
|
2
|
+
import { PLANU_CANONICAL_POLICY } from './planu-canonical-policy.js';
|
|
3
|
+
export declare function runStrictPlanuCleanup(projectPath: string): Promise<StrictPlanuCleanupResult>;
|
|
4
|
+
export declare function validateStrictPlanuLayout(projectPath: string): Promise<StrictPlanuValidationResult>;
|
|
5
|
+
export { PLANU_CANONICAL_POLICY };
|
|
6
|
+
//# sourceMappingURL=strict-planu-cleanup.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// engine/spec-migrator/strict-planu-cleanup.ts — SPEC-1017
|
|
2
|
+
// Destructive cleanup for non-canonical files under planu/.
|
|
3
|
+
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
9
|
+
import { safeUnlink } from './git-aware-fs.js';
|
|
10
|
+
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalReleaseFile, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
async function pathIsDirectory(path) {
|
|
13
|
+
try {
|
|
14
|
+
return (await stat(path)).isDirectory();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function gitRmCached(projectPath, relPath) {
|
|
21
|
+
if (!existsSync(join(projectPath, '.git'))) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await execFileAsync('git', ['rm', '--cached', '--quiet', '--ignore-unmatch', relPath], {
|
|
26
|
+
cwd: projectPath,
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* best-effort */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function stripFrontmatter(content) {
|
|
35
|
+
return content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
36
|
+
}
|
|
37
|
+
function appendSectionIfMissing(specContent, heading, body) {
|
|
38
|
+
if (body.trim().length === 0 || new RegExp(`\\n## ${heading}(\\n|$)`).test(specContent)) {
|
|
39
|
+
return specContent;
|
|
40
|
+
}
|
|
41
|
+
return `${specContent.trimEnd()}\n\n## ${heading}\n\n${body.trim()}\n`;
|
|
42
|
+
}
|
|
43
|
+
async function mergeLegacySpecFile(projectPath, specDir, fileName) {
|
|
44
|
+
const specPath = join(specDir, 'spec.md');
|
|
45
|
+
const legacyPath = join(specDir, fileName);
|
|
46
|
+
if (!existsSync(legacyPath) || !existsSync(specPath)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const [specContent, legacyContent] = await Promise.all([
|
|
50
|
+
readFile(specPath, 'utf-8'),
|
|
51
|
+
readFile(legacyPath, 'utf-8'),
|
|
52
|
+
]);
|
|
53
|
+
const body = stripFrontmatter(legacyContent);
|
|
54
|
+
const section = fileName === 'progress.md' ? 'Progress' : fileName === 'technical.md' ? 'Technical' : 'Files';
|
|
55
|
+
const merged = appendSectionIfMissing(specContent, section, body);
|
|
56
|
+
if (merged !== specContent) {
|
|
57
|
+
await atomicWriteFile(specPath, merged, {
|
|
58
|
+
forceEdit: {
|
|
59
|
+
reason: `SPEC-1017 strict cleanup is merging legacy ${fileName} into canonical spec.md.`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await safeUnlink(projectPath, legacyPath);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
async function removePath(projectPath, absolutePath) {
|
|
67
|
+
const rel = relative(projectPath, absolutePath);
|
|
68
|
+
if (await pathIsDirectory(absolutePath)) {
|
|
69
|
+
await gitRmCached(projectPath, rel);
|
|
70
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await safeUnlink(projectPath, absolutePath);
|
|
74
|
+
await rm(absolutePath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
async function updateGitignore(projectPath) {
|
|
77
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
78
|
+
let current = '';
|
|
79
|
+
try {
|
|
80
|
+
current = await readFile(gitignorePath, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* missing .gitignore is fine */
|
|
84
|
+
}
|
|
85
|
+
const required = [
|
|
86
|
+
'planu/*.html',
|
|
87
|
+
'planu/status.json',
|
|
88
|
+
'planu/CHANGELOG.md',
|
|
89
|
+
'planu/.housekeeping-history.jsonl',
|
|
90
|
+
'planu/audits/',
|
|
91
|
+
'planu/handoffs/',
|
|
92
|
+
'planu/data/',
|
|
93
|
+
'planu/state/',
|
|
94
|
+
'planu/.locks/',
|
|
95
|
+
'planu/specs/data/',
|
|
96
|
+
'planu/specs/**/.analysis.json',
|
|
97
|
+
'planu/specs/**/technical-report.html',
|
|
98
|
+
'planu/specs/**/reference/',
|
|
99
|
+
'planu/specs/**/*.bak.*',
|
|
100
|
+
];
|
|
101
|
+
const missing = required.filter((entry) => !current.split('\n').includes(entry));
|
|
102
|
+
if (missing.length === 0) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const separator = current === '' || current.endsWith('\n') ? '' : '\n';
|
|
106
|
+
await atomicWriteFile(gitignorePath, `${current}${separator}# Planu generated/runtime\n${missing.join('\n')}\n`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
async function walkSpecDirectory(projectPath, specDir, result) {
|
|
110
|
+
const entries = await readdir(specDir).catch(() => []);
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const full = join(specDir, entry);
|
|
113
|
+
if (mustMergeBeforeDeleteSpecFile(entry)) {
|
|
114
|
+
if (await mergeLegacySpecFile(projectPath, specDir, entry)) {
|
|
115
|
+
result.merged.push(relative(projectPath, full));
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry === 'reference' || !isCanonicalSpecFile(entry)) {
|
|
120
|
+
await removePath(projectPath, full);
|
|
121
|
+
result.deleted.push(relative(projectPath, full));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function runStrictPlanuCleanup(projectPath) {
|
|
126
|
+
const result = { deleted: [], merged: [], gitignoreUpdated: false };
|
|
127
|
+
const planuDir = join(projectPath, 'planu');
|
|
128
|
+
if (!existsSync(planuDir)) {
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
const entries = await readdir(planuDir).catch(() => []);
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const full = join(planuDir, entry);
|
|
134
|
+
const isDir = await pathIsDirectory(full);
|
|
135
|
+
if (isDir && !isCanonicalPlanuRootDir(entry)) {
|
|
136
|
+
await removePath(projectPath, full);
|
|
137
|
+
result.deleted.push(relative(projectPath, full));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!isDir && !isCanonicalPlanuRootFile(entry)) {
|
|
141
|
+
await removePath(projectPath, full);
|
|
142
|
+
result.deleted.push(relative(projectPath, full));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const releasesDir = join(planuDir, 'releases');
|
|
146
|
+
for (const entry of await readdir(releasesDir).catch(() => [])) {
|
|
147
|
+
const rel = `releases/${entry}`;
|
|
148
|
+
if (!isCanonicalReleaseFile(rel)) {
|
|
149
|
+
const full = join(releasesDir, entry);
|
|
150
|
+
await removePath(projectPath, full);
|
|
151
|
+
result.deleted.push(relative(projectPath, full));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const specsDir = join(planuDir, 'specs');
|
|
155
|
+
for (const entry of await readdir(specsDir).catch(() => [])) {
|
|
156
|
+
const full = join(specsDir, entry);
|
|
157
|
+
if (!(await pathIsDirectory(full)) || entry === 'data') {
|
|
158
|
+
await removePath(projectPath, full);
|
|
159
|
+
result.deleted.push(relative(projectPath, full));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
await walkSpecDirectory(projectPath, full, result);
|
|
163
|
+
}
|
|
164
|
+
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
export async function validateStrictPlanuLayout(projectPath) {
|
|
168
|
+
const offenders = [];
|
|
169
|
+
const planuDir = join(projectPath, 'planu');
|
|
170
|
+
const entries = await readdir(planuDir).catch(() => []);
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const full = join(planuDir, entry);
|
|
173
|
+
const isDir = await pathIsDirectory(full);
|
|
174
|
+
if ((isDir && !isCanonicalPlanuRootDir(entry)) || (!isDir && !isCanonicalPlanuRootFile(entry))) {
|
|
175
|
+
offenders.push(relative(projectPath, full));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const entry of await readdir(join(planuDir, 'releases')).catch(() => [])) {
|
|
179
|
+
const rel = `releases/${entry}`;
|
|
180
|
+
if (!isCanonicalReleaseFile(rel)) {
|
|
181
|
+
offenders.push(`planu/${rel}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const specDir of await readdir(join(planuDir, 'specs')).catch(() => [])) {
|
|
185
|
+
const full = join(planuDir, 'specs', specDir);
|
|
186
|
+
if (!(await pathIsDirectory(full)) || specDir === 'data') {
|
|
187
|
+
offenders.push(relative(projectPath, full));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const entry of await readdir(full).catch(() => [])) {
|
|
191
|
+
if (!isCanonicalSpecFile(entry)) {
|
|
192
|
+
offenders.push(relative(projectPath, join(full, entry)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { ok: offenders.length === 0, offenders, contract: canonicalContractText() };
|
|
197
|
+
}
|
|
198
|
+
export { PLANU_CANONICAL_POLICY };
|
|
199
|
+
//# sourceMappingURL=strict-planu-cleanup.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Spec } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Legacy compatibility entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* SPEC-1017 makes `planu/index.html` and generated per-spec reports forbidden
|
|
6
|
+
* project-tree artifacts. Keep this function for old call sites, but make it
|
|
7
|
+
* a best-effort read-only refresh so callers stop reintroducing legacy files.
|
|
8
8
|
*/
|
|
9
9
|
export declare function regenerateSpecSummaryHtml(projectPath: string, specs: Spec[], _changedSpecIds?: string[]): Promise<void>;
|
|
10
10
|
//# sourceMappingURL=spec-summary-html.d.ts.map
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parseFrontmatter } from './frontmatter-parser.js';
|
|
4
|
-
import { generateDashboardHtml } from './spec-summary-html/dashboard-renderer.js';
|
|
5
|
-
import { computeSpecDataHash, extractEmbeddedHash, HASH_MARKER, } from './spec-summary-html/hash-utils.js';
|
|
6
|
-
import { detectAvailablePages } from './doc-generator/portal/portal-page-detector.js';
|
|
7
4
|
/**
|
|
8
5
|
* Scan planu/specs/ filesystem for spec.md files and build minimal Spec objects
|
|
9
6
|
* from frontmatter. This catches specs not tracked in the JSON store.
|
|
@@ -87,38 +84,16 @@ function mergeSpecs(storeSpecs, fsSpecs) {
|
|
|
87
84
|
return merged;
|
|
88
85
|
}
|
|
89
86
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
87
|
+
* Legacy compatibility entrypoint.
|
|
88
|
+
*
|
|
89
|
+
* SPEC-1017 makes `planu/index.html` and generated per-spec reports forbidden
|
|
90
|
+
* project-tree artifacts. Keep this function for old call sites, but make it
|
|
91
|
+
* a best-effort read-only refresh so callers stop reintroducing legacy files.
|
|
95
92
|
*/
|
|
96
93
|
export async function regenerateSpecSummaryHtml(projectPath, specs, _changedSpecIds) {
|
|
97
|
-
const portalPath = join(projectPath, 'planu');
|
|
98
|
-
const outputPath = join(portalPath, 'index.html');
|
|
99
94
|
try {
|
|
100
95
|
const fsSpecs = await scanFilesystemSpecs(projectPath);
|
|
101
|
-
|
|
102
|
-
await mkdir(portalPath, { recursive: true });
|
|
103
|
-
// Detect available portal pages before generating to populate navbar + quick links
|
|
104
|
-
const availablePages = await detectAvailablePages(portalPath);
|
|
105
|
-
// Skip writing if spec data has not changed (avoids git noise on every tool call)
|
|
106
|
-
const newHash = computeSpecDataHash(allSpecs, availablePages);
|
|
107
|
-
try {
|
|
108
|
-
const existing = await readFile(outputPath, 'utf-8');
|
|
109
|
-
if (extractEmbeddedHash(existing) === newHash) {
|
|
110
|
-
return; // data unchanged — skip write
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// file does not exist yet — proceed to write
|
|
115
|
-
}
|
|
116
|
-
const html = generateDashboardHtml(allSpecs, availablePages);
|
|
117
|
-
const htmlWithHash = html.replace('<!DOCTYPE html>', `<!DOCTYPE html>\n<!-- ${HASH_MARKER} ${newHash} -->`);
|
|
118
|
-
await writeFile(outputPath, htmlWithHash, 'utf-8');
|
|
119
|
-
// SPEC-466: Per-spec reports are legacy and no longer regenerated.
|
|
120
|
-
// Only the global index.html is generated. Remove this block entirely once
|
|
121
|
-
// all clients have migrated (regeneratePerSpecReports stays as dead code for now).
|
|
96
|
+
void mergeSpecs(specs, fsSpecs);
|
|
122
97
|
}
|
|
123
98
|
catch {
|
|
124
99
|
/* best-effort — never fail the caller */
|
package/dist/storage/gaps-log.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// storage/gaps-log.ts — SPEC-739: Hash-chained gaps log (JSONL per project)
|
|
2
2
|
//
|
|
3
|
-
// Layout: planu/research/gaps.jsonl
|
|
3
|
+
// Layout: ~/.planu/data/projects/<projectId>/research/gaps.jsonl
|
|
4
4
|
//
|
|
5
5
|
// Each entry is a JSON object on its own line. The `sha` field is a SHA-256 hash
|
|
6
6
|
// of the entry's canonical JSON (excluding `sha`), chained via `prevSha`.
|
|
@@ -8,12 +8,12 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
8
8
|
import { appendFile, readFile, mkdir } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { isNativeActive, fastAppendGapEntry, fastVerifyGapsChain } from '../engine/core-bridge.js';
|
|
11
|
+
import { projectDataDir } from './base-store.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Path helper
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
|
-
function gapsLogPath(
|
|
15
|
-
|
|
16
|
-
return join('planu', 'research', 'gaps.jsonl');
|
|
15
|
+
function gapsLogPath(projectId) {
|
|
16
|
+
return join(projectDataDir(projectId), 'research', 'gaps.jsonl');
|
|
17
17
|
}
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
// Node error type guard
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// storage/transition-log.ts — SPEC-723: Hash-chained transition log (JSONL per project)
|
|
2
2
|
//
|
|
3
|
-
// Layout: planu/data/projects/<projectId>/transition-log.jsonl
|
|
3
|
+
// Layout: ~/.planu/data/projects/<projectId>/transition-log.jsonl
|
|
4
4
|
//
|
|
5
5
|
// Each entry is a JSON object on its own line. The `sha` field is a SHA-256 hash
|
|
6
6
|
// of the entry's canonical JSON (excluding `sha`), chained via `prevSha`.
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
8
|
import { appendFile, readFile, mkdir } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
+
import { projectDataDir } from './base-store.js';
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Path helper
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
function transitionLogPath(projectId) {
|
|
14
|
-
return join(
|
|
15
|
+
return join(projectDataDir(projectId), 'transition-log.jsonl');
|
|
15
16
|
}
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Node error type guard
|
|
@@ -7,8 +7,8 @@ export function registerAuditSpecsDriftTool(server) {
|
|
|
7
7
|
description: 'Run a two-tier drift audit over all done specs. ' +
|
|
8
8
|
'Tier-1: deterministic checks (missing files, broken refs). ' +
|
|
9
9
|
'Tier-2: LLM-based semantic drift for ambiguous cases (budget-capped). ' +
|
|
10
|
-
'Produces a prioritised P0/P1/P2 markdown report
|
|
11
|
-
'and appends a drift_review entry to
|
|
10
|
+
'Produces a prioritised P0/P1/P2 markdown report in external Planu project data ' +
|
|
11
|
+
'and appends a drift_review entry to external pending state. ' +
|
|
12
12
|
'Specs superseded by newer specs (per SPEC-746 graph) are excluded.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
projectPath: z.string().min(1).max(4096).describe('Absolute path to project root.'),
|
|
@@ -50,7 +50,7 @@ export function registerAuditSpecsDriftTool(server) {
|
|
|
50
50
|
``,
|
|
51
51
|
p0 + p1 + p2 === 0
|
|
52
52
|
? '✓ No drift detected.'
|
|
53
|
-
: `⚠ ${p0 + p1 + p2} total issues found. Review
|
|
53
|
+
: `⚠ ${p0 + p1 + p2} total issues found. Review external Planu pending state for the drift_review entry.`,
|
|
54
54
|
].join('\n'),
|
|
55
55
|
},
|
|
56
56
|
],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Spec, PostCreationSuggestion, ProjectKnowledge } from '../../types/index.js';
|
|
2
|
+
export declare function getAsyncAnalysisPath(projectPath: string, specId: string): string;
|
|
2
3
|
/** Auto-setup git branch (non-blocking). Returns branch info or undefined. */
|
|
3
4
|
export declare function setupGitBranch(projectId: string, specId: string): Promise<{
|
|
4
5
|
branch: string;
|
|
@@ -13,7 +14,7 @@ export declare function generatePostCreationSuggestions(projectPath: string, des
|
|
|
13
14
|
/**
|
|
14
15
|
* SPEC-781: Run heavy analysis (contradiction detection, complexity advice) in a
|
|
15
16
|
* fire-and-forget fashion after the spec has already been persisted synchronously.
|
|
16
|
-
* Writes results to
|
|
17
|
+
* Writes results to Planu's external project data dir and appends an
|
|
17
18
|
* autopilot-log entry on completion. Never throws — fully best-effort.
|
|
18
19
|
*/
|
|
19
20
|
export declare function runAutopilotAsync(specId: string, projectPath: string, _description: string): void;
|
|
@@ -10,9 +10,13 @@ import { emitAutopilotEvent } from '../../engine/autopilot/event-bus.js';
|
|
|
10
10
|
import { incrementSpecCount } from '../../engine/autopilot/state-updater.js';
|
|
11
11
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
12
12
|
import { join, dirname } from 'node:path';
|
|
13
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
13
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
14
14
|
import { appendAutopilotLogEntry } from '../../storage/autopilot-log-store.js';
|
|
15
15
|
const ASYNC_ANALYSIS_HOOK = 'create-spec-async-analysis';
|
|
16
|
+
export function getAsyncAnalysisPath(projectPath, specId) {
|
|
17
|
+
const projectId = hashProjectPath(projectPath);
|
|
18
|
+
return join(projectDataDir(projectId), 'analysis', `${specId}.json`);
|
|
19
|
+
}
|
|
16
20
|
/** Auto-setup git branch (non-blocking). Returns branch info or undefined. */
|
|
17
21
|
export async function setupGitBranch(projectId, specId) {
|
|
18
22
|
const result = await tryAutoSetupGit(projectId, specId);
|
|
@@ -161,7 +165,7 @@ export async function generatePostCreationSuggestions(projectPath, description,
|
|
|
161
165
|
/**
|
|
162
166
|
* SPEC-781: Run heavy analysis (contradiction detection, complexity advice) in a
|
|
163
167
|
* fire-and-forget fashion after the spec has already been persisted synchronously.
|
|
164
|
-
* Writes results to
|
|
168
|
+
* Writes results to Planu's external project data dir and appends an
|
|
165
169
|
* autopilot-log entry on completion. Never throws — fully best-effort.
|
|
166
170
|
*/
|
|
167
171
|
export function runAutopilotAsync(specId, projectPath, _description) {
|
|
@@ -169,20 +173,14 @@ export function runAutopilotAsync(specId, projectPath, _description) {
|
|
|
169
173
|
const start = Date.now();
|
|
170
174
|
void (async () => {
|
|
171
175
|
try {
|
|
172
|
-
const { glob } = await import('glob');
|
|
173
|
-
const specFiles = await glob(join(projectPath, 'planu/specs', `${specId}-*`, 'spec.md'));
|
|
174
|
-
const specDir = specFiles[0] !== undefined ? dirname(specFiles[0]) : null;
|
|
175
176
|
const analysisResult = {
|
|
176
177
|
specId,
|
|
177
178
|
completedAt: new Date().toISOString(),
|
|
178
179
|
pendingAnalysis: false,
|
|
179
180
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await mkdir(dirname(analysisPath), { recursive: true });
|
|
184
|
-
await writeFile(analysisPath, JSON.stringify(analysisResult, null, 2), 'utf-8');
|
|
185
|
-
}
|
|
181
|
+
const analysisPath = getAsyncAnalysisPath(projectPath, specId);
|
|
182
|
+
await mkdir(dirname(analysisPath), { recursive: true });
|
|
183
|
+
await writeFile(analysisPath, JSON.stringify(analysisResult, null, 2), 'utf-8');
|
|
186
184
|
await appendAutopilotLogEntry(projectId, {
|
|
187
185
|
specId,
|
|
188
186
|
hookName: ASYNC_ANALYSIS_HOOK,
|
|
@@ -26,7 +26,7 @@ export async function buildSpecContext(params) {
|
|
|
26
26
|
const knowledge = await knowledgeStore.getKnowledge(projectId);
|
|
27
27
|
const existingSpecs = await specStore.listSpecs(projectId);
|
|
28
28
|
// License: check active spec limit (exclude completed specs)
|
|
29
|
-
const activeSpecs = existingSpecs.filter((s) => s.status !== 'done');
|
|
29
|
+
const activeSpecs = existingSpecs.filter((s) => s.status !== 'done' && s.status !== 'discarded');
|
|
30
30
|
const tier = await getCurrentTier();
|
|
31
31
|
const specLimit = checkLimits(tier, activeSpecs.length, 'maxActiveSpecs');
|
|
32
32
|
/* v8 ignore start -- license limit requires paid tier test env */
|