@theglitchking/persistent-planning 3.0.0 → 3.1.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 (69) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.documentation/INDEX.md +216 -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 +48 -0
  11. package/.documentation/architecture/REGISTRY.md +53 -0
  12. package/{docs/reference.md → .documentation/architecture/context-engineering-principles.md} +20 -1
  13. package/{docs → .documentation/architecture}/lg-mode.md +17 -8
  14. package/.documentation/archive/README.md +46 -0
  15. package/.documentation/backups/INDEX.md +43 -0
  16. package/.documentation/backups/REGISTRY.md +38 -0
  17. package/.documentation/database/INDEX.md +43 -0
  18. package/.documentation/database/REGISTRY.md +38 -0
  19. package/.documentation/devops/INDEX.md +43 -0
  20. package/.documentation/devops/REGISTRY.md +38 -0
  21. package/.documentation/drafts/.gitkeep +1 -0
  22. package/.documentation/features/INDEX.md +43 -0
  23. package/.documentation/features/REGISTRY.md +38 -0
  24. package/.documentation/plans/INDEX.md +43 -0
  25. package/.documentation/plans/REGISTRY.md +38 -0
  26. package/.documentation/procedures/INDEX.md +42 -0
  27. package/.documentation/procedures/REGISTRY.md +51 -0
  28. package/.documentation/procedures/plan-completion-and-archive.md +151 -0
  29. package/.documentation/quickstart/INDEX.md +42 -0
  30. package/.documentation/quickstart/REGISTRY.md +51 -0
  31. package/{docs → .documentation/quickstart}/examples.md +20 -0
  32. package/.documentation/reference/INDEX.md +42 -0
  33. package/.documentation/reference/REGISTRY.md +51 -0
  34. package/{docs → .documentation/reference}/workspace-json.md +11 -5
  35. package/.documentation/security/INDEX.md +43 -0
  36. package/.documentation/security/REGISTRY.md +38 -0
  37. package/.documentation/standards/INDEX.md +43 -0
  38. package/.documentation/standards/REGISTRY.md +53 -0
  39. package/{docs → .documentation/standards}/atom-granularity.md +12 -5
  40. package/.documentation/standards/mandatory-closing-phases.md +125 -0
  41. package/.documentation/testing/INDEX.md +42 -0
  42. package/.documentation/testing/REGISTRY.md +51 -0
  43. package/.documentation/testing/test-suite.md +88 -0
  44. package/.documentation/troubleshooting/INDEX.md +43 -0
  45. package/.documentation/troubleshooting/REGISTRY.md +38 -0
  46. package/.documentation/workflows/INDEX.md +43 -0
  47. package/.documentation/workflows/REGISTRY.md +38 -0
  48. package/CHANGELOG.md +59 -0
  49. package/README.md +67 -7
  50. package/commands/archive-plan.md +66 -0
  51. package/commands/plan-status.md +62 -0
  52. package/commands/start-atom.md +3 -3
  53. package/commands/start-planning.md +2 -2
  54. package/commands/start-task.md +2 -2
  55. package/hooks/session-start.js +79 -8
  56. package/package.json +8 -4
  57. package/scripts/archive-plan.sh +146 -0
  58. package/scripts/detect-mode.sh +67 -0
  59. package/scripts/init-atom.sh +139 -0
  60. package/scripts/init-phase.sh +141 -0
  61. package/scripts/init-planning.sh +256 -0
  62. package/scripts/init-task.sh +118 -0
  63. package/scripts/lib/planning.sh +161 -0
  64. package/scripts/plan-status.sh +121 -0
  65. package/skills/persistent-planning/SKILL.md +34 -9
  66. package/templates/lg/atom.md +45 -0
  67. package/templates/lg/notes.md +40 -0
  68. package/templates/lg/phase.md +58 -0
  69. package/templates/lg/task.md +54 -0
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: archive-plan
3
+ description: "Retire a completed plan into the gitignored .planning/.archive/ directory. Refuses to archive a plan that still has unchecked boxes unless --force is passed. Works in both sm and lg mode."
4
+ ---
5
+
6
+ # /archive-plan
7
+
8
+ Move a finished plan out of the active tree so `.planning/` only ever shows work
9
+ that is still live. Nothing is deleted — archiving is a move plus a status stamp,
10
+ and is reversed by moving the directory back.
11
+
12
+ ---
13
+
14
+ ## Usage
15
+
16
+ ```
17
+ /archive-plan <plan-slug>
18
+ /archive-plan --all-complete
19
+ /archive-plan <plan-slug> --force # archive a plan that is not finished
20
+ /archive-plan <plan-slug> --dry-run
21
+ ```
22
+
23
+ ## What To Do
24
+
25
+ When the user runs `/archive-plan`:
26
+
27
+ 1. Run the script:
28
+
29
+ ```bash
30
+ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")/../scripts"
31
+ bash "${SCRIPT_DIR}/archive-plan.sh" "<plan-slug>"
32
+ ```
33
+
34
+ If the script isn't found at that path, try in order:
35
+
36
+ ```bash
37
+ bash scripts/archive-plan.sh "<plan-slug>"
38
+ bash ~/.claude/skills/persistent-planning/scripts/archive-plan.sh "<plan-slug>"
39
+ ```
40
+
41
+ 2. If the script refuses because the plan is incomplete, **do not reach for
42
+ `--force` on your own.** Run `/plan-status`, tell the user what is still
43
+ unchecked, and let them decide.
44
+
45
+ ## What it does
46
+
47
+ 1. Verifies the plan is complete (every checkbox checked, or `status: done`).
48
+ 2. Ensures `.planning/.archive/` exists and is listed in `.gitignore` — completed
49
+ plans are local history, not something every clone carries.
50
+ 3. Moves `.planning/<slug>/` to `.planning/.archive/<slug>/`, using `git mv` when
51
+ the plan is tracked so history follows it.
52
+ 4. Stamps the plan's top-level artifact: `status: archived` plus `archived_on`
53
+ (frontmatter), or a dated footer for sm plans that have no frontmatter.
54
+
55
+ ## Restoring
56
+
57
+ ```bash
58
+ mv .planning/.archive/<slug> .planning/<slug>
59
+ ```
60
+
61
+ Then set `status:` back to `active` and drop the `archived_on` stamp.
62
+
63
+ ## See also
64
+
65
+ - `/plan-status` — see which plans are complete
66
+ - `.documentation/procedures/plan-completion-and-archive.md` — the full completion protocol
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: plan-status
3
+ description: "Show completion status for every plan under .planning/ — which are in progress, blocked, or complete and ready to archive. Works in both sm and lg mode."
4
+ ---
5
+
6
+ # /plan-status
7
+
8
+ Report where every plan stands. Completion is derived from the plan artifacts
9
+ themselves (checkbox state + frontmatter `status:`), so it cannot drift away from
10
+ what the plan actually says.
11
+
12
+ ---
13
+
14
+ ## Usage
15
+
16
+ ```
17
+ /plan-status
18
+ ```
19
+
20
+ ## What To Do
21
+
22
+ Run the status script:
23
+
24
+ ```bash
25
+ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")/../scripts"
26
+ bash "${SCRIPT_DIR}/plan-status.sh"
27
+ ```
28
+
29
+ If the script isn't found at that path, try in order:
30
+
31
+ ```bash
32
+ bash scripts/plan-status.sh
33
+ bash ~/.claude/skills/persistent-planning/scripts/plan-status.sh
34
+ ```
35
+
36
+ Then read the table back to the user. If any plan is listed `COMPLETE`, offer to
37
+ archive it with `/archive-plan <slug>`.
38
+
39
+ ## Verdicts
40
+
41
+ | Verdict | Meaning |
42
+ |---|---|
43
+ | `in progress` | Unchecked boxes remain |
44
+ | `blocked` | An atom in the plan carries `status: blocked` |
45
+ | `COMPLETE` | Every box checked, or the top-level artifact says `status: done` — ready to archive |
46
+ | `empty` | Plan artifact exists but has no checkboxes yet |
47
+ | `archived` | Already retired (only visible if left in the active tree) |
48
+
49
+ ## Machine-readable forms
50
+
51
+ ```bash
52
+ bash scripts/plan-status.sh --complete # one complete plan slug per line
53
+ bash scripts/plan-status.sh --nudge # one-line notice, or silence
54
+ ```
55
+
56
+ `--nudge` is what the SessionStart hook uses, so a finished plan gets mentioned
57
+ at the top of the next session rather than being rediscovered later.
58
+
59
+ ## See also
60
+
61
+ - `/archive-plan` — retire a completed plan into `.planning/.archive/`
62
+ - `.documentation/procedures/plan-completion-and-archive.md` — the full completion protocol
@@ -69,7 +69,7 @@ Not every step needs a standalone atom file. The rule of thumb:
69
69
  - **Inline checkbox in `task.md`**: simple, self-contained step that doesn't need its own context file. The user (or the active agent) handles it directly.
