create-battle-plan 1.3.0 → 1.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.
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # verify-cascade.sh — Full verification of the Battle Plan cascade system.
3
- # Checks: dates, metrics, staleness, battle plan freshness.
3
+ # Checks: dates, metrics, staleness, battle plan freshness, qualitative
4
+ # claim drift, on-disk file presence, amended-doc UPDATE discipline,
5
+ # chronological-doc dated-heading discipline.
4
6
  # Usage: tools/verify-cascade.sh
5
- # Exit 0 if clean, exit 1 if issues found.
7
+ # Exit 0 if clean (or warnings only), exit 1 if errors found.
6
8
 
7
9
  set -euo pipefail
8
10
 
@@ -14,6 +16,26 @@ TODAY=$(date +%Y-%m-%d)
14
16
  WARNINGS=0
15
17
  ERRORS=0
16
18
 
19
+ # Shared find-exclusion list. These paths/filenames are excluded from
20
+ # every check because they are autogenerated, archive-only, or carry
21
+ # their own freshness semantics that don't follow the cascade rules:
22
+ # - examples/ → template scaffolding shown to users
23
+ # - superpowers/ → plugin-skill content; not cascade docs
24
+ # - archive/ → frozen-in-time historical snapshots
25
+ # - social/ → drafts of outbound posts; cascade-irrelevant
26
+ # - today-archive/ → daily flushed task surfaces
27
+ # - today.md → autogenerated by tools/tasks/render-today.js
28
+ # - CLAUDE.md → system instructions, not a cascade doc
29
+ FIND_EXCLUDES=(
30
+ -not -path "*/examples/*"
31
+ -not -path "*/superpowers/*"
32
+ -not -path "*/archive/*"
33
+ -not -path "*/social/*"
34
+ -not -path "*/today-archive/*"
35
+ -not -name "today.md"
36
+ -not -name "CLAUDE.md"
37
+ )
38
+
17
39
  echo "=== Battle Plan Verification ==="
18
40
  echo "Date: $TODAY"
19
41
  echo ""
@@ -38,7 +60,7 @@ while IFS= read -r doc; do
38
60
  echo "WARNING: No Last Updated line in $doc"
39
61
  WARNINGS=$((WARNINGS + 1))
40
62
  fi
41
- done < <(find "$DOCS_DIR" -name "*.md" -not -path "*/examples/*" 2>/dev/null)
63
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
42
64
 
43
65
  # --- Check 2: Metrics consistency ---
44
66
  echo ""
@@ -73,7 +95,7 @@ if [ -f "$BATTLE_PLAN" ]; then
73
95
  echo "WARNING: $doc ($doc_date) is newer than battle plan ($bp_date)"
74
96
  WARNINGS=$((WARNINGS + 1))
75
97
  fi
76
- done < <(find "$DOCS_DIR" -name "*.md" -not -path "*/examples/*" 2>/dev/null)
98
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
77
99
  else
78
100
  echo "WARNING: Battle plan not found at $BATTLE_PLAN"
79
101
  WARNINGS=$((WARNINGS + 1))
@@ -96,7 +118,7 @@ while IFS= read -r doc; do
96
118
  echo "WARNING: No TL;DR in $doc"
97
119
  WARNINGS=$((WARNINGS + 1))
98
120
  fi
99
- done < <(find "$DOCS_DIR" -name "*.md" -not -path "*/examples/*" 2>/dev/null)
121
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
100
122
 
101
123
  # --- Check 5: Stale inline references ---
102
124
  echo ""
@@ -119,7 +141,7 @@ while IFS= read -r doc; do
119
141
  # Skip metrics.yml references (handled by check-metrics)
120
142
  [[ "$ref" == *"metrics.yml"* ]] && continue
121
143
 
122
- ref_path=$(find "$DOCS_DIR" -name "$ref_file" -not -path "*/examples/*" 2>/dev/null | head -1)
144
+ ref_path=$(find "$DOCS_DIR" -name "$ref_file" -not -path "*/examples/*" -not -path "*/superpowers/*" 2>/dev/null | head -1)
123
145
  if [ -z "$ref_path" ]; then
124
146
  echo "WARNING: Referenced file $ref_file not found (from $doc)"
125
147
  WARNINGS=$((WARNINGS + 1))
