@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
@@ -1,13 +1,196 @@
1
1
  #!/usr/bin/env node
2
2
  // persistent-planning SessionStart hook.
3
- // Delegates entirely to @theglitchking/claude-plugin-runtime — this plugin
4
- // has no mcp.json wiring or project-side reconcile, so only the update
5
- // check runs.
3
+ //
4
+ // Three jobs:
5
+ // 1. The update check, delegated entirely to @theglitchking/claude-plugin-runtime.
6
+ // 2. A completion nudge: if any plan under .planning/ has every box checked
7
+ // but is still sitting in the active tree, say so, so the agent archives it
8
+ // instead of rediscovering a finished plan next session.
9
+ // 3. A drift warning: if .claude/skills/persistent-planning is a real directory
10
+ // rather than a symlink into this package, it is a frozen v1 copy and every
11
+ // /start-planning is running stale code. See issue #10.
12
+ // 4. Under updatePolicy: auto, actually repair that drift. This hook is the only
13
+ // code that runs every session in a marketplace install -- that shape has no
14
+ // node_modules in the plugin cache and never runs npm postinstall -- so it is
15
+ // the only delivery path a fix can reach existing broken installs through.
6
16
 
7
17
  import { runSessionStart } from "@theglitchking/claude-plugin-runtime";
18
+ import { spawnSync } from "node:child_process";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
+ import { fileURLToPath } from "node:url";
22
+ import { driftedSkills, readSkillVersion } from "../scripts/lib/skill-link.js";
8
23
 
