create-battle-plan 1.4.0 → 1.4.2

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/bin/cli.js CHANGED
@@ -84,7 +84,11 @@ function getDirs(dir) {
84
84
  }
85
85
  }
86
86
 
87
- function pickFolder(projectSlug) {
87
+ function isDirEmpty(dir) {
88
+ try { return fs.readdirSync(dir).length === 0; } catch { return false; }
89
+ }
90
+
91
+ function pickFolder(shortName) {
88
92
  return new Promise((resolve) => {
89
93
  // Pause readline so we can use raw mode
90
94
  closeReadline();
@@ -97,8 +101,14 @@ function pickFolder(projectSlug) {
97
101
  function getOptions() {
98
102
  const dirs = getDirs(cwd);
99
103
  const options = [];
104
+ if (isDirEmpty(cwd)) {
105
+ options.push({
106
+ label: `${GREEN}● Install in this folder ${BOLD}(${path.basename(cwd)}/)${RESET}${GREEN} — no subfolder${RESET}`,
107
+ action: 'here_no_sub',
108
+ });
109
+ }
100
110
  options.push({ label: `${GREEN}+ Create new folder here${RESET}`, action: 'create' });
101
- options.push({ label: `${CYAN}» Install here as ${BOLD}${projectSlug}/${RESET}`, action: 'here' });
111
+ options.push({ label: `${CYAN}» Install here as ${BOLD}${shortName}/${RESET}`, action: 'here' });
102
112
  if (path.dirname(cwd) !== cwd) {
103
113
  options.push({ label: `${DIM}../${RESET} ${DIM}(up)${RESET}`, action: 'up' });
104
114
  }
@@ -116,14 +126,14 @@ function pickFolder(projectSlug) {
116
126
  let output = '';
117
127
 
118
128
  if (mode === 'input') {
119
- output += `${CLEAR_LINE}\r${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}\x1b[K`;
129
+ output += `${CLEAR_LINE}\r${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}\x1b[K`;
120
130
  process.stdout.write(output);
121
131
  return;
122
132
  }
123
133
 
124
134
  output += `\x1b[H\x1b[2J`; // clear screen
125
135
  output += `\n`;
126
- output += `${DIM}[6/6]${RESET} ${BOLD}Where do you want to install it?${RESET}\n`;
136
+ output += `${DIM}[7/7]${RESET} ${BOLD}Where do you want to install it?${RESET}\n`;
127
137
  output += `${DIM} ${display}${RESET}\n`;
128
138
  output += `\n`;
129
139
  output += `${DIM} ↑↓ navigate · enter select · q cancel${RESET}\n`;
@@ -156,12 +166,14 @@ function pickFolder(projectSlug) {
156
166
  const opt = options[selected];
157
167
  if (opt.action === 'create') {
158
168
  mode = 'input';
159
- inputBuffer = projectSlug;
169
+ inputBuffer = shortName;
160
170
  process.stdout.write(`\x1b[H\x1b[2J`);
161
171
  process.stdout.write(`\n`);
162
- process.stdout.write(`${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
172
+ process.stdout.write(`${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
163
173
  } else if (opt.action === 'here') {
164
- finish(path.join(cwd, projectSlug));
174
+ finish(path.join(cwd, shortName));
175
+ } else if (opt.action === 'here_no_sub') {
176
+ finish(cwd);
165
177
  } else if (opt.action === 'up') {
166
178
  cwd = path.dirname(cwd);
167
179
  selected = 0;
@@ -189,7 +201,7 @@ function pickFolder(projectSlug) {
189
201
  // Backspace
190
202
  inputBuffer = inputBuffer.slice(0, -1);
191
203
  process.stdout.write(`\r${CLEAR_LINE}`);
192
- process.stdout.write(`${DIM}[6/6]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
204
+ process.stdout.write(`${DIM}[7/7]${RESET} ${BOLD}Folder name:${RESET} ${inputBuffer}`);
193
205
  } else if (key === '\x1b' || key === '\x03') {
194
206
  // Escape or ctrl-c → back to browse
195
207
  mode = 'browse';
@@ -210,7 +222,7 @@ function pickFolder(projectSlug) {
210
222
  cleanup();
211
223
  process.stdout.write(`\x1b[H\x1b[2J`);
212
224
  console.log('');
213
- console.log(`${DIM}[6/6]${RESET} ${BOLD}Location:${RESET} ${shortPath(dir)}`);
225
+ console.log(`${DIM}[7/7]${RESET} ${BOLD}Location:${RESET} ${shortPath(dir)}`);
214
226
  console.log('');
215
227
  resolve(dir);
216
228
  }
@@ -297,37 +309,55 @@ async function main() {
297
309
 
298
310
  initReadline();
299
311
 
300
- // Question 1: Project name
301
- const projectName = await ask(`${DIM}[1/6]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `);
312
+ // Question 1: Project description (one sentence — used for context, not the folder name)
313
+ const projectName = await ask(`${DIM}[1/7]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `);
302
314
  if (!projectName) { console.log('Project name is required.'); process.exit(1); }
303
315
  console.log('');
304
316
 
305
- // Question 2: Time horizon
317
+ // Question 2: Short name (the actual folder slug). Default = cwd basename when sensible.
318
+ const cwdBasename = path.basename(process.cwd());
319
+ const cwdSlug = slugify(cwdBasename);
320
+ const cwdIsEmpty = (() => {
321
+ try { return fs.readdirSync(process.cwd()).length === 0; } catch { return false; }
322
+ })();
323
+ const sentenceSlug = slugify(projectName);
324
+ const truncatedSentenceSlug = sentenceSlug.split('-').slice(0, 3).join('-');
325
+ const genericNames = new Set(['projects', 'code', 'src', 'work', 'dev', 'repos', 'workspace', 'documents', 'desktop']);
326
+ const defaultShortName = (cwdIsEmpty && cwdSlug && !genericNames.has(cwdSlug))
327
+ ? cwdSlug
328
+ : (truncatedSentenceSlug || 'my-battle-plan');
329
+ const shortNameRaw = await ask(
330
+ `${DIM}[2/7]${RESET} ${BOLD}Short name for the folder?${RESET} ${DIM}(default: ${defaultShortName})${RESET}\n> `
331
+ );
332
+ const shortName = slugify(shortNameRaw) || defaultShortName;
333
+ console.log('');
334
+
335
+ // Question 3: Time horizon
306
336
  const horizon = await ask(
307
- `${DIM}[2/6]${RESET} ${BOLD}What's your time horizon?${RESET} ${DIM}(e.g., "3 weeks to demo day", "6 months to launch", "ongoing")${RESET}\n> `
337
+ `${DIM}[3/7]${RESET} ${BOLD}What's your time horizon?${RESET} ${DIM}(e.g., "3 weeks to demo day", "6 months to launch", "ongoing")${RESET}\n> `
308
338
  );
309
339
  console.log('');
310
340
 
311
- // Question 3: Metrics
341
+ // Question 4: Metrics
312
342
  const metricsRaw = await ask(
313
- `${DIM}[3/6]${RESET} ${BOLD}What are the 3-5 key metrics you want to track?${RESET} ${DIM}(comma-separated, e.g., "outreach sent, calls booked, LOIs signed")${RESET}\n> `
343
+ `${DIM}[4/7]${RESET} ${BOLD}What are the 3-5 key metrics you want to track?${RESET} ${DIM}(comma-separated, e.g., "outreach sent, calls booked, LOIs signed")${RESET}\n> `
314
344
  );
315
345
  if (!metricsRaw) { console.log('At least one metric is required.'); process.exit(1); }
316
346
  const metrics = metricsRaw.split(',').map((m) => m.trim()).filter(Boolean);
317
347
  console.log('');
318
348
 
319
- // Question 4: Domains
349
+ // Question 5: Domains
320
350
  const suggested = suggestDomains(projectName);
321
351
  const domainsRaw = await ask(
322
- `${DIM}[4/6]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggested}${RESET}\n> `
352
+ `${DIM}[5/7]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggested}${RESET}\n> `
323
353
  );
324
354
  if (!domainsRaw) { console.log('At least one domain is required.'); process.exit(1); }
325
355
  const domains = domainsRaw.split(',').map((d) => d.trim().toLowerCase()).filter(Boolean);
326
356
  console.log('');
327
357
 
328
- // Question 5: People
358
+ // Question 6: People
329
359
  const peopleRaw = await ask(
330
- `${DIM}[5/6]${RESET} ${BOLD}Who are the key people you'll be working with?${RESET} ${DIM}(format: "Name:Role, Name:Role" — or press enter to skip)${RESET}\n> `
360
+ `${DIM}[6/7]${RESET} ${BOLD}Who are the key people you'll be working with?${RESET} ${DIM}(format: "Name:Role, Name:Role" — or press enter to skip)${RESET}\n> `
331
361
  );
332
362
  const people = peopleRaw
333
363
  ? peopleRaw.split(',').map((p) => {
@@ -337,9 +367,8 @@ async function main() {
337
367
  : [];
338
368
  console.log('');
339
369
 
340
- // Question 6: Interactive folder picker
341
- const projectSlug = slugify(projectName) || 'my-battle-plan';
342
- const targetDir = await pickFolder(projectSlug);
370
+ // Question 7: Interactive folder picker
371
+ const targetDir = await pickFolder(shortName);
343
372
 
344
373
  // Re-init readline for any future questions
345
374
  initReadline();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-battle-plan",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Scaffold a Battle Plan project — a markdown-based context system for LLM-powered project management",
5
5
  "bin": {
6
6
  "create-battle-plan": "./bin/cli.js"
@@ -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 "========================="
@@ -1,310 +0,0 @@
1
- # Battle Plan — System Prompt
2
-
3
- You are helping manage an interconnected documentation system. Every document stays in sync through a cascade protocol. Follow these rules exactly.
4
-
5
- ---
6
-
7
- ## Two-View Model — Read This First
8
-
9
- **The cascade is your orientation layer. `docs/today.md` is the user's operating surface. The chat is the user's only UI.**
10
-
11
- The user should never have to look at the cascade, the battle plan, `tasks.yml`, or any internal markdown to operate the system. The chat with you is the UI; the cascade is your memory; `docs/today.md` is a thin clickable surface in their editor for ticking through the day. Everything else exists for *you*, not them.
12
-
13
- - **Your view (the cascade):** `docs/battle-plan.md` at the top, source docs below it, `metrics.yml` as numeric truth, `tasks.yml` as the structured task log. Narrative, deep, linked. You read and write this freely — it is how you reconstruct project state and cascade new information.
14
- - **User's view:** `docs/today.md`, generated by `tools/tasks/render-today.js` from `tasks.yml`. Rendered in Obsidian Tasks plugin format — query blocks project pill-styled lists over a raw `## Task data` section at the bottom (lane-grouped within each priority bucket). The user checks boxes in Obsidian; `tools/tasks/flush-today.js` reconciles those edits back into `tasks.yml`. When they want deep context, they ask you in chat — you traverse the cascade on their behalf.
15
-
16
- **Rules:**
17
- - Never grow the battle plan's TL;DR into a wall of prose. Keep header blocks terse; append Daily Log entries chronologically.
18
- - When the user drops new tasks, add them via `node tools/tasks/add.js "..." [--due YYYY-MM-DD] [--tag X] [--priority 1|2|3] [--lane LANE] [--implication PATH]`. Don't bury tasks in battle plan prose.
19
- - After any task mutation (add, complete, snooze, triage), run `node tools/tasks/render-today.js` so `docs/today.md` stays fresh.
20
- - `verify-cascade.sh` Check 6 confirms `today.md` is not stale relative to `tasks.yml`.
21
-
22
- ---
23
-
24
- ## Task Lanes — what goes where
25
-
26
- Every task has a `lane` (groups by *primary action*, not topic). The default vocabulary:
27
-
28
- | Lane | Primary action |
29
- |---|---|
30
- | `build` | Build, design, or document the product itself (MVP, demos, architecture, integrations) |
31
- | `outreach` | Cold DMs / InMails / posts / templates / cold-email infra-as-pipeline |
32
- | `discovery` | Chase or nurture a *named human relationship* — warm intros, follow-ups, scheduling specific calls |
33
- | `infra` | Plumbing — DNS, GCP/AWS, deploy, env, secrets, cold-email domain warm-up |
34
- | `fundraising` | Apply to or maintain relationships with accelerators / VCs / angels |
35
- | `meta` | Doc/process work with no other natural lane (default fallback) |
36
-
37
- **Adapt this set to your project.** Lanes are configurable in `tools/tasks/lib/tasks.js` (`VALID_LANES`) — when you change them, also update `tools/tasks/migrate-lanes.js` keyword buckets and the `LANE_DISPLAY` / `LANE_ORDER` tables in `tools/tasks/render-today.js`.
38
-
39
- **Personalities don't get their own lane.** A specific advisor or co-founder's input flows into all the action lanes. A task like "ask <advisor> about X" is `discovery` (relationship), `build` (product feedback), or `meta` (process), depending on what *closing* the task produces.
40
-
41
- ### Strategic vs routine — what belongs in `tasks.yml`
42
-
43
- `tasks.yml` is for **ad-hoc strategic / build / discovery-protocol / high-stakes individual conversations.** Examples: a milestone call with a specific stakeholder where multiple people join; a piece of pitch copy that needs to be written by a specific date; a load-bearing architecture decision that gates other work.
44
-
45
- Routine lead-by-name follow-ups ("X accepted, send DM") belong in the **outreach blitz pipeline**, not in `tasks.yml`. The blitz already surfaces those through `daily-targets.js` + `leads.csv` flags + accepted-not-replied detection on timers. Don't duplicate that work as tasks.
46
-
47
- When uncertain, default to the blitz. `tasks.yml` is a journal of strategic intent, not a worklist.
48
-
49
- ---
50
-
51
- ## Weekly Triage — `/weekly-triage`
52
-
53
- Run weekly to keep `tasks.yml` honest. The skill (`.claude/commands/weekly-triage.md`) walks the user through every open task one at a time using `AskUserQuestion`'s arrow-key UI. Each decision (`done` / `snooze N` / `demote` / `merge X` / `delete` / `lane LANE` / `priority N` / `keep`) is applied to `tasks.yml` immediately — no batching.
54
-
55
- Two automation layers support this:
56
-
57
- - **`tools/tasks/triage.js`** — read-only data layer. Surfaces overdue/stale tasks, recent commits mentioning each task ID (signals "this is probably done"), and *implications drift* (when a task's linked doc hasn't been modified since the task was created — meaning the cascade hasn't actually reached the doc). Output as Markdown by default, JSON via `--json` for programmatic consumers.
58
- - **`tools/tasks/triage-due.js`** — lightweight SessionStart-hook nudge. Silent unless one of three thresholds trips: time-based (≥7d since last triage), stale-task (≥20 open tasks ≥14d old), or volume (≥60 open). Wired in `.claude/settings.json`. The skill stamps `last_triage_at` in `tasks.yml` on completion to suppress the nudge until the next cycle.
59
-
60
- When you see the SessionStart nudge fire, mention it in chat ("📋 Last triage was 9d ago — want to run `/weekly-triage`?") but never auto-invoke. The user decides.
61
-
62
- ### Implications field
63
-
64
- Tasks may carry `implications: [docs/path-a.md, docs/path-b.md]` — a list of docs that should change when the task closes. `triage.js` flags drift when a linked doc's last git commit predates the task: the status flip happened, but the cascaded doc-update didn't. When you create a task whose closure should mutate a specific doc, pass `--implication path/to/doc.md` to `add.js` so triage can hold you accountable later.
65
-
66
- ### `blocked_by` field
67
-
68
- Tasks may also carry `blocked_by: [TASK-IDs]` — an array of TASK-IDs that must close before this task is actionable. Set via `add.js --blocked-by N` (repeatable; comma-separated also accepted; each ID validated against existing rows). When at least one blocker is still open:
69
-
70
- - `triage.js` shows a `🚧 Blocked by:` line listing each blocker's id, status, and title (open ones flagged 🚧, closed ones ✅).
71
- - The stale-flag and snooze-or-demote suggestion are **suppressed** — a task waiting on a deliberate blocker shouldn't be penalized for not progressing.
72
- - The replacement suggestion becomes "blocked — chase blocker(s) or demote".
73
- - Stats gain a `Blocked by another open task: N` line.
74
- - `render-today.js` emits a `🚧 blocked-by:TASK-N,TASK-M` token on the task's line — but only for *still-open* blockers, so the token disappears once the blocker closes.
75
-
76
- When the user describes a task that genuinely depends on another, set the blocker explicitly (`--blocked-by N`) instead of letting the dependency live in prose. Closure of the blocker doesn't auto-close the dependent — the user picks the action during the next triage.
77
-
78
- ---
79
-
80
- ## Task archive — `node tools/tasks/archive.js`
81
-
82
- `tasks.yml` is append-only by design (audit trail), but it shouldn't grow forever. The archive script moves any `status: done|cancelled` row with `done_at < today - 14d` into `tasks-archive.yaml` (created on first run, same schema, sorted by `done_at` ascending).
83
-
84
- - **Default retention:** 14 days. Pass `--days N` to override, or `--all` to archive every closed row regardless of age.
85
- - **Idempotent:** dedups by `id` against the existing archive. Safe to run on every `/wrap-up`.
86
- - **Wired into `/wrap-up` Step 4.5b** — runs daily as part of end-of-day routine.
87
- - **Backfill safety:** closed rows missing `done_at` get stamped today and kept one cycle (so a date-less row doesn't get archived without chronological position).
88
- - **Re-importing a task:** intentional friction. Manually move the row from `tasks-archive.yaml` → `tasks.yml` and set `status: open`.
89
-
90
- ---
91
-
92
- ## The Cascade Protocol
93
-
94
- **Trigger:** Any incoming information that relates to the project — calls, messages, research, signals, status changes, decisions.
95
-
96
- When triggered, update in this exact order:
97
-
98
- ### Step 0: Update `metrics.yml`
99
- If any key metric changed, update `metrics.yml` first. This is the numeric source of truth.
100
-
101
- ### Step 1: Update Battle Plan (`docs/battle-plan.md`)
102
- - Update the **TL;DR** with current status
103
- - Update the **Key Metrics** table (numbers reference metrics.yml)
104
- - Update **Today's Priorities** if relevant
105
- - Append to **Daily Log** for today
106
-
107
- ### Step 2: Update Cascade Docs
108
- Update only the docs relevant to the new information. Route new info to the appropriate domain doc under `docs/`. Common patterns:
109
-
110
- | Info type | Route to... |
111
- |-----------|------------|
112
- | Conversation, call, or meeting | `docs/external-insights.md` — append as new dated session |
113
- | Evidence for/against a hypothesis | The relevant domain doc — amend the claim with an `[UPDATE]` block |
114
- | Outreach sent/received | The relevant market or sales doc — update tracking tables |
115
- | Competitor intel | The relevant strategy or market doc |
116
- | New foundational knowledge | The relevant research or domain doc |
117
-
118
- If no doc exists for the info, append it to the closest domain overview doc. Only create a new file if the info doesn't fit anywhere.
119
-
120
- ### Step 3: Update Dates
121
- Run `tools/touch-date.sh` on every file you modified in this session:
122
-
123
- ```bash
124
- tools/touch-date.sh docs/battle-plan.md docs/validation/hypotheses.md [etc.]
125
- ```
126
-
127
- ### Step 4: Verify
128
- Run `tools/verify-cascade.sh` and fix any issues it reports:
129
-
130
- ```bash
131
- tools/verify-cascade.sh
132
- ```
133
-
134
- ---
135
-
136
- ## Source Reference Rules
137
-
138
- ### Registry Metrics (Tier 1 — deterministic)
139
- Numbers defined in `metrics.yml`. Reference as: `[**N**](metrics.yml#field_name)`
140
-
141
- This renders as a bold clickable number. Example: `[**42**](metrics.yml#outreach_sent)`
142
-
143
- These are verified by exact numeric comparison via `tools/check-metrics.sh`.
144
-
145
- ### Inline Metrics (Tier 2 — LLM-verified)
146
- Less common numbers from another doc. Reference as: `[**N**](source-doc.md#section-slug)`
147
-
148
- Example: `60% of time on evidence [**60**](external-insights.md#session-2-key-insights)`
149
-
150
- **Rule:** Every number referenced from another document MUST include a source annotation. Only numbers native to a doc (where they originate) have no annotation.
151
-
152
- ---
153
-
154
- ## Document Format
155
-
156
- Every doc in `docs/` must have this frontmatter:
157
-
158
- ```markdown
159
- # Document Title
160
-
161
- **Last Updated:** 2026-04-07
162
- **Status:** Active | Draft | Archived
163
- **Role:** source-of-truth | cascade-target
164
- **Compression:** chronological | amended | none
165
-
166
- **TL;DR:** One paragraph summary with key numbers and source references.
167
-
168
- ---
169
- ```
170
-
171
- - **Last Updated** must match today's date on any file modified in the current session.
172
- - **Status:** `Active` = live, `Draft` = WIP, `Archived` = excluded from cascade.
173
- - **Role:** `source-of-truth` = authoritative for its numbers. `cascade-target` = references numbers from elsewhere.
174
- - **Compression:** required field. One of `chronological`, `amended`, or `none` (see Compression Modes section below).
175
- - **TL;DR** must exist and contain all key metrics that appear in the doc.
176
-
177
- ---
178
-
179
- ## Compression Modes & Timestamping Rules
180
-
181
- Every doc declares a `Compression:` mode in frontmatter. This tells the `/distill` command (and humans) how new info gets added to the doc and how old info gets compressed when it grows too long. The mode IS the timestamping rule for new info.
182
-
183
- ### `Compression: chronological`
184
- The doc is an append-only log of dated entries. Each new piece of info goes in a new dated section.
185
-
186
- - **Timestamping rule:** every new entry MUST start with a dated heading: `## Session N (YYYY-MM-DD) — <title>`, `## YYYY-MM-DD — <title>`, or `## DD Month YYYY — <title>`. No exceptions.
187
- - **Examples:** `docs/battle-plan.md` (daily log), `docs/validation/external-insights.md` (conversation journal).
188
- - **`/distill` behavior:** keeps the N most recent dated sections verbatim, archives the rest into `docs/archive/<same-path>`, replaces them with a thorough summary.
189
-
190
- ### `Compression: amended`
191
- The doc is a living reference. Claims are amended in place over time.
192
-
193
- - **Timestamping rule:** every new finding that revises an existing claim MUST be added as an inline `> **[UPDATE YYYY-MM-DD · Source: ...]**` block placed immediately above the claim it modifies. Brand-new claims with no prior version don't need a stamp; they're stamped implicitly by the doc's `Last Updated` date and git history.
194
- - **Examples:** `docs/validation/hypotheses.md`, `docs/market/icp-and-targets.md`, `docs/market/competitive-landscape.md`.
195
- - **`/distill` behavior:** collapses old `[UPDATE]` blocks into the body text (preserving their content as integrated current-state), archives the raw blocks verbatim. Keeps the N most recent amendments per section inline.
196
-
197
- ### `Compression: none`
198
- The doc is a static thesis or reference. It gets rewritten, not amended. Git history is the timeline.
199
-
200
- - **Timestamping rule:** none. Just edit the doc and let `Last Updated` + git track changes.
201
- - **Examples:** `docs/strategy/product-thesis.md`, `docs/research/domain-101.md`.
202
- - **`/distill` behavior:** refuses to run. If a `none` doc has grown unwieldy, rewrite it manually or change its `Compression:` mode first.
203
-
204
- ### Why this matters
205
- The TL;DR is current state, not history. It can't tell `/distill` what's new vs old. The `Compression:` mode + timestamping rule is the only mechanism that makes distillation deterministic. Skipping the timestamp on a new entry in a `chronological` or `amended` doc is a bug; it will get silently absorbed into the wrong era during distillation.
206
-
207
- When in doubt about which mode a new doc should use: chronological logs choose `chronological`, claim trackers choose `amended`, everything else is `none`.
208
-
209
- ---
210
-
211
- ## Vault Rules
212
-
213
- 1. **Update, don't duplicate.** Amend with `> **[UPDATE YYYY-MM-DD · Source: ...]**`
214
- 2. **Cross-link everything.** Claims reference their source doc.
215
- 3. **Confidence levels:** `Unvalidated` | `Soft signal` | `Practitioner-validated` | `Data-validated`
216
- 4. **Source everything.** Who said it, when, confidence level.
217
- 5. **Minimize file count.** Append, don't create new files.
218
-
219
- ---
220
-
221
- ## The `/wrap-up` Protocol
222
-
223
- When the user says `/wrap-up`, run this end-of-day sequence:
224
-
225
- **Step 1 — Scan:** Read the battle plan. Identify all tasks for today. Categorize: done, partially done, not started, new.
226
-
227
- **Step 2 — Present:** Show the user: "Here's today's status: [list]. Does this look right?"
228
-
229
- **Step 3 — Prompt:** Ask: "Anything else happen today? Even small things — a reply, an accept, a thought, a link. Everything counts."
230
-
231
- **Step 4 — Cascade:** With all info gathered, run the full cascade (Steps 0-4 above).
232
-
233
- **Step 5 — Report:** Print:
234
- - Metrics changed today (before → after)
235
- - Docs updated
236
- - Verification warnings (if any)
237
- - Tomorrow's top priorities
238
-
239
- **Step 6 — Commit:** Ask: "Want me to commit today's updates?" If yes, commit with message: `eod YYYY-MM-DD: [summary]`
240
-
241
- ---
242
-
243
- ## Outreach System (Add-on)
244
-
245
- **Trigger:** If `outreach/leads.csv` exists in the project, the outreach system is active.
246
-
247
- ### Overview
248
-
249
- The outreach system tracks a LinkedIn (or any channel) outreach pipeline through `outreach/leads.csv`. This CSV is the **single source of truth** for all outreach metrics — `metrics.yml` is derived from it, never edited directly for outreach numbers.
250
-
251
- ### First-Time Setup
252
-
253
- If `outreach/leads.csv` exists but `.outreach-initialized` does NOT exist, read `outreach/README.md` and follow the Interactive Setup instructions to onboard the user.
254
-
255
- ### Daily Workflow Integration
256
-
257
- The outreach system plugs into the cascade protocol:
258
-
259
- 1. User runs `node tools/outreach/daily-targets.js` → generates blitz checklist
260
- 2. User sends messages, ticks checkboxes
261
- 3. User runs `node tools/outreach/flush-targets.js` → updates leads.csv
262
- 4. `flush-targets.js` calls `sync-metrics.js` → derives metrics.yml from CSV
263
- 5. `sync-metrics.js` calls `update-dashboard.js` → regenerates mermaid dashboard
264
- 6. The cascade protocol takes over: metrics.yml → battle-plan.md → domain docs
265
-
266
- ### Scripts Reference
267
-
268
- | Script | Purpose |
269
- |--------|---------|
270
- | `tools/outreach/daily-targets.js [N]` | Generate daily blitz checklist |
271
- | `tools/outreach/flush-targets.js` | Process checked items from blitz |
272
- | `tools/outreach/flush-updates.js` | Parse free-form natural language updates |
273
- | `tools/outreach/flush-accepts.js` | Batch-process connection accepts |
274
- | `tools/outreach/flush-inbox.js` | Add leads from manual URL list |
275
- | `tools/outreach/sync-metrics.js` | Derive metrics.yml from leads.csv |
276
- | `tools/outreach/update-dashboard.js` | Regenerate mermaid conversion dashboard |
277
- | `tools/outreach/stats.js` | Print pipeline summary |
278
- | `tools/outreach/lookup.js "Name"` | Fuzzy-search leads |
279
-
280
- ### Metrics Derivation
281
-
282
- These metrics in `metrics.yml` are **derived** from leads.csv (never hand-edit):
283
-
284
- - `outreach_sent` = leads with status past `new` or `contacted_at` set
285
- - `responses` = `replied_at` set or status past `replied`
286
- - `invitations_accepted` = leads tagged `accepted`
287
- - `discovery_calls` = `call_at` in the past or status `call_done`
288
- - `calls_booked` = status `call_booked` (snapshot)
289
- - `verbal_commitments` = status `verbal`, `loi`, or `paying`
290
-
291
- ### Template System
292
-
293
- Message templates live in `tools/outreach/templates.json`. The daily blitz assigns templates based on the `country_template_map` field (or round-robin if no mapping). Template performance (sent/accepted/replied/calls) is tracked automatically and displayed in the blitz checklist.
294
-
295
- ### Mermaid Dashboard
296
-
297
- `docs/analysis/icp-conversion.md` is auto-generated — never hand-edit. It contains:
298
- - Overall funnel chart (contacted → accepted → replied → call → verbal)
299
- - Conversion breakdown by role, company size, country, company type
300
- - Template A/B comparison
301
- - Kill/Keep/Scale verdicts per segment
302
-
303
- View in any mermaid-capable renderer (GitHub, VS Code preview, etc.).
304
-
305
- ### Adapting the System
306
-
307
- - **Different metrics:** Edit the derivation rules in `tools/outreach/sync-metrics.js`
308
- - **Different time horizon:** The system tracks weekly breakdowns — adjust `daily-targets.js` count parameter
309
- - **Different channels:** The `channel` column supports any value (connection, inmail, email, etc.)
310
- - **Different statuses:** Add to `VALID_STATUS` in `tools/outreach/lib/leads.js` and update derivation rules