@planu/cli 5.3.69 → 5.4.1

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,20 @@
1
+ ## [5.4.1] - 2026-08-29
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1675): strip FILES/FUNCTIONS/TEST metadata markers from contradiction analysis
5
+ - fix(SPEC-1676): rebuild expired project knowledge graph and re-stamp cached reuse
6
+
7
+
8
+ ## [5.4.0] - 2026-08-29
9
+
10
+ ### Features
11
+ - feat(SPEC-1672): install phase skills with managed-by ownership marker on init hosts
12
+
13
+ ### Chores
14
+ - chore(deps): upgrade stryker to v10 majors
15
+ - chore(deps): update 13 patch/minor dependencies and adapt readFile mock casts
16
+
17
+
1
18
  ## [5.3.69] - 2026-08-29
2
19
 
3
20
  ### Features
@@ -1 +1 @@
1
- {"schemaVersion":1,"commit":"3cfd6dac20d6444035162379c6284620d6d8961a"}
1
+ {"schemaVersion":1,"commit":"c920f236390f64d91a02ca3e0566e59210f00a49"}
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: planu-dogfood-bug
3
+ description: File a bug spec the moment a Planu tool is observed behaving wrong, silent, or stale while using Planu — never deferred to a backlog or a code TODO.
4
+ triggers:
5
+ - planu bug
6
+ - this tool is wrong
7
+ - unexpected planu output
8
+ - planu returned stale data
9
+ - dogfood bug
10
+ version: 1.0.0
11
+ managed-by: planu
12
+ ---
13
+
14
+ # /planu-dogfood-bug — File a Dogfood Bug Immediately
15
+
16
+ ## When to invoke
17
+
18
+ Use this skill the moment a Planu MCP tool returns wrong, stale, silent, or irrelevant output while it is being used for its own SDD lifecycle. Invoke it in the same turn the bug is observed — never later.
19
+
20
+ **Trigger phrases**: "this tool is wrong", "unexpected Planu output", "Planu returned stale data", "dogfood bug"
21
+
22
+ ## How it works
23
+
24
+ The normative tool-call sequence for this phase:
25
+
26
+ 1. **Create the spec now** — call `create_spec` in the same turn the bug is observed, with:
27
+ - **Reproducer** — exact tool call, exact response observed, exact expected response
28
+ - **Root cause hypothesis** — pointer to file path and line number in `src/` where the bug likely lives
29
+ - BDD acceptance criteria carrying `FILES:`, `FUNCTIONS:`, and `TEST:` markers per scenario
30
+ - Files to create/modify with exact paths
31
+ - Implementation order scoped for a cheap implementer
32
+ 2. **Move to review** — call `update_status(review)` so the bug enters the normal spec lifecycle, then continue the original task
33
+
34
+ ## Anti-patterns
35
+
36
+ - **Never defer to a backlog** — "I'll note this for later" is not acceptable; file the spec in the same turn
37
+ - **Never leave a code TODO** — code TODOs rot; the tracked spec is the only durable record of the bug
38
+ - Treating a bug as "too small for a spec" — every dogfood bug becomes a spec, regardless of size
39
+ - Filing multiple observed bugs sequentially instead of in parallel `create_spec` calls in one message
40
+
41
+ ## Ownership
42
+
43
+ This skill is managed by Planu (`managed-by: planu` in its frontmatter). Removing that marker forks the skill from Planu's updates — future `init_project` runs will treat it as user-owned and stop refreshing it.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: planu-implement
3
+ description: Implement an approved Planu spec — packages the handoff contract, moves the spec to implementing, and works in an isolated worktree with revert-proof tests and no narrative comments.
4
+ triggers:
5
+ - implement the spec
6
+ - start implementing
7
+ - build this spec
8
+ - begin implementation
9
+ - implement this feature
10
+ version: 1.0.0
11
+ managed-by: planu
12
+ ---
13
+
14
+ # /planu-implement — Implement an Approved Spec
15
+
16
+ ## When to invoke
17
+
18
+ Use this skill once a spec is `approved` and implementation is about to begin.
19
+
20
+ **Trigger phrases**: "implement the spec", "start implementing", "build this spec", "begin implementation"
21
+
22
+ ## How it works
23
+
24
+ The normative tool-call sequence for this phase:
25
+
26
+ 1. **Package the handoff** — call `package_handoff` to build the operating contract for the implementer: objective, BDD acceptance criteria, files to modify/create, ownership, test plan, risks, out-of-scope, current state, next action. Persist it outside chat history
27
+ 2. **Move to implementing** — call `update_status(implementing)` only after the handoff package exists
28
+ 3. **Isolate the work** — implement inside an isolated git worktree created from the current base branch, never on the shared checkout
29
+ 4. **Write revert-proof tests** — for every guard or fix, write the test first, revert the source change and watch the test fail, then restore the source and confirm it passes. Report the observed failure output, not just the final green run
30
+ 5. **No narrative comments** — code is self-documenting; no step-by-step comments, no `(SPEC-NNNN: ...)` rationale blocks, no TODO/FIXME markers
31
+ 6. **Validate** — call `validate` when implementation is complete and inspect the completed job's `structuredContent`, not the initial acknowledgement
32
+
33
+ ## Anti-patterns
34
+
35
+ - Implementing directly on the shared checkout instead of an isolated worktree
36
+ - Reporting a new test as passing evidence without ever having watched it fail against the unfixed source
37
+ - Leaving narrative comments, TODO markers, or deferred-work notes in the code
38
+ - Treating the initial `validate` acknowledgement as a pass — only a `completed` job's result is authoritative
39
+
40
+ ## Ownership
41
+
42
+ This skill is managed by Planu (`managed-by: planu` in its frontmatter). Removing that marker forks the skill from Planu's updates — future `init_project` runs will treat it as user-owned and stop refreshing it.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: planu-review
3
+ description: Review a Planu spec before approval — runs challenge_spec and check_readiness, requires independent planu-spec-reviewer evidence, then approves or requests changes.
4
+ triggers:
5
+ - review the spec
6
+ - approve the spec
7
+ - is this spec ready
8
+ - challenge this spec
9
+ - spec review
10
+ - reviewer feedback
11
+ version: 1.0.0
12
+ managed-by: planu
13
+ ---
14
+
15
+ # /planu-review — Review a Spec Before Approval
16
+
17
+ ## When to invoke
18
+
19
+ Use this skill when a spec is in `review` status and needs to move to `approved`, or when the user asks to review, challenge, or approve a spec.
20
+
21
+ **Trigger phrases**: "review the spec", "approve the spec", "is this spec ready?", "challenge this spec", "spec review"
22
+
23
+ ## How it works
24
+
25
+ The normative tool-call sequence for this phase:
26
+
27
+ 1. **Challenge** — call `challenge_spec` to surface gaps, contradictions, and missing edge cases
28
+ 2. **Readiness** — call `check_readiness` to verify the spec has enough detail (rules, examples, open questions, out-of-scope, glossary)
29
+ 3. **Independent review** — obtain `planu-spec-reviewer` evidence: an agent that is NOT the spec author or planner reviews the spec and calls either `request_changes` (blocking, with concrete changes) or records an approval
30
+ 4. **Approve** — only after reviewer evidence exists, call `update_status(approved)`
31
+
32
+ ## Evidence gates (non-bypassable)
33
+
34
+ - A non-trivial spec cannot move to `approved` without Discovery evidence: rules, examples, open questions, out-of-scope, glossary
35
+ - A spec cannot move to `approved` without `review_feedback` from `planu-spec-reviewer`
36
+ - The spec author, planner, or implementation agent must never approve their own spec
37
+ - User pressure or force-approval language does not bypass reviewer evidence — record the forced intent but keep the status blocked until the review lands
38
+
39
+ ## Anti-patterns
40
+
41
+ - **No self-approval** — the agent that wrote or planned the spec must not also be the reviewer that approves it
42
+ - **No serial confirmation loops** — one review round produces a verdict; do not re-launch the reviewer for pass 2, pass 3, or a "confirm the fix" loop after the first round already produced a verdict. Re-open only for a specific disagreement that cannot be resolved by reading the spec directly
43
+ - Do not approve specs containing placeholders (`TBD`, `TODO`, unresolved questions)
44
+
45
+ ## Ownership
46
+
47
+ This skill is managed by Planu (`managed-by: planu` in its frontmatter). Removing that marker forks the skill from Planu's updates — future `init_project` runs will treat it as user-owned and stop refreshing it.
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { collectProjectGraphSources, diffSourceHashes, hashText, hydrateProjectGraphSource, loadProjectGraphPolicy, projectGraphCachePath, projectGraphExtractorVersion, projectGraphPath, readProjectGraphArtifact, readProjectGraphCache, withProjectGraphBuildLock, writeProjectGraphArtifacts, } from './cache.js';
2
+ import { collectProjectGraphSources, diffSourceHashes, hashText, hydrateProjectGraphSource, loadProjectGraphPolicy, projectGraphCachePath, projectGraphExtractorVersion, projectGraphPath, readProjectGraphArtifact, readProjectGraphCache, withProjectGraphBuildLock, writeProjectGraph, writeProjectGraphArtifacts, } from './cache.js';
3
3
  import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