@@ -134,7 +156,7 @@ while IFS= read -r doc; do
134
156
  WARNINGS=$((WARNINGS + 1))
135
157
  fi
136
158
  done < <(grep -oE '\(→ [^)]+\)' "$doc" 2>/dev/null || true)
137
- done < <(find "$DOCS_DIR" -name "*.md" -not -path "*/examples/*" 2>/dev/null)
159
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
138
160
 
139
161
  # --- Check 6: today.md freshness (task subsystem) ---
140
162
  echo ""
@@ -142,16 +164,276 @@ echo "--- Check 6: today.md Freshness ---"
142
164
 
143
165
  TASKS_YML="$REPO_ROOT/tasks.yml"
144
166
  TODAY_MD="$DOCS_DIR/today.md"
167
+
145
168
  if [ -f "$TASKS_YML" ] && [ -f "$TODAY_MD" ]; then
146
169
  if [ "$TASKS_YML" -nt "$TODAY_MD" ]; then
147
170
  echo "WARNING: tasks.yml is newer than docs/today.md — run \`node tools/tasks/render-today.js\`"
148
171
  WARNINGS=$((WARNINGS + 1))
172
+ else
173
+ echo "today.md is fresh relative to tasks.yml"
149
174
  fi
150
175
  elif [ -f "$TASKS_YML" ] && [ ! -f "$TODAY_MD" ]; then
151
176
  echo "WARNING: tasks.yml exists but docs/today.md does not — run \`node tools/tasks/render-today.js\`"
152
177
  WARNINGS=$((WARNINGS + 1))
153
178
  fi
154
179
 