9
- await runSessionStart({
10
- packageName: "@theglitchking/persistent-planning",
11
- pluginName: "persistent-planning",
12
- configFile: "persistent-planning.json",
13
- });
24
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
25
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
+
27
+ function planNudge() {
28
+ try {
29
+ if (!existsSync(join(projectRoot, ".planning"))) return "";
30
+ const script = join(packageRoot, "scripts", "plan-status.sh");
31
+ if (!existsSync(script)) return "";
32
+ const r = spawnSync("bash", [script, "--nudge"], {
33
+ cwd: projectRoot,
34
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectRoot },
35
+ encoding: "utf8",
36
+ timeout: 5000,
37
+ });
38
+ return (r.stdout || "").trim();
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+
44
+ function updatePolicy() {
45
+ try {
46
+ const raw = readFileSync(join(projectRoot, ".claude", "persistent-planning.json"), "utf8");
47
+ return JSON.parse(raw)?.updatePolicy || "nudge";
48
+ } catch {
49
+ return "nudge";
50
+ }
51
+ }
52
+
53
+ function packageVersion() {
54
+ try {
55
+ return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ const CONFIG = join(projectRoot, ".claude", "persistent-planning.json");
62
+
63
+ function readConfig() {
64
+ try { return JSON.parse(readFileSync(CONFIG, "utf8")) || {}; } catch { return {}; }
65
+ }
66
+
67
+ /**
68
+ * Repair a drifted skill dir, at most once per plugin version.
69
+ *
70
+ * Runs the package's own linker, which reclaims the stale dir, symlinks, and stamps
71
+ * the version marker. Spawned rather than imported so a failure is contained in a
72
+ * child process and cannot take the hook down with it.
73
+ *
74
+ * Returns a one-line summary to fold into the session notice, or "".
75
+ */
76
+ function autoRepair() {
77
+ try {
78
+ const policy = updatePolicy();
79
+ if (policy !== "auto") return ""; // nudge/off: warn only
80
+
81
+ // Fast path first — two lstats, and the overwhelmingly common answer is "nothing
82
+ // to do". Never pay for the migration on a healthy install.
83
+ if (!driftedSkills(projectRoot, packageRoot, "skills").length) return "";
84
+
85
+ const version = packageVersion();
86
+ const cfg = readConfig();
87
+ if (version && cfg.repairedSkillsForVersion === version) return ""; // once per version
88
+
89
+ const linker = join(packageRoot, "scripts", "link-skills.js");
90
+ if (!existsSync(linker)) return "";
91
+
92
+ const r = spawnSync(process.execPath, [linker], {
93
+ cwd: projectRoot,
94
+ env: { ...process.env, INIT_CWD: projectRoot },
95
+ encoding: "utf8",
96
+ timeout: 10000,
97
+ });
98
+
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.
101
+ try {
102
+ writeFileSync(CONFIG, JSON.stringify({ ...cfg, repairedSkillsForVersion: version }, null, 2) + "\n", "utf8");
103
+ } catch { /* config unwritable — the drift warning still fires every session */ }
104
+
105
+ const remaining = driftedSkills(projectRoot, packageRoot, "skills");
106
+ if (remaining.length || r.status !== 0) return ""; // let driftNudge() report it
107
+ return "[persistent-planning] repaired a stale skill directory (originals kept as .bak-*).";
108
+ } catch {
109
+ return "";
110
+ }
111
+ }
112
+
113
+ function driftNudge() {
114
+ try {
115
+ // A user who turned updates off did not ask to be nagged about them either.
116
+ if (updatePolicy() === "off") return "";
117
+ const drifted = driftedSkills(projectRoot, packageRoot, "skills");
118
+ if (!drifted.length) return "";
119
+
120
+ let pkgVersion = "unknown";
121
+ try {
122
+ pkgVersion = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version;
123
+ } catch { /* keep "unknown" */ }
124
+
125
+ const lines = drifted.map((d) => {
126
+ const found = readSkillVersion(projectRoot, d.name) || "unversioned (pre-3.1.0 copy)";
127
+ const why = d.state === "real-dir"
128
+ ? "is a real directory, not a symlink into the installed package"
129
+ : `is a symlink to ${d.target}, which is not this package`;
130
+ return ` - ${d.dest}\n ${why}\n running: ${found} | installed: ${pkgVersion}`;
131
+ });
132
+
133
+ return [
134
+ "[persistent-planning] STALE SKILL DIRECTORY — commands are running frozen code.",
135
+ ...lines,
136
+ " Plans generated from a pre-3.1.0 copy silently omit the mandatory validate and",
137
+ " documentation phases and the whole 'On Completion' archive block.",
138
+ " Fix: npx persistent-planning relink",
139
+ ].join("\n");
140
+ } catch {
141
+ // Fail open. A broken drift check must never cost anyone their session start.
142
+ return "";
143
+ }
144
+ }
145
+
146
+ // Repair before reporting, so a successful auto-repair does not also warn.
147
+ const repaired = autoRepair();
148
+ const notice = [planNudge(), repaired, driftNudge()].filter(Boolean).join("\n\n");
149
+
150
+ if (!notice) {
151
+ await runSessionStart({
152
+ packageName: "@theglitchking/persistent-planning",
153
+ pluginName: "persistent-planning",
154
+ configFile: "persistent-planning.json",
155
+ });
156
+ } else {
157
+ // ponytail: the runtime owns the one SessionStart JSON response and exposes no
158
+ // hook for appending to it, and a second response line on stdout is invalid.
159
+ // So intercept its single write and merge the nudge into that same payload.
160
+ // Fails open — anything unexpected is written through untouched.
161
+ const realWrite = process.stdout.write.bind(process.stdout);
162
+ let merged = false;
163
+ process.stdout.write = (chunk, ...rest) => {
164
+ if (!merged) {
165
+ try {
166
+ const payload = JSON.parse(String(chunk));
167
+ const out = payload?.hookSpecificOutput;
168
+ if (out && out.hookEventName === "SessionStart") {
169
+ out.additionalContext = out.additionalContext
170
+ ? `${out.additionalContext}\n${notice}`
171
+ : notice;
172
+ merged = true;
173
+ return realWrite(JSON.stringify(payload) + "\n", ...rest);
174
+ }
175
+ } catch {
176
+ // Not the response payload — fall through and write it as-is.
177
+ }
178
+ }
179
+ return realWrite(chunk, ...rest);
180
+ };
181
+
182
+ await runSessionStart({
183
+ packageName: "@theglitchking/persistent-planning",
184
+ pluginName: "persistent-planning",
185
+ configFile: "persistent-planning.json",
186
+ });
187
+
188
+ // The runtime can return without emitting (e.g. it chdir-failed). Say our piece.
189
+ if (!merged) {
190
+ realWrite(
191
+ JSON.stringify({
192
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: notice },
193
+ }) + "\n"
194
+ );
195
+ }
196
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@theglitchking/persistent-planning",
3
- "version": "3.0.1",
3
+ "version": "3.4.0",
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": {
7
7
  "persistent-planning": "bin/persistent-planning.js"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "node scripts/link-skills.js"
10
+ "postinstall": "node scripts/link-skills.js",
11
+ "test": "bash tests/run.sh"
11
12
  },
12
13
  "files": [
13
14
  "bin/",
@@ -16,7 +17,8 @@
16
17
  "scripts/",
17
18
  "templates/",
18
19
  "skills/",
19
- "docs/",
20
+ ".documentation/",
21
+ "!.documentation/reports",
20
22
  ".claude-plugin/",
21
23
  "README.md",
22
24
  "LICENSE",
@@ -57,6 +59,7 @@
57
59
  "commander": "^14.0.0"
58
60
  },
59
61
  "devDependencies": {
62
+ "@theglitchking/hit-em-with-the-docs": "^2.9.0",
60
63
  "@theglitchking/semantic-pages": "^0.10.0"
61
64
  }
62
65
  }
@@ -0,0 +1,143 @@
1
+ #!/bin/bash
2
+ ###############################################################################
3
+ # archive-plan.sh - Retire a completed plan into .planning/.archive/
4
+ #
5
+ # Usage:
6
+ # bash scripts/archive-plan.sh <plan-slug>
7
+ # bash scripts/archive-plan.sh --all-complete
8
+ # bash scripts/archive-plan.sh <plan-slug> --force # archive an unfinished plan
9
+ # bash scripts/archive-plan.sh <plan-slug> --dry-run
10
+ #
11
+ # What it does:
12
+ # 1. Refuses unless the plan is complete (every checkbox checked, or the
13
+ # top-level artifact declares `status: done`) -- override with --force.
14
+ # 2. Ensures .planning/.archive/ exists and is gitignored.
15
+ # 3. Moves .planning/<slug>/ to .planning/.archive/<slug>/ (git mv when the
16
+ # plan is tracked, so history follows it).
17
+ # 4. Stamps the plan's top-level artifact with status: archived + archived_on.
18
+ #
19
+ # Nothing is ever deleted. Restore by moving the directory back out.
20
+ ###############################################################################
21
+ set -uo pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ source "${SCRIPT_DIR}/lib/planning.sh"
25
+
26
+ SLUG=""
27
+ ALL_COMPLETE=0
28
+ FORCE=0
29
+ DRY_RUN=0
30
+
31
+ while [[ $# -gt 0 ]]; do
32
+ case "$1" in
33
+ --all-complete) ALL_COMPLETE=1; shift ;;
34
+ --force) FORCE=1; shift ;;
35
+ --dry-run) DRY_RUN=1; shift ;;
36
+ -h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
37
+ -*) planning_err "Unknown option: $1"; exit 1 ;;
38
+ *) [[ -z "$SLUG" ]] && SLUG="$1"; shift ;;
39
+ esac
40
+ done
41
+
42
+ if [[ -z "$SLUG" && $ALL_COMPLETE -eq 0 ]]; then
43
+ planning_err "A plan slug is required (or --all-complete)."
44
+ echo "Usage: bash scripts/archive-plan.sh <plan-slug> [--force] [--dry-run]"
45
+ exit 1
46
+ fi
47
+
48
+ ROOT=$(planning_root)
49
+ PLANNING="$(planning_dir)"
50
+ ARCHIVE="${PLANNING}/.archive"
51
+ TODAY=$(planning_today)
52
+
53
+ # .planning/.archive/ must be gitignored: completed plans are local history, not
54
+ # something every clone should carry. Shared with the init scripts so there is one
55
+ # rule, not two — it also warns when a blanket .planning/ entry is hiding everything.
56
+ ensure_gitignored() {
57
+ planning_ensure_archive_gitignored "${ROOT}" 1
58
+ }
59
+
60
+ plan_root_artifact() {
61
+ for f in "$1/phase.md" "$1/task_plan.md" "$1/task.md"; do
62
+ [[ -f "$f" ]] && { echo "$f"; return; }
63
+ done
64
+ }
65
+
66
+ # Stamp the plan's top-level artifact so an archived plan reads as archived even
67
+ # when someone opens the file directly, with no directory context.
68
+ stamp_archived() {
69
+ local file="$1"
70
+ [[ -f "$file" ]] || return 0
71
+ if head -1 "$file" | grep -q '^---$'; then
72
+ # Frontmatter: flip status and add archived_on before the closing delimiter.
73
+ awk -v today="$TODAY" '
74
+ NR == 1 { print; next }
75
+ !done_fm && /^status:[[:space:]]*/ { print "status: archived"; seen_status = 1; next }
76
+ !done_fm && /^---$/ {
77
+ if (!seen_status) print "status: archived"
78
+ print "archived_on: \"" today "\""
79
+ print; done_fm = 1; next
80
+ }
81
+ { print }
82
+ ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
83
+ else
84
+ printf '\n---\n\n**Archived %s** — retired by `scripts/archive-plan.sh`.\n' "$TODAY" >> "$file"
85
+ fi
86
+ }
87
+
88
+ archive_one() {
89
+ local slug="$1"
90
+ local src="${PLANNING}/${slug}"
91
+ local dest="${ARCHIVE}/${slug}"
92
+
93
+ if [[ ! -d "$src" ]]; then
94
+ planning_err "No such plan: .planning/${slug}/"
95
+ return 1
96
+ fi
97
+ if [[ -e "$dest" ]]; then
98
+ planning_err "Already archived: .planning/.archive/${slug}/ exists. Move or rename it first."
99
+ return 1
100
+ fi
101
+
102
+ if [[ $FORCE -eq 0 ]]; then
103
+ local complete
104
+ complete=$(bash "${SCRIPT_DIR}/plan-status.sh" --complete | grep -Fx "$slug" || true)
105
+ if [[ -z "$complete" ]]; then
106
+ planning_err "Plan '${slug}' is not complete — refusing to archive."
107
+ echo " Check what is outstanding with: bash scripts/plan-status.sh"
108
+ echo " Archive anyway with: bash scripts/archive-plan.sh ${slug} --force"
109
+ return 1
110
+ fi
111
+ fi
112
+
113
+ if [[ $DRY_RUN -eq 1 ]]; then
114
+ planning_log "[dry-run] would archive .planning/${slug}/ -> .planning/.archive/${slug}/"
115
+ return 0
116
+ fi
117
+
118
+ ensure_gitignored
119
+ mkdir -p "$ARCHIVE"
120
+
121
+ if git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 \
122
+ && [[ -n "$(git -C "$ROOT" ls-files ".planning/${slug}" 2>/dev/null)" ]]; then
123
+ git -C "$ROOT" mv ".planning/${slug}" ".planning/.archive/${slug}"
124
+ else
125
+ mv "$src" "$dest"
126
+ fi
127
+
128
+ stamp_archived "$(plan_root_artifact "$dest")"
129
+ planning_ok "Archived .planning/${slug}/ -> .planning/.archive/${slug}/"
130
+ }
131
+
132
+ RC=0
133
+ if [[ $ALL_COMPLETE -eq 1 ]]; then
134
+ mapfile -t SLUGS < <(bash "${SCRIPT_DIR}/plan-status.sh" --complete)
135
+ if [[ ${#SLUGS[@]} -eq 0 || -z "${SLUGS[0]}" ]]; then
136
+ planning_log "No complete plans to archive."
137
+ exit 0
138
+ fi
139
+ for s in "${SLUGS[@]}"; do archive_one "$s" || RC=1; done
140
+ else
141
+ archive_one "$SLUG" || RC=1
142
+ fi
143
+ exit $RC
@@ -130,6 +130,13 @@ planning_render_and_log \
130
130
  "ATOM_DATE_PLACEHOLDER=${TODAY}" \
131
131
  "ATOM_SEQUENCE_PLACEHOLDER=${NEXT_SEQUENCE}"
132
132
 
133
+ # Write the atom into its task's list. Same gap as init-task.sh had with
134
+ # phase.md: the atom file was created and never referenced anywhere (#12).
135
+ planning_insert_list_item \
136
+ "${TASK_DIR}/task.md" \
137
+ "## Atoms" \
138
+ "- [ ] **${ATOM_NAME}** (\`atoms/${ATOM_SLUG}.md\`, sequence: ${NEXT_SEQUENCE})"
139
+
133
140
  echo ""
134
141
  planning_ok "Atom '${ATOM_NAME}' ready"
135
142
  echo ""
@@ -125,12 +125,64 @@ planning_render_and_log \
125
125
  "NOTES_SCOPE_PLACEHOLDER=${PHASE_SLUG}" \
126
126
  "NOTES_DATE_PLACEHOLDER=${TODAY}"
127
127
 
128
- # Add .planning/ to .gitignore if missing (preserve existing behavior)
129
- GITIGNORE="${ROOT}/.gitignore"
130
- if [[ -f "$GITIGNORE" ]] && ! grep -q "^\.planning/" "$GITIGNORE"; then
131
- echo ".planning/" >> "$GITIGNORE"
132
- planning_ok "Added .planning/ to .gitignore"
133
- fi
128
+ # Scaffold the two mandatory closing tasks as real task directories.
129
+ #
130
+ # They used to exist only as two checkbox lines in phase.md, so the most important
131
+ # tasks in every plan were the only ones with no artifact for a subagent to read and
132
+ # nowhere to record decisions — and every real plan hand-built them (issue #12).
133
+ scaffold_closer() { # <slug> <title> <atom>...
134
+ local slug="$1" title="$2"; shift 2
135
+ local dir="${PHASE_DIR}/${slug}"
136
+ mkdir -p "${dir}/atoms"
137
+
138
+ planning_render_and_log \
139
+ "${TEMPLATE_DIR}/task.md" \
140
+ "${dir}/task.md" \
141
+ "closer task at ${dir}/task.md" \
142
+ "TASK_TITLE_PLACEHOLDER=${title}" \
143
+ "TASK_SLUG_PLACEHOLDER=${slug}" \
144
+ "PHASE_SLUG_PLACEHOLDER=${PHASE_SLUG}" \
145
+ "TASK_DATE_PLACEHOLDER=${TODAY}"
146
+
147
+ planning_render_and_log \
148
+ "${TEMPLATE_DIR}/notes.md" \
149
+ "${dir}/notes.md" \
150
+ "closer notes at ${dir}/notes.md" \
151
+ "NOTES_TITLE_PLACEHOLDER=${title} — Notes" \
152
+ "NOTES_SCOPE_PLACEHOLDER=${PHASE_SLUG}/${slug}" \
153
+ "NOTES_DATE_PLACEHOLDER=${TODAY}"
154
+
155
+ # mandatory: true is what plan-status.sh gates on. parallelizable stays false —
156
+ # a closer gates on everything before it.
157
+ [[ -f "${dir}/task.md" ]] && sed -i 's/^mandatory: false$/mandatory: true/' "${dir}/task.md"
158
+
159
+ local atom
160
+ for atom in "$@"; do
161
+ planning_insert_list_item "${dir}/task.md" "## Atoms" "- [ ] ${atom}"
162
+ done
163
+ }
164
+
165
+ scaffold_closer \
166
+ "validate-success-through-comprehensive-testing" \
167
+ "Validate success through comprehensive testing" \
168
+ "Prove every claim this phase makes with a test that fails if it breaks" \
169
+ "Test the invariant after mutation, not the shape of a freshly rendered template" \
170
+ "Cover the failure mode that prompted the work, not just the happy path" \
171
+ "Run the full suite green before closing this task"
172
+
173
+ scaffold_closer \
174
+ "documentation-pass-create-update-deprecate-docs" \
175
+ "Documentation pass — create/update/deprecate docs" \
176
+ "Create docs for what was built: what it is, where it lives, how it works" \
177
+ "Add a troubleshooting entry for the symptom someone will actually search for" \
178
+ "Update every existing doc this phase made wrong — a stale doc is worse than a missing one" \
179
+ "Retire superseded docs with \`npx hewtd archive <file>\` — never by deleting" \
180
+ "If hit-em-with-the-docs is installed you MUST use it: \`npx hewtd integrate <file> -a\` to create, \`npx hewtd maintain --quick\` to regenerate indexes" \
181
+ "Run \`npx hewtd audit\` and resolve every error" \
182
+ "Never hand-edit INDEX.md or REGISTRY.md — they are generated"
183
+
184
+ # Only completed plans are local history — see planning_ensure_archive_gitignored.
185
+ planning_ensure_archive_gitignored "${ROOT}"
134
186
 
135
187
  echo ""
136
188
  planning_ok "Phase '${PHASE_NAME}' ready at .planning/${PHASE_SLUG}/"
@@ -105,6 +105,19 @@ if [ ! -f "$TASK_PLAN_FILE" ]; then
105
105
  - [ ] Phase 2: Research/gather information
106
106
  - [ ] Phase 3: Execute/build
107
107
  - [ ] Phase 4: Review and deliver
108
+ - [ ] Phase 5: Validate success through comprehensive testing (MANDATORY)
109
+ - [ ] 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)
110
+
111
+ > Phases 5 and 6 are mandatory and MUST stay the last two phases of this plan.
112
+ > Insert new work above them; never after. Renumber them if you add phases.
113
+ >
114
+ > - **Validate success**: prove the work with tests that fail if the change breaks.
115
+ > - **Documentation pass**: create/update/deprecate every doc this change touches --
116
+ > what it is, where it lives, how to fix it, how to operate it, why it matters.
117
+ > If hit-em-with-the-docs is installed, you MUST use it for this pass --
118
+ > `npx hewtd integrate <file> -a` to create, `npx hewtd archive <file>` to retire,
119
+ > `npx hewtd maintain --quick` to regenerate indexes. Never hand-edit INDEX.md or
120
+ > REGISTRY.md; they are generated.
108
121
 
109
122
  ## Key Questions
110
123
  1. [Question to answer]
@@ -126,9 +139,22 @@ if [ ! -f "$TASK_PLAN_FILE" ]; then
126
139
  - Update status after each phase
127
140
  - Mark completed phases with [x]
128
141
  - Save research to notes.md instead of stuffing context
142
+
143
+ ## On Completion
144
+ This plan is complete when every checkbox above is checked. That IS the
145
+ completion signal -- there is no separate status to remember to flip.
146
+
147
+ When complete, archive it instead of deleting it:
148
+
149
+ /plan-status # confirm this plan reads COMPLETE
150
+ /archive-plan TASK_SLUG_PLACEHOLDER # -> .planning/.archive/TASK_SLUG_PLACEHOLDER/
151
+
152
+ Archiving moves this directory to the gitignored .planning/.archive/ and stamps
153
+ it archived. Nothing is destroyed.
129
154
  EOF
130
155
 
131
156
  sed -i "s/TASK_NAME_PLACEHOLDER/${TASK_NAME}/g" "$TASK_PLAN_FILE"
157
+ sed -i "s/TASK_SLUG_PLACEHOLDER/${TASK_SLUG}/g" "$TASK_PLAN_FILE"
132
158
  echo -e "${GREEN}+${NC} Created task_plan.md"
133
159
  else
134
160
  echo -e "${YELLOW}*${NC} task_plan.md already exists (not overwritten)"
@@ -182,12 +208,21 @@ else
182
208
  fi
183
209
 
184
210
  # Step 5: Update .gitignore
211
+ #
212
+ # Only completed plans are local history. Active plans are what a teammate or a
213
+ # subagent reads, so they stay shareable (issue #15). This script is deliberately
214
+ # self-contained -- no lib/planning.sh -- so the rule is duplicated here rather than
215
+ # sourced; scripts/lib/planning.sh has the lg-mode copy.
185
216
  if [ -f "${PROJECT_ROOT}/.gitignore" ]; then
186
- if ! grep -q "^\.planning/" "${PROJECT_ROOT}/.gitignore"; then
187
- echo ".planning/" >> "${PROJECT_ROOT}/.gitignore"
188
- echo -e "${GREEN}+${NC} Added .planning/ to .gitignore"
217
+ if grep -qE "^\.planning/?$" "${PROJECT_ROOT}/.gitignore"; then
218
+ echo -e "${YELLOW}*${NC} .gitignore ignores all of .planning/ -- plans will not be shared."
219
+ echo -e " Active plans are what teammates and subagents read. To share them,"
220
+ echo -e " replace the '.planning/' line with '.planning/.archive/'."
221
+ elif grep -qE "^\.planning/\.archive/?$" "${PROJECT_ROOT}/.gitignore"; then
222
+ echo -e "${YELLOW}*${NC} .planning/.archive/ already in .gitignore"
189
223
  else
190
- echo -e "${YELLOW}*${NC} .planning/ already in .gitignore"
224
+ printf '\n# Completed plans retired by scripts/archive-plan.sh (local history)\n.planning/.archive/\n' >> "${PROJECT_ROOT}/.gitignore"
225
+ echo -e "${GREEN}+${NC} Added .planning/.archive/ to .gitignore (active plans stay shareable)"
191
226
  fi
192
227
  else
193
228
  echo -e "${YELLOW}*${NC} No .gitignore found (not added)"
@@ -109,6 +109,13 @@ planning_render_and_log \
109
109
  "NOTES_SCOPE_PLACEHOLDER=${PARENT_PHASE}/${TASK_SLUG}" \
110
110
  "NOTES_DATE_PLACEHOLDER=${TODAY}"
111
111
 
112
+ # Write the task into its phase's list, above the mandatory closers. Before this,
113
+ # init-task.sh never touched phase.md and every list was maintained by hand (#12).
114
+ planning_insert_list_item \
115
+ "${PHASE_DIR}/phase.md" \
116
+ "## Tasks" \
117
+ "- [ ] **${TASK_NAME}** (\`${TASK_SLUG}\`)"
118
+
112
119
  echo ""
113
120
  planning_ok "Task '${TASK_NAME}' ready at .planning/${PARENT_PHASE}/${TASK_SLUG}/"
114
121
  echo ""