4
4
  import { extractHandoffGraph } from './extractors/handoff-extractor.js';
5
5
  import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
@@ -210,13 +210,16 @@ async function buildProjectKnowledgeGraphUnlocked(args) {
210
210
  Array.isArray(existingGraph.nodes) &&
211
211
  Array.isArray(existingGraph.edges);
212
212
  if (hasConsistentExistingGraph && diff.changed.length === 0 && diff.removed.length === 0) {
213
+ const refreshedGeneratedAt = new Date().toISOString();
213
214
  const graphWithCurrentOversizedSources = {
214
215
  ...existingGraph,
216
+ generatedAt: refreshedGeneratedAt,
215
217
  oversizedSources,
216
218
  };
219
+ await writeProjectGraph(args.projectId, args.policy, graphWithCurrentOversizedSources);
217
220
  publishFreshProjectGraphProjection({
218
221
  projectId: args.projectId,
219
- generatedAt: existingGraph.generatedAt,
222
+ generatedAt: refreshedGeneratedAt,
220
223
  generation: existingGraph.generation,
221
224
  });
222
225
  return {
@@ -55,7 +55,8 @@ async function ensureFreshProjectGraph(args) {
55
55
  const freshness = await getProjectGraphFreshness(args);
56
56
  if (!freshness.exists ||
57
57
  freshness.reason === 'source_changed' ||
58
- freshness.reason === 'corrupt') {
58
+ freshness.reason === 'corrupt' ||
59
+ freshness.reason === 'expired') {
59
60
  await buildProjectKnowledgeGraph(args);
60
61
  return getProjectGraphFreshness(args);
61
62
  }
@@ -203,14 +203,35 @@ function assertsActionNearScope(clause, outOfScopeItem) {
203
203
  }
204
204
  return false;
205
205
  }
206
+ const METADATA_MARKER_KEYWORD = /\b(?:FILES?|FUNCTIONS?|TESTS?)\s*:/gi;
207
+ const SENTENCE_BOUNDARY_AFTER_MARKER = /\.\s+(?=[A-Z])/;
208
+ function stripMetadataMarkers(text) {
209
+ const markers = [...text.matchAll(METADATA_MARKER_KEYWORD)];
210
+ if (markers.length === 0) {
211
+ return text;
212
+ }
213
+ let result = '';
214
+ let cursor = 0;
215
+ for (let i = 0; i < markers.length; i++) {
216
+ const start = markers[i]?.index ?? 0;
217
+ result += text.slice(cursor, start);
218
+ const nextMarkerStart = markers[i + 1]?.index ?? text.length;
219
+ const searchRegion = text.slice(start, nextMarkerStart);
220
+ const boundaryMatch = SENTENCE_BOUNDARY_AFTER_MARKER.exec(searchRegion);
221
+ cursor = boundaryMatch ? start + boundaryMatch.index + 1 : nextMarkerStart;
222
+ }
223
+ result += text.slice(cursor);
224
+ return result.replace(/\s+/g, ' ').trim();
225
+ }
206
226
  /**
207
227
  * Determine whether a criterion text contradicts an out-of-scope item.
208
228
  * Uses substring match first, then keyword overlap as fuzzy fallback.
209
229
  */
210
230
  function contradicts(criterionText, outOfScopeItem) {
211
- const normCriterion = normalize(criterionText);
231
+ const strippedCriterionText = stripMetadataMarkers(criterionText);
232
+ const normCriterion = normalize(strippedCriterionText);
212
233
  const normScope = normalize(outOfScopeItem);
213
- const clauses = splitClauses(criterionText);
234
+ const clauses = splitClauses(strippedCriterionText);
214
235
  const relevantClauses = clauses.filter((clause) => clauseMentionsScope(clause, outOfScopeItem));
215
236
  const hasConditionalException = /\b(?:unless|except)\b/i.test(criterionText);
216
237
  if (!hasConditionalException &&
@@ -26,14 +26,14 @@ function resolveHost(input) {
26
26
  // ---------------------------------------------------------------------------
27
27
  // Writers per host
28
28
  // ---------------------------------------------------------------------------
29
- async function writeSkillForClaudeCode(name, content, description, projectPath, overwrite) {
29
+ async function writeSkillForClaudeCode(name, content, description, projectPath, overwrite, managedByPlanu) {
30
30
  const dirPath = join(projectPath, '.claude', 'skills', name);
31
31
  const skillFilePath = join(dirPath, 'SKILL.md');
32
32
  // Overwrite protection
33
33
  try {
34
34
  const existing = await readFile(skillFilePath, 'utf-8');
35
35
  const existingHash = hashContent(existing);
36
- const newContent = buildSkillMd(name, description, content);
36
+ const newContent = buildSkillMd(name, description, content, managedByPlanu);
37
37
  const newHash = hashContent(newContent);
38
38
  if (existingHash !== newHash && !overwrite) {
39
39
  throw new Error(`Skill already exists at ${skillFilePath}. Pass overwriteExisting: true to replace.`);
@@ -46,13 +46,14 @@ async function writeSkillForClaudeCode(name, content, description, projectPath,
46
46
  // File doesn't exist — proceed
47
47
  }
48
48
  await mkdir(dirPath, { recursive: true });
49
- const skillContent = buildSkillMd(name, description, content);
49
+ const skillContent = buildSkillMd(name, description, content, managedByPlanu);
50
50
  await writeFile(skillFilePath, skillContent, 'utf-8');
51
51
  return { dirPath, skillFilePath };
52
52
  }
53
- function buildSkillMd(name, description, body) {
53
+ function buildSkillMd(name, description, body, managedByPlanu) {
54
54
  const desc = description.length > 0 ? description : `Auto-generated skill: ${name}`;
55
- return `---\nname: ${name}\ndescription: "${desc}"\n---\n\n${body}\n`;
55
+ const managedByLine = managedByPlanu ? 'managed-by: planu\n' : '';
56
+ return `---\nname: ${name}\ndescription: "${desc}"\n${managedByLine}---\n\n${body}\n`;
56
57
  }
57
58
  async function writeSkillForCodex(name, content, description, projectPath) {
58
59
  // For Codex, append to AGENTS.md with markers
@@ -80,13 +81,13 @@ async function writeSkillForCodex(name, content, description, projectPath) {
80
81
  await writeFile(agentsMdPath, updated, 'utf-8');
81
82
  return { dirPath: projectPath, skillFilePath: agentsMdPath };
82
83
  }
83
- async function writeSkillForGemini(name, content, description, projectPath) {
84
+ async function writeSkillForGemini(name, content, description, projectPath, managedByPlanu) {
84
85
  const geminiDir = join(projectPath, '.gemini');
85
86
  const skillsDir = join(geminiDir, 'skills');
86
87
  await mkdir(skillsDir, { recursive: true });
87
88
  const skillFilePath = join(skillsDir, `${name}.md`);
88
89
  const desc = description.length > 0 ? description : name;
89
- const skillContent = buildSkillMd(name, desc, content);
90
+ const skillContent = buildSkillMd(name, desc, content, managedByPlanu);
90
91
  await writeFile(skillFilePath, skillContent, 'utf-8');
91
92
  return { dirPath: skillsDir, skillFilePath };
92
93
  }
@@ -96,6 +97,7 @@ async function writeSkillForGemini(name, content, description, projectPath) {
96
97
  export async function handleCreateSkill(input) {
97
98
  const { projectPath, name, content, overwriteExisting } = input;
98
99
  const description = input.description ?? '';
100
+ const managedByPlanu = input.managedByPlanu === true;
99
101
  const host = resolveHost(input);
100
102
  const languageValidation = validateEnglishOnlyArtifactText(`${name}\n\n${description}\n\n${content}`, 'skill');
101
103
  if (!languageValidation.ok) {
@@ -121,13 +123,13 @@ export async function handleCreateSkill(input) {
121
123
  try {
122
124
  switch (host) {
123
125
  case 'claude-code':
124
- result = await writeSkillForClaudeCode(name, content, description, projectPath, overwriteExisting === true);
126
+ result = await writeSkillForClaudeCode(name, content, description, projectPath, overwriteExisting === true, managedByPlanu);
125
127
  break;
126
128
  case 'codex':
127
129
  result = await writeSkillForCodex(name, content, description, projectPath);
128
130
  break;
129
131
  case 'gemini':
130
- result = await writeSkillForGemini(name, content, description, projectPath);
132
+ result = await writeSkillForGemini(name, content, description, projectPath, managedByPlanu);
131
133
  break;
132
134
  }
133
135
  }
@@ -12,6 +12,9 @@ const CORE_SKILL_TEMPLATES = [
12
12
  'planu-native.md',
13
13
  'planu-multi-teammate-review.md',
14
14
  'planu-context-assets.md',
15
+ 'planu-review.md',
16
+ 'planu-implement.md',
17
+ 'planu-dogfood-bug.md',
15
18
  ];
16
19
  function addHost(hosts, host) {
17
20
  if (!hosts.includes(host)) {
@@ -107,6 +110,9 @@ async function canRefreshCoreSkill(projectPath, host, name) {
107
110
  return (existing.includes(`<!-- planu:skills:${name} -->`) ||
108
111
  !existing.includes(`## Skill: ${name}`));
109
112
  }
113
+ if (existing.includes('managed-by: planu')) {
114
+ return true;
115
+ }
110
116
  return existing.includes(`name: ${name}`) && existing.includes(`# /${name}`);
111
117
  }
112
118
  catch {
@@ -130,6 +136,7 @@ async function installCoreSkillsForHost(projectPath, host) {
130
136
  description: template.description,
131
137
  content: template.content,
132
138
  overwriteExisting: true,
139
+ managedByPlanu: true,
133
140
  });
134
141
  if (result.isError === true) {
135
142
  continue;
@@ -73,6 +73,7 @@ export interface CreateSkillInput {
73
73
  host: HostId | 'auto';
74
74
  overwriteExisting?: boolean;
75
75
  description?: string;
76
+ managedByPlanu?: boolean;
76
77
  }
77
78
  /** Result of the create_skill MCP tool handler. */
78
79
  export interface CreateSkillResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.69",
3
+ "version": "5.4.1",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -129,7 +129,7 @@
129
129
  ],
130
130
  "license": "SEE LICENSE IN LICENSE",
131
131
  "dependencies": {
132
- "@anthropic-ai/sdk": "^0.120.0",
132
+ "@anthropic-ai/sdk": "^0.122.0",
133
133
  "@hono/node-server": "2.1.1",
134
134
  "@modelcontextprotocol/sdk": "^1.30.0",
135
135
  "glob": "^13.0.6",
@@ -142,40 +142,40 @@
142
142
  "@commitlint/config-conventional": "^21.2.2",
143
143
  "@eslint/js": "^10.0.1",
144
144
  "@lhci/cli": "0.15.1",
145
- "@noble/hashes": "2.3.0",
145
+ "@noble/hashes": "2.4.0",
146
146
  "@playwright/test": "1.62.1",
147
147
  "@scure/base": "2.3.0",
148
- "@secretlint/secretlint-rule-no-homedir": "^13.0.4",
149
- "@secretlint/secretlint-rule-preset-recommend": "^13.0.4",
150
- "@stryker-mutator/core": "^9.6.1",
151
- "@stryker-mutator/vitest-runner": "^9.6.1",
148
+ "@secretlint/secretlint-rule-no-homedir": "^13.0.5",
149
+ "@secretlint/secretlint-rule-preset-recommend": "^13.0.5",
150
+ "@stryker-mutator/core": "^10.0.0",
151
+ "@stryker-mutator/vitest-runner": "^10.0.0",
152
152
  "@supabase/supabase-js": "^2.112.4",
153
- "@types/node": "^26.2.0",
153
+ "@types/node": "^26.4.0",
154
154
  "@types/qrcode": "1.5.6",
155
155
  "@typescript/native": "npm:typescript@^7.0.2",
156
156
  "@vitejs/plugin-vue": "^6.0.8",
157
157
  "@vitest/coverage-v8": "^4.1.11",
158
- "@vue/test-utils": "^2.4.11",
159
- "eslint": "10.9.0",
158
+ "@vue/test-utils": "^2.5.0",
159
+ "eslint": "10.9.1",
160
160
  "eslint-config-prettier": "^10.1.8",
161
161
  "eslint-import-resolver-typescript": "^4.4.5",
162
162
  "eslint-plugin-import": "^2.32.0",
163
- "happy-dom": "^20.11.6",
163
+ "happy-dom": "^20.11.12",
164
164
  "husky": "^9.1.7",
165
165
  "javascript-obfuscator": "^5.6.0",
166
166
  "jiti": "2.7.0",
167
- "knip": "^6.32.2",
168
- "lint-staged": "^17.3.0",
167
+ "knip": "^6.32.3",
168
+ "lint-staged": "^17.4.1",
169
169
  "madge": "^8.0.0",
170
170
  "prettier": "^3.9.6",
171
171
  "qrcode": "1.5.4",
172
- "secretlint": "^13.0.4",
172
+ "secretlint": "^13.0.5",
173
173
  "tsc-alias": "^1.9.2",
174
174
  "type-coverage": "^2.30.1",
175
175
  "typescript": "npm:@typescript/typescript6@^6.0.2",
176
- "typescript-eslint": "^8.67.0",
176
+ "typescript-eslint": "^8.68.0",
177
177
  "vite": "^8.2.2",
178
178
  "vitest": "^4.1.11",
179
- "vue": "^3.5.41"
179
+ "vue": "^3.5.42"
180
180
  }
181
181
  }
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.3.69",
5
+ "version": "5.4.1",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",