@theglitchking/persistent-planning 3.0.1 → 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 (76) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.documentation/INDEX.md +224 -0
  4. package/.documentation/README.md +90 -0
  5. package/.documentation/REGISTRY.md +80 -0
  6. package/.documentation/agents/INDEX.md +43 -0
  7. package/.documentation/agents/REGISTRY.md +38 -0
  8. package/.documentation/api/INDEX.md +43 -0
  9. package/.documentation/api/REGISTRY.md +38 -0
  10. package/.documentation/architecture/INDEX.md +50 -0
  11. package/.documentation/architecture/REGISTRY.md +57 -0
  12. package/{docs/reference.md → .documentation/architecture/context-engineering-principles.md} +20 -1
  13. package/{docs → .documentation/architecture}/lg-mode.md +33 -8
  14. package/.documentation/architecture/lg-plan-artifact-lifecycle.md +104 -0
  15. package/.documentation/architecture/skill-linking-and-reclaim.md +84 -0
  16. package/.documentation/archive/README.md +46 -0
  17. package/.documentation/backups/INDEX.md +43 -0
  18. package/.documentation/backups/REGISTRY.md +38 -0
  19. package/.documentation/database/INDEX.md +43 -0
  20. package/.documentation/database/REGISTRY.md +38 -0
  21. package/.documentation/devops/INDEX.md +43 -0
  22. package/.documentation/devops/REGISTRY.md +38 -0
  23. package/.documentation/drafts/.gitkeep +1 -0
  24. package/.documentation/features/INDEX.md +43 -0
  25. package/.documentation/features/REGISTRY.md +38 -0
  26. package/.documentation/plans/INDEX.md +43 -0
  27. package/.documentation/plans/REGISTRY.md +38 -0
  28. package/.documentation/procedures/INDEX.md +43 -0
  29. package/.documentation/procedures/REGISTRY.md +53 -0
  30. package/.documentation/procedures/plan-completion-and-archive.md +185 -0
  31. package/.documentation/procedures/plugin-update-delivery.md +99 -0
  32. package/.documentation/quickstart/INDEX.md +42 -0
  33. package/.documentation/quickstart/REGISTRY.md +51 -0
  34. package/{docs → .documentation/quickstart}/examples.md +20 -0
  35. package/.documentation/reference/INDEX.md +49 -0
  36. package/.documentation/reference/REGISTRY.md +55 -0
  37. package/.documentation/reference/mandatory-frontmatter.md +71 -0
  38. package/.documentation/reference/skill-version-marker.md +70 -0
  39. package/{docs → .documentation/reference}/workspace-json.md +11 -5
  40. package/.documentation/security/INDEX.md +43 -0
  41. package/.documentation/security/REGISTRY.md +38 -0
  42. package/.documentation/standards/INDEX.md +43 -0
  43. package/.documentation/standards/REGISTRY.md +53 -0
  44. package/{docs → .documentation/standards}/atom-granularity.md +12 -5
  45. package/.documentation/standards/mandatory-closing-phases.md +145 -0
  46. package/.documentation/testing/INDEX.md +42 -0
  47. package/.documentation/testing/REGISTRY.md +51 -0
  48. package/.documentation/testing/test-suite.md +126 -0
  49. package/.documentation/troubleshooting/INDEX.md +43 -0
  50. package/.documentation/troubleshooting/REGISTRY.md +53 -0
  51. package/.documentation/troubleshooting/plan-never-reads-complete.md +109 -0
  52. package/.documentation/troubleshooting/stale-skill-dir-drift.md +115 -0
  53. package/.documentation/workflows/INDEX.md +43 -0
  54. package/.documentation/workflows/REGISTRY.md +38 -0
  55. package/CHANGELOG.md +198 -0
  56. package/README.md +67 -7
  57. package/bin/persistent-planning.js +56 -6
  58. package/commands/archive-plan.md +66 -0
  59. package/commands/plan-status.md +62 -0
  60. package/commands/start-atom.md +3 -3
  61. package/commands/start-planning.md +2 -2
  62. package/commands/start-task.md +2 -2
  63. package/hooks/session-start.js +191 -8
  64. package/package.json +6 -3
  65. package/scripts/archive-plan.sh +143 -0
  66. package/scripts/init-atom.sh +7 -0
  67. package/scripts/init-phase.sh +58 -6
  68. package/scripts/init-planning.sh +39 -4
  69. package/scripts/init-task.sh +7 -0
  70. package/scripts/lib/planning.sh +122 -0
  71. package/scripts/lib/skill-link.js +177 -0
  72. package/scripts/link-skills.js +38 -0
  73. package/scripts/plan-status.sh +196 -0
  74. package/skills/persistent-planning/SKILL.md +37 -9
  75. package/templates/lg/phase.md +22 -3
  76. package/templates/lg/task.md +4 -3
@@ -159,3 +159,125 @@ planning_render_and_log() {
159
159
  ;;
160
160
  esac
161
161
  }
