@theglitchking/persistent-planning 3.1.0 → 3.4.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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.documentation/INDEX.md +15 -7
  4. package/.documentation/REGISTRY.md +2 -2
  5. package/.documentation/agents/INDEX.md +2 -2
  6. package/.documentation/agents/REGISTRY.md +2 -2
  7. package/.documentation/api/INDEX.md +2 -2
  8. package/.documentation/api/REGISTRY.md +2 -2
  9. package/.documentation/architecture/INDEX.md +5 -3
  10. package/.documentation/architecture/REGISTRY.md +8 -4
  11. package/.documentation/architecture/lg-mode.md +16 -0
  12. package/.documentation/architecture/lg-plan-artifact-lifecycle.md +104 -0
  13. package/.documentation/architecture/skill-linking-and-reclaim.md +84 -0
  14. package/.documentation/backups/INDEX.md +2 -2
  15. package/.documentation/backups/REGISTRY.md +2 -2
  16. package/.documentation/database/INDEX.md +2 -2
  17. package/.documentation/database/REGISTRY.md +2 -2
  18. package/.documentation/devops/INDEX.md +2 -2
  19. package/.documentation/devops/REGISTRY.md +2 -2
  20. package/.documentation/features/INDEX.md +2 -2
  21. package/.documentation/features/REGISTRY.md +2 -2
  22. package/.documentation/plans/INDEX.md +2 -2
  23. package/.documentation/plans/REGISTRY.md +2 -2
  24. package/.documentation/procedures/INDEX.md +4 -3
  25. package/.documentation/procedures/REGISTRY.md +6 -4
  26. package/.documentation/procedures/plan-completion-and-archive.md +34 -0
  27. package/.documentation/procedures/plugin-update-delivery.md +99 -0
  28. package/.documentation/quickstart/INDEX.md +2 -2
  29. package/.documentation/quickstart/REGISTRY.md +2 -2
  30. package/.documentation/reference/INDEX.md +10 -3
  31. package/.documentation/reference/REGISTRY.md +8 -4
  32. package/.documentation/reference/mandatory-frontmatter.md +71 -0
  33. package/.documentation/reference/skill-version-marker.md +70 -0
  34. package/.documentation/security/INDEX.md +2 -2
  35. package/.documentation/security/REGISTRY.md +2 -2
  36. package/.documentation/standards/INDEX.md +2 -2
  37. package/.documentation/standards/REGISTRY.md +2 -2
  38. package/.documentation/standards/mandatory-closing-phases.md +21 -1
  39. package/.documentation/testing/INDEX.md +2 -2
  40. package/.documentation/testing/REGISTRY.md +2 -2
  41. package/.documentation/testing/test-suite.md +38 -0
  42. package/.documentation/troubleshooting/INDEX.md +8 -8
  43. package/.documentation/troubleshooting/REGISTRY.md +20 -5
  44. package/.documentation/troubleshooting/plan-never-reads-complete.md +109 -0
  45. package/.documentation/troubleshooting/stale-skill-dir-drift.md +115 -0
  46. package/.documentation/workflows/INDEX.md +2 -2
  47. package/.documentation/workflows/REGISTRY.md +2 -2
  48. package/CHANGELOG.md +149 -0
  49. package/bin/persistent-planning.js +56 -6
  50. package/hooks/session-start.js +115 -3
  51. package/package.json +1 -1
  52. package/scripts/archive-plan.sh +3 -6
  53. package/scripts/init-atom.sh +7 -0
  54. package/scripts/init-phase.sh +58 -6
  55. package/scripts/init-planning.sh +18 -6
  56. package/scripts/init-task.sh +7 -0
  57. package/scripts/lib/planning.sh +122 -0
  58. package/scripts/lib/skill-link.js +177 -0
  59. package/scripts/link-skills.js +38 -0
  60. package/scripts/plan-status.sh +85 -10
  61. package/skills/persistent-planning/SKILL.md +5 -2
  62. package/templates/lg/phase.md +7 -4
  63. package/templates/lg/task.md +3 -2
@@ -3,7 +3,7 @@ title: Troubleshooting Registry
3
3
  tier: reference
4
4
  domains: [troubleshooting]
5
5
  status: active
6
- last_updated: '2026-08-31'
6
+ last_updated: '2026-09-04'
7
7
  version: '1.0.0'
