@planu/cli 4.3.19 → 4.3.20

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,9 @@
1
+ ## [4.3.20] - 2026-05-27
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1073): make lifecycle state writes idempotent
5
+
6
+
1
7
  ## [4.3.19] - 2026-05-27
2
8
 
3
9
  ### Bug Fixes
@@ -7,7 +7,7 @@ import { specStore } from '../../../storage/index.js';
7
7
  const CORE_ACTION_BUDGET_MS = 2000;
8
8
  async function actuallyAppendReleasesPending(ctx, opts) {
9
9
  const { projectPath, projectId, specId } = ctx;
10
- if (!projectPath) {
10
+ if (!projectPath || ctx.newStatus !== 'done') {
11
11
  return;
12
12
  }
13
13
  if (opts.signal.aborted) {
@@ -34,6 +34,7 @@ async function actuallyAppendReleasesPending(ctx, opts) {
34
34
  /* file doesn't exist yet — start fresh */
35
35
  }
36
36
  const completedAt = new Date().toISOString().substring(0, 10);
37
+ pendingList = pendingList.filter((entry) => entry.specId !== specId);
37
38
  pendingList.push({ specId, title: spec?.title ?? specId, completedAt });
38
39
  await writeFile(pendingPath, JSON.stringify(pendingList, null, 2), 'utf-8');
39
40
  // Auto-commit planu/ changes so pending.json is never left unstaged
@@ -6,9 +6,10 @@ import { appendReleasesPending } from './core/append-releases.js';
6
6
  const FAST_HOOK_BUDGET_MS = 2000;
7
7
  export async function runCascade(ctx, opts) {
8
8
  // Core actions: awaited in parallel, each with its own 2-second budget
9
+ const releasePendingPromise = ctx.newStatus === 'done' ? appendReleasesPending(ctx) : Promise.resolve({ ok: true });
9
10
  const [sessionResult, releasesResult] = await Promise.all([
10
11
  writeSessionJson(ctx),
11
- appendReleasesPending(ctx),
12
+ releasePendingPromise,
12
13
  ]);
13
14
  const dispatched = [];
14
15
  const skipped = [];
@@ -135,6 +135,10 @@ export async function updateEpicProgressInSession(projectPath, specs) {
135
135
  catch {
136
136
  /* start fresh */
137
137
  }
138
+ const terminalSpecIds = new Set(specs
139
+ .filter((spec) => spec.status === 'done' || spec.status === 'discarded')
140
+ .map((spec) => spec.id));
141
+ index.activeSpecs = index.activeSpecs.filter((specId) => !terminalSpecIds.has(specId));
138
142
  index.epicProgress = epicProgress;
139
143
  index.updatedAt = new Date().toISOString();
140
144
  await writeFile(filePath, JSON.stringify(index, null, 2), 'utf-8');
@@ -289,42 +289,6 @@ export async function runDoneActions(projectId, specId, gitBranch) {
289
289
  clearPending(specId, 'generateSessionContext');
290
290
  }
291
291
  })(),
292
- // SPEC-649: Append spec to releases/pending.json — changelog written only at release time
293
- // SPEC-660: Dedup — pending.json append runs once per spec within 5s window
294
- (async () => {
295
- if (hasPending(specId, 'appendPending')) {
296
- return;
297
- }
298
- markPending(specId, 'appendPending');
299
- try {
300
- const { join } = await import('node:path');
301
- const { readFile, writeFile, mkdir } = await import('node:fs/promises');
302
- const releasesDir = join(projectPath, 'planu', 'releases');
303
- const pendingPath = join(releasesDir, 'pending.json');
304
- await mkdir(releasesDir, { recursive: true });
305
- const spec = await specStore.getSpec(projectId, specId);
306
- let pendingList = [];
307
- try {
308
- const raw = await readFile(pendingPath, 'utf-8');
309
- const parsed = JSON.parse(raw);
310
- if (Array.isArray(parsed)) {
311
- pendingList = parsed;
312
- }
313
- }
314
- catch {
315
- /* file doesn't exist yet — start fresh */
316
- }
317
- const completedAt = new Date().toISOString().substring(0, 10);
318
- pendingList.push({ specId, title: spec?.title ?? specId, completedAt });
319
- await writeFile(pendingPath, JSON.stringify(pendingList, null, 2), 'utf-8');
320
- }
321
- catch {
322
- /* best-effort */
323
- }
324
- finally {
325
- clearPending(specId, 'appendPending');
326
- }
327
- })(),
328
292
  // SPEC-629: Delete ephemeral prompt.md — file served its purpose once implementing starts
329
293
  (async () => {
330
294
  /* v8 ignore start */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.3.19",
3
+ "version": "4.3.20",
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.3.19",
38
- "@planu/core-darwin-x64": "4.3.19",
39
- "@planu/core-linux-arm64-gnu": "4.3.19",
40
- "@planu/core-linux-arm64-musl": "4.3.19",
41
- "@planu/core-linux-x64-gnu": "4.3.19",
42
- "@planu/core-linux-x64-musl": "4.3.19",
43
- "@planu/core-win32-arm64-msvc": "4.3.19",
44
- "@planu/core-win32-x64-msvc": "4.3.19"
37
+ "@planu/core-darwin-arm64": "4.3.20",
38
+ "@planu/core-darwin-x64": "4.3.20",
39
+ "@planu/core-linux-arm64-gnu": "4.3.20",
40
+ "@planu/core-linux-arm64-musl": "4.3.20",
41
+ "@planu/core-linux-x64-gnu": "4.3.20",
42
+ "@planu/core-linux-x64-musl": "4.3.20",
43
+ "@planu/core-win32-arm64-msvc": "4.3.20",
44
+ "@planu/core-win32-x64-msvc": "4.3.20"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
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.3.19",
4
+ "version": "4.3.20",
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.3.19",
5
+ "version": "4.3.20",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",