@theglitchking/persistent-planning 3.1.0 → 3.4.1
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.documentation/INDEX.md +15 -7
- package/.documentation/REGISTRY.md +2 -2
- package/.documentation/agents/INDEX.md +2 -2
- package/.documentation/agents/REGISTRY.md +2 -2
- package/.documentation/api/INDEX.md +2 -2
- package/.documentation/api/REGISTRY.md +2 -2
- package/.documentation/architecture/INDEX.md +5 -3
- package/.documentation/architecture/REGISTRY.md +8 -4
- package/.documentation/architecture/lg-mode.md +16 -0
- package/.documentation/architecture/lg-plan-artifact-lifecycle.md +104 -0
- package/.documentation/architecture/skill-linking-and-reclaim.md +84 -0
- package/.documentation/backups/INDEX.md +2 -2
- package/.documentation/backups/REGISTRY.md +2 -2
- package/.documentation/database/INDEX.md +2 -2
- package/.documentation/database/REGISTRY.md +2 -2
- package/.documentation/devops/INDEX.md +2 -2
- package/.documentation/devops/REGISTRY.md +2 -2
- package/.documentation/features/INDEX.md +2 -2
- package/.documentation/features/REGISTRY.md +2 -2
- package/.documentation/plans/INDEX.md +2 -2
- package/.documentation/plans/REGISTRY.md +2 -2
- package/.documentation/procedures/INDEX.md +4 -3
- package/.documentation/procedures/REGISTRY.md +6 -4
- package/.documentation/procedures/plan-completion-and-archive.md +34 -0
- package/.documentation/procedures/plugin-update-delivery.md +106 -0
- package/.documentation/quickstart/INDEX.md +2 -2
- package/.documentation/quickstart/REGISTRY.md +2 -2
- package/.documentation/reference/INDEX.md +10 -3
- package/.documentation/reference/REGISTRY.md +8 -4
- package/.documentation/reference/mandatory-frontmatter.md +71 -0
- package/.documentation/reference/skill-version-marker.md +70 -0
- package/.documentation/security/INDEX.md +2 -2
- package/.documentation/security/REGISTRY.md +2 -2
- package/.documentation/standards/INDEX.md +2 -2
- package/.documentation/standards/REGISTRY.md +2 -2
- package/.documentation/standards/mandatory-closing-phases.md +21 -1
- package/.documentation/testing/INDEX.md +2 -2
- package/.documentation/testing/REGISTRY.md +2 -2
- package/.documentation/testing/test-suite.md +38 -0
- package/.documentation/troubleshooting/INDEX.md +8 -8
- package/.documentation/troubleshooting/REGISTRY.md +20 -5
- package/.documentation/troubleshooting/plan-never-reads-complete.md +109 -0
- package/.documentation/troubleshooting/stale-skill-dir-drift.md +115 -0
- package/.documentation/workflows/INDEX.md +2 -2
- package/.documentation/workflows/REGISTRY.md +2 -2
- package/CHANGELOG.md +176 -0
- package/bin/persistent-planning.js +56 -6
- package/hooks/session-start.js +157 -15
- package/package.json +1 -1
- package/scripts/archive-plan.sh +3 -6
- package/scripts/init-atom.sh +7 -0
- package/scripts/init-phase.sh +58 -6
- package/scripts/init-planning.sh +18 -6
- package/scripts/init-task.sh +7 -0
- package/scripts/lib/planning.sh +122 -0
- package/scripts/lib/skill-link.js +177 -0
- package/scripts/link-skills.js +38 -0
- package/scripts/plan-status.sh +85 -10
- package/skills/persistent-planning/SKILL.md +5 -2
- package/templates/lg/phase.md +7 -4
- package/templates/lg/task.md +3 -2
|
@@ -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
|
+
}
|
package/scripts/link-skills.js
CHANGED
|
@@ -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
|
+
}
|
package/scripts/plan-status.sh
CHANGED
|
@@ -45,22 +45,91 @@ plan_root_artifact() {
|
|
|
45
45
|
done
|
|
46
46
|
}
|
|
47
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
|
+
|
|
48
66
|
# frontmatter_status <file> -> value of `status:` inside the leading --- block
|
|
49
67
|
frontmatter_status() {
|
|
50
|
-
|
|
51
|
-
awk 'NR==1 && $0 != "---" { exit }
|
|
52
|
-
NR>1 && $0 == "---" { exit }
|
|
53
|
-
/^status:[[:space:]]*/ { sub(/^status:[[:space:]]*/, ""); gsub(/["\047]/, ""); print; exit }' "$1"
|
|
68
|
+
frontmatter_field "$1" "status"
|
|
54
69
|
}
|
|
55
70
|
|
|
56
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.
|
|
57
80
|
count_boxes() {
|
|
58
|
-
local total checked
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
61
103
|
echo "$total $checked"
|
|
62
104
|
}
|
|
63
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
|
+
|
|
64
133
|
COMPLETE=()
|
|
65
134
|
ROWS=()
|
|
66
135
|
|
|
@@ -74,13 +143,19 @@ for plan_path in "$PLANNING"/*/; do
|
|
|
74
143
|
|
|
75
144
|
read -r total checked <<<"$(count_boxes "${plan_path%/}")"
|
|
76
145
|
status=$(frontmatter_status "$root")
|
|
77
|
-
blocked=$(
|
|
146
|
+
blocked=$(frontmatter_files_with "${plan_path%/}" "status" "blocked" | wc -l | tr -d ' ')
|
|
147
|
+
pending_mandatory=$(unfinished_mandatory "${plan_path%/}")
|
|
78
148
|
|
|
79
149
|
if [[ "$status" == "archived" ]]; then
|
|
80
150
|
verdict="archived"
|
|
81
151
|
elif [[ "$status" == "done" ]] || { [[ "$total" -gt 0 ]] && [[ "$checked" -eq "$total" ]]; }; then
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
159
|
elif [[ "$blocked" -gt 0 ]]; then
|
|
85
160
|
verdict="blocked"
|
|
86
161
|
elif [[ "$total" -eq 0 ]]; then
|
|
@@ -161,7 +161,7 @@ Create this file FIRST for any complex task:
|
|
|
161
161
|
- [ ] Phase 3: Execute/build
|
|
162
162
|
- [ ] Phase 4: Review and deliver
|
|
163
163
|
- [ ] Phase 5: Validate success through comprehensive testing (MANDATORY)
|
|
164
|
-
- [ ] Phase 6: Documentation pass -- create/update/deprecate docs (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
165
|
|
|
166
166
|
> Phases 5 and 6 are mandatory and MUST stay the last two phases of this plan.
|
|
167
167
|
> Insert new work above them; never after. Renumber them if you add phases.
|
|
@@ -169,7 +169,10 @@ Create this file FIRST for any complex task:
|
|
|
169
169
|
> - **Validate success**: prove the work with tests that fail if the change breaks.
|
|
170
170
|
> - **Documentation pass**: create/update/deprecate every doc this change touches --
|
|
171
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, use it for this pass
|
|
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.
|
|
173
176
|
|
|
174
177
|
## Key Questions
|
|
175
178
|
1. [Question to answer]
|
package/templates/lg/phase.md
CHANGED
|
@@ -20,9 +20,9 @@ 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
|
-
|
|
24
|
-
- [ ] **Validate success through comprehensive testing** (MANDATORY — second-to-last)
|
|
25
|
-
- [ ] **Documentation pass — create/update/deprecate docs
|
|
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
26
|
|
|
27
27
|
> The two mandatory closing tasks above MUST remain the last two tasks of this phase.
|
|
28
28
|
> Add new tasks above them; never after. A phase cannot be marked `done` until both are
|
|
@@ -31,7 +31,10 @@ so subagent teams can pick them up concurrently.]
|
|
|
31
31
|
> - **Validate success**: prove the phase's work with tests that fail if it breaks.
|
|
32
32
|
> - **Documentation pass**: create/update/deprecate every doc this phase touches —
|
|
33
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, use it for this pass
|
|
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.
|
|
35
38
|
|
|
36
39
|
## Decisions Made
|
|
37
40
|
[Phase-level decisions that affect multiple tasks. Format: `**Decision**: rationale`.]
|
package/templates/lg/task.md
CHANGED
|
@@ -9,6 +9,7 @@ plan_kind: task
|
|
|
9
9
|
parent: PHASE_SLUG_PLACEHOLDER
|
|
10
10
|
depends_on: []
|
|
11
11
|
parallelizable: false
|
|
12
|
+
mandatory: false
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
# Task: TASK_TITLE_PLACEHOLDER
|
|
@@ -29,8 +30,8 @@ under `atoms/` that subagents read via the planning MCP.
|
|
|
29
30
|
Inline checkbox vs. standalone atom file — see anti-pattern doc:
|
|
30
31
|
.documentation/standards/atom-granularity.md.]
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
_(no atoms yet — add inline checkboxes here for simple atoms,
|
|
34
|
+
or run `/start-atom "Atom name" --parent TASK_SLUG_PLACEHOLDER` for complex ones)_
|
|
34
35
|
|
|
35
36
|
## Decisions Made
|
|
36
37
|
[Task-level decisions. Format: `**Decision**: rationale`.]
|