70
70
  - **Standalone atom file via `/start-atom`**: a step that will be **handed off to a subagent**. Atoms get their own files because subagents need explicit inputs / outputs / acceptance criteria they can read structured rather than parse from prose.
71
71
 
72
- See `docs/atom-granularity.md` for full anti-pattern guidance with examples.
72
+ See `.documentation/standards/atom-granularity.md` for full anti-pattern guidance with examples.
73
73
 
74
74
  ## Sequence semantics
75
75
 
@@ -89,5 +89,5 @@ This command refuses to run if the project is in sm mode. To switch:
89
89
 
90
90
  - `/start-planning` — initialize the planning structure (phase in lg)
91
91
  - `/start-task` — add a task under a phase
92
- - `docs/lg-mode.md` — full lg-mode layer model
93
- - `docs/atom-granularity.md` — when to spawn an atom file vs. inline checkbox
92
+ - `.documentation/architecture/lg-mode.md` — full lg-mode layer model
93
+ - `.documentation/standards/atom-granularity.md` — when to spawn an atom file vs. inline checkbox
@@ -111,5 +111,5 @@ Names are converted to URL-friendly slugs:
111
111
 
112
112
  - `/start-task` — add a task under a phase (lg mode only)
113
113
  - `/start-atom` — add an atom (subagent hand-off unit) under a task (lg mode only)
