@theglitchking/persistent-planning 3.4.0 → 3.4.2

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.
@@ -6,13 +6,13 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Official marketplace for persistent-planning - Persistent markdown-based planning for Claude Code. Context engineering pattern with task directories, progress tracking, and working memory that persists across sessions.",
9
- "version": "3.4.0"
9
+ "version": "3.4.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "persistent-planning",
14
14
  "description": "Persistent markdown-based planning for Claude Code. Uses on-disk markdown files as working memory for planning, progress tracking, and knowledge storage. Plans persist across sessions and support multiple concurrent tasks.",
15
- "version": "3.4.0",
15
+ "version": "3.4.2",
16
16
  "author": {
17
17
  "name": "TheGlitchKing"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "persistent-planning",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "Persistent markdown-based planning with task directories, progress tracking, and context engineering for multi-step Claude Code workflows",
5
5
  "author": {
6
6
  "name": "TheGlitchKing"
@@ -68,6 +68,13 @@ its own.
68
68
  failure is contained.
69
69
  - **Fails open.** Every path is wrapped. A broken migration must never cost someone
70
70
  their session start.
71
+ - **Never let a dependency be fatal.** Load the runtime with `await import()` in a
72
+ try/catch. A static import makes an unresolvable dependency fatal for the whole hook,
73
+ including the repair — the delivery path must survive what it is delivering a fix for
74
+ (#18). Only the update check genuinely needs the runtime.
75
+ - **Stamp on success, never on failure.** Marking a version done after a failed attempt
76
+ turns a transient problem into a permanent one: the repair never runs again, even once
77
+ the cause is gone. Retrying costs one spawn per session while genuinely drifted.
71
78
  - **One stdout write.** The runtime owns the single `SessionStart` JSON response and
72
79
  exposes no append hook; a second response line is invalid. Merge into the existing
73
80
  intercept.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,66 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.4.2] - 2026-09-05
6
+
7
+ ### Changed — the local `update` guard moves upstream into the runtime
8
+
9
+ 3.4.0 added a `preAction` guard in `bin/persistent-planning.js` because the
10
+ runtime's `update` resolved installation state from `<cwd>/node_modules` and
11
+ `CLAUDE_PLUGIN_ROOT` only, found neither in a marketplace-only install, and
12
+ exited 0 after an `npm update` that touched nothing. The guard worked, but it
13
+ was a workaround living in one plugin while every sibling stayed broken —
14
+ babel-fish#17 was the same defect reported against babel-fish.
15
+
16
+ Fixed properly in `@theglitchking/claude-plugin-runtime` 0.1.1, which this
17
+ release requires. `resolveInstall()` there probes npm dep → `CLAUDE_PLUGIN_ROOT`
18
+ → the marketplace record in `~/.claude/plugins/installed_plugins.json`, so the
19
+ 26-line guard is deleted rather than reimplemented.
20
+
21
+ The result is strictly better than the guard it replaces. Previously a
22
+ marketplace install satisfied the guard and fell through to a no-op `npm update`
23
+ that exited 0; now it is recognised, named by path, and refused with a pointer
24
+ at `/plugin` — the only thing that can actually update a plugin-cache copy.
25
+
26
+ The dependency floor is `^0.1.1`, not `^0.1.0`: the shared plugin dep tree at
27
+ `~/.claude/plugins/npm-cache` pins the runtime at 0.1.0 in its lockfile, and
28
+ 0.1.0 still satisfies `^0.1.0`, so the broken copy would have survived.
29
+
30
+ ### Fixed — the CLI test depended on the developer's own machine
31
+
32
+ `tests/run.sh` ran `update` in a temp workspace but inherited the real `$HOME`,
33
+ so once the runtime learned to read `~/.claude/plugins/installed_plugins.json`
34
+ the assertion passed or failed depending on whether the person running it
35
+ happened to have the plugin installed. `HOME` is now overridden, and the
36
+ marketplace branch is asserted against a fixture rather than a machine.
37
+
38
+ ## [3.4.1] - 2026-09-04
39
+
40
+ ### Fixed — the SessionStart hook could not survive a missing runtime (#18)
41
+
42
+ `hooks/session-start.js` imported `@theglitchking/claude-plugin-runtime` statically. That
43
+ package resolves out of the shared `~/.claude/plugins/npm-cache/`, which a marketplace
44
+ plugin cache does not carry itself, so when it could not resolve the **entire hook died** —
45
+ `ERR_MODULE_NOT_FOUND` before any of its work ran.
46
+
47
+ That took down the drift warning and `autoRepair` along with the update check: the two
48
+ things #10 built precisely so a fix could reach an install that is already broken. The
49
+ delivery path had a single point of failure the payload was meant to survive.
50
+
51
+ - **The runtime is loaded lazily now**, in a try/catch returning `null`. A resolution
52
+ failure costs only the update check; the completion nudge, drift warning and repair all
53
+ still run, and the hook emits its own valid `SessionStart` payload since nothing else
54
+ will.
55
+ - **A failed repair no longer stamps the version.** `autoRepair` previously stamped
56
+ `repairedSkillsForVersion` regardless of outcome, to avoid retrying every session — which
57
+ turned a *transient* failure into a permanent one. Reproduced: one session without a
58
+ resolvable runtime marked 3.4.0 done, and the repair was then skipped forever even once
59
+ the runtime returned. It stamps on success only; a failure costs one spawn per session
60
+ while genuinely drifted, and the drift warning fires alongside it naming the manual fix.
61
+
62
+ Both defects came from the #10 work and shipped in 3.4.0. Test suite grows to 125
63
+ assertions, including a fixture with no `node_modules` at all.
64
+
5
65
  ## [3.4.0] - 2026-09-04
6
66
 
7
67
  ### Changed — only completed plans are gitignored (#15)
@@ -55,32 +55,6 @@ program
55
55
  .description("Persistent markdown-based planning for Claude Code.")
56
56
  .version(version);
57
57
 
58
- // The runtime's `update` resolves the installed version, finds none in a
59
- // marketplace-only install, prints "Current: (not installed) / Now: (not installed)"
60
- // and exits 0 -- a clean success for a no-op. Fail before it gets that far, and say
61
- // where we looked. Same defect as runRelink's missing third probe (issue #10, fix #4).
62
- // Deliberately NOT including the package this CLI runs from: under
63
- // `npx --no @theglitchking/persistent-planning update` that copy always exists, which
64
- // is what let the original bug report see a clean exit 0 for a no-op. Only a *managed*
65
- // install -- a project dependency or a marketplace plugin root -- is updatable.
66
- function installCandidates(cwd) {
67
- const out = [join(cwd, "node_modules", "@theglitchking", "persistent-planning", "package.json")];
68
- if (process.env.CLAUDE_PLUGIN_ROOT) out.push(join(process.env.CLAUDE_PLUGIN_ROOT, "package.json"));
69
- return out;
70
- }
71
-
72
- program.hook("preAction", (_thisCmd, actionCmd) => {
73
- if (actionCmd.name() !== "update") return;
74
- const candidates = installCandidates(process.cwd());
75
- if (candidates.some((p) => existsSync(p))) return;
76
- console.error("update failed: no managed install of @theglitchking/persistent-planning found.");
77
- console.error("Looked in:");
78
- for (const p of candidates) console.error(` ${p}`);
79
- console.error("\nInstall it with `npm i -D @theglitchking/persistent-planning`,");
80
- console.error("or update the marketplace plugin with `/plugin` inside Claude Code.");
81
- process.exit(1);
82
- });
83
-
84
58
  registerUpdateCommands(program, {
85
59
  packageName: PKG,
86
60
  pluginName: "persistent-planning",
@@ -3,6 +3,11 @@
3
3
  //
4
4
  // Three jobs:
5
5
  // 1. The update check, delegated entirely to @theglitchking/claude-plugin-runtime.
6
+ // That import is dynamic on purpose: the runtime resolves out of the shared
7
+ // ~/.claude/plugins/npm-cache/, which is not guaranteed to be populated. A
8
+ // static import made an unresolvable runtime fatal for the WHOLE hook, taking
9
+ // jobs 2-4 down with it — including the repair that is the only way a fix
10
+ // reaches an already-broken install. Jobs 2-4 need no runtime at all.
6
11
  // 2. A completion nudge: if any plan under .planning/ has every box checked
7
12
  // but is still sitting in the active tree, say so, so the agent archives it
8
13
  // instead of rediscovering a finished plan next session.
@@ -14,13 +19,23 @@
14
19
  // node_modules in the plugin cache and never runs npm postinstall -- so it is
15
20
  // the only delivery path a fix can reach existing broken installs through.
16
21
 
17
- import { runSessionStart } from "@theglitchking/claude-plugin-runtime";
18
22
  import { spawnSync } from "node:child_process";
19
23
  import { dirname, join, resolve } from "node:path";
20
24
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
25
  import { fileURLToPath } from "node:url";
22
26
  import { driftedSkills, readSkillVersion } from "../scripts/lib/skill-link.js";
23
27
 
28
+ // Resolve the runtime lazily. Returns null when it cannot be loaded, which costs
29
+ // only the update check — never the drift warning or the repair.
30
+ async function loadRunSessionStart() {
31
+ try {
32
+ const mod = await import("@theglitchking/claude-plugin-runtime");
33
+ return typeof mod?.runSessionStart === "function" ? mod.runSessionStart : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
24
39
  const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
25
40
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
41
 
@@ -96,14 +111,18 @@ function autoRepair() {
96
111
  timeout: 10000,
97
112
  });
98
113
 
99
- // Stamp regardless of outcome: a repair that cannot succeed must not retry every
100
- // session. A later release moves the version and gets one fresh attempt.
114
+ // Stamp only on success. Stamping a failure turns a transient problem — an
115
+ // unresolvable runtime, a locked file — into a permanent one: the version is
116
+ // marked done and the repair never runs again, even once the cause is gone.
117
+ // A failure costs one spawn per session while genuinely drifted, and the drift
118
+ // warning fires alongside it naming the manual fix, so nobody is stuck silently.
119
+ const remaining = driftedSkills(projectRoot, packageRoot, "skills");
120
+ if (remaining.length || r.status !== 0) return ""; // let driftNudge() report it
121
+
101
122
  try {
102
123
  writeFileSync(CONFIG, JSON.stringify({ ...cfg, repairedSkillsForVersion: version }, null, 2) + "\n", "utf8");
103
- } catch { /* config unwritable — the drift warning still fires every session */ }
124
+ } catch { /* config unwritable — worst case the repair is attempted again */ }
104
125
 
105
- const remaining = driftedSkills(projectRoot, packageRoot, "skills");
106
- if (remaining.length || r.status !== 0) return ""; // let driftNudge() report it
107
126
  return "[persistent-planning] repaired a stale skill directory (originals kept as .bak-*).";
108
127
  } catch {
109
128
  return "";
@@ -147,12 +166,27 @@ function driftNudge() {
147
166
  const repaired = autoRepair();
148
167
  const notice = [planNudge(), repaired, driftNudge()].filter(Boolean).join("\n\n");
149
168
 
150
- if (!notice) {
151
- await runSessionStart({
152
- packageName: "@theglitchking/persistent-planning",
153
- pluginName: "persistent-planning",
154
- configFile: "persistent-planning.json",
155
- });
169
+ const runSessionStart = await loadRunSessionStart();
170
+ const RUNTIME_OPTS = {
171
+ packageName: "@theglitchking/persistent-planning",
172
+ pluginName: "persistent-planning",
173
+ configFile: "persistent-planning.json",
174
+ };
175
+
176
+ function emitNoticeOnly() {
177
+ // No runtime, so nobody else will emit the response. Say our piece and stop.
178
+ if (!notice) return;
179
+ process.stdout.write(
180
+ JSON.stringify({
181
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: notice },
182
+ }) + "\n"
183
+ );
184
+ }
185
+
186
+ if (!runSessionStart) {
187
+ emitNoticeOnly();
188
+ } else if (!notice) {
189
+ await runSessionStart(RUNTIME_OPTS);
156
190
  } else {
157
191
  // ponytail: the runtime owns the one SessionStart JSON response and exposes no
158
192
  // hook for appending to it, and a second response line on stdout is invalid.
@@ -179,11 +213,7 @@ if (!notice) {
179
213
  return realWrite(chunk, ...rest);
180
214
  };
181
215
 
182
- await runSessionStart({
183
- packageName: "@theglitchking/persistent-planning",
184
- pluginName: "persistent-planning",
185
- configFile: "persistent-planning.json",
186
- });
216
+ await runSessionStart(RUNTIME_OPTS);
187
217
 
188
218
  // The runtime can return without emitting (e.g. it chdir-failed). Say our piece.
189
219
  if (!merged) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theglitchking/persistent-planning",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "Persistent markdown-based planning with task directories, progress tracking, and context engineering for multi-step Claude Code workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "homepage": "https://github.com/TheGlitchKing/persistent-planning#readme",
57
57
  "dependencies": {
58
- "@theglitchking/claude-plugin-runtime": "^0.1.0",
58
+ "@theglitchking/claude-plugin-runtime": "^0.1.1",
59
59
  "commander": "^14.0.0"
60
60
  },
61
61
  "devDependencies": {