create-battle-plan 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +434 -0
- package/package.json +31 -0
- package/template/.cascaderc.example +6 -0
- package/template/.claude/commands/distill.md +186 -0
- package/template/.claude/commands/good-morning.md +74 -0
- package/template/.claude/commands/wrap-up.md +61 -0
- package/template/.claude/settings.json +3 -0
- package/template/.githooks/pre-commit +41 -0
- package/template/CLAUDE.md +154 -0
- package/template/docs/README.md +62 -0
- package/template/tools/check-metrics.sh +91 -0
- package/template/tools/init-project.sh +245 -0
- package/template/tools/setup-hooks.sh +14 -0
- package/template/tools/sync-metrics.sh +82 -0
- package/template/tools/touch-date.sh +31 -0
- package/template/tools/verify-cascade.sh +154 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function ask(question) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function slugify(text) {
|
|
19
|
+
return text
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
22
|
+
.replace(/^-|-$/g, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function metricKey(text) {
|
|
26
|
+
return text
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
30
|
+
.replace(/^_|_$/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function capitalize(text) {
|
|
34
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function copyDir(src, dest) {
|
|
38
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
39
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
40
|
+
const srcPath = path.join(src, entry.name);
|
|
41
|
+
const destPath = path.join(dest, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
copyDir(srcPath, destPath);
|
|
44
|
+
} else {
|
|
45
|
+
fs.copyFileSync(srcPath, destPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BOLD = '\x1b[1m';
|
|
51
|
+
const DIM = '\x1b[2m';
|
|
52
|
+
const GREEN = '\x1b[32m';
|
|
53
|
+
const CYAN = '\x1b[36m';
|
|
54
|
+
const YELLOW = '\x1b[33m';
|
|
55
|
+
const WHITE = '\x1b[37m';
|
|
56
|
+
const RESET = '\x1b[0m';
|
|
57
|
+
|
|
58
|
+
function banner() {
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(`${BOLD}${WHITE} ___ ____ ___ ___ _ ____${RESET}`);
|
|
61
|
+
console.log(`${BOLD}${WHITE} |__] |__| | | | |___${RESET}`);
|
|
62
|
+
console.log(`${BOLD}${WHITE} |__] | | | | |___ |___${RESET}`);
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(`${BOLD}${WHITE} ___ _ ____ _ _${RESET}`);
|
|
65
|
+
console.log(`${BOLD}${WHITE} |__] | |__| |\\ |${RESET}`);
|
|
66
|
+
console.log(`${BOLD}${WHITE} | |___ | | | \\|${RESET}`);
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(`${DIM} A markdown-based context system${RESET}`);
|
|
69
|
+
console.log(`${DIM} for LLM-powered projects${RESET}`);
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(`${DIM} ─────────────────────────────${RESET}`);
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cascadeDiagram(domains, metrics) {
|
|
76
|
+
const domainStr = domains.slice(0, 3).join(' ');
|
|
77
|
+
const dots = domains.length > 3 ? ' ...' : '';
|
|
78
|
+
const metricStr = metrics.slice(0, 3).map((m) => metricKey(m)).join(', ');
|
|
79
|
+
|
|
80
|
+
console.log(`${DIM} Your cascade:${RESET}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(`${CYAN} new info ──→ ${WHITE}metrics.yml${RESET}`);
|
|
83
|
+
console.log(`${DIM} │${RESET}`);
|
|
84
|
+
console.log(`${DIM} ▼${RESET}`);
|
|
85
|
+
console.log(`${CYAN} ${WHITE}battle-plan.md${RESET}`);
|
|
86
|
+
console.log(`${DIM} / | \\${RESET}`);
|
|
87
|
+
console.log(`${CYAN} ${WHITE}${domainStr}${dots}${RESET}`);
|
|
88
|
+
console.log(`${DIM} │${RESET}`);
|
|
89
|
+
console.log(`${DIM} ▼${RESET}`);
|
|
90
|
+
console.log(`${GREEN} verify-cascade.sh ${BOLD}✓${RESET}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
banner();
|
|
96
|
+
|
|
97
|
+
// Question 1: Project name
|
|
98
|
+
const projectName = await ask(`${DIM}[1/6]${RESET} ${BOLD}What's your project in one sentence?${RESET}\n> `);
|
|
99
|
+
if (!projectName) {
|
|
100
|
+
console.log('Project name is required.');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
// Question 2: Time horizon
|
|
106
|
+
const horizon = await ask(
|
|
107
|
+
`${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> `
|
|
108
|
+
);
|
|
109
|
+
console.log('');
|
|
110
|
+
|
|
111
|
+
// Question 3: Metrics
|
|
112
|
+
const metricsRaw = await ask(
|
|
113
|
+
`${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> `
|
|
114
|
+
);
|
|
115
|
+
if (!metricsRaw) {
|
|
116
|
+
console.log('At least one metric is required.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const metrics = metricsRaw.split(',').map((m) => m.trim()).filter(Boolean);
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
// Question 4: Domains
|
|
123
|
+
const suggestedDomains = suggestDomains(projectName);
|
|
124
|
+
const domainsRaw = await ask(
|
|
125
|
+
`${DIM}[4/6]${RESET} ${BOLD}What domains does your work cover?${RESET} ${DIM}(comma-separated)\nSuggested based on your project: ${suggestedDomains}${RESET}\n> `
|
|
126
|
+
);
|
|
127
|
+
if (!domainsRaw) {
|
|
128
|
+
console.log('At least one domain is required.');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const domains = domainsRaw.split(',').map((d) => d.trim().toLowerCase()).filter(Boolean);
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
// Question 5: People
|
|
135
|
+
const peopleRaw = await ask(
|
|
136
|
+
`${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> `
|
|
137
|
+
);
|
|
138
|
+
const people = peopleRaw
|
|
139
|
+
? peopleRaw.split(',').map((p) => {
|
|
140
|
+
const [name, role] = p.split(':').map((s) => s.trim());
|
|
141
|
+
return { name: name || '', role: role || '' };
|
|
142
|
+
}).filter((p) => p.name)
|
|
143
|
+
: [];
|
|
144
|
+
console.log('');
|
|
145
|
+
|
|
146
|
+
// Question 6: Where to install
|
|
147
|
+
const defaultDir = `./${slugify(projectName) || 'my-battle-plan'}`;
|
|
148
|
+
const dirAnswer = await ask(
|
|
149
|
+
`${DIM}[6/6]${RESET} ${BOLD}Where do you want to install it?${RESET} ${DIM}(default: ${defaultDir})${RESET}\n> `
|
|
150
|
+
);
|
|
151
|
+
const targetDir = path.resolve(dirAnswer || defaultDir);
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
rl.close();
|
|
155
|
+
|
|
156
|
+
// --- Scaffold ---
|
|
157
|
+
console.log(`${DIM} ─────────────────────────────${RESET}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(`${CYAN} Scaffolding...${RESET}`);
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
163
|
+
console.log(`${YELLOW}Warning: ${targetDir} already exists and is not empty.${RESET}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Copy template
|
|
168
|
+
const templateDir = path.join(__dirname, '..', 'template');
|
|
169
|
+
copyDir(templateDir, targetDir);
|
|
170
|
+
console.log(`${DIM} + CLAUDE.md (system prompt)${RESET}`);
|
|
171
|
+
console.log(`${DIM} + tools/ (verification scripts)${RESET}`);
|
|
172
|
+
console.log(`${DIM} + .claude/commands/ (slash commands)${RESET}`);
|
|
173
|
+
console.log(`${DIM} + .githooks/pre-commit${RESET}`);
|
|
174
|
+
|
|
175
|
+
// Make shell scripts executable
|
|
176
|
+
const toolsDir = path.join(targetDir, 'tools');
|
|
177
|
+
if (fs.existsSync(toolsDir)) {
|
|
178
|
+
for (const f of fs.readdirSync(toolsDir)) {
|
|
179
|
+
if (f.endsWith('.sh')) {
|
|
180
|
+
fs.chmodSync(path.join(toolsDir, f), 0o755);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const hookFile = path.join(targetDir, '.githooks', 'pre-commit');
|
|
185
|
+
if (fs.existsSync(hookFile)) {
|
|
186
|
+
fs.chmodSync(hookFile, 0o755);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const today = new Date().toISOString().split('T')[0];
|
|
190
|
+
|
|
191
|
+
// Create domain directories and docs
|
|
192
|
+
for (const domain of domains) {
|
|
193
|
+
const domainDir = path.join(targetDir, 'docs', domain);
|
|
194
|
+
fs.mkdirSync(domainDir, { recursive: true });
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
path.join(domainDir, `${domain}-overview.md`),
|
|
197
|
+
`# ${capitalize(domain)} Overview
|
|
198
|
+
|
|
199
|
+
**Last Updated:** ${today}
|
|
200
|
+
**Status:** Draft
|
|
201
|
+
**Role:** cascade-target
|
|
202
|
+
**Compression:** amended
|
|
203
|
+
|
|
204
|
+
**TL;DR:** Initial ${domain} document for ${projectName}. To be filled in as the project progresses.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Notes
|
|
209
|
+
|
|
210
|
+
_Start adding content here._
|
|
211
|
+
`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`${DIM} + docs/ (${domains.length} domain${domains.length > 1 ? 's' : ''})${RESET}`);
|
|
216
|
+
|
|
217
|
+
// Create metrics.yml
|
|
218
|
+
const metricsContent = [
|
|
219
|
+
`# metrics.yml — project-wide metrics registry for ${projectName}`,
|
|
220
|
+
'# The LLM updates this file FIRST in any cascade, before touching docs.',
|
|
221
|
+
'# Scripts verify all (→ metrics.yml#field) references against these values.',
|
|
222
|
+
'',
|
|
223
|
+
`last_updated: ${today}`,
|
|
224
|
+
'',
|
|
225
|
+
...metrics.map((m) => `${metricKey(m)}: 0`),
|
|
226
|
+
'',
|
|
227
|
+
].join('\n');
|
|
228
|
+
fs.writeFileSync(path.join(targetDir, 'metrics.yml'), metricsContent);
|
|
229
|
+
console.log(`${DIM} + metrics.yml (${metrics.length} metric${metrics.length > 1 ? 's' : ''})${RESET}`);
|
|
230
|
+
|
|
231
|
+
// Create battle plan
|
|
232
|
+
const metricsTable = metrics
|
|
233
|
+
.map((m) => `| ${m} | _set target_ | **0** (→ metrics.yml#${metricKey(m)}) |`)
|
|
234
|
+
.join('\n');
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(
|
|
237
|
+
path.join(targetDir, 'docs', 'battle-plan.md'),
|
|
238
|
+
`# Battle Plan — ${projectName}
|
|
239
|
+
|
|
240
|
+
**Last Updated:** ${today}
|
|
241
|
+
**Status:** Active
|
|
242
|
+
**Role:** source-of-truth
|
|
243
|
+
**Compression:** chronological
|
|
244
|
+
|
|
245
|
+
**TL;DR:** ${projectName} — just initialized. Time horizon: ${horizon || 'not set'}. All metrics at 0. First priority: fill in the battle plan with real tasks and targets.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Rules for This Document
|
|
250
|
+
|
|
251
|
+
1. Every task has an assigned date — no "sometime this week"
|
|
252
|
+
2. Tasks move, never disappear — if slipped, add new date + reason
|
|
253
|
+
3. New info updates the battle plan FIRST, before any other doc
|
|
254
|
+
4. Everything links — tasks reference the doc they depend on or produce
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Key Metrics
|
|
259
|
+
|
|
260
|
+
| Metric | Target | Current |
|
|
261
|
+
|--------|--------|---------|
|
|
262
|
+
${metricsTable}
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Today's Priorities
|
|
267
|
+
|
|
268
|
+
- [ ] Set targets for each metric
|
|
269
|
+
- [ ] Fill in this week's tasks
|
|
270
|
+
- [ ] Record any existing conversations in external-insights.md
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## This Week
|
|
275
|
+
|
|
276
|
+
_Add day-by-day tasks here._
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Daily Log
|
|
281
|
+
|
|
282
|
+
_Append-only. Three lines per day._
|
|
283
|
+
`
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Create external-insights.md
|
|
287
|
+
const peopleSections = people.length
|
|
288
|
+
? people.map((p) => `### ${p.name} — ${p.role}\n_No sessions recorded yet._\n`).join('\n')
|
|
289
|
+
: '_Add key people here as you start conversations._\n';
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(
|
|
292
|
+
path.join(targetDir, 'docs', 'external-insights.md'),
|
|
293
|
+
`# External Insights
|
|
294
|
+
|
|
295
|
+
**Last Updated:** ${today}
|
|
296
|
+
**Status:** Active
|
|
297
|
+
**Role:** cascade-target
|
|
298
|
+
**Compression:** chronological
|
|
299
|
+
|
|
300
|
+
**TL;DR:** All external conversations, calls, and meetings for ${projectName}. 0 sessions recorded so far.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## How to Use This Document
|
|
305
|
+
|
|
306
|
+
Every conversation gets appended as a dated session. Record everything — even "small" chats contain signal.
|
|
307
|
+
|
|
308
|
+
### Template
|
|
309
|
+
|
|
310
|
+
\`\`\`markdown
|
|
311
|
+
## Session N (YYYY-MM-DD) — [Person Name], [Role/Company]
|
|
312
|
+
|
|
313
|
+
### Context
|
|
314
|
+
[Why this conversation happened]
|
|
315
|
+
|
|
316
|
+
### Key insights
|
|
317
|
+
1. **Insight title.** Detail. \`Confidence: [level]\`
|
|
318
|
+
|
|
319
|
+
### Raw quotes (if available)
|
|
320
|
+
> "Quote here"
|
|
321
|
+
|
|
322
|
+
### Action items
|
|
323
|
+
- [ ] Follow-up X
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## People
|
|
329
|
+
|
|
330
|
+
${peopleSections}
|
|
331
|
+
`
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
console.log(`${DIM} + docs/battle-plan.md${RESET}`);
|
|
335
|
+
console.log(`${DIM} + docs/external-insights.md${RESET}`);
|
|
336
|
+
|
|
337
|
+
// Save onboarding answers for Claude to read on first /good-morning
|
|
338
|
+
fs.writeFileSync(
|
|
339
|
+
path.join(targetDir, '.battle-plan-onboarding.json'),
|
|
340
|
+
JSON.stringify(
|
|
341
|
+
{
|
|
342
|
+
project_name: projectName,
|
|
343
|
+
horizon,
|
|
344
|
+
metrics,
|
|
345
|
+
domains,
|
|
346
|
+
people,
|
|
347
|
+
installed_at: today,
|
|
348
|
+
},
|
|
349
|
+
null,
|
|
350
|
+
2
|
|
351
|
+
) + '\n'
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Mark as initialized
|
|
355
|
+
fs.writeFileSync(path.join(targetDir, '.battle-plan-initialized'), `Initialized on ${today}\n`);
|
|
356
|
+
|
|
357
|
+
console.log('');
|
|
358
|
+
|
|
359
|
+
// Initialize git repo
|
|
360
|
+
try {
|
|
361
|
+
const { execSync } = require('child_process');
|
|
362
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
363
|
+
execSync('git config core.hooksPath .githooks', { cwd: targetDir, stdio: 'pipe' });
|
|
364
|
+
execSync('git add -A', { cwd: targetDir, stdio: 'pipe' });
|
|
365
|
+
execSync('git commit -m "Initial battle plan scaffold"', {
|
|
366
|
+
cwd: targetDir,
|
|
367
|
+
stdio: 'pipe',
|
|
368
|
+
env: { ...process.env, GIT_AUTHOR_NAME: 'Battle Plan', GIT_AUTHOR_EMAIL: 'noreply@battleplan.dev', GIT_COMMITTER_NAME: 'Battle Plan', GIT_COMMITTER_EMAIL: 'noreply@battleplan.dev' },
|
|
369
|
+
});
|
|
370
|
+
console.log(`${DIM} + git repo initialized${RESET}`);
|
|
371
|
+
} catch {
|
|
372
|
+
// git not available or failed — not critical
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- Done ---
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(`${DIM} ─────────────────────────────${RESET}`);
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log(`${GREEN}${BOLD} Ready.${RESET}`);
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log(`${BOLD} Project:${RESET} ${projectName}`);
|
|
382
|
+
console.log(`${BOLD} Location:${RESET} ${targetDir}`);
|
|
383
|
+
console.log(`${BOLD} Horizon:${RESET} ${horizon || 'not set'}`);
|
|
384
|
+
console.log(`${BOLD} Metrics:${RESET} ${metrics.join(', ')}`);
|
|
385
|
+
console.log(`${BOLD} Domains:${RESET} ${domains.join(', ')}`);
|
|
386
|
+
if (people.length) {
|
|
387
|
+
console.log(`${BOLD} People:${RESET} ${people.map((p) => `${p.name} (${p.role})`).join(', ')}`);
|
|
388
|
+
}
|
|
389
|
+
console.log('');
|
|
390
|
+
|
|
391
|
+
cascadeDiagram(domains, metrics);
|
|
392
|
+
|
|
393
|
+
console.log(`${DIM} ─────────────────────────────${RESET}`);
|
|
394
|
+
console.log('');
|
|
395
|
+
console.log(`${CYAN}${BOLD} Next steps:${RESET}`);
|
|
396
|
+
console.log('');
|
|
397
|
+
console.log(` ${BOLD}cd ${path.relative(process.cwd(), targetDir)}${RESET}`);
|
|
398
|
+
console.log(` ${BOLD}claude${RESET}`);
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(` Then type ${GREEN}${BOLD}/good-morning${RESET} to start`);
|
|
401
|
+
console.log(` your first session.`);
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(`${DIM} Claude already knows your project, metrics,`);
|
|
404
|
+
console.log(` and team. Just dump context into the chat —`);
|
|
405
|
+
console.log(` call transcripts, research, meeting notes,`);
|
|
406
|
+
console.log(` replies — it'll cascade into the right docs.${RESET}`);
|
|
407
|
+
console.log('');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function suggestDomains(projectDescription) {
|
|
411
|
+
const desc = projectDescription.toLowerCase();
|
|
412
|
+
const suggestions = [];
|
|
413
|
+
|
|
414
|
+
if (/market|customer|user|audience|segment|icp/.test(desc)) suggestions.push('market');
|
|
415
|
+
if (/valid|test|hypothes|experiment|interview/.test(desc)) suggestions.push('validation');
|
|
416
|
+
if (/strat|position|compete|pricing|business/.test(desc)) suggestions.push('strategy');
|
|
417
|
+
if (/research|learn|study|paper|domain/.test(desc)) suggestions.push('research');
|
|
418
|
+
if (/content|write|blog|newsletter|social/.test(desc)) suggestions.push('content');
|
|
419
|
+
if (/logist|ops|supply|shipping|fulfil/.test(desc)) suggestions.push('logistics');
|
|
420
|
+
if (/product|feature|build|ship|release/.test(desc)) suggestions.push('product');
|
|
421
|
+
if (/sales|outreach|pipeline|deal|close/.test(desc)) suggestions.push('sales');
|
|
422
|
+
if (/fund|invest|pitch|raise|capital/.test(desc)) suggestions.push('fundraising');
|
|
423
|
+
|
|
424
|
+
if (suggestions.length === 0) {
|
|
425
|
+
suggestions.push('market', 'validation', 'strategy', 'research');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return suggestions.join(', ');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
main().catch((err) => {
|
|
432
|
+
console.error(err);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-battle-plan",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a Battle Plan project — a markdown-based context system for LLM-powered project management",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-battle-plan": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"template/"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"llm",
|
|
16
|
+
"project-management",
|
|
17
|
+
"markdown",
|
|
18
|
+
"battle-plan",
|
|
19
|
+
"context",
|
|
20
|
+
"ai"
|
|
21
|
+
],
|
|
22
|
+
"author": "Paul von Kunhardt",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/paulkunhardt/battle-plan"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Distill a long doc — compress older content into a thorough summary, archive verbatim raw content, lose nothing.
|
|
3
|
+
argument-hint: <path/to/doc.md> [keep:N]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are running the `/distill` command. The user wants to compress a doc that's grown too long by **distilling** older sections into a thorough summary while archiving the verbatim raw content into `docs/archive/`. **Distilling preserves essence — never lose information.** The full raw content always survives in the archive.
|
|
7
|
+
|
|
8
|
+
## Arguments
|
|
9
|
+
|
|
10
|
+
- **Doc path** (required): `$1` — relative path from repo root to the doc to distill
|
|
11
|
+
- **Keep count** (optional): `keep:N` — how many of the most recent dated entries to keep verbatim. Default: 2.
|
|
12
|
+
|
|
13
|
+
If `$ARGUMENTS` is empty, ask the user which doc to distill and how many entries to keep.
|
|
14
|
+
|
|
15
|
+
## Step 0: Read frontmatter — choose mode by `Compression:` field
|
|
16
|
+
|
|
17
|
+
Read the target doc's frontmatter. The `Compression:` field determines how `/distill` operates:
|
|
18
|
+
|
|
19
|
+
| Compression mode | What `/distill` does |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `chronological` | Doc is an append-only log with dated section headings (`## Session N (YYYY-MM-DD)`, `## YYYY-MM-DD`, etc.). Distill keeps the N most recent dated sections verbatim, archives the rest, replaces them with a thorough summary. |
|
|
22
|
+
| `amended` | Doc is a living reference with in-place `> **[UPDATE YYYY-MM-DD]**` amendment blocks above claims. Distill collapses old amendment blocks (older than a cutoff or beyond the most recent N per section) into the body text, archives the raw amendment blocks. |
|
|
23
|
+
| `none` | Doc is a static thesis/reference. **Refuse to run.** Tell the user: "This doc is `Compression: none` — it's not designed for distillation. Edits to static docs are version-controlled by git. If you think this doc should be compressible, change its frontmatter first." |
|
|
24
|
+
| _missing_ | **Refuse to run.** Tell the user: "No `Compression:` field in frontmatter. Add one of `chronological`, `amended`, or `none` per CLAUDE.md before distilling." |
|
|
25
|
+
|
|
26
|
+
Then proceed to the appropriate workflow below.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Workflow A — `Compression: chronological`
|
|
31
|
+
|
|
32
|
+
### Step 1: Identify dated sections
|
|
33
|
+
|
|
34
|
+
Read the doc. Find headings that contain a date (`## Session N (YYYY-MM-DD)`, `### YYYY-MM-DD`, `## DD Month YYYY`, etc.). List them with their timestamps.
|
|
35
|
+
|
|
36
|
+
**Default split:** Keep the N most recent dated sections verbatim, archive everything before them. N defaults to 2 unless `keep:N` was passed.
|
|
37
|
+
|
|
38
|
+
**Confirm with user before proceeding** — unless they explicitly said "go" or "do it" in the original prompt.
|
|
39
|
+
|
|
40
|
+
### Step 2: Determine archive path
|
|
41
|
+
|
|
42
|
+
Archive lives at `docs/archive/<same-relative-path>` mirroring the doc's location.
|
|
43
|
+
|
|
44
|
+
Example: `docs/validation/sven-moritz-insights.md` → `docs/archive/validation/sven-moritz-insights.md`
|
|
45
|
+
|
|
46
|
+
If the archive file does NOT exist, create the parent directory (`mkdir -p`) and initialize it with this header (always include the frontmatter so verify-cascade skips it cleanly):
|
|
47
|
+
|
|
48
|
+
```markdown
|
|
49
|
+
# Archive: <Original Doc Title>
|
|
50
|
+
|
|
51
|
+
**Last Updated:** YYYY-MM-DD
|
|
52
|
+
**Status:** Archived
|
|
53
|
+
**Role:** cascade-target
|
|
54
|
+
**Compression:** none
|
|
55
|
+
|
|
56
|
+
Raw content archived from `<original/path.md>`. Append-only — most recent archives at the top.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If the archive file exists, prepend the new dated section above existing archived content (newest at top).
|
|
62
|
+
|
|
63
|
+
### Step 3: Append the new archive section (verbatim)
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
## Archived YYYY-MM-DD — <description of what's being archived>
|
|
67
|
+
|
|
68
|
+
> Sections moved here from `<original/path.md>` on YYYY-MM-DD. These were the verbatim contents at time of archive.
|
|
69
|
+
|
|
70
|
+
<EXACT verbatim content of the older sections — copy them with zero changes>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Description should be specific: "Sessions 1-4 (2026-03-26 to 2026-03-30)" or "Daily logs from Jan-Mar 2026".
|
|
76
|
+
|
|
77
|
+
### Step 4: Replace the archived sections in the original doc
|
|
78
|
+
|
|
79
|
+
In the original doc, remove the older sections you just archived. Replace them with:
|
|
80
|
+
|
|
81
|
+
1. **Archive notice** at the position where the archived sections used to be:
|
|
82
|
+
|
|
83
|
+
```markdown
|
|
84
|
+
> **📦 Distilled history:** <Description of what was archived> — full raw content in [archive](../archive/<relative-path>.md). Last distillation: YYYY-MM-DD.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The relative path needs to navigate from the doc's location to `docs/archive/`. Use `../` as needed.
|
|
88
|
+
|
|
89
|
+
2. **A thorough summary** of the archived sections, immediately after the notice. The summary must:
|
|
90
|
+
- Be substantive — capture key insights, decisions, evidence, quotes future readers (or LLMs) need
|
|
91
|
+
- Preserve all numbers, names, dates, and concrete claims
|
|
92
|
+
- Use clear subheadings if covering multiple sessions/topics
|
|
93
|
+
- Reference the archive for verbatim details: e.g. "_See full transcript in [archive](../archive/...md#session-1-2026-03-26)._"
|
|
94
|
+
- Be marked clearly as a summary, not original content
|
|
95
|
+
|
|
96
|
+
```markdown
|
|
97
|
+
## Summary of <description> (distilled YYYY-MM-DD)
|
|
98
|
+
|
|
99
|
+
[Thorough summary here. Preserve key insights, quotes, numbers. Cross-link to archive for verbatim.]
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Leave the kept verbatim sections completely untouched.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Workflow B — `Compression: amended`
|
|
109
|
+
|
|
110
|
+
### Step 1: Identify amendment blocks
|
|
111
|
+
|
|
112
|
+
Read the doc. Find all `> **[UPDATE YYYY-MM-DD · Source: ...]**` blocks. Group them by their parent section/claim.
|
|
113
|
+
|
|
114
|
+
Show the user the list and propose which ones to collapse. **Default rule:** within each section, keep the N most recent amendments inline; collapse the older ones into the main body text and archive the raw blocks.
|
|
115
|
+
|
|
116
|
+
**Confirm with user before proceeding.**
|
|
117
|
+
|
|
118
|
+
### Step 2: Create/update the archive file
|
|
119
|
+
|
|
120
|
+
Same path scheme as Workflow A. Same archive frontmatter.
|
|
121
|
+
|
|
122
|
+
### Step 3: Append the new archive section (verbatim)
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
## Archived YYYY-MM-DD — Collapsed amendments from <doc>
|
|
126
|
+
|
|
127
|
+
> Amendment blocks moved here from `<original/path.md>` on YYYY-MM-DD. These were the verbatim `[UPDATE]` blocks at time of distillation.
|
|
128
|
+
|
|
129
|
+
### Section: <name of section the amendments belonged to>
|
|
130
|
+
|
|
131
|
+
<EXACT verbatim copy of the old `> **[UPDATE ...]**` blocks, preserving order and source citations>
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Step 4: Rewrite the original doc body
|
|
137
|
+
|
|
138
|
+
For each collapsed amendment, **integrate its content into the main claim text** (not a separate block). The main body should now read as the current consensus state, with the amendment evidence absorbed. Then add a single notice at the section level:
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
> **📦 Distilled history:** <N> older amendments collapsed into body — see [archive](../archive/<relative-path>.md#section-...) for raw blocks. Last distillation: YYYY-MM-DD.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Keep the most recent N amendment blocks inline as-is.
|
|
145
|
+
|
|
146
|
+
**Critical:** never silently drop a claim. If two old amendments contradict, preserve both as "[date X said A; date Y said B; current view: ...]" in the body.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 5 (both workflows): Update dates and verify
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
tools/touch-date.sh <original-doc> <archive-doc>
|
|
154
|
+
tools/verify-cascade.sh
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Fix any errors verify-cascade reports.
|
|
158
|
+
|
|
159
|
+
## Step 6: Report
|
|
160
|
+
|
|
161
|
+
Tell the user:
|
|
162
|
+
- Mode used (`chronological` or `amended`)
|
|
163
|
+
- What was distilled (sections/amendments + date range)
|
|
164
|
+
- Where the archive lives (path)
|
|
165
|
+
- What stayed verbatim
|
|
166
|
+
- Line count of original doc (before vs after)
|
|
167
|
+
- Any verification warnings
|
|
168
|
+
|
|
169
|
+
Offer to commit:
|
|
170
|
+
```
|
|
171
|
+
git add <original-doc> <archive-doc>
|
|
172
|
+
git commit -m "distill: compress <doc-name> — moved <description> to archive"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Important rules
|
|
176
|
+
|
|
177
|
+
- **Never lose data.** Full verbatim content goes to the archive — exactly as it was.
|
|
178
|
+
- **Preserve metric references.** Keep `[**N**](metrics.yml#field)` links intact in both archive and summary.
|
|
179
|
+
- **Don't touch the TL;DR or frontmatter** — those stay on the main doc (the TL;DR may reference distilled summaries, but is current-state, not history).
|
|
180
|
+
- **Don't distill the most recent sections** — they're the active context.
|
|
181
|
+
- **The summary must be useful enough that an LLM reading only the main doc has full context for current decisions.** The archive is for the rare case when something old becomes relevant again.
|
|
182
|
+
- **If `Compression:` is missing or `none`, refuse to run.** Don't guess.
|
|
183
|
+
|
|
184
|
+
## Arguments passed
|
|
185
|
+
|
|
186
|
+
$ARGUMENTS
|