114
- - `docs/workspace-json.md` — workspace.json schema reference
115
- - `docs/lg-mode.md` — full lg-mode guide (phase / task / atom / notes layer model)
114
+ - `.documentation/reference/workspace-json.md` — workspace.json schema reference
115
+ - `.documentation/architecture/lg-mode.md` — full lg-mode guide (phase / task / atom / notes layer model)
@@ -79,5 +79,5 @@ That writes `.planning/.meta/workspace.json` with `mode: "lg"` and creates the f
79
79
 
80
80
  - `/start-planning` — initialize the planning structure (phase in lg, task in sm)
81
81
  - `/start-atom` — add an atom under a task
82
- - `docs/lg-mode.md` — full lg-mode layer model
83
- - `docs/atom-granularity.md` — when to spawn an atom file vs. add an inline checkbox
82
+ - `.documentation/architecture/lg-mode.md` — full lg-mode layer model
83
+ - `.documentation/standards/atom-granularity.md` — when to spawn an atom file vs. add an inline checkbox
@@ -1,13 +1,84 @@
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
+ // Two 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.
6
9
 
7
10
  import { runSessionStart } from "@theglitchking/claude-plugin-runtime";
11
+ import { spawnSync } from "node:child_process";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { existsSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
8
15
 
9
- await runSessionStart({
10
- packageName: "@theglitchking/persistent-planning",
11
- pluginName: "persistent-planning",
12
- configFile: "persistent-planning.json",
13
- });
16
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
17
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
18
+
19
+ function planNudge() {
20
+ try {
21
+ if (!existsSync(join(projectRoot, ".planning"))) return "";
22
+ const script = join(packageRoot, "scripts", "plan-status.sh");
23
+ if (!existsSync(script)) return "";
24
+ const r = spawnSync("bash", [script, "--nudge"], {
25
+ cwd: projectRoot,
26
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectRoot },
27
+ encoding: "utf8",
28
+ timeout: 5000,
29
+ });
30
+ return (r.stdout || "").trim();
31
+ } catch {
32
+ return "";
33
+ }
34
+ }
35
+
36
+ const notice = planNudge();
37
+
38
+ if (!notice) {
39
+ await runSessionStart({
40
+ packageName: "@theglitchking/persistent-planning",
41
+ pluginName: "persistent-planning",
42
+ configFile: "persistent-planning.json",
43
+ });
44
+ } else {
45
+ // ponytail: the runtime owns the one SessionStart JSON response and exposes no
46
+ // hook for appending to it, and a second response line on stdout is invalid.
47
+ // So intercept its single write and merge the nudge into that same payload.
48
+ // Fails open — anything unexpected is written through untouched.
49
+ const realWrite = process.stdout.write.bind(process.stdout);
50
+ let merged = false;
51
+ process.stdout.write = (chunk, ...rest) => {
52
+ if (!merged) {
53
+ try {
54
+ const payload = JSON.parse(String(chunk));
55
+ const out = payload?.hookSpecificOutput;
56
+ if (out && out.hookEventName === "SessionStart") {
57
+ out.additionalContext = out.additionalContext
58
+ ? `${out.additionalContext}\n${notice}`
59
+ : notice;
60
+ merged = true;
61
+ return realWrite(JSON.stringify(payload) + "\n", ...rest);
62
+ }
63
+ } catch {
64
+ // Not the response payload — fall through and write it as-is.
65
+ }
66
+ }
67
+ return realWrite(chunk, ...rest);
68
+ };
69
+
70
+ await runSessionStart({
71
+ packageName: "@theglitchking/persistent-planning",
72
+ pluginName: "persistent-planning",
73
+ configFile: "persistent-planning.json",
74
+ });
75
+
76
+ // The runtime can return without emitting (e.g. it chdir-failed). Say our piece.
77
+ if (!merged) {
78
+ realWrite(
79
+ JSON.stringify({
80
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: notice },
81
+ }) + "\n"
82
+ );
83
+ }
84
+ }
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@theglitchking/persistent-planning",
3
- "version": "3.0.0",
3
+ "version": "3.1.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/",
14
15
  "commands/",
