@planu/cli 5.7.3 → 5.7.5
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 +20 -0
- package/dist/.planu-build.json +1 -1
- package/dist/config/registries/legacy-guidance.json +26 -0
- package/dist/engine/spec-migrator/drift-detector.js +2 -2
- package/dist/engine/spec-migrator/fold-technical.js +60 -19
- package/dist/engine/spec-migrator/placeholder-technical.d.ts +2 -0
- package/dist/engine/spec-migrator/placeholder-technical.js +18 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +6 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +34 -14
- package/dist/tools/heal-spec-docs.js +1 -13
- package/dist/tools/init-project/handler.js +18 -45
- package/dist/tools/init-project/legacy-guidance-scanner.d.ts +12 -0
- package/dist/tools/init-project/legacy-guidance-scanner.js +176 -0
- package/dist/tools/init-project/portable-index-reconciler.d.ts +20 -1
- package/dist/tools/init-project/portable-index-reconciler.js +61 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/legacy-guidance.d.ts +20 -0
- package/dist/types/legacy-guidance.js +3 -0
- package/dist/types/spec-format.d.ts +1 -0
- package/dist/types/ssr-migration.d.ts +1 -1
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [5.7.5] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(init-project): address implementation review blockers for SPEC-1716
|
|
5
|
+
- fix(init-project): neutralize stale legacy guidance and fold divergent technical.md residuals
|
|
6
|
+
|
|
7
|
+
### Chores
|
|
8
|
+
- chore(planu): discard 29 triaged drafts and record SPEC-1716 cycle state
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [5.7.4] - 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(cleanup): run runtime hygiene in report mode and exempt .runtime from offender scans
|
|
15
|
+
- fix(cleanup): ignore planu/.runtime and untrack leaked runtime files in strict cleanup
|
|
16
|
+
|
|
17
|
+
### Chores
|
|
18
|
+
- chore(runtime): untrack leaked planu runtime state and pin repo-local ignores
|
|
19
|
+
|
|
20
|
+
|
|
1
21
|
## [5.7.3] - 2026-08-31
|
|
2
22
|
|
|
3
23
|
### Bug Fixes
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"858730d3bc7c59966cbff5c34cae03fa636843eb"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hostSurfaceFiles": [
|
|
3
|
+
"CLAUDE.md",
|
|
4
|
+
"AGENTS.md",
|
|
5
|
+
".cursorrules",
|
|
6
|
+
".windsurfrules",
|
|
7
|
+
".github/copilot-instructions.md",
|
|
8
|
+
".clinerules",
|
|
9
|
+
".cursor/rules/planu.mdc",
|
|
10
|
+
".agents/skills/planu-sdd.md"
|
|
11
|
+
],
|
|
12
|
+
"hostSurfaceDirectories": [{ "dir": ".claude/rules", "extension": ".md" }],
|
|
13
|
+
"legacyFilenameAllowlist": [
|
|
14
|
+
".claude/rules/sdd-planu.md",
|
|
15
|
+
".claude/rules/planu-workflow.md",
|
|
16
|
+
".cursor/rules/planu.mdc",
|
|
17
|
+
".agents/skills/planu-sdd.md"
|
|
18
|
+
],
|
|
19
|
+
"managedByMarkers": ["Auto-generated by Planu", "Auto-generated by `init_project`", "managed-by: planu"],
|
|
20
|
+
"contradictoryPhrases": [
|
|
21
|
+
"spec.md + technical.md + progress.md",
|
|
22
|
+
"technical.md — architecture",
|
|
23
|
+
"keep technical.md and progress.md"
|
|
24
|
+
],
|
|
25
|
+
"replacementGuidance": "Specs live in `planu/specs/SPEC-XXX-slug/spec.md`. Technical planning, file ownership, and progress live inline in `## Technical`, `## Files`, and `## Progress` sections. Do not create standalone technical.md or progress.md files."
|
|
26
|
+
}
|
|
@@ -3,7 +3,7 @@ import { readdir, stat, access, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isOldFormat } from './lean-migration.js';
|
|
5
5
|
import { fastScanSpecsAsync } from '../core-bridge.js';
|
|
6
|
-
import { isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalSpecFile, } from './planu-canonical-policy.js';
|
|
6
|
+
import { isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalSpecFile, isRuntimeHomeRootDir, } from './planu-canonical-policy.js';
|
|
7
7
|
async function scanPlanuRoot(projectPath) {
|
|
8
8
|
const rootFiles = [];
|
|
9
9
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -14,7 +14,7 @@ async function scanPlanuRoot(projectPath) {
|
|
|
14
14
|
try {
|
|
15
15
|
const info = await stat(fullPath);
|
|
16
16
|
if (info.isDirectory()) {
|
|
17
|
-
if (!isCanonicalPlanuRootDir(entry)) {
|
|
17
|
+
if (!isCanonicalPlanuRootDir(entry) && !isRuntimeHomeRootDir(entry)) {
|
|
18
18
|
rootFiles.push(entry);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
// engine/spec-migrator/fold-technical.ts — SPEC-752
|
|
2
2
|
// Folds legacy technical.md body into spec.md's ## Technical section, then deletes technical.md.
|
|
3
3
|
// Uses the same pattern as unified-migration.ts (SPEC-630).
|
|
4
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
7
7
|
import { safeUnlink } from './git-aware-fs.js';
|
|
8
|
+
import { isPlaceholderTechnical } from './placeholder-technical.js';
|
|
9
|
+
const FOLDED_HEADING = '### Folded from legacy technical.md';
|
|
10
|
+
/**
|
|
11
|
+
* Insert `techBody` under a `### Folded from legacy technical.md` subsection,
|
|
12
|
+
* placed right after the existing `## Technical` section (before the next
|
|
13
|
+
* top-level `## ` heading, or at the end of the document when none follows).
|
|
14
|
+
*/
|
|
15
|
+
function foldIntoTechnicalSection(specContent, techBody) {
|
|
16
|
+
const headingMatch = /\n## Technical(\n|$)/.exec(specContent);
|
|
17
|
+
const insertion = `\n${FOLDED_HEADING}\n\n${techBody}\n`;
|
|
18
|
+
if (!headingMatch) {
|
|
19
|
+
return `${specContent.trimEnd()}\n\n## Technical\n${insertion}`;
|
|
20
|
+
}
|
|
21
|
+
const afterHeading = headingMatch.index + headingMatch[0].length;
|
|
22
|
+
const nextHeadingMatch = /\n## /.exec(specContent.slice(afterHeading));
|
|
23
|
+
if (!nextHeadingMatch) {
|
|
24
|
+
return `${specContent.trimEnd()}\n${insertion}`;
|
|
25
|
+
}
|
|
26
|
+
const insertAt = afterHeading + nextHeadingMatch.index;
|
|
27
|
+
return specContent.slice(0, insertAt) + insertion + specContent.slice(insertAt);
|
|
28
|
+
}
|
|
29
|
+
async function removeResidualTechnicalMd(techPath, projectPath) {
|
|
30
|
+
if (projectPath) {
|
|
31
|
+
// safeUnlink is already idempotent for a missing file (git rm --ignore-unmatch,
|
|
32
|
+
// unlink swallows ENOENT) and rethrows every other failure.
|
|
33
|
+
await safeUnlink(projectPath, techPath);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await unlink(techPath);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err.code !== 'ENOENT') {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
8
45
|
/**
|
|
9
46
|
* Fold technical.md content into spec.md under a `## Technical` section.
|
|
10
47
|
* Idempotent: returns already_unified if spec.md already has `## Technical`.
|
|
@@ -20,22 +57,32 @@ export async function foldTechnicalIntoSpec(specDir, projectPath) {
|
|
|
20
57
|
const specContent = await readFile(specPath, 'utf-8');
|
|
21
58
|
// Idempotency check: already has ## Technical section.
|
|
22
59
|
// SPEC-1008: also delete any residual technical.md so the spec dir ends
|
|
23
|
-
// up in the canonical single-file layout.
|
|
24
|
-
//
|
|
60
|
+
// up in the canonical single-file layout.
|
|
61
|
+
// SPEC-1716: before deleting, read the residual — divergent content is
|
|
62
|
+
// folded under a "Folded from legacy technical.md" subsection instead of
|
|
63
|
+
// silently dropped.
|
|
25
64
|
if (/\n## Technical(\n|$)/.test(specContent)) {
|
|
65
|
+
let residualContent;
|
|
26
66
|
try {
|
|
27
|
-
|
|
28
|
-
await safeUnlink(projectPath, techPath);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
const { unlink } = await import('node:fs/promises');
|
|
32
|
-
await unlink(techPath);
|
|
33
|
-
}
|
|
67
|
+
residualContent = await readFile(techPath, 'utf-8');
|
|
34
68
|
}
|
|
35
69
|
catch {
|
|
36
|
-
|
|
70
|
+
return { ok: true, reason: 'already_unified' };
|
|
37
71
|
}
|
|
38
|
-
|
|
72
|
+
const residualBody = residualContent.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
73
|
+
const alreadyContained = residualBody.length === 0 ||
|
|
74
|
+
specContent.includes(residualBody) ||
|
|
75
|
+
isPlaceholderTechnical(residualBody);
|
|
76
|
+
if (alreadyContained) {
|
|
77
|
+
await removeResidualTechnicalMd(techPath, projectPath);
|
|
78
|
+
return { ok: true, reason: 'already_unified' };
|
|
79
|
+
}
|
|
80
|
+
const unified = foldIntoTechnicalSection(specContent, residualBody);
|
|
81
|
+
const before = Buffer.byteLength(specContent, 'utf-8');
|
|
82
|
+
const after = Buffer.byteLength(unified, 'utf-8');
|
|
83
|
+
await atomicWriteFile(specPath, unified);
|
|
84
|
+
await removeResidualTechnicalMd(techPath, projectPath);
|
|
85
|
+
return { ok: true, reason: 'folded_residual', byteDelta: after - before };
|
|
39
86
|
}
|
|
40
87
|
// Try reading technical.md
|
|
41
88
|
let techContent;
|
|
@@ -51,13 +98,7 @@ export async function foldTechnicalIntoSpec(specDir, projectPath) {
|
|
|
51
98
|
const before = Buffer.byteLength(specContent, 'utf-8');
|
|
52
99
|
const after = Buffer.byteLength(unified, 'utf-8');
|
|
53
100
|
await atomicWriteFile(specPath, unified);
|
|
54
|
-
|
|
55
|
-
await safeUnlink(projectPath, techPath);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
const { unlink } = await import('node:fs/promises');
|
|
59
|
-
await unlink(techPath);
|
|
60
|
-
}
|
|
101
|
+
await removeResidualTechnicalMd(techPath, projectPath);
|
|
61
102
|
return { ok: true, reason: 'ok', byteDelta: after - before };
|
|
62
103
|
}
|
|
63
104
|
catch (err) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// engine/spec-migrator/placeholder-technical.ts — SPEC-1716
|
|
2
|
+
// Shared placeholder-stub detector for technical.md bodies. Lives in engine/ so
|
|
3
|
+
// both engine (fold-technical.ts) and tools (heal-spec-docs.ts) can import it —
|
|
4
|
+
// engine/ must never import from tools/.
|
|
5
|
+
export function isPlaceholderTechnical(content) {
|
|
6
|
+
if (content.includes('(pending)')) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (/--[a-z]/.test(content)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (content.includes('(to be determined)') &&
|
|
13
|
+
!/\.(ts|tsx|js|jsx|py|rb|go|java|cs)/.test(content)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=placeholder-technical.js.map
|
|
@@ -2,6 +2,7 @@ import type { PlanuCanonicalPathPolicy } from '../../types/index.js';
|
|
|
2
2
|
export declare const PLANU_CANONICAL_POLICY: PlanuCanonicalPathPolicy;
|
|
3
3
|
export declare function isCanonicalPlanuRootFile(name: string): boolean;
|
|
4
4
|
export declare function isCanonicalPlanuRootDir(name: string): boolean;
|
|
5
|
+
export declare function isRuntimeHomeRootDir(name: string): boolean;
|
|
5
6
|
export declare function isCanonicalSpecFile(name: string): boolean;
|
|
6
7
|
export declare function isCanonicalSpecDir(name: string): boolean;
|
|
7
8
|
export declare function mustMergeBeforeDeleteSpecFile(name: string): boolean;
|
|
@@ -48,12 +48,18 @@ const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
|
48
48
|
const SPEC_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecFiles);
|
|
49
49
|
const SPEC_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecDirs);
|
|
50
50
|
const LEGACY_MERGE_SET = new Set(PLANU_CANONICAL_POLICY.legacyMergeBeforeDeleteFiles);
|
|
51
|
+
const RUNTIME_HOME_TOP_DIR = PLANU_CANONICAL_POLICY.runtimeHomeDir
|
|
52
|
+
.replace(/^planu\//, '')
|
|
53
|
+
.replace(/\/$/, '');
|
|
51
54
|
export function isCanonicalPlanuRootFile(name) {
|
|
52
55
|
return ROOT_FILE_SET.has(name);
|
|
53
56
|
}
|
|
54
57
|
export function isCanonicalPlanuRootDir(name) {
|
|
55
58
|
return ROOT_DIR_SET.has(name);
|
|
56
59
|
}
|
|
60
|
+
export function isRuntimeHomeRootDir(name) {
|
|
61
|
+
return name === RUNTIME_HOME_TOP_DIR;
|
|
62
|
+
}
|
|
57
63
|
export function isCanonicalSpecFile(name) {
|
|
58
64
|
return SPEC_FILE_SET.has(name);
|
|
59
65
|
}
|
|
@@ -8,7 +8,7 @@ import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
|
8
8
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
9
9
|
import { safeUnlink } from './git-aware-fs.js';
|
|
10
10
|
import { scanLegacyArtifacts } from './legacy-classifier.js';
|
|
11
|
-
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isLegacyReleasePendingFile, isCanonicalSpecDir, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
11
|
+
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isLegacyReleasePendingFile, isCanonicalSpecDir, isCanonicalSpecFile, isRuntimeHomeRootDir, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
// SPEC-1699: legacy planu/ root files folded into project.json by init_project's
|
|
14
14
|
// migration. Destructive cleanup must never delete them outright — only the
|
|
@@ -44,7 +44,6 @@ async function gitRmCached(projectPath, relPath) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
// This helper is only called for directories from removePath.
|
|
48
47
|
const args = ['rm', '--cached', '--quiet', '--ignore-unmatch', '-r', relPath];
|
|
49
48
|
await execFileAsync('git', args, {
|
|
50
49
|
cwd: projectPath,
|
|
@@ -82,6 +81,7 @@ function buildPlanuIgnoreBlock() {
|
|
|
82
81
|
'planu/data/',
|
|
83
82
|
'planu/state/',
|
|
84
83
|
'planu/.locks/',
|
|
84
|
+
'planu/.runtime/',
|
|
85
85
|
'planu/specs/**',
|
|
86
86
|
'!planu/specs/**/',
|
|
87
87
|
'!planu/specs/**/spec.md',
|
|
@@ -163,16 +163,23 @@ async function shouldDeleteOrphanSpecDir(specDir) {
|
|
|
163
163
|
// Keeping them without spec.md leaves a permanently non-canonical orphan behind.
|
|
164
164
|
return !entries.includes('spec.md');
|
|
165
165
|
}
|
|
166
|
+
async function runReportModeCleanup(projectPath) {
|
|
167
|
+
const validation = await validateStrictPlanuLayout(projectPath);
|
|
168
|
+
const planuExists = existsSync(join(projectPath, 'planu'));
|
|
169
|
+
const gitignoreUpdated = planuExists ? await updateGitignore(projectPath) : false;
|
|
170
|
+
const untracked = planuExists ? await untrackRuntimeFiles(projectPath) : [];
|
|
171
|
+
return {
|
|
172
|
+
mode: 'report',
|
|
173
|
+
deleted: [],
|
|
174
|
+
merged: [],
|
|
175
|
+
proposed: validation.offenders,
|
|
176
|
+
gitignoreUpdated,
|
|
177
|
+
untracked,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
166
180
|
export async function runStrictPlanuCleanup(projectPath, authority = { mode: 'report' }) {
|
|
167
181
|
if (authority.mode === 'report') {
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
mode: 'report',
|
|
171
|
-
deleted: [],
|
|
172
|
-
merged: [],
|
|
173
|
-
proposed: validation.offenders,
|
|
174
|
-
gitignoreUpdated: false,
|
|
175
|
-
};
|
|
182
|
+
return runReportModeCleanup(projectPath);
|
|
176
183
|
}
|
|
177
184
|
const result = {
|
|
178
185
|
mode: 'execute',
|
|
@@ -180,6 +187,7 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
180
187
|
merged: [],
|
|
181
188
|
proposed: [],
|
|
182
189
|
gitignoreUpdated: false,
|
|
190
|
+
untracked: [],
|
|
183
191
|
};
|
|
184
192
|
const planuDir = join(projectPath, 'planu');
|
|
185
193
|
if (!existsSync(planuDir)) {
|
|
@@ -189,10 +197,7 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
189
197
|
for (const entry of entries) {
|
|
190
198
|
const full = join(planuDir, entry);
|
|
191
199
|
const isDir = await pathIsDirectory(full);
|
|
192
|
-
if (isDir && entry === 'releases') {
|
|
193
|
-
// SPEC-1699: 'releases' is no longer a canonical root dir, but its
|
|
194
|
-
// pending.json still needs the dedicated fold-into-project.json
|
|
195
|
-
// migration below before its directory can be removed.
|
|
200
|
+
if (isDir && (entry === 'releases' || isRuntimeHomeRootDir(entry))) {
|
|
196
201
|
continue;
|
|
197
202
|
}
|
|
198
203
|
if (isDir && !isCanonicalPlanuRootDir(entry)) {
|
|
@@ -236,8 +241,20 @@ export async function runStrictPlanuCleanup(projectPath, authority = { mode: 're
|
|
|
236
241
|
await walkSpecDirectory(projectPath, full, result);
|
|
237
242
|
}
|
|
238
243
|
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
244
|
+
result.untracked = await untrackRuntimeFiles(projectPath);
|
|
239
245
|
return result;
|
|
240
246
|
}
|
|
247
|
+
async function untrackRuntimeFiles(projectPath) {
|
|
248
|
+
const trackedRuntimeFiles = await listTrackedFiles(projectPath, 'planu/.runtime');
|
|
249
|
+
if (trackedRuntimeFiles.length === 0) {
|
|
250
|
+
return trackedRuntimeFiles;
|
|
251
|
+
}
|
|
252
|
+
for (const relPath of trackedRuntimeFiles) {
|
|
253
|
+
await gitRmCached(projectPath, relPath);
|
|
254
|
+
}
|
|
255
|
+
const stillTracked = new Set(await listTrackedFiles(projectPath, 'planu/.runtime'));
|
|
256
|
+
return trackedRuntimeFiles.filter((relPath) => !stillTracked.has(relPath));
|
|
257
|
+
}
|
|
241
258
|
function resolveSpecDirsForValidation(projectPath, specPath) {
|
|
242
259
|
if (!specPath?.trim()) {
|
|
243
260
|
return null;
|
|
@@ -253,6 +270,9 @@ export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
|
253
270
|
for (const entry of entries) {
|
|
254
271
|
const full = join(planuDir, entry);
|
|
255
272
|
const isDir = await pathIsDirectory(full);
|
|
273
|
+
if (isDir && isRuntimeHomeRootDir(entry)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
256
276
|
if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
|
|
257
277
|
(!isDir && !isCanonicalPlanuRootFile(entry))) {
|
|
258
278
|
offenders.push(relative(projectPath, full));
|
|
@@ -13,6 +13,7 @@ import { replaceSectionInSpec } from '../engine/spec-format/replace-section.js';
|
|
|
13
13
|
import { executionRemove } from '../engine/execution/deadline-io.js';
|
|
14
14
|
import { buildMigrationDirective, scanLegacyArtifacts, } from '../engine/spec-migrator/legacy-classifier.js';
|
|
15
15
|
import { escapeRegex } from '../core/shared/strings.js';
|
|
16
|
+
import { isPlaceholderTechnical } from '../engine/spec-migrator/placeholder-technical.js';
|
|
16
17
|
const SPANISH_INDICATORS = [
|
|
17
18
|
'de',
|
|
18
19
|
'del',
|
|
@@ -36,19 +37,6 @@ const SPANISH_INDICATORS = [
|
|
|
36
37
|
// ---------------------------------------------------------------------------
|
|
37
38
|
// Placeholder detection
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
|
-
function isPlaceholderTechnical(content) {
|
|
40
|
-
if (content.includes('(pending)')) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (/--[a-z]/.test(content)) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
if (content.includes('(to be determined)') &&
|
|
47
|
-
!/\.(ts|tsx|js|jsx|py|rb|go|java|cs)/.test(content)) {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
40
|
function hasInlineTechnicalFilesPlaceholder(content) {
|
|
53
41
|
return /^##\s+Technical\b[\s\S]*?^##\s+Files\b[\s\S]*?(?:\(pending\)|\(to be determined\)|\bTBD\b|--[a-z])/im.test(content);
|
|
54
42
|
}
|
|
@@ -21,8 +21,9 @@ import { regeneratePages } from '../../engine/doc-generator/portal/index.js';
|
|
|
21
21
|
import { detectStackPatterns } from './stack-detector.js';
|
|
22
22
|
import { buildProjectConfig } from './config-builder.js';
|
|
23
23
|
import { runSpecMigrations, listProjectSpecs } from './migration-runner.js';
|
|
24
|
-
import { reconcilePortableSpecIndex,
|
|
24
|
+
import { reconcilePortableSpecIndex, buildAlreadyInitializedResult, buildAuthorizedMigrationResult, } from './portable-index-reconciler.js';
|
|
25
25
|
import { refreshCoreHostAssets } from './host-assets-writer.js';
|
|
26
|
+
import { scanLegacyGuidance, summarizeLegacyGuidanceScan } from './legacy-guidance-scanner.js';
|
|
26
27
|
import { runScaffoldWriter } from './scaffold-writer.js';
|
|
27
28
|
import { readAutoInstallFlag, orchestrateSkillInstalls, runHealthCheckWithBaseline, runConventionScanSafe, fireTelemetry, } from './lifecycle-helpers.js';
|
|
28
29
|
import { injectProactiveRules } from '../../engine/claude-md-injector/index.js';
|
|
@@ -223,28 +224,12 @@ export async function handleInitProject(params, server) {
|
|
|
223
224
|
const authorizedMigrations = params.authorizedMigrations ?? [];
|
|
224
225
|
if (isUpdate && authorizedMigrations.length === 0) {
|
|
225
226
|
const reconciliation = await reconcilePortableSpecIndex(projectPath, projectId);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
structuredContent: {
|
|
235
|
-
projectId,
|
|
236
|
-
...(reconciliation.failures.length === 0 ? { projectPath } : {}),
|
|
237
|
-
isUpdate: true,
|
|
238
|
-
repositoryFilesChanged,
|
|
239
|
-
authorizedMigrations: [],
|
|
240
|
-
importedSpecIds: reconciliation.importedSpecIds,
|
|
241
|
-
failures: reconciliation.failures,
|
|
242
|
-
skippedLegacy: reconciliation.skippedLegacy,
|
|
243
|
-
strayRepoRemovals: reconciliation.strayRepoRemovals,
|
|
244
|
-
migratedLegacyPaths: legacyMigration.migrated,
|
|
245
|
-
},
|
|
246
|
-
...(reconciliation.failures.length > 0 ? { isError: true } : {}),
|
|
247
|
-
};
|
|
227
|
+
const canRefresh = reconciliation.failures.length === 0;
|
|
228
|
+
const legacyGuidanceScan = canRefresh
|
|
229
|
+
? await scanLegacyGuidance(projectPath)
|
|
230
|
+
: { healed: [], warnings: [] };
|
|
231
|
+
const refreshedAssets = canRefresh ? await refreshCoreHostAssets(projectPath) : [];
|
|
232
|
+
return buildAlreadyInitializedResult(projectId, reconciliation, legacyMigration.migrated, refreshedAssets, legacyGuidanceScan, projectPath);
|
|
248
233
|
}
|
|
249
234
|
if (isUpdate) {
|
|
250
235
|
if (existing === null) {
|
|
@@ -261,26 +246,8 @@ export async function handleInitProject(params, server) {
|
|
|
261
246
|
const migrationResult = await runSpecMigrations(projectPath, projectId, existing, {
|
|
262
247
|
repositoryMigration: 'planu-spec-format-v1',
|
|
263
248
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
type: 'text',
|
|
268
|
-
text: migrationResult.criticalMigrationFailures.length === 0
|
|
269
|
-
? 'Authorized planu-spec-format-v1 migration completed.'
|
|
270
|
-
: `Authorized migration completed with ${String(migrationResult.criticalMigrationFailures.length)} blocking issue(s).`,
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
structuredContent: {
|
|
274
|
-
projectId,
|
|
275
|
-
projectPath,
|
|
276
|
-
isUpdate: true,
|
|
277
|
-
authorizedMigrations,
|
|
278
|
-
repositoryFilesChanged: migrationResult.changedPaths ?? [],
|
|
279
|
-
migrationReportPath: migrationResult.migrationReportPath,
|
|
280
|
-
criticalMigrationFailures: migrationResult.criticalMigrationFailures,
|
|
281
|
-
},
|
|
282
|
-
...(migrationResult.criticalMigrationFailures.length > 0 ? { isError: true } : {}),
|
|
283
|
-
};
|
|
249
|
+
const legacyGuidanceScan = await scanLegacyGuidance(projectPath).catch(() => ({ healed: [], warnings: [] }));
|
|
250
|
+
return buildAuthorizedMigrationResult(projectId, projectPath, authorizedMigrations, migrationResult, legacyGuidanceScan);
|
|
284
251
|
}
|
|
285
252
|
// Get global config for defaults
|
|
286
253
|
const globalConfig = await globalStore.getGlobalConfig();
|
|
@@ -439,6 +406,7 @@ export async function handleInitProject(params, server) {
|
|
|
439
406
|
// Write scaffold files: rules, git setup, constitution, CLAUDE.md, lint, architecture rules
|
|
440
407
|
const scaffoldResult = await runScaffoldWriter(projectPath, projectId, knowledge, recommendedSkills, params.permissionsMode, params.pluginsMode, autoInstallFromConfig);
|
|
441
408
|
const { platform, generatedRules, rulesWritten, additionalFilesWritten, hooksInstalled, gitFlowType, gitignoreUpdated, gitRepoDetected, layoutOffenders, constitutionInitialized, claudeMdUpdated, eslintCreated, prettierCreated, lintSuggestions, architectureRulesWritten, architectureRulesSkipped, planuWorkflowInjected, planuHooksConfigured, planuRulesWritten, gitAutoStageInjected, } = scaffoldResult;
|
|
409
|
+
const legacyGuidanceScan = await scanLegacyGuidance(projectPath).catch(() => ({ healed: [], warnings: [] }));
|
|
442
410
|
// SPEC-444: Inject proactive behavior rules into project CLAUDE.md
|
|
443
411
|
const proactiveRulesInjected = await injectProactiveRules(join(projectPath, 'CLAUDE.md'), '1.22.0')
|
|
444
412
|
.then(() => true)
|
|
@@ -561,6 +529,7 @@ export async function handleInitProject(params, server) {
|
|
|
561
529
|
if (skillsAutoInstalled.length > 0) {
|
|
562
530
|
collector.pushOk('skills-installed', `Auto-installed skills: ${skillsAutoInstalled.join(', ')}`);
|
|
563
531
|
}
|
|
532
|
+
summarizeLegacyGuidanceScan(collector, legacyGuidanceScan);
|
|
564
533
|
const toolResult = buildInitProjectResult({
|
|
565
534
|
projectId,
|
|
566
535
|
projectPath,
|
|
@@ -620,8 +589,12 @@ export async function handleInitProject(params, server) {
|
|
|
620
589
|
branchInfo,
|
|
621
590
|
});
|
|
622
591
|
if (toolResult.structuredContent) {
|
|
623
|
-
toolResult.structuredContent
|
|
624
|
-
|
|
592
|
+
Object.assign(toolResult.structuredContent, {
|
|
593
|
+
repositoryFilesChanged: [...migrationChangedPaths, ...legacyGuidanceScan.healed],
|
|
594
|
+
authorizedMigrations,
|
|
595
|
+
legacyGuidanceHealed: legacyGuidanceScan.healed,
|
|
596
|
+
legacyGuidanceWarnings: legacyGuidanceScan.warnings,
|
|
597
|
+
});
|
|
625
598
|
}
|
|
626
599
|
// Inject autopilotSummary into structuredContent
|
|
627
600
|
if (collector.hasEntries() && toolResult.structuredContent) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AutopilotSummaryCollector } from '../../engine/autopilot/summary-collector.js';
|
|
2
|
+
import type { LegacyGuidanceScanResult } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Scan the bounded set of Planu-installed host guidance surfaces for stale
|
|
5
|
+
* directives that contradict the current spec.md-only contract, and
|
|
6
|
+
* neutralize what can be neutralized fail-closed. Never touches files
|
|
7
|
+
* outside the inventoried scan surface (no repo-wide walk).
|
|
8
|
+
*/
|
|
9
|
+
export declare function scanLegacyGuidance(projectPath: string): Promise<LegacyGuidanceScanResult>;
|
|
10
|
+
/** Records a legacy-guidance scan outcome onto an init_project autopilot summary. */
|
|
11
|
+
export declare function summarizeLegacyGuidanceScan(collector: AutopilotSummaryCollector, scan: LegacyGuidanceScanResult): void;
|
|
12
|
+
//# sourceMappingURL=legacy-guidance-scanner.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// tools/init-project/legacy-guidance-scanner.ts — SPEC-1716
|
|
2
|
+
// Neutralizes stale Planu-installed guidance that instructs agents to create
|
|
3
|
+
// legacy standalone technical.md/progress.md files. Fail-closed by tiers:
|
|
4
|
+
// Tier 1 — delimited Planu block markers: rewrite only the block content.
|
|
5
|
+
// Tier 2 — composite fingerprint (allowlisted legacy filename + managed-by
|
|
6
|
+
// header): replace the whole file, backing up the prior content.
|
|
7
|
+
// Tier 3 — contradictory phrase alone: warning only, zero mutation.
|
|
8
|
+
// Malformed markers (opening without closing) also fail closed: warning only.
|
|
9
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { atomicWriteFile } from '../../engine/safety/atomic-write-file.js';
|
|
12
|
+
import { resolveContainedProjectFile } from '../../engine/safety/contained-project-file.js';
|
|
13
|
+
import legacyGuidanceRegistry from '../../config/registries/legacy-guidance.json' with { type: 'json' };
|
|
14
|
+
const REGISTRY = legacyGuidanceRegistry;
|
|
15
|
+
async function resolveScanSurface(projectPath) {
|
|
16
|
+
const files = [...REGISTRY.hostSurfaceFiles];
|
|
17
|
+
for (const { dir, extension } of REGISTRY.hostSurfaceDirectories) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(join(projectPath, dir), { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
27
|
+
files.push(`${dir}/${entry.name}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
function containsContradictoryPhrase(text) {
|
|
34
|
+
return REGISTRY.contradictoryPhrases.find((phrase) => text.includes(phrase));
|
|
35
|
+
}
|
|
36
|
+
const OPEN_MARKER_RE = /<!--\s*(planu[\w:-]*)\s*-->/g;
|
|
37
|
+
/**
|
|
38
|
+
* Locate delimited `<!-- planu... --> ... <!-- /planu... -->` (or repeated-tag,
|
|
39
|
+
* as emitted by claude-md-generator.ts wrap()) blocks. Returns `null` when an
|
|
40
|
+
* opening marker has no matching close — the malformed-marker fail-closed case.
|
|
41
|
+
*
|
|
42
|
+
* Uses its own cursor (a fresh RegExp with `re.lastIndex` advanced past each
|
|
43
|
+
* close marker) instead of `matchAll` over the whole string — a repeated-tag
|
|
44
|
+
* close is itself a valid open-marker match, so scanning the string without
|
|
45
|
+
* skipping past it would misread that close as the next block's opener.
|
|
46
|
+
*/
|
|
47
|
+
function findPlanuBlocks(content) {
|
|
48
|
+
const blocks = [];
|
|
49
|
+
const re = new RegExp(OPEN_MARKER_RE.source, 'g');
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = re.exec(content)) !== null) {
|
|
52
|
+
const id = match[1];
|
|
53
|
+
if (id === undefined) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const openEnd = match.index + match[0].length;
|
|
57
|
+
const closeTag = `<!-- /${id} -->`;
|
|
58
|
+
const repeatTag = match[0];
|
|
59
|
+
const closeTagStart = content.indexOf(closeTag, openEnd);
|
|
60
|
+
const repeatTagStart = content.indexOf(repeatTag, openEnd);
|
|
61
|
+
const closeStart = [closeTagStart, repeatTagStart]
|
|
62
|
+
.filter((idx) => idx !== -1)
|
|
63
|
+
.sort((a, b) => a - b)[0];
|
|
64
|
+
if (closeStart === undefined) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const closeLength = closeStart === closeTagStart ? closeTag.length : repeatTag.length;
|
|
68
|
+
blocks.push({ id, openEnd, closeStart });
|
|
69
|
+
re.lastIndex = closeStart + closeLength;
|
|
70
|
+
}
|
|
71
|
+
return blocks;
|
|
72
|
+
}
|
|
73
|
+
async function scanTier1(absPath, relPath, content) {
|
|
74
|
+
const blocks = findPlanuBlocks(content);
|
|
75
|
+
if (blocks === null) {
|
|
76
|
+
return {
|
|
77
|
+
kind: 'warning',
|
|
78
|
+
file: relPath,
|
|
79
|
+
reason: 'malformed Planu block marker: an opening marker has no matching closing marker',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const dirty = blocks.some(({ openEnd, closeStart }) => containsContradictoryPhrase(content.slice(openEnd, closeStart)));
|
|
83
|
+
if (!dirty) {
|
|
84
|
+
return { kind: 'clean' };
|
|
85
|
+
}
|
|
86
|
+
let rewritten = '';
|
|
87
|
+
let cursor = 0;
|
|
88
|
+
for (const { openEnd, closeStart } of blocks) {
|
|
89
|
+
const body = content.slice(openEnd, closeStart);
|
|
90
|
+
rewritten += content.slice(cursor, openEnd);
|
|
91
|
+
rewritten += containsContradictoryPhrase(body) ? `\n${REGISTRY.replacementGuidance}\n` : body;
|
|
92
|
+
cursor = closeStart;
|
|
93
|
+
}
|
|
94
|
+
rewritten += content.slice(cursor);
|
|
95
|
+
await atomicWriteFile(absPath, rewritten);
|
|
96
|
+
return { kind: 'healed', file: relPath };
|
|
97
|
+
}
|
|
98
|
+
async function scanTier2Or3(absPath, relPath, content) {
|
|
99
|
+
const phrase = containsContradictoryPhrase(content);
|
|
100
|
+
if (!phrase) {
|
|
101
|
+
return { kind: 'clean' };
|
|
102
|
+
}
|
|
103
|
+
const strongFingerprint = REGISTRY.legacyFilenameAllowlist.includes(relPath) &&
|
|
104
|
+
REGISTRY.managedByMarkers.some((marker) => content.includes(marker));
|
|
105
|
+
if (!strongFingerprint) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'warning',
|
|
108
|
+
file: relPath,
|
|
109
|
+
reason: `contains contradictory legacy guidance phrase: "${phrase}"`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
await atomicWriteFile(`${absPath}.planu-backup`, content);
|
|
113
|
+
await atomicWriteFile(absPath, `${REGISTRY.replacementGuidance}\n`);
|
|
114
|
+
return { kind: 'healed', file: relPath };
|
|
115
|
+
}
|
|
116
|
+
async function scanFile(projectPath, relPath) {
|
|
117
|
+
const absPath = join(projectPath, relPath);
|
|
118
|
+
let content;
|
|
119
|
+
try {
|
|
120
|
+
content = await readFile(absPath, 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { kind: 'clean' };
|
|
124
|
+
}
|
|
125
|
+
// Fail closed if the surface entry (or an ancestor directory, e.g. a
|
|
126
|
+
// symlinked .claude/rules) resolves outside the project root — never let a
|
|
127
|
+
// mutation follow a symlink off the repo.
|
|
128
|
+
try {
|
|
129
|
+
await resolveContainedProjectFile(projectPath, absPath);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return {
|
|
133
|
+
kind: 'warning',
|
|
134
|
+
file: relPath,
|
|
135
|
+
reason: 'path escapes the project directory (symlink containment check failed)',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const hasMarkers = OPEN_MARKER_RE.test(content);
|
|
139
|
+
OPEN_MARKER_RE.lastIndex = 0;
|
|
140
|
+
return hasMarkers
|
|
141
|
+
? scanTier1(absPath, relPath, content)
|
|
142
|
+
: scanTier2Or3(absPath, relPath, content);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Scan the bounded set of Planu-installed host guidance surfaces for stale
|
|
146
|
+
* directives that contradict the current spec.md-only contract, and
|
|
147
|
+
* neutralize what can be neutralized fail-closed. Never touches files
|
|
148
|
+
* outside the inventoried scan surface (no repo-wide walk).
|
|
149
|
+
*/
|
|
150
|
+
export async function scanLegacyGuidance(projectPath) {
|
|
151
|
+
const files = await resolveScanSurface(projectPath);
|
|
152
|
+
const healed = [];
|
|
153
|
+
const warnings = [];
|
|
154
|
+
for (const relPath of files) {
|
|
155
|
+
const outcome = await scanFile(projectPath, relPath);
|
|
156
|
+
if (outcome.kind === 'healed') {
|
|
157
|
+
healed.push(outcome.file);
|
|
158
|
+
}
|
|
159
|
+
else if (outcome.kind === 'warning') {
|
|
160
|
+
warnings.push({ file: outcome.file, reason: outcome.reason });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { healed, warnings };
|
|
164
|
+
}
|
|
165
|
+
/** Records a legacy-guidance scan outcome onto an init_project autopilot summary. */
|
|
166
|
+
export function summarizeLegacyGuidanceScan(collector, scan) {
|
|
167
|
+
if (scan.healed.length > 0) {
|
|
168
|
+
collector.pushOk('legacy-guidance-healed', `Neutralized stale Planu guidance in: ${scan.healed.join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
if (scan.warnings.length > 0) {
|
|
171
|
+
collector.pushSkipped('legacy-guidance-warnings', `Left untouched (weak fingerprint, needs manual review): ${scan.warnings
|
|
172
|
+
.map((w) => w.file)
|
|
173
|
+
.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=legacy-guidance-scanner.js.map
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
import type { FilesystemImportDeps, FilesystemImportFailure, PortableIndexReconciliationResult, SkippedLegacyContract } from '../../types/index.js';
|
|
1
|
+
import type { FilesystemImportDeps, FilesystemImportFailure, LegacyGuidanceScanResult, PortableIndexReconciliationResult, SkippedLegacyContract } from '../../types/index.js';
|
|
2
|
+
import type { ToolResult } from '../../types/common/primitives.js';
|
|
2
3
|
/** Autopilot-first summary of fatal reconciliation failures: what failed, why, and the next action. */
|
|
3
4
|
export declare function describeReconciliationFailures(failures: FilesystemImportFailure[]): string;
|
|
4
5
|
/** Autopilot-first summary of a successful reconciliation, including any legacy skips. */
|
|
5
6
|
export declare function describeReconciliationSuccess(repositoryFilesChanged: string[], skippedLegacy: SkippedLegacyContract[], strayRepoRemovals?: string[]): string;
|
|
6
7
|
/** Autopilot-first message for a reconciliation outcome, success or fatal-failure. */
|
|
7
8
|
export declare function describeReconciliationOutcome(reconciliation: PortableIndexReconciliationResult, repositoryFilesChanged: string[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* SPEC-1716: Build the already-initialized `init_project` result, folding the
|
|
11
|
+
* legacy-guidance scan's healed paths into the reported repository file changes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildAlreadyInitializedResult(projectId: string, reconciliation: PortableIndexReconciliationResult, legacyMigratedPaths: readonly string[], refreshedAssets: readonly string[], legacyGuidanceScan: LegacyGuidanceScanResult, projectPath: string): ToolResult;
|
|
14
|
+
/** The subset of `MigrationRunResult` (migration-runner.ts) needed to report an authorized migration. */
|
|
15
|
+
interface AuthorizedMigrationSummary {
|
|
16
|
+
criticalMigrationFailures: unknown[];
|
|
17
|
+
changedPaths?: string[];
|
|
18
|
+
migrationReportPath: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SPEC-1716: Build the authorized-migration `init_project` result, folding the
|
|
22
|
+
* legacy-guidance scan's healed paths into the reported repository file changes
|
|
23
|
+
* the same way the already-initialized fast path does.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildAuthorizedMigrationResult(projectId: string, projectPath: string, authorizedMigrations: readonly string[], migrationResult: AuthorizedMigrationSummary, legacyGuidanceScan: LegacyGuidanceScanResult): ToolResult;
|
|
8
26
|
/** Rebuild the mutable external index from the repository-owned portable contracts. */
|
|
9
27
|
export declare function reconcilePortableSpecIndex(projectPath: string, projectId: string, deps?: FilesystemImportDeps): Promise<PortableIndexReconciliationResult>;
|
|
28
|
+
export {};
|
|
10
29
|
//# sourceMappingURL=portable-index-reconciler.d.ts.map
|
|
@@ -156,6 +156,67 @@ export function describeReconciliationOutcome(reconciliation, repositoryFilesCha
|
|
|
156
156
|
? describeReconciliationSuccess(repositoryFilesChanged, reconciliation.skippedLegacy, reconciliation.strayRepoRemovals)
|
|
157
157
|
: describeReconciliationFailures(reconciliation.failures);
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* SPEC-1716: Build the already-initialized `init_project` result, folding the
|
|
161
|
+
* legacy-guidance scan's healed paths into the reported repository file changes.
|
|
162
|
+
*/
|
|
163
|
+
export function buildAlreadyInitializedResult(projectId, reconciliation, legacyMigratedPaths, refreshedAssets, legacyGuidanceScan, projectPath) {
|
|
164
|
+
const repositoryFilesChanged = [...refreshedAssets, ...legacyGuidanceScan.healed];
|
|
165
|
+
const baseText = describeReconciliationOutcome(reconciliation, [...refreshedAssets]);
|
|
166
|
+
const healedGuidanceSuffix = legacyGuidanceScan.healed.length === 0
|
|
167
|
+
? ''
|
|
168
|
+
: ` Healed stale legacy guidance in: ${legacyGuidanceScan.healed.join(', ')}.`;
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: 'text', text: baseText + healedGuidanceSuffix }],
|
|
171
|
+
structuredContent: {
|
|
172
|
+
projectId,
|
|
173
|
+
...(reconciliation.failures.length === 0 ? { projectPath } : {}),
|
|
174
|
+
isUpdate: true,
|
|
175
|
+
repositoryFilesChanged,
|
|
176
|
+
authorizedMigrations: [],
|
|
177
|
+
importedSpecIds: reconciliation.importedSpecIds,
|
|
178
|
+
failures: reconciliation.failures,
|
|
179
|
+
skippedLegacy: reconciliation.skippedLegacy,
|
|
180
|
+
strayRepoRemovals: reconciliation.strayRepoRemovals,
|
|
181
|
+
migratedLegacyPaths: legacyMigratedPaths,
|
|
182
|
+
legacyGuidanceHealed: legacyGuidanceScan.healed,
|
|
183
|
+
legacyGuidanceWarnings: legacyGuidanceScan.warnings,
|
|
184
|
+
},
|
|
185
|
+
...(reconciliation.failures.length > 0 ? { isError: true } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* SPEC-1716: Build the authorized-migration `init_project` result, folding the
|
|
190
|
+
* legacy-guidance scan's healed paths into the reported repository file changes
|
|
191
|
+
* the same way the already-initialized fast path does.
|
|
192
|
+
*/
|
|
193
|
+
export function buildAuthorizedMigrationResult(projectId, projectPath, authorizedMigrations, migrationResult, legacyGuidanceScan) {
|
|
194
|
+
const hasCriticalFailures = migrationResult.criticalMigrationFailures.length > 0;
|
|
195
|
+
const healedGuidanceSuffix = legacyGuidanceScan.healed.length === 0
|
|
196
|
+
? ''
|
|
197
|
+
: ` Healed stale legacy guidance in: ${legacyGuidanceScan.healed.join(', ')}.`;
|
|
198
|
+
const baseText = hasCriticalFailures
|
|
199
|
+
? `Authorized migration completed with ${String(migrationResult.criticalMigrationFailures.length)} blocking issue(s).`
|
|
200
|
+
: 'Authorized planu-spec-format-v1 migration completed.';
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: 'text', text: baseText + healedGuidanceSuffix }],
|
|
203
|
+
structuredContent: {
|
|
204
|
+
projectId,
|
|
205
|
+
projectPath,
|
|
206
|
+
isUpdate: true,
|
|
207
|
+
authorizedMigrations,
|
|
208
|
+
repositoryFilesChanged: [
|
|
209
|
+
...(migrationResult.changedPaths ?? []),
|
|
210
|
+
...legacyGuidanceScan.healed,
|
|
211
|
+
],
|
|
212
|
+
migrationReportPath: migrationResult.migrationReportPath,
|
|
213
|
+
criticalMigrationFailures: migrationResult.criticalMigrationFailures,
|
|
214
|
+
legacyGuidanceHealed: legacyGuidanceScan.healed,
|
|
215
|
+
legacyGuidanceWarnings: legacyGuidanceScan.warnings,
|
|
216
|
+
},
|
|
217
|
+
...(hasCriticalFailures ? { isError: true } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
159
220
|
/** Rebuild the mutable external index from the repository-owned portable contracts. */
|
|
160
221
|
export async function reconcilePortableSpecIndex(projectPath, projectId, deps = {
|
|
161
222
|
listSpecs: specStore.listSpecs,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -278,6 +278,7 @@ export * from './reviewer-tokens.js';
|
|
|
278
278
|
export * from './coach.js';
|
|
279
279
|
export * from './tdd-strict.js';
|
|
280
280
|
export * from './ssr-migration.js';
|
|
281
|
+
export * from './legacy-guidance.js';
|
|
281
282
|
export * from './workspace-overview.js';
|
|
282
283
|
export * from './mcp-config.js';
|
|
283
284
|
export * from './security/index.js';
|
package/dist/types/index.js
CHANGED
|
@@ -275,6 +275,7 @@ export * from './reviewer-tokens.js';
|
|
|
275
275
|
export * from './coach.js';
|
|
276
276
|
export * from './tdd-strict.js';
|
|
277
277
|
export * from './ssr-migration.js';
|
|
278
|
+
export * from './legacy-guidance.js';
|
|
278
279
|
// SPEC-753: workspace_overview + reconcile_status_json types
|
|
279
280
|
export * from './workspace-overview.js';
|
|
280
281
|
// SPEC-756: MCP config auto-update types
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface LegacyGuidanceWarning {
|
|
2
|
+
file: string;
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
export interface LegacyGuidanceScanResult {
|
|
6
|
+
healed: string[];
|
|
7
|
+
warnings: LegacyGuidanceWarning[];
|
|
8
|
+
}
|
|
9
|
+
export interface LegacyGuidanceRegistry {
|
|
10
|
+
hostSurfaceFiles: string[];
|
|
11
|
+
hostSurfaceDirectories: {
|
|
12
|
+
dir: string;
|
|
13
|
+
extension: string;
|
|
14
|
+
}[];
|
|
15
|
+
legacyFilenameAllowlist: string[];
|
|
16
|
+
managedByMarkers: string[];
|
|
17
|
+
contradictoryPhrases: string[];
|
|
18
|
+
replacementGuidance: string;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=legacy-guidance.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Result of folding technical.md into spec.md ## Technical section. */
|
|
2
2
|
export interface FoldTechnicalResult {
|
|
3
3
|
ok: boolean;
|
|
4
|
-
reason: 'already_unified' | 'no_technical_md' | 'ok' | 'error';
|
|
4
|
+
reason: 'already_unified' | 'no_technical_md' | 'ok' | 'error' | 'folded_residual';
|
|
5
5
|
byteDelta?: number;
|
|
6
6
|
error?: string;
|
|
7
7
|
}
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.7.
|
|
5
|
+
"version": "5.7.5",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|