@planu/cli 5.3.24 → 5.3.26
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 +15 -0
- package/README.md +1 -1
- package/dist/config/legacy-artifacts.json +26 -0
- package/dist/config/official-sdd-tools.d.ts +1 -1
- package/dist/config/official-sdd-tools.js +1 -0
- package/dist/config/registries/hosts/codex.json +2 -1
- 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/safety/assert-within-project.d.ts +2 -0
- package/dist/engine/safety/assert-within-project.js +14 -0
- package/dist/engine/scope-boundaries/contradiction-checker.js +9 -3
- package/dist/engine/spec-format/read-technical-section.js +3 -2
- package/dist/engine/spec-format/unified-spec-builder.js +27 -0
- package/dist/engine/spec-migrator/index.d.ts +0 -1
- package/dist/engine/spec-migrator/index.js +0 -1
- package/dist/engine/spec-migrator/legacy-classifier.d.ts +4 -0
- package/dist/engine/spec-migrator/legacy-classifier.js +70 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -9
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +4 -2
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +64 -53
- package/dist/storage/spec-store.js +6 -0
- package/dist/tools/export-spec.js +18 -15
- package/dist/tools/heal-spec-docs.js +15 -12
- package/dist/tools/init-project/git-setup.js +44 -40
- package/dist/tools/init-project/handler.js +2 -1
- package/dist/tools/init-project/migration-runner.d.ts +2 -1
- package/dist/tools/init-project/migration-runner.js +18 -5
- package/dist/tools/init-project/result-builder.js +9 -0
- package/dist/tools/list-specs.js +39 -0
- package/dist/tools/migrate-legacy-spec.d.ts +10 -0
- package/dist/tools/migrate-legacy-spec.js +133 -0
- package/dist/tools/register-spec-tools/analysis-tools.js +2 -0
- package/dist/tools/schema-generator-handler.js +3 -0
- package/dist/tools/schemas/output-schemas.d.ts +28 -0
- package/dist/tools/schemas/output-schemas.js +13 -0
- package/dist/tools/spec-export-handler.js +21 -8
- package/dist/tools/storage-bundle-handler.js +35 -4
- package/dist/types/project/planu-config.d.ts +3 -0
- package/dist/types/spec/core.d.ts +0 -7
- package/dist/types/spec-format.d.ts +17 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +3 -2
- package/dist/engine/spec-migrator/unified-migration.d.ts +0 -18
- package/dist/engine/spec-migrator/unified-migration.js +0 -105
|
@@ -422,7 +422,7 @@ export async function handleInitProject(params, server) {
|
|
|
422
422
|
.map((outcome) => `${outcome.name}: ${outcome.error ?? 'unknown error'}`);
|
|
423
423
|
const { configResult, migrations } = await configAndMigrations;
|
|
424
424
|
const { planuConfigPath, planuConfigGenerated, whatsNew } = configResult;
|
|
425
|
-
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = migrations;
|
|
425
|
+
const { discoveryResult, migrationResult, folderMigrationResult, legacyMigrationDirectives, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = migrations;
|
|
426
426
|
// Auto-install or queue skills (opt-out via planu.json or input param, SPEC-185)
|
|
427
427
|
const autoInstallFromConfig = autoInstallSkills ?? (await readAutoInstallFlag(planuConfigPath));
|
|
428
428
|
const { skillsAutoInstalled, skillsPendingInstall, skillsSkipped } = await orchestrateSkillInstalls(recommendedSkills, projectPath, autoInstallFromConfig);
|
|
@@ -578,6 +578,7 @@ export async function handleInitProject(params, server) {
|
|
|
578
578
|
migrationResult,
|
|
579
579
|
discoveryResult,
|
|
580
580
|
folderMigrationResult,
|
|
581
|
+
legacyMigrationDirectives,
|
|
581
582
|
criticalMigrationFailures,
|
|
582
583
|
nonCriticalMigrationWarnings,
|
|
583
584
|
migrationReportPath,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reconcilePortableSpecIndex } from './portable-index-reconciler.js';
|
|
2
|
-
import type { ProjectKnowledge, Spec } from '../../types/index.js';
|
|
2
|
+
import type { LegacyMigrationDirective, ProjectKnowledge, Spec } from '../../types/index.js';
|
|
3
3
|
export interface MigrationRunIssue {
|
|
4
4
|
phase: string;
|
|
5
5
|
severity: 'critical' | 'nonCritical';
|
|
@@ -25,6 +25,7 @@ export interface MigrationRunResult {
|
|
|
25
25
|
nonCriticalWarnings: MigrationRunIssue[];
|
|
26
26
|
migrationReportPath: string | null;
|
|
27
27
|
changedPaths?: string[];
|
|
28
|
+
legacyMigrationDirectives: LegacyMigrationDirective[];
|
|
28
29
|
}
|
|
29
30
|
interface SpecMigrationOptions {
|
|
30
31
|
repositoryMigration?: 'initialization' | 'planu-spec-format-v1';
|
|
@@ -3,9 +3,10 @@ import { specStore, knowledgeStore } from '../../storage/index.js';
|
|
|
3
3
|
import { projectDataDir, writeJson } from '../../storage/base-store.js';
|
|
4
4
|
import { filterLegacySpecs, migrateSpecs, filterUnprefixedSpecs, migrateSpecFolderNames, discoverAndFlattenSpecs, reconcileSpecPaths, migrateAllSpecsToLean, } from '../../engine/spec-migrator.js';
|
|
5
5
|
import { reconcilePortableSpecIndex } from './portable-index-reconciler.js';
|
|
6
|
+
import { scanLegacyArtifacts, buildMigrationDirective, } from '../../engine/spec-migrator/legacy-classifier.js';
|
|
6
7
|
import { createHash } from 'node:crypto';
|
|
7
8
|
import { lstat, readFile, readdir, readlink } from 'node:fs/promises';
|
|
8
|
-
import { join, relative } from 'node:path';
|
|
9
|
+
import { basename, join, relative } from 'node:path';
|
|
9
10
|
async function snapshotPlanuTree(projectPath) {
|
|
10
11
|
const root = join(projectPath, 'planu');
|
|
11
12
|
const snapshot = new Map();
|
|
@@ -155,13 +156,24 @@ export async function runSpecMigrations(projectPath, projectId, knowledge, optio
|
|
|
155
156
|
catch (err) {
|
|
156
157
|
criticalMigrationFailures.push(issueFromError('lean-spec-migration', 'critical', err));
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
const legacyMigrationDirectives = [];
|
|
159
160
|
try {
|
|
160
|
-
const {
|
|
161
|
-
await
|
|
161
|
+
const { glob } = await import('glob');
|
|
162
|
+
const specDirs = await glob(join(projectPath, 'planu/specs/SPEC-*'));
|
|
163
|
+
for (const specDir of specDirs) {
|
|
164
|
+
const specId = /^(SPEC-\d+)/i.exec(basename(specDir))?.[1]?.toUpperCase();
|
|
165
|
+
if (specId === undefined) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const hits = await scanLegacyArtifacts(specDir);
|
|
169
|
+
const directive = await buildMigrationDirective(specId, specDir, hits);
|
|
170
|
+
if (directive !== null) {
|
|
171
|
+
legacyMigrationDirectives.push(directive);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
162
174
|
}
|
|
163
175
|
catch (err) {
|
|
164
|
-
criticalMigrationFailures.push(issueFromError('
|
|
176
|
+
criticalMigrationFailures.push(issueFromError('legacy-artifact-scan', 'critical', err));
|
|
165
177
|
}
|
|
166
178
|
// SPEC-1017: strict managed planu/ cleanup after all legacy migrations.
|
|
167
179
|
try {
|
|
@@ -208,6 +220,7 @@ export async function runSpecMigrations(projectPath, projectId, knowledge, optio
|
|
|
208
220
|
nonCriticalWarnings,
|
|
209
221
|
migrationReportPath,
|
|
210
222
|
changedPaths: repositoryFilesChanged,
|
|
223
|
+
legacyMigrationDirectives,
|
|
211
224
|
};
|
|
212
225
|
}
|
|
213
226
|
/** Return all specs for the project (needed by caller for health check refs). */
|
|
@@ -149,6 +149,7 @@ export function buildInitProjectResult(input) {
|
|
|
149
149
|
specMigration: input.migrationResult,
|
|
150
150
|
specDiscovery: input.discoveryResult,
|
|
151
151
|
specFolderMigration: input.folderMigrationResult,
|
|
152
|
+
legacyMigrationDirectives: input.legacyMigrationDirectives,
|
|
152
153
|
criticalMigrationFailures: input.criticalMigrationFailures ?? [],
|
|
153
154
|
bestEffortWarnings: input.nonCriticalMigrationWarnings ?? [],
|
|
154
155
|
migrationReportPath: input.migrationReportPath ?? null,
|
|
@@ -197,6 +198,12 @@ export function buildInitProjectResult(input) {
|
|
|
197
198
|
: '';
|
|
198
199
|
const tokenTip = `\n\n💡 Token tip: Add CLAUDE_CODE_SUBAGENT_MODEL=haiku to your .env — makes all read-only subagents ` +
|
|
199
200
|
`(search, grep, file reads) use Haiku instead of your session model (4–18x cheaper for those tasks).`;
|
|
201
|
+
const legacyMigrationNextAction = input.legacyMigrationDirectives.length > 0
|
|
202
|
+
? `Legacy spec artifacts found in ${String(input.legacyMigrationDirectives.length)} spec folder(s): ` +
|
|
203
|
+
`${input.legacyMigrationDirectives.map((d) => d.specId).join(', ')}. For each, synthesize the ` +
|
|
204
|
+
`content-bearing legacyFiles (see legacyMigrationDirectives in structuredContent) into their ` +
|
|
205
|
+
`targetSection, then call migrate_legacy_spec with the merged spec.md body and filesToRetire.`
|
|
206
|
+
: null;
|
|
200
207
|
const narrative = ti('tools.init_project.success', { projectPath: result.projectPath }) +
|
|
201
208
|
`\n\nDetected: **${result.language}** / ${framework} — project ${action}. ` +
|
|
202
209
|
`Run \`create_spec\` to start planning your first feature.` +
|
|
@@ -204,6 +211,7 @@ export function buildInitProjectResult(input) {
|
|
|
204
211
|
? `\n\n⚠️ Critical spec migration warnings: ${String(input.criticalMigrationFailures?.length ?? 0)} issue(s). ` +
|
|
205
212
|
`See migrationReportPath in structuredContent${input.migrationReportPath ? ` (${input.migrationReportPath})` : ''}.`
|
|
206
213
|
: '') +
|
|
214
|
+
(legacyMigrationNextAction ? `\n\n📦 ${legacyMigrationNextAction}` : '') +
|
|
207
215
|
telemetryNote +
|
|
208
216
|
tokenTip;
|
|
209
217
|
// SPEC-491: Build interactiveQuestions for decisions that require user input
|
|
@@ -253,6 +261,7 @@ export function buildInitProjectResult(input) {
|
|
|
253
261
|
structuredContent: {
|
|
254
262
|
...result,
|
|
255
263
|
...(hasInteractive ? { interactiveQuestions } : {}),
|
|
264
|
+
...(legacyMigrationNextAction ? { nextAction: legacyMigrationNextAction } : {}),
|
|
256
265
|
},
|
|
257
266
|
};
|
|
258
267
|
}
|
package/dist/tools/list-specs.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// tools/list-specs.ts — List specs with filters
|
|
2
|
+
import { dirname } from 'node:path';
|
|
1
3
|
import { ti } from '../i18n/index.js';
|
|
2
4
|
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
3
5
|
import { buildListSpecsSummary } from '../engine/human-summary.js';
|
|
@@ -5,9 +7,34 @@ import { checkBundledVersionGap } from '../engine/version-detector/bundled-versi
|
|
|
5
7
|
import { formatRelativeDate } from '../engine/time/relative-date.js';
|
|
6
8
|
import { scanForAmbiguousCriteria } from '../engine/spec-migrator.js';
|
|
7
9
|
import { detectDrift, formatDriftMessage } from '../engine/spec-migrator/drift-detector.js';
|
|
10
|
+
import { buildMigrationDirective, scanLegacyArtifacts, } from '../engine/spec-migrator/legacy-classifier.js';
|
|
8
11
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
9
12
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
10
13
|
import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
|
|
14
|
+
async function scanLegacyMigrationDirectives(specs) {
|
|
15
|
+
const directives = [];
|
|
16
|
+
for (const spec of specs) {
|
|
17
|
+
if (!spec.specPath) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const specDir = dirname(spec.specPath);
|
|
21
|
+
const hits = await scanLegacyArtifacts(specDir);
|
|
22
|
+
const directive = await buildMigrationDirective(spec.id, specDir, hits);
|
|
23
|
+
if (directive !== null) {
|
|
24
|
+
directives.push(directive);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return directives;
|
|
28
|
+
}
|
|
29
|
+
function buildLegacyMigrationNextAction(directives) {
|
|
30
|
+
if (directives.length === 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return (`Legacy spec artifacts found in ${String(directives.length)} spec folder(s): ` +
|
|
34
|
+
`${directives.map((d) => d.specId).join(', ')}. For each, synthesize the content-bearing ` +
|
|
35
|
+
`legacyFiles into their targetSection, then call migrate_legacy_spec with the merged spec.md ` +
|
|
36
|
+
`body and filesToRetire.`);
|
|
37
|
+
}
|
|
11
38
|
/** Track which projects have been auto-discovered this session (once per project). */
|
|
12
39
|
const discoveredProjects = new Set();
|
|
13
40
|
const MAX_STRUCTURED_SPECS = 50;
|
|
@@ -264,6 +291,16 @@ export async function handleListSpecs(params) {
|
|
|
264
291
|
/* best-effort — never block list_specs */
|
|
265
292
|
}
|
|
266
293
|
}
|
|
294
|
+
let legacyMigrationDirectives = [];
|
|
295
|
+
if (detail === 'full') {
|
|
296
|
+
try {
|
|
297
|
+
legacyMigrationDirectives = await scanLegacyMigrationDirectives(allSpecs);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* best-effort — never block list_specs */
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const legacyMigrationNextAction = buildLegacyMigrationNextAction(legacyMigrationDirectives);
|
|
267
304
|
const structuredContent = {
|
|
268
305
|
projectId,
|
|
269
306
|
filters: {
|
|
@@ -291,6 +328,8 @@ export async function handleListSpecs(params) {
|
|
|
291
328
|
migrationIssues: migrationIssues ?? undefined,
|
|
292
329
|
interactiveQuestions: interactiveQuestions ?? undefined,
|
|
293
330
|
staleStatusWarnings: staleStatusWarnings.length > 0 ? staleStatusWarnings : undefined,
|
|
331
|
+
legacyMigrationDirectives: legacyMigrationDirectives.length > 0 ? legacyMigrationDirectives : undefined,
|
|
332
|
+
nextAction: legacyMigrationNextAction,
|
|
294
333
|
};
|
|
295
334
|
return {
|
|
296
335
|
content: [
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ToolResult } from '../types/index.js';
|
|
3
|
+
export declare function migrateLegacySpec(params: {
|
|
4
|
+
projectPath: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
mergedBody?: string;
|
|
7
|
+
filesToRetire: string[];
|
|
8
|
+
}): Promise<ToolResult>;
|
|
9
|
+
export declare function registerMigrateLegacySpecTool(server: McpServer): void;
|
|
10
|
+
//# sourceMappingURL=migrate-legacy-spec.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { cp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
import { atomicWriteFile } from '../engine/safety/atomic-write-file.js';
|
|
9
|
+
import { safeUnlink } from '../engine/spec-migrator/git-aware-fs.js';
|
|
10
|
+
import { safe } from './safe-handler.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const migrateLegacySpecInputSchema = {
|
|
13
|
+
projectPath: z.string().describe('Absolute path to the project root containing planu/specs'),
|
|
14
|
+
specId: z.string().min(1).describe('Spec identifier, e.g. "SPEC-1570"'),
|
|
15
|
+
mergedBody: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Full replacement content for spec.md, synthesized by the host LLM from the retired ' +
|
|
19
|
+
'content-bearing legacy files. Omit when only retiring transient files.'),
|
|
20
|
+
filesToRetire: z
|
|
21
|
+
.array(z.string().min(1))
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('Paths relative to the spec folder to back up, delete, and git-untrack (files or directories)'),
|
|
24
|
+
};
|
|
25
|
+
async function resolveSpecDir(specId, projectPath) {
|
|
26
|
+
const pattern = join(projectPath, 'planu', 'specs', `${specId}-*/`);
|
|
27
|
+
const dirs = await glob(pattern, { absolute: true });
|
|
28
|
+
return dirs[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
async function gitRmCachedRecursive(projectPath, relPath) {
|
|
31
|
+
if (!existsSync(join(projectPath, '.git'))) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await execFileAsync('git', ['rm', '--cached', '--quiet', '--ignore-unmatch', '-r', relPath], {
|
|
36
|
+
cwd: projectPath,
|
|
37
|
+
timeout: 5_000,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* best-effort */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function escapesSpecDir(specDir, relPath) {
|
|
45
|
+
const resolvedSpecDir = resolve(specDir);
|
|
46
|
+
const absolute = resolve(specDir, relPath);
|
|
47
|
+
const relativeToSpecDir = relative(resolvedSpecDir, absolute);
|
|
48
|
+
return (relativeToSpecDir === '..' ||
|
|
49
|
+
relativeToSpecDir.startsWith(`..${sep}`) ||
|
|
50
|
+
isAbsolute(relativeToSpecDir));
|
|
51
|
+
}
|
|
52
|
+
async function retireEntry(projectPath, specDir, relPath) {
|
|
53
|
+
const absolute = join(specDir, relPath);
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
const isDirectory = (await stat(absolute).catch(() => null))?.isDirectory() ?? false;
|
|
56
|
+
if (isDirectory) {
|
|
57
|
+
await cp(absolute, `${absolute}.bak.${timestamp}`, { recursive: true });
|
|
58
|
+
await gitRmCachedRecursive(projectPath, relative(projectPath, absolute));
|
|
59
|
+
await rm(absolute, { recursive: true, force: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const content = await readFile(absolute).catch(() => null);
|
|
63
|
+
if (content !== null) {
|
|
64
|
+
await writeFile(`${absolute}.bak.${timestamp}`, content);
|
|
65
|
+
}
|
|
66
|
+
await safeUnlink(projectPath, absolute);
|
|
67
|
+
await rm(absolute, { force: true });
|
|
68
|
+
}
|
|
69
|
+
export async function migrateLegacySpec(params) {
|
|
70
|
+
const { projectPath, specId, mergedBody, filesToRetire } = params;
|
|
71
|
+
const specDir = await resolveSpecDir(specId, projectPath);
|
|
72
|
+
if (specDir === null) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: `Spec folder for ${specId} not found under ${join(projectPath, 'planu', 'specs')}. Verify the specId and projectPath, then retry.`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (mergedBody !== undefined) {
|
|
84
|
+
await atomicWriteFile(join(specDir, 'spec.md'), mergedBody, {
|
|
85
|
+
forceEdit: {
|
|
86
|
+
reason: `SPEC-1570 migrate_legacy_spec is merging legacy content into spec.md for ${specId}.`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const retired = [];
|
|
91
|
+
const rejected = [];
|
|
92
|
+
for (const relPath of filesToRetire) {
|
|
93
|
+
if (escapesSpecDir(specDir, relPath)) {
|
|
94
|
+
rejected.push(relPath);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
await retireEntry(projectPath, specDir, relPath);
|
|
98
|
+
retired.push(relPath);
|
|
99
|
+
}
|
|
100
|
+
if (rejected.length > 0) {
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Migrated ${specId}: ${mergedBody !== undefined ? 'spec.md updated, ' : ''}retired ${String(retired.length)} legacy artifact(s) (${retired.join(', ')}). Rejected ${String(rejected.length)} filesToRetire entr${rejected.length === 1 ? 'y' : 'ies'} escaping the spec directory: ${rejected.join(', ')}. Pass paths relative to the spec folder only.`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
isError: true,
|
|
109
|
+
structuredContent: { specId, specMdUpdated: mergedBody !== undefined, retired, rejected },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: `Migrated ${specId}: ${mergedBody !== undefined ? 'spec.md updated, ' : ''}retired ${String(retired.length)} legacy artifact(s) (${retired.join(', ')}).`,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
isError: false,
|
|
120
|
+
structuredContent: { specId, specMdUpdated: mergedBody !== undefined, retired },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function registerMigrateLegacySpecTool(server) {
|
|
124
|
+
server.registerTool('migrate_legacy_spec', {
|
|
125
|
+
description: 'Persist a host-LLM-synthesized spec.md body and retire the legacy files it replaces: ' +
|
|
126
|
+
'each retired file/directory is backed up with a timestamped .bak copy, deleted, and ' +
|
|
127
|
+
'git-untracked. Call this after synthesizing mergedBody from the legacyFiles listed in a ' +
|
|
128
|
+
'migrate_legacy_spec directive (from init_project, list_specs, or heal_spec_docs).',
|
|
129
|
+
inputSchema: migrateLegacySpecInputSchema,
|
|
130
|
+
annotations: { readOnlyHint: false, destructiveHint: true, title: 'Migrate Legacy Spec' },
|
|
131
|
+
}, safe((args) => migrateLegacySpec(args)));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=migrate-legacy-spec.js.map
|
|
@@ -18,6 +18,7 @@ import { handleScanOrphanSpecRefs, ScanOrphanSpecRefsInputSchema, } from '../sca
|
|
|
18
18
|
import { registerGraphSpecsTool } from '../graph-specs.js';
|
|
19
19
|
import { registerAuditSpecsDriftTool } from '../audit-specs-drift.js';
|
|
20
20
|
import { handleSsrBackMigration } from '../ssr-back-migration.js';
|
|
21
|
+
import { registerMigrateLegacySpecTool } from '../migrate-legacy-spec.js';
|
|
21
22
|
/**
|
|
22
23
|
* Registers drift detection, analysis, and discovery tools (tools 11–19) on the MCP server.
|
|
23
24
|
*/
|
|
@@ -294,6 +295,7 @@ export function registerAnalysisTools(server) {
|
|
|
294
295
|
registerGraphSpecsTool(server);
|
|
295
296
|
// SPEC-744: audit_specs_drift
|
|
296
297
|
registerAuditSpecsDriftTool(server);
|
|
298
|
+
registerMigrateLegacySpecTool(server);
|
|
297
299
|
// SPEC-752: ssr_back_migration — fold legacy split-file specs into unified spec.md
|
|
298
300
|
server.registerTool('ssr_back_migration', {
|
|
299
301
|
description: 'Migrate legacy 2-file format specs into the unified spec.md format. ' +
|
|
@@ -5,6 +5,7 @@ import { dirname } from 'node:path';
|
|
|
5
5
|
import { getSpec } from '../storage/spec-store.js';
|
|
6
6
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
7
7
|
import { generateSchemas, generateApiClient } from '../engine/schema-generator.js';
|
|
8
|
+
import { assertPathWithinBase } from '../engine/safety/assert-within-project.js';
|
|
8
9
|
async function readSpecText(spec) {
|
|
9
10
|
const parts = [];
|
|
10
11
|
if (spec.description !== undefined) {
|
|
@@ -51,6 +52,7 @@ export async function handleGenerateValidationSchema(input) {
|
|
|
51
52
|
}
|
|
52
53
|
// Write files if outputPath is provided
|
|
53
54
|
if (outputPath !== undefined) {
|
|
55
|
+
assertPathWithinBase(projectPath, outputPath);
|
|
54
56
|
for (const schema of result.schemas) {
|
|
55
57
|
const ext = outputFormat === technologyValue('technology-typescript-969545') ? '.ts' : '.js';
|
|
56
58
|
const libSuffix = library === 'all' ? `.${schema.library}` : '';
|
|
@@ -105,6 +107,7 @@ export async function handleGenerateApiClient(input) {
|
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
if (outputPath !== undefined) {
|
|
110
|
+
assertPathWithinBase(projectPath, outputPath);
|
|
108
111
|
await writeOutputFile(outputPath, result.code);
|
|
109
112
|
}
|
|
110
113
|
return {
|
|
@@ -76,6 +76,20 @@ export declare const ListSpecsOutputSchema: {
|
|
|
76
76
|
commitSha: z.ZodString;
|
|
77
77
|
suggestedAction: z.ZodLiteral<"auto-fix-available">;
|
|
78
78
|
}, z.core.$strip>>>;
|
|
79
|
+
legacyMigrationDirectives: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
80
|
+
specId: z.ZodString;
|
|
81
|
+
legacyFiles: z.ZodArray<z.ZodObject<{
|
|
82
|
+
path: z.ZodString;
|
|
83
|
+
classification: z.ZodEnum<{
|
|
84
|
+
"content-bearing": "content-bearing";
|
|
85
|
+
transient: "transient";
|
|
86
|
+
}>;
|
|
87
|
+
targetSection: z.ZodOptional<z.ZodString>;
|
|
88
|
+
inlinedContent: z.ZodOptional<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>>;
|
|
90
|
+
directive: z.ZodLiteral<"migrate_legacy_spec">;
|
|
91
|
+
}, z.core.$strip>>>;
|
|
92
|
+
nextAction: z.ZodOptional<z.ZodString>;
|
|
79
93
|
};
|
|
80
94
|
/** Canonical DTO inferred from the exact MCP output contract. */
|
|
81
95
|
export declare const ListSpecsOutputObjectSchema: z.ZodObject<{
|
|
@@ -155,6 +169,20 @@ export declare const ListSpecsOutputObjectSchema: z.ZodObject<{
|
|
|
155
169
|
commitSha: z.ZodString;
|
|
156
170
|
suggestedAction: z.ZodLiteral<"auto-fix-available">;
|
|
157
171
|
}, z.core.$strip>>>;
|
|
172
|
+
legacyMigrationDirectives: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
173
|
+
specId: z.ZodString;
|
|
174
|
+
legacyFiles: z.ZodArray<z.ZodObject<{
|
|
175
|
+
path: z.ZodString;
|
|
176
|
+
classification: z.ZodEnum<{
|
|
177
|
+
"content-bearing": "content-bearing";
|
|
178
|
+
transient: "transient";
|
|
179
|
+
}>;
|
|
180
|
+
targetSection: z.ZodOptional<z.ZodString>;
|
|
181
|
+
inlinedContent: z.ZodOptional<z.ZodString>;
|
|
182
|
+
}, z.core.$strip>>;
|
|
183
|
+
directive: z.ZodLiteral<"migrate_legacy_spec">;
|
|
184
|
+
}, z.core.$strip>>>;
|
|
185
|
+
nextAction: z.ZodOptional<z.ZodString>;
|
|
158
186
|
}, z.core.$strip>;
|
|
159
187
|
export type ListSpecsStructuredContent = z.infer<typeof ListSpecsOutputObjectSchema>;
|
|
160
188
|
export declare const EstimateOutputSchema: {
|
|
@@ -92,6 +92,19 @@ export const ListSpecsOutputSchema = {
|
|
|
92
92
|
suggestedAction: z.literal('auto-fix-available'),
|
|
93
93
|
}))
|
|
94
94
|
.optional(),
|
|
95
|
+
legacyMigrationDirectives: z
|
|
96
|
+
.array(z.object({
|
|
97
|
+
specId: z.string(),
|
|
98
|
+
legacyFiles: z.array(z.object({
|
|
99
|
+
path: z.string(),
|
|
100
|
+
classification: z.enum(['content-bearing', 'transient']),
|
|
101
|
+
targetSection: z.string().optional(),
|
|
102
|
+
inlinedContent: z.string().optional(),
|
|
103
|
+
})),
|
|
104
|
+
directive: z.literal('migrate_legacy_spec'),
|
|
105
|
+
}))
|
|
106
|
+
.optional(),
|
|
107
|
+
nextAction: z.string().optional(),
|
|
95
108
|
};
|
|
96
109
|
/** Canonical DTO inferred from the exact MCP output contract. */
|
|
97
110
|
export const ListSpecsOutputObjectSchema = z.object(ListSpecsOutputSchema);
|
|
@@ -3,10 +3,8 @@ import { writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { listSpecs } from '../storage/spec-store.js';
|
|
4
4
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
5
5
|
import { filterSpecs, specsToCsv, specsToMarkdownTable, ALL_COLUMNS, } from '../engine/spec-exporter.js';
|
|
6
|
+
import { assertPathWithinBase } from '../engine/safety/assert-within-project.js';
|
|
6
7
|
import { formatSuccess, formatError, toolResult } from './response-helpers.js';
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Shared logic
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
8
|
async function buildExport(input, format) {
|
|
11
9
|
const projectId = hashProjectPath(input.projectPath);
|
|
12
10
|
const allSpecs = await listSpecs(projectId);
|
|
@@ -15,8 +13,15 @@ async function buildExport(input, format) {
|
|
|
15
13
|
const content = format === 'csv' ? specsToCsv(filtered, columns) : specsToMarkdownTable(filtered, columns);
|
|
16
14
|
let filePath;
|
|
17
15
|
if (input.outputPath) {
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
let resolvedOutputPath;
|
|
17
|
+
try {
|
|
18
|
+
resolvedOutputPath = assertPathWithinBase(input.projectPath, input.outputPath);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
22
|
+
}
|
|
23
|
+
await writeFile(resolvedOutputPath, content, 'utf8');
|
|
24
|
+
filePath = resolvedOutputPath;
|
|
20
25
|
}
|
|
21
26
|
const result = {
|
|
22
27
|
rowCount: filtered.length,
|
|
@@ -25,7 +30,7 @@ async function buildExport(input, format) {
|
|
|
25
30
|
content,
|
|
26
31
|
filePath,
|
|
27
32
|
};
|
|
28
|
-
return { text: content, result };
|
|
33
|
+
return { ok: true, text: content, result };
|
|
29
34
|
}
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
31
36
|
// CSV handler
|
|
@@ -34,7 +39,11 @@ export async function handleExportSpecsCsv(args) {
|
|
|
34
39
|
if (!args.projectPath || args.projectPath.trim().length === 0) {
|
|
35
40
|
return toolResult(formatError('projectPath is required', 'Provide the absolute path to your project root.'), undefined, true);
|
|
36
41
|
}
|
|
37
|
-
const
|
|
42
|
+
const built = await buildExport(args, 'csv');
|
|
43
|
+
if (!built.ok) {
|
|
44
|
+
return toolResult(formatError(`Invalid outputPath: ${built.message}`, 'Choose a path within projectPath.'), undefined, true);
|
|
45
|
+
}
|
|
46
|
+
const { text, result } = built;
|
|
38
47
|
const summary = [
|
|
39
48
|
`Exported ${String(result.rowCount)} specs to CSV`,
|
|
40
49
|
`Columns: ${result.columns.join(', ')}`,
|
|
@@ -51,7 +60,11 @@ export async function handleExportSpecsMarkdown(args) {
|
|
|
51
60
|
if (!args.projectPath || args.projectPath.trim().length === 0) {
|
|
52
61
|
return toolResult(formatError('projectPath is required', 'Provide the absolute path to your project root.'), undefined, true);
|
|
53
62
|
}
|
|
54
|
-
const
|
|
63
|
+
const built = await buildExport(args, 'markdown');
|
|
64
|
+
if (!built.ok) {
|
|
65
|
+
return toolResult(formatError(`Invalid outputPath: ${built.message}`, 'Choose a path within projectPath.'), undefined, true);
|
|
66
|
+
}
|
|
67
|
+
const { text, result } = built;
|
|
55
68
|
const summary = [
|
|
56
69
|
`Exported ${String(result.rowCount)} specs to Markdown table`,
|
|
57
70
|
`Columns: ${result.columns.join(', ')}`,
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { exportStorageBundle, importStorageBundle } from '../storage/storage-bundle.js';
|
|
2
|
+
import { assertPathWithinBase } from '../engine/safety/assert-within-project.js';
|
|
2
3
|
export async function handleStorageExport(input) {
|
|
3
|
-
|
|
4
|
+
let resolvedOutputPath;
|
|
5
|
+
try {
|
|
6
|
+
resolvedOutputPath = assertPathWithinBase(process.cwd(), input.outputPath);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
return {
|
|
10
|
+
content: [
|
|
11
|
+
{
|
|
12
|
+
type: 'text',
|
|
13
|
+
text: `Invalid outputPath: ${error instanceof Error ? error.message : String(error)}`,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
isError: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const bundle = await exportStorageBundle(input.projectId, resolvedOutputPath);
|
|
4
20
|
const payload = {
|
|
5
21
|
schemaVersion: 1,
|
|
6
22
|
exported: true,
|
|
7
23
|
projectId: bundle.projectId,
|
|
8
|
-
outputPath:
|
|
24
|
+
outputPath: resolvedOutputPath,
|
|
9
25
|
checksum: bundle.checksum,
|
|
10
26
|
};
|
|
11
27
|
return {
|
|
@@ -14,12 +30,27 @@ export async function handleStorageExport(input) {
|
|
|
14
30
|
};
|
|
15
31
|
}
|
|
16
32
|
export async function handleStorageImport(input) {
|
|
17
|
-
|
|
33
|
+
let resolvedInputPath;
|
|
34
|
+
try {
|
|
35
|
+
resolvedInputPath = assertPathWithinBase(process.cwd(), input.inputPath);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: `Invalid inputPath: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const bundle = await importStorageBundle(resolvedInputPath);
|
|
18
49
|
const payload = {
|
|
19
50
|
schemaVersion: 1,
|
|
20
51
|
imported: true,
|
|
21
52
|
projectId: bundle.projectId,
|
|
22
|
-
inputPath:
|
|
53
|
+
inputPath: resolvedInputPath,
|
|
23
54
|
};
|
|
24
55
|
return {
|
|
25
56
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
@@ -8,6 +8,7 @@ import type { OrchestratorInfo, ComposeMultiplatformInfo } from './platform-spec
|
|
|
8
8
|
import type { ProjectKnowledge, ProjectCompleteness } from './core.js';
|
|
9
9
|
import type { ProjectHealthReport } from './health-checker.js';
|
|
10
10
|
import type { DiscoveredRegistrySkill } from '../skill-bootstrap.js';
|
|
11
|
+
import type { LegacyMigrationDirective } from '../spec-format.js';
|
|
11
12
|
export interface PlanuConfig {
|
|
12
13
|
version: string;
|
|
13
14
|
specLocation: string;
|
|
@@ -79,6 +80,8 @@ export interface InitProjectResultInput {
|
|
|
79
80
|
migratedCount: number;
|
|
80
81
|
errors: string[];
|
|
81
82
|
} | null;
|
|
83
|
+
/** SPEC-1570: content-bearing/transient legacy artifacts pending host-LLM migration. */
|
|
84
|
+
legacyMigrationDirectives: LegacyMigrationDirective[];
|
|
82
85
|
criticalMigrationFailures?: {
|
|
83
86
|
phase: string;
|
|
84
87
|
severity: 'critical' | 'nonCritical';
|
|
@@ -179,13 +179,6 @@ export interface ModelBudget {
|
|
|
179
179
|
model: 'haiku' | 'sonnet' | 'opus';
|
|
180
180
|
budget: 800 | 2000 | 4000;
|
|
181
181
|
}
|
|
182
|
-
/** SPEC-630: Result of merging technical.md into spec.md. */
|
|
183
|
-
export interface UnifiedMigrationResult {
|
|
184
|
-
specDir: string;
|
|
185
|
-
merged: boolean;
|
|
186
|
-
reason: 'ok' | 'already-unified' | 'no-technical-md' | 'error';
|
|
187
|
-
error?: string;
|
|
188
|
-
}
|
|
189
182
|
export interface SpecSummary {
|
|
190
183
|
specId: string;
|
|
191
184
|
title: string;
|
|
@@ -91,6 +91,23 @@ export interface StrictPlanuValidationOptions {
|
|
|
91
91
|
specPath?: string;
|
|
92
92
|
includeRoot?: boolean;
|
|
93
93
|
}
|
|
94
|
+
export interface LegacyArtifactRule {
|
|
95
|
+
id: string;
|
|
96
|
+
pattern: string;
|
|
97
|
+
classification: 'content-bearing' | 'transient';
|
|
98
|
+
targetSection?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface LegacyArtifactHit {
|
|
101
|
+
path: string;
|
|
102
|
+
classification: 'content-bearing' | 'transient';
|
|
103
|
+
targetSection?: string;
|
|
104
|
+
inlinedContent?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface LegacyMigrationDirective {
|
|
107
|
+
specId: string;
|
|
108
|
+
legacyFiles: LegacyArtifactHit[];
|
|
109
|
+
directive: 'migrate_legacy_spec';
|
|
110
|
+
}
|
|
94
111
|
export interface UpdateStatusBatchInput {
|
|
95
112
|
specIds: string[];
|
|
96
113
|
status: Exclude<SpecStatus, 'done'>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.26",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"packageName": "@planu/core"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@planu/core-darwin-arm64": "5.3.
|
|
40
|
-
"@planu/core-darwin-x64": "5.3.
|
|
41
|
-
"@planu/core-linux-arm64-gnu": "5.3.
|
|
42
|
-
"@planu/core-linux-arm64-musl": "5.3.
|
|
43
|
-
"@planu/core-linux-x64-gnu": "5.3.
|
|
44
|
-
"@planu/core-linux-x64-musl": "5.3.
|
|
45
|
-
"@planu/core-win32-arm64-msvc": "5.3.
|
|
46
|
-
"@planu/core-win32-x64-msvc": "5.3.
|
|
39
|
+
"@planu/core-darwin-arm64": "5.3.26",
|
|
40
|
+
"@planu/core-darwin-x64": "5.3.26",
|
|
41
|
+
"@planu/core-linux-arm64-gnu": "5.3.26",
|
|
42
|
+
"@planu/core-linux-arm64-musl": "5.3.26",
|
|
43
|
+
"@planu/core-linux-x64-gnu": "5.3.26",
|
|
44
|
+
"@planu/core-linux-x64-musl": "5.3.26",
|
|
45
|
+
"@planu/core-win32-arm64-msvc": "5.3.26",
|
|
46
|
+
"@planu/core-win32-x64-msvc": "5.3.26"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=24.0.0"
|