@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 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": ["framework", "orchestration layer", "plugin architecture", "abstract factory"],
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 { writeFile, mkdir, readFile } from 'node:fs/promises';
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
- const planuDir = join(projectPath, 'planu');
96
- await mkdir(planuDir, { recursive: true });
97
- await writeFile(join(planuDir, 'context.md'), content + '\n', 'utf-8');
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, writeFile, mkdir } from 'node:fs/promises';
4
- import { join, dirname } from 'node:path';
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
- await mkdir(dirname(filePath), { recursive: true });
72
+ let existingContent = null;
72
73
  let current = { activeSpecs: [], updatedAt: '' };
73
74
  try {
74
- const raw = await readFile(filePath, 'utf-8');
75
- current = JSON.parse(raw);
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 writeFile(filePath, JSON.stringify(updated, null, 2), 'utf-8');
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 { writeFile, readFile, mkdir } from 'node:fs/promises';
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 planuDir = join(projectPath, 'planu');
132
- await mkdir(planuDir, { recursive: true });
133
- const contextPath = join(planuDir, 'session-context.md');
134
- await writeFile(contextPath, content, 'utf-8');
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, writeFile, mkdir } from 'node:fs/promises';
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 writeFile(specFile(projectPath, specId), JSON.stringify(state, null, 2), 'utf-8');
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
- const raw = await readFile(rootFile(projectPath), 'utf-8');
77
- index = JSON.parse(raw);
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 writeFile(rootFile(projectPath), JSON.stringify(index, null, 2), 'utf-8');
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
- const raw = await readFile(filePath, 'utf-8');
133
- index = JSON.parse(raw);
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 writeFile(filePath, JSON.stringify(index, null, 2), 'utf-8');
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
- const raw = await readFile(filePath, 'utf-8');
172
- index = JSON.parse(raw);
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 writeFile(filePath, JSON.stringify(updated, null, 2), 'utf-8');
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 raw = await readFile(rootFile(projectPath), 'utf-8');
204
- const index = JSON.parse(raw);
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 writeFile(rootFile(projectPath), JSON.stringify(index, null, 2), 'utf-8');
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,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuMinimalChangeRule: UniversalRule;
3
+ //# sourceMappingURL=planu-minimal-change.d.ts.map
@@ -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
@@ -0,0 +1,3 @@
1
+ // types/generated-artifacts.ts — stable generated artifact write contracts.
2
+ export {};
3
+ //# sourceMappingURL=generated-artifacts.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.7.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.7.4",
38
- "@planu/core-darwin-x64": "4.7.4",
39
- "@planu/core-linux-arm64-gnu": "4.7.4",
40
- "@planu/core-linux-arm64-musl": "4.7.4",
41
- "@planu/core-linux-x64-gnu": "4.7.4",
42
- "@planu/core-linux-x64-musl": "4.7.4",
43
- "@planu/core-win32-arm64-msvc": "4.7.4",
44
- "@planu/core-win32-x64-msvc": "4.7.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.18.0",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.7.4",
4
+ "version": "4.8.0",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
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.7.4",
5
+ "version": "4.8.0",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",