@planu/cli 4.7.4 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/config/minimal-implementation-gate.json +12 -4
- package/dist/engine/context-md-generator.js +5 -4
- package/dist/engine/generated-artifact-writer.d.ts +7 -0
- package/dist/engine/generated-artifact-writer.js +63 -0
- package/dist/engine/implementation-contract/renderer.js +10 -0
- package/dist/engine/next-spec-resolver/session-writer.js +10 -6
- package/dist/engine/session-context-generator.js +9 -5
- package/dist/engine/session-state/writer.js +33 -14
- package/dist/engine/universal-rules/catalog.js +2 -0
- package/dist/engine/universal-rules/rules/planu-minimal-change.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-minimal-change.js +44 -0
- package/dist/types/generated-artifacts.d.ts +13 -0
- package/dist/types/generated-artifacts.js +3 -0
- package/package.json +10 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [4.8.0] - 2026-06-24
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat(website): refine Planu landing
|
|
5
|
+
- feat(website): refine Planu landing
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [4.7.5] - 2026-06-24
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- feat: add Planu minimal-change rule and guidance inspired by Ponytail
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix: keep generated Planu session artifacts stable after lifecycle close
|
|
15
|
+
|
|
16
|
+
### Chores
|
|
17
|
+
- chore(deps): refresh push-gate dev dependency knip
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## [4.7.4] - 2026-06-23
|
|
2
21
|
|
|
3
22
|
### Bug Fixes
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"tag": "stdlib",
|
|
32
32
|
"severity": "warning",
|
|
33
33
|
"confidence": "medium",
|
|
34
|
-
"patterns": ["custom parser", "manual parser", "hand rolled parser"],
|
|
34
|
+
"patterns": ["custom parser", "manual parser", "hand rolled parser", "hand-rolled stdlib"],
|
|
35
35
|
"replacementGuidance": "Use an existing parser from the runtime or current dependency set before writing parsing code."
|
|
36
36
|
},
|
|
37
37
|
{
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"tag": "native",
|
|
40
40
|
"severity": "warning",
|
|
41
41
|
"confidence": "high",
|
|
42
|
-
"patterns": ["new public mcp tool", "new command", "new cli command"],
|
|
42
|
+
"patterns": ["new public mcp tool", "new command", "new cli command", "native platform feature"],
|
|
43
43
|
"replacementGuidance": "Prefer existing Planu SDD surfaces unless the spec proves a new public surface is required."
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"tag": "yagni",
|
|
56
56
|
"severity": "blocker",
|
|
57
57
|
"confidence": "high",
|
|
58
|
-
"patterns": ["future-proof", "just in case", "might need later", "eventually support"],
|
|
58
|
+
"patterns": ["future-proof", "just in case", "might need later", "eventually support", "speculative config"],
|
|
59
59
|
"replacementGuidance": "Remove speculative scope unless it is directly required by an acceptance criterion.",
|
|
60
60
|
"blockWhenRiskAtLeast": "medium"
|
|
61
61
|
},
|
|
@@ -64,7 +64,15 @@
|
|
|
64
64
|
"tag": "shrink",
|
|
65
65
|
"severity": "warning",
|
|
66
66
|
"confidence": "medium",
|
|
67
|
-
"patterns": [
|
|
67
|
+
"patterns": [
|
|
68
|
+
"framework",
|
|
69
|
+
"orchestration layer",
|
|
70
|
+
"plugin architecture",
|
|
71
|
+
"abstract factory",
|
|
72
|
+
"wrapper that only delegates",
|
|
73
|
+
"delegate-only wrapper",
|
|
74
|
+
"shrinkable logic"
|
|
75
|
+
],
|
|
68
76
|
"replacementGuidance": "Shrink the design to the smallest correct module/function that satisfies the spec."
|
|
69
77
|
},
|
|
70
78
|
{
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// engine/context-md-generator.ts — SPEC-635: Live ≤800-token context briefing for any LLM
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { specStore } from '../storage/index.js';
|
|
5
5
|
import { stripFrontmatter } from './frontmatter-parser.js';
|
|
6
|
+
import { stableWriteGeneratedFile, stripSessionMarkdownVolatileFields, } from './generated-artifact-writer.js';
|
|
6
7
|
const MAX_TOKENS = 800;
|
|
7
8
|
const CHARS_PER_TOKEN = 4; // rough estimate
|
|
8
9
|
const MAX_CHARS = MAX_TOKENS * CHARS_PER_TOKEN;
|
|
@@ -92,9 +93,9 @@ export async function generateContextMd(projectPath, projectId) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
const content = truncate(lines.join('\n'), MAX_CHARS);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
await stableWriteGeneratedFile(join(projectPath, 'planu', 'context.md'), content, {
|
|
97
|
+
equivalence: stripSessionMarkdownVolatileFields,
|
|
98
|
+
});
|
|
98
99
|
}
|
|
99
100
|
catch {
|
|
100
101
|
/* best-effort — never blocks status transition */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StableWriteOptions, StableWriteResult } from '../types/generated-artifacts.js';
|
|
2
|
+
export declare function withFinalNewline(content: string): string;
|
|
3
|
+
export declare function stripSessionMarkdownVolatileFields(content: string): string;
|
|
4
|
+
export declare function stableJsonStringify(value: unknown): string;
|
|
5
|
+
export declare function stripRootUpdatedAt(content: string): string;
|
|
6
|
+
export declare function stableWriteGeneratedFile(filePath: string, content: string, options?: StableWriteOptions): Promise<StableWriteResult>;
|
|
7
|
+
//# sourceMappingURL=generated-artifact-writer.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// engine/generated-artifact-writer.ts — stable writes for generated Planu artifacts.
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
export function withFinalNewline(content) {
|
|
5
|
+
return content.replace(/\s*$/u, '\n');
|
|
6
|
+
}
|
|
7
|
+
export function stripSessionMarkdownVolatileFields(content) {
|
|
8
|
+
return withFinalNewline(content)
|
|
9
|
+
.replace(/^# Planu Context — \d{4}-\d{2}-\d{2}$/mu, '# Planu Context — <date>')
|
|
10
|
+
.replace(/^> Last updated: .+$/mu, '> Last updated: <timestamp>');
|
|
11
|
+
}
|
|
12
|
+
export function stableJsonStringify(value) {
|
|
13
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
14
|
+
}
|
|
15
|
+
export function stripRootUpdatedAt(content) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(content);
|
|
18
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
19
|
+
const clone = { ...parsed };
|
|
20
|
+
delete clone.updatedAt;
|
|
21
|
+
return stableJsonStringify(clone);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Fall back to exact normalized text comparison for invalid existing JSON.
|
|
26
|
+
}
|
|
27
|
+
return withFinalNewline(content);
|
|
28
|
+
}
|
|
29
|
+
export async function stableWriteGeneratedFile(filePath, content, options = {}) {
|
|
30
|
+
const next = withFinalNewline(content);
|
|
31
|
+
let existing;
|
|
32
|
+
if (options.existingContent !== undefined) {
|
|
33
|
+
if (options.existingContent === null) {
|
|
34
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
35
|
+
await writeFile(filePath, next, 'utf-8');
|
|
36
|
+
return { written: true, reason: 'missing' };
|
|
37
|
+
}
|
|
38
|
+
existing = options.existingContent;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
existing = await readFile(filePath, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
46
|
+
await writeFile(filePath, next, 'utf-8');
|
|
47
|
+
return { written: true, reason: 'missing' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (existing === next) {
|
|
51
|
+
return { written: false, reason: 'unchanged' };
|
|
52
|
+
}
|
|
53
|
+
const existingHasStableNewline = existing.endsWith('\n') && !existing.endsWith('\n\n');
|
|
54
|
+
const equivalent = options.equivalence !== undefined &&
|
|
55
|
+
options.equivalence(existing) === options.equivalence(next);
|
|
56
|
+
if (equivalent && existingHasStableNewline) {
|
|
57
|
+
return { written: false, reason: 'unchanged' };
|
|
58
|
+
}
|
|
59
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
60
|
+
await writeFile(filePath, next, 'utf-8');
|
|
61
|
+
return { written: true, reason: 'changed' };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=generated-artifact-writer.js.map
|
|
@@ -21,6 +21,9 @@ export function buildImplementationContractSection(input) {
|
|
|
21
21
|
'### File-Level Work Plan',
|
|
22
22
|
...renderFilePlan(input.files),
|
|
23
23
|
'',
|
|
24
|
+
'### Minimal Change Guidance',
|
|
25
|
+
...renderMinimalChangeGuidance(),
|
|
26
|
+
'',
|
|
24
27
|
'### Acceptance-To-Verification Map',
|
|
25
28
|
...criteria.map((criterion, index) => renderVerificationRow(criterion.text, index + 1, testFiles, verificationCommands)),
|
|
26
29
|
'',
|
|
@@ -45,6 +48,13 @@ export function appendImplementationContractIfMissing(specBody, input) {
|
|
|
45
48
|
}
|
|
46
49
|
return `${specBody.trimEnd()}\n\n${buildImplementationContractSection(input)}`;
|
|
47
50
|
}
|
|
51
|
+
function renderMinimalChangeGuidance() {
|
|
52
|
+
return [
|
|
53
|
+
'- Before implementing, check whether the work is unnecessary, already exists in this codebase, is covered by stdlib, is covered by the native platform, or is covered by an already-installed dependency.',
|
|
54
|
+
'- Add new code only after those checks, and keep the diff to the minimum that satisfies the accepted criteria.',
|
|
55
|
+
'- Do not use minimality to remove security, trust-boundary validation, data-loss handling, accessibility, explicit requirements, or required validation.',
|
|
56
|
+
];
|
|
57
|
+
}
|
|
48
58
|
function fallbackCriteria() {
|
|
49
59
|
return [
|
|
50
60
|
{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
|
|
2
2
|
// engine/next-spec-resolver/session-writer.ts — SPEC-686: Best-effort session.json writer
|
|
3
|
-
import { readFile
|
|
4
|
-
import { join
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { stableJsonStringify, stableWriteGeneratedFile, stripRootUpdatedAt, } from '../generated-artifact-writer.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Constants
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -68,11 +69,11 @@ export async function writeOrchestrationPlan(projectPath, plan) {
|
|
|
68
69
|
export async function patchSessionJson(projectPath, patch) {
|
|
69
70
|
try {
|
|
70
71
|
const filePath = join(projectPath, 'planu', 'session.json');
|
|
71
|
-
|
|
72
|
+
let existingContent = null;
|
|
72
73
|
let current = { activeSpecs: [], updatedAt: '' };
|
|
73
74
|
try {
|
|
74
|
-
|
|
75
|
-
current = JSON.parse(
|
|
75
|
+
existingContent = await readFile(filePath, 'utf-8');
|
|
76
|
+
current = JSON.parse(existingContent);
|
|
76
77
|
}
|
|
77
78
|
catch {
|
|
78
79
|
/* start fresh */
|
|
@@ -82,7 +83,10 @@ export async function patchSessionJson(projectPath, patch) {
|
|
|
82
83
|
...patch,
|
|
83
84
|
updatedAt: new Date().toISOString(),
|
|
84
85
|
};
|
|
85
|
-
await
|
|
86
|
+
await stableWriteGeneratedFile(filePath, stableJsonStringify(updated), {
|
|
87
|
+
equivalence: stripRootUpdatedAt,
|
|
88
|
+
existingContent,
|
|
89
|
+
});
|
|
86
90
|
}
|
|
87
91
|
catch {
|
|
88
92
|
/* best-effort — never throw */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// engine/session-context-generator.ts — SPEC-496: Auto-generate portable session-context.md
|
|
2
2
|
// Writes planu/session-context.md so any LLM (Cursor, Windsurf, Aider…) can pick up project state.
|
|
3
|
-
import {
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { specStore, lessonsStore } from '../storage/index.js';
|
|
6
|
+
import { stableWriteGeneratedFile, stripSessionMarkdownVolatileFields, } from './generated-artifact-writer.js';
|
|
6
7
|
/** Reads the `version` field from the project's package.json, or returns 'unknown'. */
|
|
7
8
|
async function readProjectVersion(projectPath) {
|
|
8
9
|
try {
|
|
@@ -128,10 +129,13 @@ export async function generateSessionContext(projectPath, projectId) {
|
|
|
128
129
|
lessonsContent: buildLessonsSection(lessons),
|
|
129
130
|
generatedAt: new Date().toISOString(),
|
|
130
131
|
});
|
|
131
|
-
const
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const contextPath = join(projectPath, 'planu', 'session-context.md');
|
|
133
|
+
const writeResult = await stableWriteGeneratedFile(contextPath, content, {
|
|
134
|
+
equivalence: stripSessionMarkdownVolatileFields,
|
|
135
|
+
});
|
|
136
|
+
if (!writeResult.written) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
135
139
|
if (process.env.PLANU_ENABLE_AUTOCOMMIT !== 'true') {
|
|
136
140
|
return;
|
|
137
141
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
|
|
2
2
|
// engine/session-state/writer.ts — SPEC-633: Write session.json universal state file
|
|
3
|
-
import { readFile,
|
|
3
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
|
+
import { stableJsonStringify, stableWriteGeneratedFile, stripRootUpdatedAt, } from '../generated-artifact-writer.js';
|
|
6
7
|
const STATE_DIR = 'state';
|
|
7
8
|
const ROOT_SESSION = 'session.json';
|
|
8
9
|
function stateDir(projectPath) {
|
|
@@ -69,12 +70,15 @@ export async function writeSpecSessionState(projectPath, specId, specTitle, last
|
|
|
69
70
|
resumePrompt: resumePrompt.slice(0, 300),
|
|
70
71
|
updatedAt: new Date().toISOString(),
|
|
71
72
|
};
|
|
72
|
-
await
|
|
73
|
+
await stableWriteGeneratedFile(specFile(projectPath, specId), stableJsonStringify(state), {
|
|
74
|
+
equivalence: stripRootUpdatedAt,
|
|
75
|
+
});
|
|
73
76
|
// Update root session.json index
|
|
77
|
+
let existingIndexContent = null;
|
|
74
78
|
let index = { activeSpecs: [], updatedAt: '' };
|
|
75
79
|
try {
|
|
76
|
-
|
|
77
|
-
index = JSON.parse(
|
|
80
|
+
existingIndexContent = await readFile(rootFile(projectPath), 'utf-8');
|
|
81
|
+
index = JSON.parse(existingIndexContent);
|
|
78
82
|
}
|
|
79
83
|
catch {
|
|
80
84
|
/* start fresh */
|
|
@@ -83,7 +87,10 @@ export async function writeSpecSessionState(projectPath, specId, specTitle, last
|
|
|
83
87
|
index.activeSpecs.push(specId);
|
|
84
88
|
}
|
|
85
89
|
index.updatedAt = new Date().toISOString();
|
|
86
|
-
await
|
|
90
|
+
await stableWriteGeneratedFile(rootFile(projectPath), stableJsonStringify(index), {
|
|
91
|
+
equivalence: stripRootUpdatedAt,
|
|
92
|
+
existingContent: existingIndexContent,
|
|
93
|
+
});
|
|
87
94
|
// Auto-commit planu/ changes so session.json is never left unstaged
|
|
88
95
|
void (async () => {
|
|
89
96
|
try {
|
|
@@ -127,10 +134,11 @@ export async function updateEpicProgressInSession(projectPath, specs) {
|
|
|
127
134
|
return;
|
|
128
135
|
}
|
|
129
136
|
const filePath = rootFile(projectPath);
|
|
137
|
+
let existingContent = null;
|
|
130
138
|
let index = { activeSpecs: [], updatedAt: '' };
|
|
131
139
|
try {
|
|
132
|
-
|
|
133
|
-
index = JSON.parse(
|
|
140
|
+
existingContent = await readFile(filePath, 'utf-8');
|
|
141
|
+
index = JSON.parse(existingContent);
|
|
134
142
|
}
|
|
135
143
|
catch {
|
|
136
144
|
/* start fresh */
|
|
@@ -141,7 +149,10 @@ export async function updateEpicProgressInSession(projectPath, specs) {
|
|
|
141
149
|
index.activeSpecs = index.activeSpecs.filter((specId) => !terminalSpecIds.has(specId));
|
|
142
150
|
index.epicProgress = epicProgress;
|
|
143
151
|
index.updatedAt = new Date().toISOString();
|
|
144
|
-
await
|
|
152
|
+
await stableWriteGeneratedFile(filePath, stableJsonStringify(index), {
|
|
153
|
+
equivalence: stripRootUpdatedAt,
|
|
154
|
+
existingContent,
|
|
155
|
+
});
|
|
145
156
|
// Auto-commit planu/ changes so session.json is never left unstaged
|
|
146
157
|
void (async () => {
|
|
147
158
|
try {
|
|
@@ -166,10 +177,11 @@ export async function patchSessionJson(projectPath, patch) {
|
|
|
166
177
|
try {
|
|
167
178
|
const filePath = rootFile(projectPath);
|
|
168
179
|
await mkdir(stateDir(projectPath), { recursive: true });
|
|
180
|
+
let existingContent = null;
|
|
169
181
|
let index = { activeSpecs: [], updatedAt: '' };
|
|
170
182
|
try {
|
|
171
|
-
|
|
172
|
-
index = JSON.parse(
|
|
183
|
+
existingContent = await readFile(filePath, 'utf-8');
|
|
184
|
+
index = JSON.parse(existingContent);
|
|
173
185
|
}
|
|
174
186
|
catch {
|
|
175
187
|
/* start fresh */
|
|
@@ -179,7 +191,10 @@ export async function patchSessionJson(projectPath, patch) {
|
|
|
179
191
|
...patch,
|
|
180
192
|
updatedAt: new Date().toISOString(),
|
|
181
193
|
};
|
|
182
|
-
await
|
|
194
|
+
await stableWriteGeneratedFile(filePath, stableJsonStringify(updated), {
|
|
195
|
+
equivalence: stripRootUpdatedAt,
|
|
196
|
+
existingContent,
|
|
197
|
+
});
|
|
183
198
|
// Auto-commit planu/ changes so session.json is never left unstaged
|
|
184
199
|
void (async () => {
|
|
185
200
|
try {
|
|
@@ -200,11 +215,15 @@ export async function patchSessionJson(projectPath, patch) {
|
|
|
200
215
|
*/
|
|
201
216
|
export async function removeSpecFromSession(projectPath, specId) {
|
|
202
217
|
try {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
218
|
+
const filePath = rootFile(projectPath);
|
|
219
|
+
const existingContent = await readFile(filePath, 'utf-8');
|
|
220
|
+
const index = JSON.parse(existingContent);
|
|
205
221
|
index.activeSpecs = index.activeSpecs.filter((id) => id !== specId);
|
|
206
222
|
index.updatedAt = new Date().toISOString();
|
|
207
|
-
await
|
|
223
|
+
await stableWriteGeneratedFile(filePath, stableJsonStringify(index), {
|
|
224
|
+
equivalence: stripRootUpdatedAt,
|
|
225
|
+
existingContent,
|
|
226
|
+
});
|
|
208
227
|
// Auto-commit planu/ changes so session.json is never left unstaged
|
|
209
228
|
void (async () => {
|
|
210
229
|
try {
|
|
@@ -9,6 +9,7 @@ import { planuBddCriteriaRule } from './rules/planu-bdd-criteria.js';
|
|
|
9
9
|
import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
|
|
10
10
|
import { planuReleasePolicyRule } from './rules/planu-release-policy.js';
|
|
11
11
|
import { planuSddModelRoutingRule } from './rules/planu-sdd-model-routing.js';
|
|
12
|
+
import { planuMinimalChangeRule } from './rules/planu-minimal-change.js';
|
|
12
13
|
/**
|
|
13
14
|
* The full catalog of universal Planu rules.
|
|
14
15
|
* Order matters: rules are installed in catalog order.
|
|
@@ -19,6 +20,7 @@ export const UNIVERSAL_RULES = [
|
|
|
19
20
|
planuBddCriteriaRule,
|
|
20
21
|
planuApprovalGatesRule,
|
|
21
22
|
planuSddModelRoutingRule,
|
|
23
|
+
planuMinimalChangeRule,
|
|
22
24
|
planuReleasePolicyRule,
|
|
23
25
|
planuModesRule,
|
|
24
26
|
agentTeamsRule,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// engine/universal-rules/rules/planu-minimal-change.ts
|
|
2
|
+
// Universal rule: minimal-change implementation discipline.
|
|
3
|
+
function buildBody() {
|
|
4
|
+
return `# Planu Minimal Change
|
|
5
|
+
|
|
6
|
+
Use the smallest correct change after reading the task and the code path it touches.
|
|
7
|
+
|
|
8
|
+
Before writing code, stop at the first rung that holds:
|
|
9
|
+
|
|
10
|
+
1. Does this need to be built at all? If not, do not build it.
|
|
11
|
+
2. Does this codebase already have the helper, utility, convention, or pattern? Reuse it.
|
|
12
|
+
3. Does the standard library already do this? Use it.
|
|
13
|
+
4. Does the native platform already cover it? Use it.
|
|
14
|
+
5. Does an already-installed dependency solve it? Use it.
|
|
15
|
+
6. Can this be one clear line? Use one clear line.
|
|
16
|
+
7. Only then, write the minimum code that satisfies the approved spec.
|
|
17
|
+
|
|
18
|
+
The ladder runs after understanding, not instead of understanding. Read the relevant files, trace the real flow, and fix shared root causes rather than patching one visible symptom per caller.
|
|
19
|
+
|
|
20
|
+
Prefer deletion over addition, boring over clever, and fewer touched files over broader refactors. Do not add abstractions, dependencies, configuration, feature flags, compatibility paths, or future-proofing unless the approved spec explicitly requires them.
|
|
21
|
+
|
|
22
|
+
Minimal does not mean unsafe. Never remove or weaken:
|
|
23
|
+
|
|
24
|
+
- security checks
|
|
25
|
+
- trust-boundary validation
|
|
26
|
+
- data-loss error handling
|
|
27
|
+
- accessibility
|
|
28
|
+
- explicit user requirements
|
|
29
|
+
- required tests or validation commands
|
|
30
|
+
- SDD approval, evidence, review, or done gates
|
|
31
|
+
|
|
32
|
+
If a deliberate shortcut has a known ceiling, record the ceiling and upgrade trigger as structured Planu debt evidence instead of leaving a vague comment.
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
export const planuMinimalChangeRule = {
|
|
36
|
+
id: 'planu-minimal-change',
|
|
37
|
+
name: 'Planu Minimal Change',
|
|
38
|
+
description: 'Smallest-correct-change ladder with explicit safety boundaries.',
|
|
39
|
+
category: 'quality',
|
|
40
|
+
applicableHosts: ['all'],
|
|
41
|
+
defaultEnabled: true,
|
|
42
|
+
buildContent: (_host) => buildBody(),
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=planu-minimal-change.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface StableWriteResult {
|
|
2
|
+
written: boolean;
|
|
3
|
+
reason: 'changed' | 'missing' | 'unchanged';
|
|
4
|
+
}
|
|
5
|
+
export interface StableWriteOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Normalize content for semantic comparison. The original content is still
|
|
8
|
+
* written when formatting differs, but volatile-only differences can be skipped.
|
|
9
|
+
*/
|
|
10
|
+
equivalence?: (content: string) => string;
|
|
11
|
+
existingContent?: string | null;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=generated-artifacts.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
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",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.
|
|
38
|
-
"@planu/core-darwin-x64": "4.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.8.0",
|
|
38
|
+
"@planu/core-darwin-x64": "4.8.0",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.8.0",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.8.0",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.8.0",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.8.0",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.8.0",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.8.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"happy-dom": "^20.10.6",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"javascript-obfuscator": "^5.4.3",
|
|
197
|
-
"knip": "^6.
|
|
197
|
+
"knip": "^6.20.0",
|
|
198
198
|
"lint-staged": "^17.0.8",
|
|
199
199
|
"madge": "^8.0.0",
|
|
200
200
|
"prettier": "^3.8.4",
|
package/planu-native.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": "4.
|
|
5
|
+
"version": "4.8.0",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|