162
+
163
+ ###############################################################################
164
+ # List maintenance
165
+ #
166
+ # A phase's task list and a task's atom list are stored markdown checkboxes,
167
+ # not derived views — plan-status.sh counts them, and humans edit them. So the
168
+ # init scripts have to write them. Before this existed, init-task.sh never
169
+ # touched phase.md at all and every list was maintained by hand (issue #12).
170
+ ###############################################################################
171
+
172
+ # planning_insert_list_item <file> <section-heading> <line>
173
+ #
174
+ # Inserts <line> into the checkbox list under <section-heading> in <file>:
175
+ # - above the first item marked MANDATORY, so closers stay last by
176
+ # construction rather than by anyone remembering;
177
+ # - after the last item when the section has no MANDATORY entries;
178
+ # - dropping a "(no ... yet" placeholder, checkbox or italic, including its
179
+ # continuation lines, the first time a real item lands.
180
+ #
181
+ # Idempotent: an identical line already in the section is left alone, so
182
+ # re-running with PLANNING_FORCE=1 cannot double-insert.
183
+ #
184
+ # Anchors on the MANDATORY marker, never on line numbers — the list is a
185
+ # human-editable surface and positions do not survive contact with editing.
186
+ planning_insert_list_item() {
187
+ local file="$1" heading="$2" line="$3" tmp
188
+ [[ -f "$file" ]] || return 0
189
+
190
+ tmp="${file}.pp-tmp.$$"
191
+ awk -v heading="$heading" -v newline="$line" '
192
+ { L[NR] = $0; if ($0 == newline) seen = 1 }
193
+ END {
194
+ if (seen) { for (i = 1; i <= NR; i++) print L[i]; exit }
195
+
196
+ # Section bounds: heading .. next "## " heading (or EOF).
197
+ s = 0
198
+ for (i = 1; i <= NR; i++) {
199
+ if (L[i] == heading) { s = i; continue }
200
+ if (s && L[i] ~ /^## /) { e = i - 1; break }
201
+ }
202
+ if (!s) { for (i = 1; i <= NR; i++) print L[i]; exit }
203
+ if (!e) e = NR
204
+
205
+ # Drop the "(no ... yet" placeholder and its indented continuation lines.
206
+ for (i = s; i <= e; i++) {
207
+ if (L[i] ~ /^- \[[ xX]\] \(no / || L[i] ~ /^_\(no /) {
208
+ drop[i] = 1
209
+ for (j = i + 1; j <= e; j++) {
210
+ if (L[j] ~ /^[[:space:]]+[^[:space:]]/) drop[j] = 1; else break
211
+ }
212
+ }
213
+ }
214
+
215
+ # Anchor: first MANDATORY item, else after the last surviving list item,
216
+ # else at the end of the section.
217
+ at = 0
218
+ for (i = s; i <= e; i++)
219
+ if (!drop[i] && L[i] ~ /^- \[[ xX]\] .*MANDATORY/) { at = i; break }
220
+ if (!at) {
221
+ last = 0
222
+ for (i = s; i <= e; i++) if (!drop[i] && L[i] ~ /^- \[[ xX]\] /) last = i
223
+ if (last) {
224
+ for (j = last + 1; j <= e; j++) {
225
+ if (L[j] ~ /^[[:space:]]+[^[:space:]]/) last = j; else break
226
+ }
227
+ at = last + 1
228
+ }
229
+ }
230
+ if (!at) { at = e + 1; while (at > s && L[at-1] == "") at-- }
231
+
232
+ for (i = 1; i <= NR; i++) {
233
+ if (i == at) print newline
234
+ if (!drop[i]) print L[i]
235
+ }
236
+ if (at == NR + 1) print newline
237
+ }
238
+ ' "$file" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
239
+
240
+ if [[ -s "$tmp" ]]; then mv "$tmp" "$file"; else rm -f "$tmp"; fi
241
+ }
242
+
243
+ ###############################################################################
244
+ # .gitignore scope
245
+ #
246
+ # Only completed plans are local history. Active plans are the artifact a team
247
+ # coordinates around and that subagents read via the planning MCP — in lg mode,
248
+ # selected precisely because there IS a team, ignoring them defeats the point.
249
+ #
250
+ # v1.0.0 blanket-ignored .planning/; 3.1.0's archive-plan.sh introduced the narrow
251
+ # entry with the reasoning above but never displaced the old rule, leaving four
252
+ # shipped docs describing a behavior the code did not have (issue #15).
253
+ ###############################################################################
254
+
255
+ # planning_ensure_archive_gitignored <repo-root> [create-if-missing]
256
+ #
257
+ # Adds `.planning/.archive/` when absent. Never rewrites an existing blanket
258
+ # `.planning/` entry — that is the user's file and their call — but says once how to
259
+ # narrow it, because otherwise the plans they are about to create are invisible to
260
+ # everyone else and nothing tells them.
261
+ #
262
+ # The init scripts pass no second argument: creating a .gitignore in a repo that has
263
+ # none is not their business. archive-plan.sh passes 1 — it has just moved files into
264
+ # .archive/ and that path has to be ignored for the move to mean anything.
265
+ planning_ensure_archive_gitignored() {
266
+ local gitignore="${1}/.gitignore" create="${2:-}"
267
+ if [[ ! -f "$gitignore" ]]; then
268
+ [[ "$create" == "1" ]] || return 0
269
+ touch "$gitignore" 2>/dev/null || return 0
270
+ fi
271
+
272
+ if grep -qE '^\.planning/?$' "$gitignore" 2>/dev/null; then
273
+ planning_warn ".gitignore ignores all of .planning/ — plans will not be shared."
274
+ echo " Active plans are what teammates and subagents read. To share them," >&2
275
+ echo " replace the '.planning/' line with '.planning/.archive/'." >&2
276
+ return 0
277
+ fi
278
+
279
+ grep -qE '^\.planning/\.archive/?$' "$gitignore" 2>/dev/null && return 0
280
+
281
+ printf '\n# Completed plans retired by scripts/archive-plan.sh (local history)\n.planning/.archive/\n' >> "$gitignore"
282
+ planning_ok "Added .planning/.archive/ to .gitignore (active plans stay shareable)"
283
+ }
@@ -0,0 +1,177 @@
1
+ // Skill-dir reclaim + drift detection.
2
+ //
3
+ // Why this exists: v1's `persistent-planning install` (removed in v2.0.0) COPIED
4
+ // skills/persistent-planning/ into a consumer's .claude/skills/. The v2+ linker in
5
+ // claude-plugin-runtime only symlinks, and when it finds a real directory at the
6
+ // destination it warns and skips — forever. So v1 leftovers keep executing a frozen
7
+ // copy while the update check reports the (correct, current) plugin version. Two
8
+ // artifacts, nothing reconciling them.
9
+ //
10
+ // The fix is to reclaim the real directory BEFORE the runtime's linker runs: rename
11
+ // it aside, which leaves a clean destination the linker then symlinks normally. That
12
+ // needs no upstream change — see the phase notes for why this lives here for now.
13
+ //
14
+ // Nothing here ever deletes. A reclaimed directory is renamed to
15
+ // <name>.bak-<ISO8601>, and a failure to rename falls through to the old skip.
16
+
17
+ import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, renameSync, writeFileSync } from "node:fs";
18
+ import { join, relative, resolve, dirname } from "node:path";
19
+
20
+ export const ENV_PREFIX = "PERSISTENT_PLANNING";
21
+
22
+ function lstatSafe(p) {
23
+ try { return lstatSync(p); } catch { return null; }
24
+ }
25
+
26
+ /** Skill directory names shipped by this package. Mirrors the runtime's filter. */
27
+ export function packagedSkillNames(packageRoot, skillsSubdir = "skills") {
28
+ const dir = join(packageRoot, skillsSubdir);
29
+ if (!existsSync(dir)) return [];
30
+ try {
31
+ return readdirSync(dir, { withFileTypes: true })
32
+ .filter((d) => d.isDirectory() && !d.name.endsWith("-workspace"))
33
+ .map((d) => d.name);
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ /**
40
+ * What is actually sitting at .claude/skills/<name>, and is it ours?
41
+ *
42
+ * Returns one of:
43
+ * absent — nothing there; the linker will create the symlink
44
+ * symlink-ok — symlink pointing at this package's copy; healthy
45
+ * symlink-foreign — symlink pointing somewhere else; another install owns it
46
+ * real-dir — a real directory; v1 debris or a hand-managed copy. This is
47
+ * the drift state: nothing in the current system creates one.
48
+ */
49
+ export function describeSkill(consumerRoot, packageRoot, name, skillsSubdir = "skills") {
50
+ const dest = join(consumerRoot, ".claude", "skills", name);
51
+ const src = join(packageRoot, skillsSubdir, name);
52
+ const st = lstatSafe(dest);
53
+ if (!st) return { name, dest, src, state: "absent" };
54
+ if (st.isSymbolicLink()) {
55
+ let target = null;
56
+ try { target = readlinkSync(dest); } catch { /* unreadable link */ }
57
+ const expected = relative(dirname(dest), src);
58
+ if (target === expected) return { name, dest, src, state: "symlink-ok", target };
59
+ // A different-but-equivalent path still resolves to us; treat that as healthy.
60
+ try {
61
+ if (target && resolve(dirname(dest), target) === resolve(src)) {
62
+ return { name, dest, src, state: "symlink-ok", target };
63
+ }
64
+ } catch { /* fall through */ }
65
+ return { name, dest, src, state: "symlink-foreign", target };
66
+ }
67
+ return { name, dest, src, state: "real-dir" };
68
+ }
69
+
70
+ /** Every packaged skill's current state in a consumer repo. */
71
+ export function surveySkills(consumerRoot, packageRoot, skillsSubdir = "skills") {
72
+ return packagedSkillNames(packageRoot, skillsSubdir)
73
+ .map((n) => describeSkill(consumerRoot, packageRoot, n, skillsSubdir));
74
+ }
75
+
76
+ function backupPath(dest) {
77
+ // ISO8601, filesystem-safe. Collisions only within the same second on the same
78
+ // dir, which a suffix resolves.
79
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
80
+ let candidate = `${dest}.bak-${stamp}`;
81
+ let n = 1;
82
+ while (existsSync(candidate)) candidate = `${dest}.bak-${stamp}-${n++}`;
83
+ return candidate;
84
+ }
85
+
86
+ /**
87
+ * Rename any real directory at .claude/skills/<name> aside so the linker can symlink.
88
+ *
89
+ * Idempotent: a healthy symlink is left completely alone, so a second run is a no-op
90
+ * and produces no second backup.
91
+ *
92
+ * Returns { reclaimed: [{name, from, to}], failed: [{name, dest, error}], healthy: n }.
93
+ */
94
+ export function reclaimStaleSkillDirs(consumerRoot, packageRoot, opts = {}) {
95
+ const { skillsSubdir = "skills", log = () => {} } = opts;
96
+ const result = { reclaimed: [], failed: [], healthy: 0 };
97
+
98
+ if (process.env[`${ENV_PREFIX}_NO_RECLAIM`] === "1") {
99
+ log(`[persistent-planning] ${ENV_PREFIX}_NO_RECLAIM=1 — leaving existing skill dirs alone.`);
100
+ return result;
101
+ }
102
+
103
+ for (const info of surveySkills(consumerRoot, packageRoot, skillsSubdir)) {
104
+ if (info.state !== "real-dir") {
105
+ if (info.state === "symlink-ok") result.healthy++;
106
+ continue;
107
+ }
108
+ const to = backupPath(info.dest);
109
+ try {
110
+ renameSync(info.dest, to);
111
+ result.reclaimed.push({ name: info.name, from: info.dest, to });
112
+ log(`[persistent-planning] reclaimed stale skill dir ${info.dest} -> ${to}`);
113
+ } catch (err) {
114
+ // ponytail: never rm -rf a directory a user may have hand-edited. If we cannot
115
+ // move it, say so loudly and leave it — the old skip, but no longer silent.
116
+ result.failed.push({ name: info.name, dest: info.dest, error: err?.message || String(err) });
117
+ log(
118
+ `[persistent-planning] WARNING: ${info.dest} is a real directory, not a symlink,\n` +
119
+ ` so it will keep running a frozen copy of the skill. Could not move it aside:\n` +
120
+ ` ${err?.message || err}\n` +
121
+ ` Move or remove it, then run: npx persistent-planning relink`
122
+ );
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+
128
+ /** Skills still drifted after a reclaim attempt — what SessionStart should warn about. */
129
+ export function driftedSkills(consumerRoot, packageRoot, skillsSubdir = "skills") {
130
+ return surveySkills(consumerRoot, packageRoot, skillsSubdir)
131
+ .filter((s) => s.state === "real-dir" || s.state === "symlink-foreign");
132
+ }
133
+
134
+ // ── Version marker ──────────────────────────────────────────────────────────
135
+ //
136
+ // The drift that started issue #10 was invisible because the update check read the
137
+ // plugin's version while /start-planning executed the skill dir's copy — same number,
138
+ // different artifact. The primary signal is structural (describeSkill: a real dir is
139
+ // always suspect, because nothing in the current system creates one). This marker is
140
+ // the secondary signal: it says how far behind a drifted copy actually is, and it is
141
+ // the only signal available on install shapes where a symlink is impossible.
142
+
143
+ export const MARKER = ".version";
144
+
145
+ /** Version of the skill copy that would actually execute, or null. */
146
+ export function readSkillVersion(consumerRoot, name) {
147
+ try {
148
+ const raw = readFileSync(join(consumerRoot, ".claude", "skills", name, MARKER), "utf8");
149
+ return raw.trim() || null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Stamp linked skills with the version they came from.
157
+ *
158
+ * Only ever stamps a healthy symlink — never a real directory. Stamping a real dir
159
+ * would mark stale code as current and defeat the whole check. Best-effort: a
160
+ * read-only package dir (npm cache, plugin cache) just leaves the marker absent,
161
+ * which the drift check treats as "unknown", not "fine".
162
+ */
163
+ export function writeVersionMarkers(consumerRoot, packageRoot, version, opts = {}) {
164
+ const { skillsSubdir = "skills" } = opts;
165
+ if (!version) return { stamped: [], skipped: [] };
166
+ const out = { stamped: [], skipped: [] };
167
+ for (const info of surveySkills(consumerRoot, packageRoot, skillsSubdir)) {
168
+ if (info.state !== "symlink-ok") { out.skipped.push(info.name); continue; }
169
+ try {
170
+ writeFileSync(join(info.dest, MARKER), `${version}\n`, "utf8");
171
+ out.stamped.push(info.name);
172
+ } catch {
173
+ out.skipped.push(info.name);
174
+ }
175
+ }
176
+ return out;
177
+ }
@@ -1,12 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  // Postinstall — delegates to @theglitchking/claude-plugin-runtime.
3
3
  // See https://github.com/TheGlitchKing/claude-plugin-runtime/blob/main/docs/PLUGIN_AUTHORING_SCAFFOLD.md
4
+ //
5
+ // One thing runs before the delegation: reclaimStaleSkillDirs(). The runtime's linker
6
+ // skips a destination that is a real directory rather than a symlink, and skips it
7
+ // forever — so v1 leftovers never heal. Renaming the real dir aside first leaves a
8
+ // clean destination the runtime then links normally.
4
9
 
5
10
  import { runPostinstall } from "@theglitchking/claude-plugin-runtime";
6
11
  import { dirname, resolve } from "node:path";
7
12
  import { fileURLToPath } from "node:url";
13
+ import { reclaimStaleSkillDirs, writeVersionMarkers, ENV_PREFIX } from "./lib/skill-link.js";
14
+ import { createRequire } from "node:module";
8
15
 
9
16
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
+ const consumerRoot = process.env.INIT_CWD || process.cwd();
18
+
19
+ try {
20
+ // Skip when the runtime itself would skip: dev-in-place, or linking disabled.
21
+ const devInPlace = resolve(consumerRoot) === resolve(packageRoot);
22
+ if (!devInPlace && process.env[`${ENV_PREFIX}_SKIP_LINK`] !== "1") {
23
+ const r = reclaimStaleSkillDirs(consumerRoot, packageRoot, {
24
+ skillsSubdir: "skills",
25
+ log: (m) => console.log(m),
26
+ });
27
+ if (r.reclaimed.length) {
28
+ console.log(
29
+ `[persistent-planning] reclaimed ${r.reclaimed.length} stale skill dir(s); ` +
30
+ `originals kept alongside as .bak-* — delete them once you are satisfied.`
31
+ );
32
+ }
33
+ }
34
+ } catch (err) {
35
+ // Reclaim is best-effort. A failure here must never block the install.
36
+ console.warn(`[persistent-planning] skill reclaim skipped: ${err?.message || err}`);
37
+ }
10
38
 
11
39
  try {
12
40
  runPostinstall({
@@ -21,3 +49,13 @@ try {
21
49
  } catch (err) {
22
50
  console.warn(`[persistent-planning] postinstall failed: ${err?.message || err}`);
23
51
  }
52
+
53
+ try {
54
+ // Stamp the artifact that actually executes, so drift is detectable at all.
55
+ // Only healthy symlinks are stamped — never a real dir, which would mark stale
56
+ // code as current.
57
+ const { version } = createRequire(import.meta.url)("../package.json");
58
+ writeVersionMarkers(consumerRoot, packageRoot, version, { skillsSubdir: "skills" });
59
+ } catch {
60
+ // Marker is a diagnostic aid, not a dependency. Absent is a valid state.
61
+ }
@@ -0,0 +1,196 @@
1
+ #!/bin/bash
2
+ ###############################################################################
3
+ # plan-status.sh - Report completion status for every plan under .planning/
4
+ #
5
+ # Usage:
6
+ # bash scripts/plan-status.sh # table of every active plan
7
+ # bash scripts/plan-status.sh --nudge # one line if plans are archivable, else silence
8
+ # bash scripts/plan-status.sh --complete # print the slug of each complete plan, one per line
9
+ #
10
+ # A plan is COMPLETE when every checkbox in it is checked, or when its
11
+ # top-level artifact declares `status: done` in frontmatter. Completion is
12
+ # derived from the artifacts themselves so no separate state file can drift
13
+ # away from what the plan actually says.
14
+ #
15
+ # This is the single implementation of the scan: the SessionStart hook and the
16
+ # /plan-status slash command both shell out to it.
17
+ ###############################################################################
18
+ set -uo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ source "${SCRIPT_DIR}/lib/planning.sh"
22
+
23
+ MODE="table"
24
+ case "${1:-}" in
25
+ --nudge) MODE="nudge" ;;
26
+ --complete) MODE="complete" ;;
27
+ -h|--help)
28
+ sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
29
+ exit 0
30
+ ;;
31
+ "") ;;
32
+ *)
33
+ planning_err "Unknown option: $1 (try --help)"
34
+ exit 1
35
+ ;;
36
+ esac
37
+
38
+ PLANNING="$(planning_dir)"
39
+ [[ -d "$PLANNING" ]] || { [[ "$MODE" == "table" ]] && planning_warn "No .planning/ directory here."; exit 0; }
40
+
41
+ # plan_root_artifact <plan-dir> -> path of the plan's top-level file, or empty
42
+ plan_root_artifact() {
43
+ for f in "$1/phase.md" "$1/task_plan.md" "$1/task.md"; do
44
+ [[ -f "$f" ]] && { echo "$f"; return; }
45
+ done
46
+ }
47
+
48
+ # frontmatter_field <file> <key> -> value of `<key>:` inside the leading --- block
49
+ #
50
+ # Scoped to the frontmatter on purpose. Grepping a whole file for a frontmatter field
51
+ # matches it in prose, tables and fenced examples too — a notes.md documenting the
52
+ # contract then reads as an artifact that declares it (issue #14).
53
+ frontmatter_field() {
54
+ [[ -f "$1" ]] || return
55
+ awk -v key="$2" '
56
+ NR==1 && $0 != "---" { exit }
57
+ NR>1 && $0 == "---" { exit }
58
+ index($0, key ":") == 1 {
59
+ sub("^" key ":[[:space:]]*", "")
60
+ gsub(/["\047]/, "")
61
+ sub(/[[:space:]]+$/, "")
62
+ print; exit
63
+ }' "$1"
64
+ }
65
+
66
+ # frontmatter_status <file> -> value of `status:` inside the leading --- block
67
+ frontmatter_status() {
68
+ frontmatter_field "$1" "status"
69
+ }
70
+
71
+ # Emits "<total> <checked>" for every markdown file under a plan directory.
72
+ #
73
+ # Checkboxes inside fenced code blocks are examples, not work. A plan whose notes.md
74
+ # quotes markdown — which is what notes.md is for — used to have those quoted boxes
75
+ # counted against it, and a quoted `- [ ]` inflates only the denominator, so the plan
76
+ # became permanently uncompletable with no visible reason (issue #14).
77
+ #
78
+ # An unterminated fence swallows the rest of the file. That is the safe direction:
79
+ # ambiguous content is not counted as outstanding work.
80
+ count_boxes() {
81
+ local total=0 checked=0 f out
82
+ while IFS= read -r f; do
83
+ [[ -n "$f" ]] || continue
84
+ out=$(awk '
85
+ # Fence open/close: ``` or ~~~, optionally indented, optional info string.
86
+ # A fence closes only on the same character.
87
+ /^[[:space:]]*(```|~~~)/ {
88
+ line = $0
89
+ sub(/^[[:space:]]*/, "", line)
90
+ ch = substr(line, 1, 1)
91
+ if (!infence) { infence = 1; fencechar = ch }
92
+ else if (ch == fencechar) { infence = 0 }
93
+ next
94
+ }
95
+ infence { next }
96
+ /^[[:space:]]*- \[[ xX]\] / { t++ }
97
+ /^[[:space:]]*- \[[xX]\] / { c++ }
98
+ END { print t + 0, c + 0 }
99
+ ' "$f" 2>/dev/null) || continue
100
+ total=$((total + ${out%% *}))
101
+ checked=$((checked + ${out##* }))
102
+ done < <(find "$1" -type f -name '*.md' 2>/dev/null)
103
+ echo "$total $checked"
104
+ }
105
+
106
+ # Files under a plan whose FRONTMATTER declares <key>: <value>.
107
+ frontmatter_files_with() {
108
+ local dir="$1" key="$2" want="$3" f
109
+ while IFS= read -r f; do
110
+ [[ -n "$f" ]] || continue
111
+ [[ "$(frontmatter_field "$f" "$key")" == "$want" ]] && echo "$f"
112
+ done < <(find "$dir" -type f -name '*.md' 2>/dev/null)
113
+ }
114
+
115
+ # Counts mandatory tasks under a plan that are not yet done.
116
+ #
117
+ # The two closing tasks carry `mandatory: true`. A plan cannot be COMPLETE while one
118
+ # of them is unfinished, however the boxes happen to add up — that gate was stated in
119
+ # the templates and enforced nowhere (issue #12).
120
+ #
121
+ # Additive by design: a plan with no `mandatory: true` artifacts behaves exactly as it
122
+ # did before. Retroactively reopening finished plans would be worse than the bug.
123
+ unfinished_mandatory() {
124
+ local n=0 f st
125
+ while IFS= read -r f; do
126
+ [[ -n "$f" ]] || continue
127
+ st=$(frontmatter_status "$f")
128
+ [[ "$st" == "done" || "$st" == "archived" ]] || n=$((n + 1))
129
+ done < <(frontmatter_files_with "$1" "mandatory" "true")
130
+ echo "$n"
131
+ }
132
+
133
+ COMPLETE=()
134
+ ROWS=()
135
+
136
+ for plan_path in "$PLANNING"/*/; do
137
+ [[ -d "$plan_path" ]] || continue
138
+ slug=$(basename "$plan_path")
139
+ case "$slug" in .meta|.archive|archive) continue ;; esac
140
+
141
+ root=$(plan_root_artifact "${plan_path%/}")
142
+ [[ -n "$root" ]] || continue
143
+
144
+ read -r total checked <<<"$(count_boxes "${plan_path%/}")"
145
+ status=$(frontmatter_status "$root")
146
+ blocked=$(frontmatter_files_with "${plan_path%/}" "status" "blocked" | wc -l | tr -d ' ')
147
+ pending_mandatory=$(unfinished_mandatory "${plan_path%/}")
148
+
149
+ if [[ "$status" == "archived" ]]; then
150
+ verdict="archived"
151
+ elif [[ "$status" == "done" ]] || { [[ "$total" -gt 0 ]] && [[ "$checked" -eq "$total" ]]; }; then
152
+ if [[ "$pending_mandatory" -gt 0 ]]; then
153
+ # Every box ticked, but a mandatory closer is not done. Not complete.
154
+ verdict="in progress"
155
+ else
156
+ verdict="COMPLETE"
157
+ COMPLETE+=("$slug")
158
+ fi
159
+ elif [[ "$blocked" -gt 0 ]]; then
160
+ verdict="blocked"
161
+ elif [[ "$total" -eq 0 ]]; then
162
+ verdict="empty"
163
+ else
164
+ verdict="in progress"
165
+ fi
166
+
167
+ ROWS+=("$(printf '%-38s %-12s %s/%s boxes' "$slug" "$verdict" "$checked" "$total")")
168
+ done
169
+
170
+ case "$MODE" in
171
+ complete)
172
+ printf '%s\n' "${COMPLETE[@]+"${COMPLETE[@]}"}"
173
+ ;;
174
+ nudge)
175
+ if [[ ${#COMPLETE[@]} -gt 0 ]]; then
176
+ printf 'persistent-planning: %d plan(s) complete but not archived — %s. Archive with: /archive-plan <slug>\n' \
177
+ "${#COMPLETE[@]}" "$(IFS=', '; echo "${COMPLETE[*]}")"
178
+ fi
179
+ ;;
180
+ table)
181
+ if [[ ${#ROWS[@]} -eq 0 ]]; then
182
+ planning_warn "No plans found under .planning/."
183
+ exit 0
184
+ fi
185
+ planning_log "Plans under .planning/"
186
+ echo ""
187
+ printf ' %-38s %-12s %s\n' "PLAN" "STATUS" "PROGRESS"
188
+ printf ' %s\n' "$(printf '%.0s-' {1..70})"
189
+ printf ' %s\n' "${ROWS[@]}"
190
+ echo ""
191
+ if [[ ${#COMPLETE[@]} -gt 0 ]]; then
192
+ planning_ok "${#COMPLETE[@]} plan(s) ready to archive:"
193
+ for s in "${COMPLETE[@]}"; do echo " bash scripts/archive-plan.sh $s"; done
194
+ fi
195
+ ;;
196
+ esac
@@ -160,6 +160,19 @@ Create this file FIRST for any complex task:
160
160
  - [ ] Phase 2: Research/gather information
161
161
  - [ ] Phase 3: Execute/build
162
162
  - [ ] Phase 4: Review and deliver
163
+ - [ ] Phase 5: Validate success through comprehensive testing (MANDATORY)
164
+ - [ ] 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)
165
+
166
+ > Phases 5 and 6 are mandatory and MUST stay the last two phases of this plan.
167
+ > Insert new work above them; never after. Renumber them if you add phases.
168
+ >
169
+ > - **Validate success**: prove the work with tests that fail if the change breaks.
170
+ > - **Documentation pass**: create/update/deprecate every doc this change touches --
171
+ > what it is, where it lives, how to fix it, how to operate it, why it matters.
172
+ > If hit-em-with-the-docs is installed, you MUST use it for this pass --
173
+ > `npx hewtd integrate <file> -a` to create, `npx hewtd archive <file>` to retire,
174
+ > `npx hewtd maintain --quick` to regenerate indexes. Never hand-edit INDEX.md or
175
+ > REGISTRY.md; they are generated.
163
176
 
164
177
  ## Key Questions
165
178
  1. [Question to answer]
@@ -217,17 +230,29 @@ Large outputs go to files, not context. Keep only paths in working memory.
217
230
  ### 5. Log All Errors
218
231
  Every error goes in the "Errors Encountered" section. This builds knowledge for future tasks.
219
232
 
220
- ### 6. Easy Cleanup
221
- When a task is complete, delete the task directory:
222
- ```bash
223
- rm -rf .planning/[task-name]/
224
- ```
233
+ ### 6. Archive on Completion, Don't Delete
234
+ A plan is **complete** when every checkbox in it is checked (or its top-level
235
+ artifact declares `status: done`). Completion is derived from the artifacts, so
236
+ checking the last box IS the completion signal -- there is no separate state to
237
+ update and no way for it to drift.
225
238
 
226
- Or clean up everything:
239
+ When a plan is complete, archive it rather than deleting it:
227
240
  ```bash
228
- rm -rf .planning/
241
+ /plan-status # which plans are COMPLETE?
242
+ /archive-plan [task-name] # -> .planning/.archive/[task-name]/
229
243
  ```
230
244
 
245
+ Archiving moves the plan to the gitignored `.planning/.archive/` and stamps it
246
+ `status: archived` + `archived_on`. The active `.planning/` tree then shows only
247
+ live work. Nothing is destroyed -- restore with
248
+ `mv .planning/.archive/[task-name] .planning/[task-name]`.
249
+
250
+ The SessionStart hook checks for complete-but-unarchived plans and says so at the
251
+ top of the session, so a finished plan gets retired instead of rediscovered.
252
+
253
+ Deleting is still available (`rm -rf .planning/[task-name]/`) but throws away the
254
+ record of what was done and why. Prefer the archive.
255
+
231
256
  ## When to Use This Pattern
232
257
 
233
258
  **Use 3-file pattern for:**
@@ -254,14 +279,17 @@ rm -rf .planning/
254
279
  | Overwrite previous plans | Use `/start-planning` (auto-creates unique task dir) |
255
280
  | Start executing immediately | Run `/start-planning "task name"` FIRST |
256
281
  | Switch tasks without saving | Update task_plan.md before switching |
282
+ | `rm -rf` a finished plan | `/archive-plan <slug>` -- keeps the record, clears the tree |
283
+ | Leave finished plans in `.planning/` | Archive them; the active tree should show only live work |
284
+ | Track completion in your head or in chat | Check the boxes -- the artifact is the status |
257
285
 
258
286
  ## Advanced Patterns
259
287
 
260
- See [reference.md](../docs/reference.md) for:
288
+ See [Manus Context Engineering Principles](../../.documentation/architecture/context-engineering-principles.md) for:
261
289
  - Attention manipulation techniques
262
290
  - Error recovery patterns
263
291
  - Context optimization principles
264
292
 
265
- See [examples.md](../docs/examples.md) for:
293
+ See [Worked Examples](../../.documentation/quickstart/examples.md) for:
266
294
  - Real task examples
267
295
  - Complex workflow patterns
@@ -20,7 +20,21 @@ Each task is a bounded deliverable. Tasks default to dependencies-first scheduli
20
20
  Tasks with no inter-dependencies can be marked `parallelizable: true` in their frontmatter
21
21
  so subagent teams can pick them up concurrently.]
22
22
 
23
- - [ ] (no tasks yet — run `/start-task "Task name" --parent PHASE_SLUG_PLACEHOLDER`)
23
+ _(no tasks yet — run `/start-task "Task name" --parent PHASE_SLUG_PLACEHOLDER`)_
24
+ - [ ] **Validate success through comprehensive testing** (`validate-success-through-comprehensive-testing`) (MANDATORY — second-to-last)
25
+ - [ ] **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..** (`documentation-pass-create-update-deprecate-docs`) (MANDATORY — last)
26
+
27
+ > The two mandatory closing tasks above MUST remain the last two tasks of this phase.
28
+ > Add new tasks above them; never after. A phase cannot be marked `done` until both are
29
+ > `done`, and neither may be `parallelizable` — they gate on everything before them.
30
+ >
31
+ > - **Validate success**: prove the phase's work with tests that fail if it breaks.
32
+ > - **Documentation pass**: create/update/deprecate every doc this phase touches —
33
+ > what it is, where it lives, how to fix it, how to operate it, why it matters.
34
+ > If hit-em-with-the-docs is installed, you MUST use it for this pass --
35
+ > `npx hewtd integrate <file> -a` to create, `npx hewtd archive <file>` to retire,
36
+ > `npx hewtd maintain --quick` to regenerate indexes. Never hand-edit INDEX.md or
37
+ > REGISTRY.md; they are generated.
24
38
 
25
39
  ## Decisions Made
26
40
  [Phase-level decisions that affect multiple tasks. Format: `**Decision**: rationale`.]
@@ -30,8 +44,13 @@ so subagent teams can pick them up concurrently.]
30
44
 
31
45
  Status enum: `draft | active | paused | done | archived`
32
46
 
33
- When all tasks are `done`, mark this phase `done`. When archived, this directory and
34
- its tasks/atoms move to `.planning/archive/PHASE_SLUG_PLACEHOLDER/`.
47
+ When all tasks are `done`, mark this phase `done`. Checking every box in this phase
48
+ IS the completion signal — `/plan-status` reads it straight off the artifacts.
49
+
50
+ When complete, retire it with `/archive-plan PHASE_SLUG_PLACEHOLDER`: the whole
51
+ directory (tasks and atoms with it) moves to the gitignored
52
+ `.planning/.archive/PHASE_SLUG_PLACEHOLDER/` and the frontmatter above is stamped
53
+ `status: archived` + `archived_on`. Nothing is destroyed.
35
54
 
36
55
  ---
37
56