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 +51 -22
- package/package.json +1 -1
- package/template/tools/verify-cascade.sh +289 -7
- package/template/CLAUDE.md.backup-2026-05-11 +0 -310
package/bin/cli.js
CHANGED
|
@@ -84,7 +84,11 @@ function getDirs(dir) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function
|
|
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}${
|
|
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}[
|
|
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}[
|
|
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 =
|
|
169
|
+
inputBuffer = shortName;
|
|
160
170
|
process.stdout.write(`\x1b[H\x1b[2J`);
|
|
161
171
|
process.stdout.write(`\n`);
|
|
162
|
-
process.stdout.write(`${DIM}[
|
|
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,
|
|
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}[
|
|
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}[
|
|
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/
|
|
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:
|
|
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}[
|
|
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
|
|
341
|
+
// Question 4: Metrics
|
|
312
342
|
const metricsRaw = await ask(
|
|
313
|
-
`${DIM}[
|
|
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
|
|
349
|
+
// Question 5: Domains
|
|
320
350
|
const suggested = suggestDomains(projectName);
|
|
321
351
|
const domainsRaw = await ask(
|
|
322
|
-
`${DIM}[
|
|
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
|
|
358
|
+
// Question 6: People
|
|
329
359
|
const peopleRaw = await ask(
|
|
330
|
-
`${DIM}[
|
|
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
|
|
341
|
-
const
|
|
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,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
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|