8
8
  purpose: Quick reference for troubleshooting documentation
9
9
  ---
@@ -16,13 +16,28 @@ purpose: Quick reference for troubleshooting documentation
16
16
 
17
17
  | Metric | Value |
18
18
  |--------|-------|
19
- | Documents | 0 |
19
+ | Documents | 2 |
20
20
  | Category | advanced |
21
21
  | Load Priority | 6/10 |
22
22
 
23
- ## Documents
23
+ ## Active Documents
24
24
 
25
- *No documents in this domain yet.*
25
+ - [A plan won't read COMPLETE](plan-never-reads-complete.md)
26
+ - [Stale skill directory — commands run frozen code](stale-skill-dir-drift.md)
27
+
28
+ ## Recently Updated
29
+
30
+ - [A plan won't read COMPLETE](plan-never-reads-complete.md) - 2026-09-04
31
+ - [Stale skill directory — commands run frozen code](stale-skill-dir-drift.md) - 2026-09-04
32
+
33
+ ## By Status
34
+
35
+ | Status | Count |
36
+ |--------|-------|
37
+ | active | 2 |
38
+ | draft | 0 |
39
+ | deprecated | 0 |
40
+ | archived | 0 |
26
41
 
27
42
  ## Keywords
28
43
 
@@ -35,4 +50,4 @@ purpose: Quick reference for troubleshooting documentation
35
50
 
36
51
  ---
37
52
 
38
- *Last updated: 2026-08-31*
53
+ *Last updated: 2026-09-04*
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: A plan won't read COMPLETE
3
+ tier: guide
4
+ domains:
5
+ - troubleshooting
6
+ audience:
7
+ - developers
8
+ tags: []
9
+ status: active
10
+ last_updated: '2026-09-04'
11
+ version: 1.0.0
12
+ purpose: >-
13
+ Three reasons a plan will not read COMPLETE — an unfinished mandatory closer, a
14
+ placeholder counted as work, or a blocked artifact — and the fix for each.
15
+ estimated_read_time: 2 minutes
16
+ word_count: 264
17
+ last_validated: '2026-09-04'
18
+ backlinks: []
19
+ ---
20
+
21
+ # A plan won't read COMPLETE
22
+
23
+ `/plan-status` shows a plan stuck below 100%, or at `100%` but still `in progress`,
24
+ and it is not obvious what is outstanding. There are three causes with three
25
+ different fixes.
26
+
27
+ ## 1. Every box is ticked but it still says `in progress`
28
+
29
+ ```
30
+ my-phase in progress 13/13 boxes
31
+ ```
32
+
33
+ A **mandatory closing task is not done.** Ticking its checkbox in `phase.md` is not
34
+ enough — the closer's own `task.md` must carry `status: done`.
35
+
36
+ ```bash
37
+ grep -l '^mandatory: true' .planning/my-phase/*/task.md | xargs grep -H '^status:'
38
+ ```
39
+
40
+ Anything not `done` or `archived` is what is holding the plan open. Finish the work,
41
+ then set `status: done` in that task's frontmatter.
42
+
43
+ This is deliberate. A closer that was ticked but never performed is precisely the
44
+ failure the gate exists to catch — see
45
+ [`mandatory:` frontmatter](../reference/mandatory-frontmatter.md).
46
+
47
+ ## 2. The count includes a box that isn't work
48
+
49
+ ```
50
+ my-phase in progress 0/5 boxes # but only 2 real tasks exist
51
+ ```
52
+
53
+ Older plans carry the placeholder lines as **checkboxes**:
54
+
55
+ ```markdown
56
+ - [ ] (no tasks yet — run `/start-task …`)
57
+ - [ ] (no atoms yet — add inline checkboxes here for simple atoms,
58
+ or run `/start-atom …` for complex ones)
59
+ ```
60
+
61
+ `plan-status.sh` counts every `- [ ]` under a plan, so those inflate the total and the
62
+ plan cannot finish until someone ticks a line that says nothing exists.
63
+
64
+ **Fix:** delete the placeholder line, or add a real task/atom — from 3.3.0 the init
65
+ scripts remove it automatically on first insert. New plans ship the placeholders as
66
+ italic text, which is not counted.
67
+
68
+ ## 2b. Quoted markdown used to count (fixed in 3.3.1)
69
+
70
+ Before 3.3.1, three readers scanned whole files: the checkbox counter, the mandatory
71
+ gate, and blocked detection. A `notes.md` that merely *documented* the contract tripped
72
+ all three — a fenced `- [ ]` example inflated the denominator, a fenced `mandatory: true`
73
+ held the plan open with the box count reading a perfect `n/n`, and a fenced
74
+ `status: blocked` produced a false `blocked` verdict.
75
+
76
+ Frontmatter fields are now read from frontmatter only, and checkbox counting skips fenced
77
+ code blocks. If you are on an older version, move the example out of the plan or upgrade.
78
+
79
+ ## 3. Something under the plan is `status: blocked`
80
+
81
+ ```
82
+ my-phase blocked 7/20 boxes
83
+ ```
84
+
85
+ `plan-status.sh` reports `blocked` when any artifact under the plan has
86
+ `status: blocked`. Find it:
87
+
88
+ ```bash
89
+ grep -rl '^status: blocked' .planning/my-phase/
90
+ ```
91
+
92
+ That is a report, not a defect — the plan is telling you what it is waiting on.
93
+
94
+ ## Checking the whole picture
95
+
96
+ ```bash
97
+ bash scripts/plan-status.sh # table: in progress / blocked / COMPLETE / empty
98
+ bash scripts/plan-status.sh --complete # one complete slug per line, for scripts
99
+ ```
100
+
101
+ `archive-plan.sh` refuses an incomplete plan using the same check, so anything above
102
+ that blocks COMPLETE also blocks archiving. `--force` overrides it; nothing is ever
103
+ deleted either way.
104
+
105
+ ## Related
106
+
107
+ - [Plan completion and archive](../procedures/plan-completion-and-archive.md)
108
+ - [lg plan artifact lifecycle](../architecture/lg-plan-artifact-lifecycle.md)
109
+ - [Mandatory closing phases](../standards/mandatory-closing-phases.md)
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Stale skill directory — commands run frozen code
3
+ tier: guide
4
+ domains:
5
+ - troubleshooting
6
+ audience:
7
+ - developers
8
+ tags: []
9
+ status: active
10
+ last_updated: '2026-09-04'
11
+ version: 1.0.0
12
+ purpose: Symptom, cause, detection and repair for a stale .claude/skills/persistent-planning
13
+ directory that makes commands run frozen pre-3.1.0 code (issue #10).
14
+ estimated_read_time: 3 minutes
15
+ word_count: 403
16
+ last_validated: '2026-09-04'
17
+ backlinks: []
18
+ ---
19
+
20
+ # Stale skill directory — commands run frozen code
21
+
22
+ `/start-planning` produces a plan that is missing the mandatory validate and
23
+ documentation phases, and has no `## On Completion` archive block. No warning is
24
+ shown. `npx persistent-planning status` reports the current version. Everything
25
+ exits 0.
26
+
27
+ This is the failure mode from [issue #10](https://github.com/the-glitch-kingdom/persistent-planning/issues/10),
28
+ fixed in 3.2.0.
29
+
30
+ ## Symptom
31
+
32
+ A generated `task_plan.md` ends at Phase 4. A correct one ends:
33
+
34
+ ```
35
+ - [ ] Phase 5: Validate success through comprehensive testing (MANDATORY)
36
+ - [ ] Phase 6: Documentation pass -- create/update/deprecate as many docs as needed to capture what was done, where it lives, how to troubleshoot it, etc. etc.. etc.. (MANDATORY)
37
+ ```
38
+
39
+ and carries an `## On Completion` section naming `/plan-status` and `/archive-plan`.
40
+
41
+ ## Cause
42
+
43
+ `.claude/skills/persistent-planning/` is a **real directory** instead of a symlink
44
+ into the installed package. v1's `persistent-planning install` (removed in 2.0.0)
45
+ copied the skill dir. The v2+ linker only symlinks, and when it found a real
46
+ directory at the destination it warned once during `npm install` and skipped —
47
+ permanently.
48
+
49
+ The result: the update check read the *plugin's* version while `/start-planning`
50
+ executed the *skill dir's* frozen copy. Same version number, different artifact,
51
+ nothing reconciling them. `commands/start-planning.md` lists
52
+ `~/.claude/skills/persistent-planning/scripts/init-planning.sh` among its fallback
53
+ paths, and in a v1-era skill dir that file exists — so the stale copy wins.
54
+
55
+ ## Detect
56
+
57
+ **Check the shape, not the contents:**
58
+
59
+ ```bash
60
+ ls -ld .claude/skills/persistent-planning
61
+ ```
62
+
63
+ A symlink is healthy. A real directory is drift — nothing in the current system
64
+ creates one.
65
+
66
+ **Confirm how far behind, by diff:**
67
+
68
+ ```bash
69
+ diff <path-to-package>/scripts/init-planning.sh \
70
+ .claude/skills/persistent-planning/scripts/init-planning.sh
71
+ ```
72
+
73
+ > **Do not verify by keyword grep.** Counting `Phase` or `MANDATORY` occurrences
74
+ > reported the known-stale copies as healthy during triage; only `diff` exposed the
75
+ > 23-line gap. See [Skill version marker](../reference/skill-version-marker.md) for
76
+ > the version signal.
77
+
78
+ From 3.2.0 the SessionStart hook detects this for you and prints the path, the
79
+ running version vs the installed one, and the fix.
80
+
81
+ ## Fix
82
+
83
+ On 3.2.0 or later, with `updatePolicy: auto`, the next session repairs it by itself
84
+ — once per plugin version. Otherwise:
85
+
86
+ ```bash
87
+ npx persistent-planning relink
88
+ ```
89
+
90
+ Either way the original is renamed to `.claude/skills/persistent-planning.bak-<timestamp>`.
91
+ **Nothing is deleted.** Delete the backup yourself once you are satisfied.
92
+
93
+ If the directory is tracked in git — v1's installer sometimes committed it — the
94
+ repair shows up as deleted files plus a new symlink. That is a real commit for that
95
+ repo to take, and a prompt to decide whether a skill dir belongs in version control
96
+ at all.
97
+
98
+ ## When repair cannot run
99
+
100
+ `relink` reports a rename failure and leaves the directory alone rather than
101
+ deleting anything. Move it aside by hand and re-run. To manage the directory
102
+ yourself and silence the machinery:
103
+
104
+ ```bash
105
+ PERSISTENT_PLANNING_NO_RECLAIM=1 # never reclaim
106
+ PERSISTENT_PLANNING_SKIP_LINK=1 # never link at all
107
+ ```
108
+
109
+ `updatePolicy: off` also suppresses both the warning and the repair.
110
+
111
+ ## Related
112
+
113
+ - [Skill linking and reclaim](../architecture/skill-linking-and-reclaim.md) — how it works
114
+ - [Plugin update delivery](../procedures/plugin-update-delivery.md) — why the hook, not postinstall
115
+ - [Skill version marker](../reference/skill-version-marker.md) — the `.version` contract
@@ -3,7 +3,7 @@ title: Workflows Documentation Index
3
3
  tier: reference
4
4
  domains: [workflows]
5
5
  status: active
6
- last_updated: '2026-08-31'
6
+ last_updated: '2026-09-04'
7
7
  version: '1.0.0'
8
8
  purpose: Complete listing of workflows documentation
9
9
  ---
@@ -40,4 +40,4 @@ This domain covers: workflow, workflows, process, processes, flow, flowchart, se
40
40
 
41
41
  ---
42
42
 
43
- *Last updated: 2026-08-31*
43
+ *Last updated: 2026-09-04*
@@ -3,7 +3,7 @@ title: Workflows Registry
3
3
  tier: reference
4
4
  domains: [workflows]
5
5
  status: active
6
- last_updated: '2026-08-31'
6
+ last_updated: '2026-09-04'
7
7
  version: '1.0.0'
8
8
  purpose: Quick reference for workflows documentation
9
9
  ---
@@ -35,4 +35,4 @@ purpose: Quick reference for workflows documentation
35
35
 
36
36
  ---
37
37
 
38
- *Last updated: 2026-08-31*
38
+ *Last updated: 2026-09-04*
package/CHANGELOG.md CHANGED
@@ -2,6 +2,155 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.4.0] - 2026-09-04
6
+
7
+ ### Changed — only completed plans are gitignored (#15)
8
+
9
+ Both init scripts blanket-ignored the entire `.planning/` tree, a v1.0.0 behavior
10
+ (`fb35f19`) that 3.1.0 implicitly reversed and never removed. `archive-plan.sh` had
11
+ already decided only completed plans are local history, and four shipped docs —
12
+ `SKILL.md`, `lg-mode.md` (twice), `README.md` — described `.planning/.archive/` as *the*
13
+ gitignored path. The code did the opposite.
14
+
15
+ In lg mode the contradiction was sharpest: that mode is selected because ≥2 git authors
16
+ exist, plans are what subagents read via the planning MCP, and the first thing
17
+ `init-phase.sh` did was guarantee no teammate would ever receive them. The blanket entry
18
+ also suppressed `archive-plan.sh`'s narrow one — its first guard returns early when the
19
+ whole tree is ignored — so the narrow entry could never appear in a repo where
20
+ `/start-planning` had run, which is every repo.
21
+
22
+ - **Active plans are tracked; only `.planning/.archive/` is ignored.** One rule, shared by
23
+ both lg init and `archive-plan.sh` via `planning_ensure_archive_gitignored`;
24
+ `init-planning.sh` keeps its own copy since it is deliberately self-contained.
25
+ - **An existing blanket `.planning/` line is never rewritten.** It is the user's file. The
26
+ tool says once how to narrow it and moves on — otherwise the plans someone is about to
27
+ create are invisible to their team and nothing tells them.
28
+ - **Init still never creates a `.gitignore`** where none exists. `archive-plan.sh` does,
29
+ because it has just moved files into `.archive/` and that path must be ignored for the
30
+ move to mean anything.
31
+
32
+ sm plan output remains byte-identical. Test suite grows to 119 assertions.
33
+
34
+ ## [3.3.1] - 2026-09-04
35
+
36
+ ### Fixed — a plan that documented markdown could not complete (#14)
37
+
38
+ `plan-status.sh` had one correct frontmatter reader and three that scanned whole files:
39
+ the checkbox counter, the mandatory gate, and blocked detection. A `notes.md` that merely
40
+ *documented* the contract — which is what notes.md is for — tripped all three:
41
+
42
+ - a fenced `- [ ]` example inflated the denominator, so the plan could never reach
43
+ COMPLETE. A quoted `- [x]` cancelled out; a quoted `- [ ]` did not.
44
+ - a fenced `mandatory: true` held the plan open with the box count showing a perfect
45
+ `1/1` — **no visible signal at all**, and the documented triage steps pointed at a
46
+ notes file that was pure prose. Regression from #13.
47
+ - a fenced `status: blocked` produced a false `blocked` verdict, masked whenever the plan
48
+ was otherwise COMPLETE, so it surfaced only intermittently.
49
+
50
+ `archive-plan.sh` inherited all three through its pre-flight and refused to archive,
51
+ advising `--force` — wrong advice for a miscount.
52
+
53
+ The fix splits in two, because these are not the same defect:
54
+
55
+ - **Checkboxes are body content**, so counting is now fence-aware — backtick and tilde
56
+ fences, tagged or not, indented, closing only on their own character. An unterminated
57
+ fence swallows the rest of the file: ambiguous content is not counted as outstanding.
58
+ - **`status:` and `mandatory:` are frontmatter fields**, so they are read from the leading
59
+ `---` block via the existing scoped reader, generalised as `frontmatter_field`. That is
60
+ strictly stronger than fence-stripping — it also rejects the field appearing in prose or
61
+ a table.
62
+
63
+ All consumers — `archive-plan.sh`, `/plan-status`, the SessionStart hook — shell out to
64
+ `plan-status.sh` and needed no change. Test suite grows to 109 assertions.
65
+
66
+ ## [3.3.0] - 2026-09-04
67
+
68
+ ### Fixed — lg task lists were maintained by hand, and the mandatory closers were unenforceable (#12)
69
+
70
+ `init-task.sh` never wrote to `phase.md` — it referenced the file only in an existence
71
+ check — and `init-atom.sh` never wrote the atom into its task. So **every lg task list was
72
+ maintained by hand**: create six task directories and the phase still reported
73
+ `(no tasks yet)`. The "closers must stay last" rule therefore governed a list no tool
74
+ produced, and the test that claimed to cover it only ever inspected a freshly rendered
75
+ template, which can never be wrong.
76
+
77
+ - **Creating a child writes it into its parent's list.** `planning_insert_list_item` in
78
+ `scripts/lib/planning.sh` inserts above the first `MANDATORY` entry — so the closers stay
79
+ last *by construction* — appends after the last item in sections with no MANDATORY
80
+ entries, drops the placeholder on first use, is idempotent under `PLANNING_FORCE=1`, and
81
+ is section-scoped. It anchors on the MANDATORY marker, never on line numbers, because the
82
+ list is a human-editable surface.
83
+ - **The closers are real task directories now.** `init-phase.sh` scaffolds
84
+ `validate-success-through-comprehensive-testing/` and
85
+ `documentation-pass-create-update-deprecate-docs/` with `task.md` (`mandatory: true`,
86
+ `parallelizable: false`), `notes.md`, `atoms/`, and seeded atoms. Previously the two most
87
+ important tasks in every plan were the only ones with no artifact a subagent could read.
88
+ - **Placeholders stopped counting as work.** `(no tasks yet …)` and `(no atoms yet …)`
89
+ shipped as real checkboxes, and `plan-status.sh` counts every box under a plan — a fresh
90
+ phase with two tasks reported `0/5`, a plan could reach 100% by ticking lines asserting
91
+ nothing existed, and a finished phase stayed open until someone ticked "no tasks yet".
92
+ They are italic text now; `plan-status.sh` needed no change.
93
+ - **The completion gate is real.** `plan-status.sh` refuses `COMPLETE` while any
94
+ `mandatory: true` task's own `status:` is not `done`. Ticking its checkbox is not enough.
95
+ `archive-plan.sh` inherits this through its existing pre-flight rather than duplicating
96
+ the rule. Strictly additive: plans with no `mandatory:` frontmatter behave exactly as
97
+ before.
98
+
99
+ Test suite grows to 99 assertions, every new one mutating the plan before asserting.
100
+ sm mode verified byte-identical against the pre-change render. New docs:
101
+ `.documentation/architecture/lg-plan-artifact-lifecycle.md`,
102
+ `.documentation/reference/mandatory-frontmatter.md`,
103
+ `.documentation/troubleshooting/plan-never-reads-complete.md`.
104
+
105
+ ## [3.2.0] - 2026-09-04
106
+
107
+ ### Fixed — Stale skill directories run frozen code (#10)
108
+
109
+ A consumer repo could run a **pre-3.1.0 copy** of the skill indefinitely while the update
110
+ check reported the current version — no nudge, no warning, exit 0 everywhere. Plans
111
+ generated that way silently omitted the mandatory validate and documentation phases and
112
+ the entire `## On Completion` archive block: precisely the guarantees 3.1.0 was released
113
+ to make.
114
+
115
+ **Cause.** v1's `persistent-planning install` (removed in 2.0.0) *copied*
116
+ `skills/persistent-planning/` into `.claude/skills/`. The v2+ linker only symlinks, and
117
+ when it finds a real directory at the destination it warns and skips — permanently. So
118
+ `installedVersion()` reported the plugin's version while `/start-planning` executed the
119
+ skill dir's frozen copy. Two artifacts, nothing reconciling them. (The linker was never
120
+ copying, as first reported; it was refusing to reclaim what v1 left behind.)
121
+
122
+ - **Reclaim instead of skip** — `scripts/lib/skill-link.js` renames a real directory to
123
+ `<name>.bak-<ISO8601>` before the runtime's linker runs, leaving a clean destination it
124
+ then symlinks normally. Nothing is ever deleted; a failed rename falls through to the
125
+ old skip but says so loudly and names the fix. Idempotent — a healthy symlink is left
126
+ alone, so a second run makes no second backup. Opt out with
127
+ `PERSISTENT_PLANNING_NO_RECLAIM=1`.
128
+ - **The drift is detectable now.** The primary signal is structural: a real directory at
129
+ `.claude/skills/<name>` is *always* drift, because nothing in the current system creates
130
+ one. A `.version` marker beside `SKILL.md` is the secondary signal — how far behind a
131
+ drifted copy is, and the only signal available where symlinks are impossible. Only
132
+ healthy symlinks are stamped; stamping a stale copy would mark it current.
133
+ - **SessionStart reports it**, naming the path, the running vs installed version, the
134
+ user-visible consequence, and `npx persistent-planning relink`. Merged into the existing
135
+ single JSON response; fails open; silent under `updatePolicy: off`.
136
+ - **SessionStart repairs it under `updatePolicy: auto`**, once per plugin version. This is
137
+ the part that reaches the installs that are actually broken: a marketplace install has
138
+ no `node_modules` in the plugin cache and **never runs npm postinstall**, so a fix
139
+ shipped only through postinstall would reach nobody affected. The hook is the one thing
140
+ that runs every session in that shape. `nudge` warns without touching the filesystem;
141
+ `off` does nothing at all.
142
+ - **No more clean exits for no-ops.** `update` with no managed install exits 1 and lists
143
+ every path probed, instead of printing `(not installed)` twice and exiting 0. `relink`
144
+ gained `CLAUDE_PLUGIN_ROOT/scripts/` and self-package probes — it previously could not
145
+ run at all in a marketplace-only install, the shape that needs it most — and no longer
146
+ swallows the linker's exit code.
147
+
148
+ Test suite grows to 74 assertions, including a marketplace-shaped fixture with no
149
+ `node_modules`. New docs: `.documentation/troubleshooting/stale-skill-dir-drift.md`,
150
+ `.documentation/architecture/skill-linking-and-reclaim.md`,
151
+ `.documentation/reference/skill-version-marker.md`,
152
+ `.documentation/procedures/plugin-update-delivery.md`.
153
+
5
154
  ## [3.1.0] - 2026-08-31
6
155
 
7
156
  ### Added — Status tracking and completion archive (#4)
@@ -5,25 +5,49 @@ import { registerUpdateCommands } from "@theglitchking/claude-plugin-runtime";
5
5
  import { createRequire } from "node:module";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { existsSync } from "node:fs";
8
- import { join, resolve } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
9
10
 
10
11
  const require_ = createRequire(import.meta.url);
11
12
  const { version } = require_("../package.json");
12
13
 
13
14
  const PKG = "@theglitchking/persistent-planning";
14
15
 
16
+ // Where the linker can live, most specific first. The third probe matters: in a
17
+ // marketplace-only install there is no node_modules and cwd is the consumer repo,
18
+ // so the first two both miss and relink -- the one command that repairs a stale
19
+ // skill dir -- could not run at all in exactly the shape that needs it.
20
+ function linkerCandidates(cwd) {
21
+ const out = [
22
+ join(cwd, "node_modules", "@theglitchking", "persistent-planning", "scripts", "link-skills.js"),
23
+ resolve(process.cwd(), "scripts", "link-skills.js"),
24
+ ];
25
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
26
+ out.push(join(process.env.CLAUDE_PLUGIN_ROOT, "scripts", "link-skills.js"));
27
+ }
28
+ // The package this CLI is itself running from — correct for `npx <pkg> relink`.
29
+ out.push(resolve(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "link-skills.js"));
30
+ return out;
31
+ }
32
+
15
33
  function runRelink(cwd) {
16
- const linker = join(cwd, "node_modules", "@theglitchking", "persistent-planning", "scripts", "link-skills.js");
17
- const script = existsSync(linker) ? linker : resolve(process.cwd(), "scripts", "link-skills.js");
18
- if (!existsSync(script)) {
19
- console.error("link-skills.js not found — is the package installed?");
34
+ const candidates = linkerCandidates(cwd);
35
+ const script = candidates.find((p) => existsSync(p));
36
+ if (!script) {
37
+ // ponytail: never exit 0 after doing nothing — a clean success for a no-op is
38
+ // how the stale-skill-dir bug stayed invisible for five months (issue #10).
39
+ console.error("relink failed: link-skills.js not found. Looked in:");
40
+ for (const p of candidates) console.error(` ${p}`);
41
+ console.error("\nIs @theglitchking/persistent-planning installed, or CLAUDE_PLUGIN_ROOT set?");
42
+ process.exitCode = 1;
20
43
  return;
21
44
  }
22
- spawnSync(process.execPath, [script], {
45
+ const r = spawnSync(process.execPath, [script], {
23
46
  cwd,
24
47
  env: { ...process.env, INIT_CWD: cwd },
25
48
  stdio: "inherit",
26
49
  });
50
+ if (r.status !== 0) process.exitCode = r.status ?? 1;
27
51
  }
28
52
 
29
53
  program
@@ -31,6 +55,32 @@ program
31
55
  .description("Persistent markdown-based planning for Claude Code.")
32
56
  .version(version);
33
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
+
34
84
  registerUpdateCommands(program, {
35
85
  packageName: PKG,
36
86
  pluginName: "persistent-planning",