15
16
  "hooks/",
16
- "scripts/link-skills.js",
17
+ "scripts/",
18
+ "templates/",
17
19
  "skills/",
18
- "docs/",
20
+ ".documentation/",
21
+ "!.documentation/reports",
19
22
  ".claude-plugin/",
20
23
  "README.md",
21
24
  "LICENSE",
@@ -56,6 +59,7 @@
56
59
  "commander": "^14.0.0"
57
60
  },
58
61
  "devDependencies": {
62
+ "@theglitchking/hit-em-with-the-docs": "^2.9.0",
59
63
  "@theglitchking/semantic-pages": "^0.10.0"
60
64
  }
61
65
  }
@@ -0,0 +1,146 @@
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. Idempotent.
55
+ ensure_gitignored() {
56
+ local gitignore="${ROOT}/.gitignore"
57
+ grep -qE '^\.planning/?$' "$gitignore" 2>/dev/null && return 0 # whole tree already ignored
58
+ grep -qE '^\.planning/\.archive/?$' "$gitignore" 2>/dev/null && return 0
59
+ printf '\n# Completed plans retired by scripts/archive-plan.sh (local history)\n.planning/.archive/\n' >> "$gitignore"
60
+ planning_ok "Added .planning/.archive/ to .gitignore"
61
+ }
62
+
63
+ plan_root_artifact() {
64
+ for f in "$1/phase.md" "$1/task_plan.md" "$1/task.md"; do
65
+ [[ -f "$f" ]] && { echo "$f"; return; }
66
+ done
67
+ }
68
+
69
+ # Stamp the plan's top-level artifact so an archived plan reads as archived even
70
+ # when someone opens the file directly, with no directory context.
71
+ stamp_archived() {
72
+ local file="$1"
73
+ [[ -f "$file" ]] || return 0
74
+ if head -1 "$file" | grep -q '^---$'; then
75
+ # Frontmatter: flip status and add archived_on before the closing delimiter.
76
+ awk -v today="$TODAY" '
77
+ NR == 1 { print; next }
78
+ !done_fm && /^status:[[:space:]]*/ { print "status: archived"; seen_status = 1; next }
79
+ !done_fm && /^---$/ {
80
+ if (!seen_status) print "status: archived"
81
+ print "archived_on: \"" today "\""
82
+ print; done_fm = 1; next
83
+ }
84
+ { print }
85
+ ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
86
+ else
87
+ printf '\n---\n\n**Archived %s** — retired by `scripts/archive-plan.sh`.\n' "$TODAY" >> "$file"
88
+ fi
89
+ }
90
+
91
+ archive_one() {
92
+ local slug="$1"
93
+ local src="${PLANNING}/${slug}"
94
+ local dest="${ARCHIVE}/${slug}"
95
+
96
+ if [[ ! -d "$src" ]]; then
97
+ planning_err "No such plan: .planning/${slug}/"
98
+ return 1
99
+ fi
100
+ if [[ -e "$dest" ]]; then
101
+ planning_err "Already archived: .planning/.archive/${slug}/ exists. Move or rename it first."
102
+ return 1
103
+ fi
104
+
105
+ if [[ $FORCE -eq 0 ]]; then
106
+ local complete
107
+ complete=$(bash "${SCRIPT_DIR}/plan-status.sh" --complete | grep -Fx "$slug" || true)
108
+ if [[ -z "$complete" ]]; then
109
+ planning_err "Plan '${slug}' is not complete — refusing to archive."
110
+ echo " Check what is outstanding with: bash scripts/plan-status.sh"
111
+ echo " Archive anyway with: bash scripts/archive-plan.sh ${slug} --force"
112
+ return 1
113
+ fi
114
+ fi
115
+
116
+ if [[ $DRY_RUN -eq 1 ]]; then
117
+ planning_log "[dry-run] would archive .planning/${slug}/ -> .planning/.archive/${slug}/"
118
+ return 0
119
+ fi
120
+
121
+ ensure_gitignored
122
+ mkdir -p "$ARCHIVE"
123
+
124
+ if git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 \
125
+ && [[ -n "$(git -C "$ROOT" ls-files ".planning/${slug}" 2>/dev/null)" ]]; then
126
+ git -C "$ROOT" mv ".planning/${slug}" ".planning/.archive/${slug}"
127
+ else
128
+ mv "$src" "$dest"
129
+ fi
130
+
131
+ stamp_archived "$(plan_root_artifact "$dest")"
132
+ planning_ok "Archived .planning/${slug}/ -> .planning/.archive/${slug}/"
133
+ }
134
+
135
+ RC=0
136
+ if [[ $ALL_COMPLETE -eq 1 ]]; then
137
+ mapfile -t SLUGS < <(bash "${SCRIPT_DIR}/plan-status.sh" --complete)
138
+ if [[ ${#SLUGS[@]} -eq 0 || -z "${SLUGS[0]}" ]]; then
139
+ planning_log "No complete plans to archive."
140
+ exit 0
141
+ fi
142
+ for s in "${SLUGS[@]}"; do archive_one "$s" || RC=1; done
143
+ else
144
+ archive_one "$SLUG" || RC=1
145
+ fi
146
+ exit $RC
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ ###############################################################################
3
+ # detect-mode.sh - Sm/lg mode detection for persistent-planning v3.0
4
+ #
5
+ # Usage:
6
+ # bash scripts/detect-mode.sh [--explain]
7
+ #
8
+ # Output:
9
+ # "sm" or "lg" on stdout. With --explain, also prints a one-line decision
10
+ # banner to stderr explaining which signals fired.
11
+ #
12
+ # Heuristic:
13
+ # - If $CLAUDE_PROJECT_DIR/.planning/.meta/workspace.json exists with a
14
+ # "mode" field, that wins (override is sticky).
15
+ # - Otherwise count distinct git authors over the last 90 days. Two or
16
+ # more distinct emails → "lg". Otherwise "sm".
17
+ # - If the repo isn't a git repo or git fails for any reason, default
18
+ # to "sm" (the safer/less-surprising option for solo flows).
19
+ #
20
+ # Why this design:
21
+ # - Solo devs get the historical sm experience with zero ceremony.
22
+ # - Multi-contributor projects get the layered model surface immediately.
23
+ # - The decision banner avoids "magic" — the user sees what fired.
24
+ # - The workspace.json override means a single --mode flag sticks for
25
+ # all subsequent runs (no flag needed every time).
26
+ ###############################################################################
27
+ set -e
28
+
29
+ EXPLAIN=0
30
+ if [[ "${1:-}" == "--explain" ]]; then
31
+ EXPLAIN=1
32
+ fi
33
+
34
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
35
+ WORKSPACE_JSON="${PROJECT_ROOT}/.planning/.meta/workspace.json"
36
+
37
+ # 1. Override wins if workspace.json exists
38
+ if [[ -f "$WORKSPACE_JSON" ]]; then
39
+ # Naive jq-free read: grep the mode line. workspace.json is small/structured.
40
+ MODE_FROM_JSON=$(grep -oE '"mode"[[:space:]]*:[[:space:]]*"(sm|lg)"' "$WORKSPACE_JSON" | sed -E 's/.*"(sm|lg)".*/\1/')
41
+ if [[ "$MODE_FROM_JSON" == "sm" || "$MODE_FROM_JSON" == "lg" ]]; then
42
+ echo "$MODE_FROM_JSON"
43
+ if [[ $EXPLAIN -eq 1 ]]; then
44
+ echo "[mode] $MODE_FROM_JSON (from .planning/.meta/workspace.json)" >&2
45
+ fi
46
+ exit 0
47
+ fi
48
+ fi
49
+
50
+ # 2. Auto-detect via git author count over 90 days
51
+ DETECTED="sm"
52
+ CONTRIB=0
53
+ if command -v git >/dev/null 2>&1 && git -C "$PROJECT_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
54
+ CONTRIB=$(git -C "$PROJECT_ROOT" log --since="90 days ago" --format='%ae' 2>/dev/null | sort -u | wc -l | tr -d ' ')
55
+ if [[ "$CONTRIB" -ge 2 ]]; then
56
+ DETECTED="lg"
57
+ fi
58
+ fi
59
+
60
+ echo "$DETECTED"
61
+ if [[ $EXPLAIN -eq 1 ]]; then
62
+ if [[ "$DETECTED" == "lg" ]]; then
63
+ echo "[mode] lg (auto-detected: $CONTRIB distinct git authors over the last 90 days; threshold ≥ 2)" >&2
64
+ else
65
+ echo "[mode] sm (auto-detected: $CONTRIB distinct git authors over the last 90 days; threshold for lg is ≥ 2)" >&2
66
+ fi
67
+ fi
@@ -0,0 +1,139 @@
1
+ #!/bin/bash
2
+ ###############################################################################
3
+ # init-atom.sh - Create a new atom (subagent hand-off unit) under a task in lg mode
4
+ #
5
+ # Usage:
6
+ # bash scripts/init-atom.sh "Atom Name" --parent <task-slug>
7
+ #
8
+ # Creates:
9
+ # .planning/<phase-slug>/<task-slug>/atoms/<atom-slug>.md
10
+ #
11
+ # The atom's sequence number is auto-assigned: 1 + (max existing sequence in
12
+ # the same task's atoms/ directory). Atoms within a task default to sequential
13
+ # processing.
14
+ #
15
+ # --parent must be a task-slug. The phase is auto-resolved by walking the
16
+ # .planning/ tree to find the task.
17
+ ###############################################################################
18
+ set -e
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ source "${SCRIPT_DIR}/lib/planning.sh"
22
+
23
+ # Parse arguments
24
+ ATOM_NAME=""
25
+ PARENT_TASK=""
26
+ while [[ $# -gt 0 ]]; do
27
+ case "$1" in
28
+ --parent)
29
+ PARENT_TASK="$2"
30
+ shift 2
31
+ ;;
32
+ -h|--help)
33
+ cat <<EOF
34
+ Usage: bash scripts/init-atom.sh "Atom Name" --parent <task-slug>
35
+
36
+ Examples:
37
+ bash scripts/init-atom.sh "Update zod schema" --parent hewtd-schema-extension
38
+ bash scripts/init-atom.sh "Add plan-tier tests" --parent hewtd-schema-extension
39
+ EOF
40
+ exit 0
41
+ ;;
42
+ *)
43
+ if [[ -z "$ATOM_NAME" ]]; then
44
+ ATOM_NAME="$1"
45
+ fi
46
+ shift
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ if [[ -z "$ATOM_NAME" || -z "$PARENT_TASK" ]]; then
52
+ planning_err "Both atom name and --parent <task-slug> are required."
53
+ echo "Usage: bash scripts/init-atom.sh \"Atom Name\" --parent <task-slug>"
54
+ exit 1
55
+ fi
56
+
57
+ ATOM_SLUG=$(planning_slugify "$ATOM_NAME")
58
+
59
+ if [[ -z "$ATOM_SLUG" ]]; then
60
+ planning_err "Atom name must contain at least one alphanumeric character."
61
+ exit 1
62
+ fi
63
+
64
+ MODE=$(planning_mode)
65
+ if [[ "$MODE" != "lg" ]]; then
66
+ planning_err "Current mode is '$MODE' but init-atom.sh requires lg mode."
67
+ exit 1
68
+ fi
69
+
70
+ ROOT=$(planning_root)
71
+ PLANNING="$(planning_dir)"
72
+ TODAY=$(planning_today)
73
+ TEMPLATE_DIR="${ROOT}/templates/lg"
74
+
75
+ if [[ ! -d "$TEMPLATE_DIR" ]]; then
76
+ TEMPLATE_DIR="${SCRIPT_DIR}/../templates/lg"
77
+ fi
78
+ if [[ ! -d "$TEMPLATE_DIR" ]]; then
79
+ planning_err "Cannot locate templates/lg directory."
80
+ exit 1
81
+ fi
82
+
83
+ # Find the parent task — walk .planning/<phase>/<task>/task.md
84
+ TASK_DIR=""
85
+ for phase_path in "${PLANNING}"/*/; do
86
+ candidate="${phase_path%/}/${PARENT_TASK}"
87
+ if [[ -f "${candidate}/task.md" ]]; then
88
+ TASK_DIR="$candidate"
89
+ break
90
+ fi
91
+ done
92
+
93
+ if [[ -z "$TASK_DIR" ]]; then
94
+ planning_err "Parent task '${PARENT_TASK}' not found under .planning/<phase>/."
95
+ echo ""
96
+ echo "Available tasks:"
97
+ for phase_path in "${PLANNING}"/*/; do
98
+ phase_slug=$(basename "$phase_path")
99
+ [[ "$phase_slug" == ".meta" || "$phase_slug" == "archive" ]] && continue
100
+ for task_path in "${phase_path%/}"/*/; do
101
+ task_slug=$(basename "$task_path")
102
+ if [[ -f "${task_path}task.md" ]]; then
103
+ echo " --parent ${task_slug} (in phase: ${phase_slug})"
104
+ fi
105
+ done
106
+ done
107
+ exit 1
108
+ fi
109
+
110
+ ATOMS_DIR="${TASK_DIR}/atoms"
111
+ mkdir -p "$ATOMS_DIR"
112
+
113
+ # Auto-assign sequence: 1 + max existing sequence in the atoms/ dir
114
+ NEXT_SEQUENCE=1
115
+ if compgen -G "${ATOMS_DIR}/*.md" >/dev/null 2>&1; then
116
+ MAX_SEQ=$(grep -h '^sequence:' "${ATOMS_DIR}"/*.md 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1)
117
+ if [[ -n "$MAX_SEQ" ]]; then
118
+ NEXT_SEQUENCE=$((MAX_SEQ + 1))
119
+ fi
120
+ fi
121
+
122
+ planning_log "Initializing atom: ${ATOM_NAME} (parent: ${PARENT_TASK}, sequence: ${NEXT_SEQUENCE})"
123
+
124
+ planning_render_and_log \
125
+ "${TEMPLATE_DIR}/atom.md" \
126
+ "${ATOMS_DIR}/${ATOM_SLUG}.md" \
127
+ "atom at ${ATOMS_DIR}/${ATOM_SLUG}.md (sequence: ${NEXT_SEQUENCE})" \
128
+ "ATOM_TITLE_PLACEHOLDER=${ATOM_NAME}" \
129
+ "TASK_SLUG_PLACEHOLDER=${PARENT_TASK}" \
130
+ "ATOM_DATE_PLACEHOLDER=${TODAY}" \
131
+ "ATOM_SEQUENCE_PLACEHOLDER=${NEXT_SEQUENCE}"
132
+
133
+ echo ""
134
+ planning_ok "Atom '${ATOM_NAME}' ready"
135
+ echo ""
136
+ echo "Next steps:"
137
+ echo " 1. Edit ${ATOMS_DIR}/${ATOM_SLUG}.md to define what to do, inputs, outputs, and acceptance criteria"
138
+ echo " 2. A subagent will pick up this atom when status: ready"
139
+ echo " 3. Status transitions: ready → in_progress → done (or → blocked)"