180
+ # --- Check 7: Qualitative wrappers near metric links ---
181
+ #
182
+ # Surfaces phrases that DEPEND on the linked metric's current value
183
+ # ("exceeded N", "target hit ✓", "Nx better than baseline", etc.).
184
+ # When the metric value changes — which can happen automatically via
185
+ # sync-metrics — these wrapper phrases can become factually wrong
186
+ # without the metric substitution itself looking suspicious.
187
+ #
188
+ # Example failure mode: a doc once said "we've now exceeded our 40-DM
189
+ # target [**42**](metrics.yml#outreach_sent)". A week later the metric
190
+ # updates to 38 (e.g. dead leads recounted, derivation logic changed).
191
+ # The `[**38**]` substitution is silent; the word "exceeded" is now
192
+ # factually wrong but the link still resolves and looks fine.
193
+ #
194
+ # This is heuristic and informational: hits are WARNINGS, never ERRORS.
195
+ # Tune by editing the QUALITATIVE_REGEX below or excluding paths.
196
+ echo ""
197
+ echo "--- Check 7: Qualitative Wrappers Near Metric Links ---"
198
+
199
+ # Phrases that flag a metric-linked number as a fragile narrative claim.
200
+ # Each is matched on a line that also contains a `metrics.yml#` reference.
201
+ QUALITATIVE_REGEX='(exceeded|target (was|hit)|hit ✓|/[0-9]+ ✓|[0-9]+x better|[0-9]+x the rate|[0-9]+x higher|doubled|tripled|crossed [0-9]+|achieved|surpassed|outperformed)'
202
+
203
+ QW_FILES_FOUND=0
204
+ while IFS= read -r doc; do
205
+ [ -z "$doc" ] && continue
206
+ [[ "$doc" == "$DOCS_DIR/README.md" ]] && continue
207
+
208
+ # Pull lines that contain BOTH a metric link AND a qualitative wrapper.
209
+ # Using `grep -P` would be cleaner but BSD grep on macOS lacks -P, so we
210
+ # chain two extended-regex matches instead.
211
+ matches=$(grep -nE 'metrics\.yml#' "$doc" 2>/dev/null \
212
+ | grep -iE "$QUALITATIVE_REGEX" \
213
+ || true)
214
+
215
+ if [ -n "$matches" ]; then
216
+ QW_FILES_FOUND=$((QW_FILES_FOUND + 1))
217
+ rel="${doc#$REPO_ROOT/}"
218
+ echo "WARNING: qualitative wrapper near metric link in $rel"
219
+ WARNINGS=$((WARNINGS + 1))
220
+ while IFS= read -r m; do
221
+ [ -z "$m" ] && continue
222
+ # Trim long lines for readability — keep first 200 chars.
223
+ lineno=$(echo "$m" | cut -d: -f1)
224
+ preview=$(echo "$m" | cut -d: -f2- | cut -c1-200)
225
+ echo " L$lineno: $preview"
226
+ done <<<"$matches"
227
+ fi
228
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
229
+
230
+ if [ $QW_FILES_FOUND -eq 0 ]; then
231
+ echo "No qualitative wrappers found near metric links."
232
+ else
233
+ echo ""
234
+ echo " ↑ Re-validate each phrase against the current metric value."
235
+ echo " A wrapper that was true at write-time can become factually"
236
+ echo " wrong after sync-metrics updates the linked number."
237
+ fi
238
+
239
+ # --- Check 8: Personal-surface files present on disk ---
240
+ #
241
+ # tasks.yml + events.yml + events-archive.yml are typically gitignored
242
+ # (they are your personal daily-task / events surface — not shared with
243
+ # collaborators, and not visible to a teammate's Claude session). They
244
+ # must still exist on disk for render-today.js / due-for-gate.js /
245
+ # /wrap-up to work.
246
+ #
247
+ # Failure mode this catches: a routine gitignore commit that uses
248
+ # `git rm <file>` instead of `git rm --cached <file>` deletes the
249
+ # on-disk copies. Nothing crashes — the scripts silently no-op when
250
+ # the files are missing: today.md renders empty, due-for-gate returns
251
+ # []. Fail loud here so the same mistake surfaces immediately next
252
+ # time instead of after several days of empty surfaces.
253
+ #
254
+ # Fresh installs: a file that's missing AND has never been tracked in
255
+ # git is treated as "not bootstrapped yet" rather than "lost" — silently
256
+ # noted, never errored. Run `tools/init-project.sh` (or just create the
257
+ # empty files) to bootstrap. The error only fires when a file was once
258
+ # tracked but is now gone from disk — that's the real recovery scenario.
259
+ echo ""
260
+ echo "--- Check 8: Personal-Surface Files On Disk ---"
261
+
262
+ CHECK8_MISSING=0
263
+ CHECK8_NOT_BOOTSTRAPPED=0
264
+ HAS_GIT=0
265
+ if git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
266
+ HAS_GIT=1
267
+ fi
268
+
269
+ for f in tasks.yml events.yml events-archive.yml; do
270
+ [ -f "$REPO_ROOT/$f" ] && continue
271
+
272
+ # Was this file ever tracked? If so, missing-on-disk is a real recovery
273
+ # scenario. If never tracked (or no git at all), treat as "not bootstrapped".
274
+ was_tracked=0
275
+ if [ $HAS_GIT -eq 1 ]; then
276
+ if [ -n "$(git -C "$REPO_ROOT" rev-list --all -- "$f" 2>/dev/null | head -1)" ]; then
277
+ was_tracked=1
278
+ fi
279
+ fi
280
+
281
+ if [ $was_tracked -eq 1 ]; then
282
+ echo "ERROR: $f is missing on disk (but was previously tracked in git)."
283
+ echo " This file is typically gitignored but must exist locally. Likely cause:"
284
+ echo " someone ran 'git rm $f' (not 'git rm --cached $f') in a past"
285
+ echo " gitignore commit. Recover with:"
286
+ echo " git log --all --diff-filter=D -- $f # find the deletion commit <C>"
287
+ echo " git show <C>^:$f > $f # restore the last tracked version"
288
+ ERRORS=$((ERRORS + 1))
289
+ CHECK8_MISSING=$((CHECK8_MISSING + 1))
290
+ else
291
+ CHECK8_NOT_BOOTSTRAPPED=$((CHECK8_NOT_BOOTSTRAPPED + 1))
292
+ fi
293
+ done
294
+
295
+ if [ $CHECK8_MISSING -eq 0 ] && [ $CHECK8_NOT_BOOTSTRAPPED -eq 0 ]; then
296
+ echo "All personal-surface files present."
297
+ elif [ $CHECK8_MISSING -eq 0 ] && [ $CHECK8_NOT_BOOTSTRAPPED -gt 0 ]; then
298
+ echo "Note: $CHECK8_NOT_BOOTSTRAPPED personal-surface file(s) not yet bootstrapped (never tracked). Run \`tools/init-project.sh\` or create empty stubs to start using them."
299
+ fi
300
+
301
+ # --- Check 9: Amended-doc UPDATE block discipline ---
302
+ #
303
+ # Amended-compression docs require every revision to be a
304
+ # `> **[UPDATE YYYY-MM-DD · Source: ...]**` block placed immediately
305
+ # above the claim it modifies. Without UPDATE blocks, /distill cannot
306
+ # tell what's new vs old when collapsing the doc, and the timeline of
307
+ # how a claim evolved is silently lost.
308
+ #
309
+ # Heuristic: if an amended doc was modified vs origin/main (or HEAD if
310
+ # origin/main is unavailable) and the diff added content lines but no
311
+ # new UPDATE block, warn. Allows some false positives (e.g. UPDATE
312
+ # block continuation lines, frontmatter touches) — tunable below.
313
+ #
314
+ # Skipped silently when there's no git history yet (fresh repo).
315
+ echo ""
316
+ echo "--- Check 9: Amended-Doc UPDATE Block Discipline ---"
317
+
318
+ # Pick the diff base. Prefer origin/main; fall back to HEAD.
319
+ DIFF_BASE=""
320
+ if git -C "$REPO_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
321
+ DIFF_BASE="origin/main"
322
+ elif git -C "$REPO_ROOT" rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
323
+ DIFF_BASE="HEAD"
324
+ fi
325
+
326
+ if [ -z "$DIFF_BASE" ]; then
327
+ echo "No git base ref to diff against (no origin/main, no HEAD) — skipping Check 9."
328
+ else
329
+ CHECK9_HITS=0
330
+ while IFS= read -r doc; do
331
+ [ -z "$doc" ] && continue
332
+ [[ "$doc" == "$DOCS_DIR/README.md" ]] && continue
333
+
334
+ compression=$(grep '^\*\*Compression:\*\*' "$doc" | head -1 | sed 's/\*\*Compression:\*\* //' || true)
335
+ [[ "$compression" != "amended" ]] && continue
336
+
337
+ rel="${doc#$REPO_ROOT/}"
338
+
339
+ # Diff against base. Suppress errors if file is untracked.
340
+ diff_out=$(git -C "$REPO_ROOT" diff "$DIFF_BASE" -- "$rel" 2>/dev/null || true)
341
+ [ -z "$diff_out" ] && continue
342
+
343
+ # Count added content lines (excluding diff headers, frontmatter, blank lines).
344
+ # Frontmatter lines start with **Last Updated:**, **Status:**, etc.
345
+ added_content=$(echo "$diff_out" \
346
+ | grep -E '^\+[^+]' \
347
+ | grep -vE '^\+\s*$' \
348
+ | grep -vE '^\+\*\*(Last Updated|Status|Role|Compression|TL;DR):' \
349
+ | wc -l | tr -d ' ')
350
+
351
+ # Count added UPDATE blocks.
352
+ added_updates=$(echo "$diff_out" \
353
+ | grep -cE '^\+> \*\*\[UPDATE [0-9]{4}-[0-9]{2}-[0-9]{2}' \
354
+ || true)
355
+
356
+ # Warn if content added but no UPDATE block added.
357
+ if [ "$added_content" -gt 0 ] && [ "$added_updates" -eq 0 ]; then
358
+ echo "WARNING: $rel — $added_content content line(s) added vs $DIFF_BASE but no new UPDATE block."
359
+ echo " Amended-compression docs require '> **[UPDATE YYYY-MM-DD · Source: ...]**' for every revision."
360
+ WARNINGS=$((WARNINGS + 1))
361
+ CHECK9_HITS=$((CHECK9_HITS + 1))
362
+ fi
363
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
364
+
365
+ if [ $CHECK9_HITS -eq 0 ]; then
366
+ echo "All amended-doc edits have proper UPDATE blocks (or no amended docs modified)."
367
+ fi
368
+ fi
369
+
370
+ # --- Check 10: Chronological-doc dated-heading discipline ---
371
+ #
372
+ # Chronological-compression docs require every new entry to start with
373
+ # a dated heading: `## YYYY-MM-DD — <title>`, `## Session N (YYYY-MM-DD)
374
+ # — <title>`, `### Day N — <weekday>`, etc. Undated entries get
375
+ # silently absorbed into the wrong era during /distill, scrambling the
376
+ # timeline of a doc that exists specifically to preserve a timeline.
377
+ #
378
+ # Heuristic: for each chronological doc modified vs the diff base, find
379
+ # every heading added in the diff (lines starting with +## or +###) and
380
+ # verify it matches one of the dated patterns. Standard fixed section
381
+ # headings (TL;DR, Status, Pipeline, etc.) are exempted via a second
382
+ # allowlist regex — extend FIXED_HEADING_REGEX below if your project
383
+ # introduces new fixed section names.
384
+ #
385
+ # Skipped silently when there's no git history yet (fresh repo).
386
+ echo ""
387
+ echo "--- Check 10: Chronological-Doc Dated Headings ---"
388
+
389
+ if [ -z "$DIFF_BASE" ]; then
390
+ echo "No git base ref to diff against — skipping Check 10."
391
+ else
392
+ # Allowed heading patterns (extended-regex):
393
+ # - "## 2026-05-21" or "## 2026-05-21 — ..."
394
+ # - "## Session 17" or "## Session 17 (2026-05-20) — ..."
395
+ # - "### Day 53" or "### Day 53 — ..."
396
+ DATED_HEADING_REGEX='^\+#{2,3} (([0-9]{4}-[0-9]{2}-[0-9]{2})|((Day|Session) [0-9]+))'
397
+ # Standard fixed headings used in battle-plan / hypotheses / insights
398
+ # docs. Extend this list if your project introduces new fixed sections.
399
+ FIXED_HEADING_REGEX='^\+#{2,4} (Daily Log|Sessions|Recent Sessions|TL;DR|Status|Pipeline|Key Metrics|Weekly plan|Contingency Plans|Documents This Plan Depends On|Rules for This Document|Next milestones|Scope locks|Role|Compression|Timeline|Goal|Notes|References|Cross-references|Validation protocol|Kill criteria|Hypothesis statement|Confidence|Impact)'
400
+
401
+ CHECK10_HITS=0
402
+ while IFS= read -r doc; do
403
+ [ -z "$doc" ] && continue
404
+ [[ "$doc" == "$DOCS_DIR/README.md" ]] && continue
405
+
406
+ compression=$(grep '^\*\*Compression:\*\*' "$doc" | head -1 | sed 's/\*\*Compression:\*\* //' || true)
407
+ [[ "$compression" != "chronological" ]] && continue
408
+
409
+ rel="${doc#$REPO_ROOT/}"
410
+ diff_out=$(git -C "$REPO_ROOT" diff "$DIFF_BASE" -- "$rel" 2>/dev/null || true)
411
+ [ -z "$diff_out" ] && continue
412
+
413
+ # Find added headings (## or ###) that match neither dated nor fixed patterns.
414
+ bad_headings=$(echo "$diff_out" \
415
+ | grep -E '^\+#{2,3} ' \
416
+ | grep -vE "$DATED_HEADING_REGEX" \
417
+ | grep -vE "$FIXED_HEADING_REGEX" \
418
+ || true)
419
+
420
+ if [ -n "$bad_headings" ]; then
421
+ echo "WARNING: $rel — undated heading(s) added vs $DIFF_BASE:"
422
+ while IFS= read -r h; do
423
+ [ -z "$h" ] && continue
424
+ echo " ${h:1:200}"
425
+ done <<<"$bad_headings"
426
+ echo " Chronological-compression docs require '## YYYY-MM-DD — <title>' or '## Session N (YYYY-MM-DD) — <title>' or '### Day N — <weekday>'."
427
+ WARNINGS=$((WARNINGS + 1))
428
+ CHECK10_HITS=$((CHECK10_HITS + 1))
429
+ fi
430
+ done < <(find "$DOCS_DIR" -name "*.md" "${FIND_EXCLUDES[@]}" 2>/dev/null)
431
+
432
+ if [ $CHECK10_HITS -eq 0 ]; then
433
+ echo "All chronological-doc new headings are properly dated (or no chronological docs modified)."
434
+ fi
435
+ fi
436
+
155
437
  # --- Summary ---
156
438
  echo ""
157
439
  echo "========================="