@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cli/commands/spec.js +20 -1
  3. package/dist/cli/commands/status.js +18 -1
  4. package/dist/config/license-plans.json +1 -0
  5. package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
  6. package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
  7. package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
  8. package/dist/engine/autopilot/action-registry.js +5 -14
  9. package/dist/engine/autopilot/state-updater.js +13 -10
  10. package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
  11. package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
  12. package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
  13. package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
  14. package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
  15. package/dist/engine/git/planu-autocommit.d.ts +1 -0
  16. package/dist/engine/git/planu-autocommit.js +6 -0
  17. package/dist/engine/git-hook-injector.js +3 -3
  18. package/dist/engine/handoff-artifacts/io.js +3 -2
  19. package/dist/engine/handoff-packager.js +2 -1
  20. package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
  21. package/dist/engine/hooks/full-spectrum-generator.js +5 -3
  22. package/dist/engine/release/postmortem-generator.d.ts +1 -1
  23. package/dist/engine/release/postmortem-generator.js +3 -2
  24. package/dist/engine/safety/cross-process-lock.js +2 -2
  25. package/dist/engine/session/checkpoint-writer.js +0 -1
  26. package/dist/engine/session-context-generator.js +4 -1
  27. package/dist/engine/spec-audit/index.js +2 -2
  28. package/dist/engine/spec-audit/report-writer.d.ts +1 -1
  29. package/dist/engine/spec-audit/report-writer.js +5 -4
  30. package/dist/engine/spec-migrator/index.d.ts +1 -0
  31. package/dist/engine/spec-migrator/index.js +1 -0
  32. package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
  33. package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
  34. package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
  35. package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
  36. package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
  37. package/dist/engine/spec-summary-html.d.ts +5 -5
  38. package/dist/engine/spec-summary-html.js +7 -32
  39. package/dist/storage/gaps-log.js +4 -4
  40. package/dist/storage/transition-log.js +3 -2
  41. package/dist/tools/audit-specs-drift.js +3 -3
  42. package/dist/tools/create-spec/post-creation.d.ts +2 -1
  43. package/dist/tools/create-spec/post-creation.js +9 -11
  44. package/dist/tools/create-spec/spec-builder.js +1 -1
  45. package/dist/tools/create-spec.js +42 -18
  46. package/dist/tools/flag-spec-gap.d.ts +1 -1
  47. package/dist/tools/flag-spec-gap.js +1 -1
  48. package/dist/tools/generate-dashboard.js +3 -3
  49. package/dist/tools/housekeeping-sweep.js +16 -0
  50. package/dist/tools/init-project/git-setup.js +11 -2
  51. package/dist/tools/init-project/handler.js +1 -27
  52. package/dist/tools/init-project/migration-runner.js +8 -0
  53. package/dist/tools/license-gate.d.ts +1 -0
  54. package/dist/tools/license-gate.js +5 -1
  55. package/dist/tools/list-specs.js +13 -0
  56. package/dist/tools/register-sdd-tools.d.ts +1 -1
  57. package/dist/tools/register-sdd-tools.js +1 -0
  58. package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
  59. package/dist/tools/spec-lock-handler.js +1 -1
  60. package/dist/tools/sync-spec-state-handler.js +7 -0
  61. package/dist/tools/tool-registry/group-misc.js +4 -4
  62. package/dist/tools/update-status/batch.d.ts +3 -0
  63. package/dist/tools/update-status/batch.js +96 -0
  64. package/dist/tools/update-status/dod-gates.js +1 -1
  65. package/dist/tools/update-status/file-sync.js +3 -1
  66. package/dist/tools/update-status/index.js +15 -2
  67. package/dist/tools/update-status-actions.js +2 -6
  68. package/dist/tools/validate.js +27 -0
  69. package/dist/tools/workspace-dashboard-handler.js +6 -9
  70. package/dist/types/git.d.ts +1 -1
  71. package/dist/types/spec-format.d.ts +26 -0
  72. 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-464: Clean non-canonical files from planu/
2
- // Canonical structure: specs/ + status.json + conventions.json + index.html + roadmap.html
3
- import { readdir, stat } from 'node:fs/promises';
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 result = {
35
- deletedRootFiles: [],
36
- deletedSpecFiles: [],
37
- totalDeleted: 0,
38
- };
39
- // 1. Clean planu/ root
40
- let entries;
41
- try {
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
- catch {
64
- // File disappeared between readdir and stat — ignore
15
+ else {
16
+ deletedRootFiles.push(normalized);
65
17
  }
66
18
  }
67
- // 2. Clean per-spec directories
68
- const specsDir = join(planuDir, 'specs');
69
- let specDirs;
70
- try {
71
- specDirs = await readdir(specsDir);
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
- * Generate planu/index.html with specs overview + regenerate per-spec reports.
4
- * Merges store specs with filesystem specs to ensure all specs appear.
5
- * Designed to be called from list_specs, update_status, create_spec.
6
- * Best-effort: never throws, never blocks the caller.
7
- * Skips writing the file when spec data has not changed (hash comparison).
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 { writeFile, mkdir, readFile, readdir } from 'node:fs/promises';
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
- * Generate planu/index.html with specs overview + regenerate per-spec reports.
91
- * Merges store specs with filesystem specs to ensure all specs appear.
92
- * Designed to be called from list_specs, update_status, create_spec.
93
- * Best-effort: never throws, never blocks the caller.
94
- * Skips writing the file when spec data has not changed (hash comparison).
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
- const allSpecs = mergeSpecs(specs, fsSpecs);
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 */
@@ -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(_projectId) {
15
- // Per-spec: stored in planu/research/gaps.jsonl (project-level, not per-spec-id)
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('planu', 'data', 'projects', projectId, 'transition-log.jsonl');
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 at planu/research/audit-full-<ts>.md ' +
11
- 'and appends a drift_review entry to planu/pending.json. ' +
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 \`planu/pending.json\` for the drift_review entry.`,
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 `planu/specs/SPEC-XXX/.analysis.json` and appends an
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 `planu/specs/SPEC-XXX/.analysis.json` and appends an
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
- // Write .analysis.json to spec directory
181
- if (specDir !== null) {
182
- const analysisPath = join(specDir, '.analysis.json');
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 */