twin-cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,85 +1,139 @@
1
1
  # Twin-Driven Development
2
2
 
3
- > **Old TDD**: write the tests first. **New TDD**: write the twin first.
3
+ Old TDD meant write the tests first. New TDD means write the twin first.
4
4
 
5
- A `.twin` file encodes your decision-making DNA your taste, your biases, your heuristics. Drop it into any project and your AI tools make decisions the way you would.
6
-
7
- The twin is not a PRD. It's not a project spec. It's **you**.
5
+ A `.twin` file encodes how you think. Drop it into any project. Your AI does not wait for instructions. It decides what to build next and builds it. Not human in the loop. Twin in the loop.
8
6
 
9
7
  ## The Problem
10
8
 
11
- AI agents have velocity but no vector. They'll do whatever you tell them — but when you stop telling them, they stop. You're not writing code, but you're always on call. Checking at 2am. Writing the next batch. Going back to sleep.
9
+ You give an agent a task list. It finishes. It stops. Now you have to decide what comes next.
12
10
 
13
- **We call this Vampire Coding.** Technically hands-off. Practically consumed.
11
+ So you check at 2am. Write the next batch. Go back to sleep. Wake at 5am. Check again. You are not writing code but you are on call around the clock.
14
12
 
15
- The root cause: agents don't know what you *would* build next. They don't have your taste.
13
+ This is Vampire Coding. Hands-off in theory. Consumed in practice.
16
14
 
17
- ## The Solution
15
+ The root cause: agents do not know what you would build next. They can execute. They cannot decide.
18
16
 
19
- Three commands. That's it.
17
+ ## The Fix
20
18
 
21
- ```bash
22
- twin init # encode your taste
23
- twin plan # your twin decides what to build
24
- twin build # your twin builds it
19
+ Three commands.
20
+
21
+ ```
22
+ twin init # encode how you think
23
+ twin plan # your twin decides what to build next
24
+ twin build # your twin builds it without you
25
25
  ```
26
26
 
27
- ## Prerequisites
27
+ The difference between a `.twin` file and a Claude MD or a rules file: those are instructions. This is a decision-maker. Your twin knows how you think and decides what comes next.
28
+
29
+ Run `twin plan` once and you get the next batch of features you would have chosen. Run it again and you get the batch after that. Keep running it and you are watching your own product roadmap unfold without writing a single task yourself.
30
+
31
+ ## Before You Start
28
32
 
29
33
  - **Node.js 18+** — [nodejs.org](https://nodejs.org)
30
- - **OpenRouter API key** — [openrouter.ai/keys](https://openrouter.ai/keys) (for `twin init` and `twin plan`)
31
- - **Claude Code** — [docs.anthropic.com/en/docs/claude-code](https://docs.anthropic.com/en/docs/claude-code) (for `twin build`)
34
+ - **Claude Code** — [docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code) (powers all three commands)
32
35
 
33
36
  ## Quick Start
34
37
 
38
+ Each twin project starts in its own folder. Create one, then run from inside it.
39
+
35
40
  ```bash
36
- # 1. Set your OpenRouter API key
37
- export OPENROUTER_API_KEY="your-key-here"
41
+ # 0. Create a project folder
42
+ mkdir my-app && cd my-app
38
43
 
39
- # 2. Create your twin
44
+ # 1. Create your twin (once)
40
45
  npx twin-cli init
41
46
  # → Asks your name, then 5 questions about how you build
42
- # → Generates dru.twin (or whatever your name is)
47
+ # → Generates yourname.twin
43
48
 
44
- # 3. Generate your first plan
49
+ # 2. Generate your first plan
45
50
  npx twin-cli plan
46
51
  # → Reads your twin, asks about your product, writes prd.json
47
52
 
48
- # 4. Let your twin build it
53
+ # 3. Let your twin build
54
+ npx twin-cli build
55
+ # → Spawns Claude Code in a loop
56
+ # → Builds each story, updates prd.json as it goes
57
+ ```
58
+
59
+ ## Example: Building a Habit Tracker
60
+
61
+ A full walkthrough from zero to working app.
62
+
63
+ ```bash
64
+ # Create the project
65
+ mkdir habit-tracker && cd habit-tracker
66
+
67
+ # Create your twin
68
+ npx twin-cli init
69
+ # → "What should we call you?" → Dru
70
+ # → Answer 5 questions about how you build
71
+ # → Generates dru.twin
72
+
73
+ # Generate the plan
74
+ npx twin-cli plan
75
+ # → "What are you building?" → A habit app with GitHub-style tracking and timers
76
+ # → "Who is it for?" → People who want to build daily habits
77
+ # → Writes prd.json with 3-5 user stories based on YOUR taste
78
+
79
+ # Build it
80
+ npx twin-cli build
81
+ # → Claude picks the first story, builds it, marks it done
82
+ # → Picks the next story, builds it, marks it done
83
+ # → You watch it happen in real time
84
+
85
+ # Want more features? Plan again.
86
+ npx twin-cli plan
87
+ # → Sees what is done, generates the next batch
49
88
  npx twin-cli build
50
- # → Spawns Claude Code in a loop, builds each story, updates prd.json as it goes
51
89
  ```
52
90
 
53
- Get an OpenRouter key at [openrouter.ai/keys](https://openrouter.ai/keys).
91
+ Your `.twin` file is portable. Copy it into any new project and run `twin plan` to start.
92
+
93
+ ## Project Ideas
94
+
95
+ Twin works best on new projects. Some things to try:
54
96
 
55
- ## The Loop: init plan build → plan
97
+ - Habit tracker with streaks and daily timers
98
+ - Personal landing page with email capture and a changelog
99
+ - Micro-SaaS dashboard for tracking one metric
100
+ - CLI tool that solves a problem you keep solving by hand
101
+ - PWA that replaces a spreadsheet you use each day
56
102
 
57
- This is the core workflow. Your twin drives the whole cycle:
103
+ ## The Loop
58
104
 
59
- 1. **`twin init`** create your taste profile (once)
60
- 2. **`twin plan`** — your twin generates tasks that match how you'd prioritize
61
- 3. **`twin build`** — your twin builds autonomously, updating prd.json as stories complete
62
- 4. **`twin plan`** again — your twin sees what's done and plans what's next
105
+ `init plan build plan`
63
106
 
64
- Each iteration, Claude Code starts fresh but reads the files on disk — your twin, the PRD, and a progress log. The files are the memory. Your taste stays consistent across every iteration.
107
+ Your twin drives the whole cycle.
108
+
109
+ 1. **`twin init`** — encode how you think (once)
110
+ 2. **`twin plan`** — your twin generates tasks that match how you prioritize
111
+ 3. **`twin build`** — your twin builds on its own, updating `prd.json` as stories complete
112
+ 4. **`twin plan` again** — your twin reads what shipped and decides what comes next
113
+
114
+ You did not write a task list. The twin wrote it. You did not pick the next feature. The twin picked it. That is the shift: from reactive to proactive.
115
+
116
+ Each iteration, Claude Code starts fresh but reads the files on disk. Your twin. The PRD. A progress log. The files are the memory. Your taste stays consistent across runs.
65
117
 
66
118
  ## Commands
67
119
 
68
120
  ### `twin init`
69
- Asks your name, then 5 questions about how you build things. Generates `yourname.twin` — your decision-making DNA.
121
+
122
+ Asks your name, then 5 questions about how you build things. Generates `yourname.twin`.
70
123
 
71
124
  ### `twin plan`
72
- Reads your `.twin` file + project context, generates 3-5 atomic capabilities that match your taste.
73
125
 
74
- Writes **`prd.json`** — structured JSON with user stories and status tracking.
126
+ Reads your `.twin` file and project context. Generates 3-5 capabilities that match your taste. Writes `prd.json` with user stories and status tracking.
75
127
 
76
- If no `product.md` exists, `twin plan` asks 2 quick questions to set up your project context first. Running it again adds new stories without duplicating existing ones.
128
+ If no `product.md` exists, `twin plan` asks 2 quick questions to set up your project context first. Running it again adds new stories without duplicating old ones.
77
129
 
78
130
  ### `twin build`
131
+
79
132
  Runs an autonomous build loop using Claude Code. Each iteration:
80
- - Reads your twin file for taste
133
+
134
+ - Reads your twin file for how you think
81
135
  - Reads `prd.json` for open stories
82
- - Picks the next story to build (the model decides, based on your taste)
136
+ - Picks the next story to build (guided by your taste)
83
137
  - Builds it, commits, and marks it done in `prd.json`
84
138
  - Appends learnings to `progress.md`
85
139
  - Repeats until all stories are done or max iterations hit
@@ -89,29 +143,27 @@ twin build # default: 5 iterations
89
143
  twin build --max-iterations 10 # custom limit
90
144
  ```
91
145
 
92
- Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and available in your PATH.
146
+ Requires Claude Code installed and available in your PATH.
93
147
 
94
148
  ## What Goes in a `.twin` File
95
149
 
96
- - **Execution Bias** — do you plan or build first? Ship ugly or wait for polish?
97
- - **Quality Compass** — how do you define "done"? What does "good" mean to you?
98
- - **Decision-Making Style** — how do you get unstuck? How do you evaluate tradeoffs?
99
- - **Strongly Held Beliefs** — what do you believe that others would disagree with?
100
- - **Anti-Patterns** — what do you refuse to do?
150
+ - **Execution bias** — do you plan first or build first? Ship rough or wait for polish?
151
+ - **Quality compass** — how do you define "done"? What does "good" look like to you?
152
+ - **Decision-making style** — how do you get unstuck? How do you weigh trade-offs?
153
+ - **Strong beliefs** — what do you believe that most would disagree with?
154
+ - **Anti-patterns** — what do you refuse to do?
101
155
 
102
156
  ## What Does NOT Go in a `.twin` File
103
157
 
104
- - Your product's target user (that's a product doc)
105
- - Your tech stack (that's a project config)
106
- - Your roadmap (that's a PRD)
107
-
108
- **The twin encodes how you think. Project files encode what you're building. The twin is the founder. The project files are the company.**
158
+ - Your product's target user (that goes in a product doc)
159
+ - Your tech stack (that goes in project config)
160
+ - Your roadmap (that goes in a PRD)
109
161
 
110
- ## Philosophy
162
+ The twin encodes how you think. The project files encode what you are building. The twin is the founder. The project files are the company.
111
163
 
112
- This project follows the skateboard-to-car model of iterative delivery.
164
+ ## What Comes Next
113
165
 
114
- What comes next: `twin tweak` for natural language updates, `twin share` for publishing your taste publicly.
166
+ `twin tweak` for natural language updates. `twin share` for publishing your taste publicly.
115
167
 
116
168
  ## License
117
169
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "twin-cli",
3
- "version": "0.1.0",
4
- "description": "Encode your decision-making DNA into a .twin file. The .env of taste.",
3
+ "version": "0.2.1",
4
+ "description": "Your twin builds while you sleep. Not human in the loop. Twin in the loop.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "twin": "./bin/twin.js"
package/src/build.js CHANGED
@@ -56,27 +56,33 @@ ${progressContent ? `\n## progress.md\n${progressContent}` : ''}
56
56
  `;
57
57
  }
58
58
 
59
- function extractTextFromEvent(jsonLine) {
59
+ function parseEvent(jsonLine) {
60
60
  try {
61
61
  const event = JSON.parse(jsonLine);
62
62
 
63
63
  // Text delta from assistant message streaming
64
64
  if (event.type === 'assistant' && event.message?.content) {
65
- // Full message content (final)
66
- return event.message.content
65
+ const text = event.message.content
67
66
  .filter((block) => block.type === 'text')
68
67
  .map((block) => block.text)
69
68
  .join('');
69
+ if (text) return { type: 'text', text };
70
70
  }
71
71
 
72
72
  // Partial streaming text delta
73
73
  if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
74
- return event.delta.text;
74
+ return { type: 'text', text: event.delta.text };
75
75
  }
76
76
 
77
- // Result message with text
78
- if (event.type === 'result' && event.result) {
79
- return null; // Already captured via deltas
77
+ // Tool use show what Claude is doing
78
+ if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
79
+ const name = event.content_block.name || 'working';
80
+ return { type: 'tool', name };
81
+ }
82
+
83
+ // Tool result
84
+ if (event.type === 'content_block_start' && event.content_block?.type === 'tool_result') {
85
+ return { type: 'tool_done' };
80
86
  }
81
87
  } catch {
82
88
  // Not valid JSON or unexpected shape — skip
@@ -98,16 +104,41 @@ function runIteration(prompt, cwd) {
98
104
 
99
105
  let output = '';
100
106
  let buffer = '';
101
- let hasReceivedText = false;
107
+ let lastActivity = Date.now();
108
+ let statusLine = false; // true when a status line is showing
109
+
110
+ const TOOL_LABELS = {
111
+ Read: 'Reading file',
112
+ Write: 'Writing file',
113
+ Edit: 'Editing file',
114
+ Bash: 'Running command',
115
+ Glob: 'Searching files',
116
+ Grep: 'Searching code',
117
+ };
118
+
119
+ function clearStatus() {
120
+ if (statusLine) {
121
+ process.stdout.write('\r\x1b[K');
122
+ statusLine = false;
123
+ }
124
+ }
125
+
126
+ function showStatus(msg) {
127
+ clearStatus();
128
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
129
+ process.stdout.write(`\r\x1b[2m${msg} (${elapsed}s)\x1b[0m`);
130
+ statusLine = true;
131
+ }
102
132
 
103
133
  // Heartbeat timer — show elapsed time while waiting
104
134
  const startTime = Date.now();
105
135
  const heartbeat = setInterval(() => {
106
136
  const elapsed = Math.round((Date.now() - startTime) / 1000);
107
- if (!hasReceivedText) {
108
- process.stdout.write(`\rWorking... (${elapsed}s)`);
137
+ const idle = Date.now() - lastActivity;
138
+ if (idle > 5_000) {
139
+ showStatus('Working...');
109
140
  }
110
- }, 10_000);
141
+ }, 5_000);
111
142
 
112
143
  claude.stdout.on('data', (chunk) => {
113
144
  buffer += chunk.toString();
@@ -116,15 +147,20 @@ function runIteration(prompt, cwd) {
116
147
 
117
148
  for (const line of lines) {
118
149
  if (!line.trim()) continue;
119
- const text = extractTextFromEvent(line);
120
- if (text) {
121
- if (!hasReceivedText) {
122
- // Clear the heartbeat line on first real output
123
- process.stdout.write('\r\x1b[K');
124
- hasReceivedText = true;
125
- }
126
- process.stdout.write(text);
127
- output += text;
150
+ const event = parseEvent(line);
151
+ if (!event) continue;
152
+
153
+ lastActivity = Date.now();
154
+
155
+ if (event.type === 'text') {
156
+ clearStatus();
157
+ process.stdout.write(event.text);
158
+ output += event.text;
159
+ } else if (event.type === 'tool') {
160
+ const label = TOOL_LABELS[event.name] || event.name;
161
+ showStatus(label);
162
+ } else if (event.type === 'tool_done') {
163
+ clearStatus();
128
164
  }
129
165
  }
130
166
  });
@@ -135,17 +171,15 @@ function runIteration(prompt, cwd) {
135
171
 
136
172
  claude.on('close', (code) => {
137
173
  clearInterval(heartbeat);
174
+ clearStatus();
138
175
  // Process any remaining buffer
139
176
  if (buffer.trim()) {
140
- const text = extractTextFromEvent(buffer.trim());
141
- if (text) {
142
- process.stdout.write(text);
143
- output += text;
177
+ const event = parseEvent(buffer.trim());
178
+ if (event?.type === 'text') {
179
+ process.stdout.write(event.text);
180
+ output += event.text;
144
181
  }
145
182
  }
146
- if (!hasReceivedText) {
147
- process.stdout.write('\r\x1b[K'); // Clear heartbeat line
148
- }
149
183
  resolvePromise({ output, code });
150
184
  });
151
185
 
@@ -214,7 +248,9 @@ export async function build(maxIterations = 5) {
214
248
  if (output.includes(ALL_DONE_SIGNAL)) {
215
249
  console.log(`\n${'='.repeat(60)}`);
216
250
  console.log(' All stories complete!');
217
- console.log(`${'='.repeat(60)}\n`);
251
+ console.log(`${'='.repeat(60)}`);
252
+ console.log('\nNext step — plan more features:');
253
+ console.log(' twin plan\n');
218
254
  break;
219
255
  }
220
256
 
@@ -225,7 +261,9 @@ export async function build(maxIterations = 5) {
225
261
  if (remaining.length === 0) {
226
262
  console.log(`\n${'='.repeat(60)}`);
227
263
  console.log(' All stories complete!');
228
- console.log(`${'='.repeat(60)}\n`);
264
+ console.log(`${'='.repeat(60)}`);
265
+ console.log('\nNext step — plan more features:');
266
+ console.log(' twin plan\n');
229
267
  break;
230
268
  }
231
269
  console.log(`\nStory complete. ${remaining.length} remaining.\n`);
package/src/generate.js CHANGED
@@ -42,9 +42,8 @@ Rules:
42
42
  - Keep it under 80 lines total
43
43
  - No preamble, no explanation — just the .twin file content`;
44
44
 
45
- export async function generateTwin(apiKey, name, interviewText) {
45
+ export async function generateTwin(name, interviewText) {
46
46
  const content = await callLLM(
47
- apiKey,
48
47
  SYSTEM_PROMPT,
49
48
  `The builder's name is ${name}.\n\nHere are the interview answers. Generate the .twin file.\n\n${interviewText}`,
50
49
  );
@@ -53,5 +52,6 @@ export async function generateTwin(apiKey, name, interviewText) {
53
52
  const outPath = resolve(process.cwd(), filename);
54
53
  await writeFile(outPath, content, 'utf-8');
55
54
  console.log(`Done! Your twin file is at: ${outPath}`);
56
- console.log('\nDrop this file into any project and your AI tools will know your taste.');
55
+ console.log('\nNext step plan what to build:');
56
+ console.log(' twin plan');
57
57
  }
package/src/init.js CHANGED
@@ -1,4 +1,3 @@
1
- import { requireKey, checkKey } from './llm.js';
2
1
  import { createPrompter } from './prompt.js';
3
2
  import { generateTwin } from './generate.js';
4
3
 
@@ -11,13 +10,7 @@ const QUESTIONS = [
11
10
  ];
12
11
 
13
12
  export async function init() {
14
- const key = requireKey();
15
-
16
- console.log('\nChecking API connection...');
17
- await checkKey(key);
18
- console.log('Connected.\n');
19
-
20
- console.log('--- twin init ---');
13
+ console.log('\n--- twin init ---');
21
14
  console.log('Answer a few questions. Say as much or as little as you want.');
22
15
  console.log('Your answers will be used to generate your .twin file.\n');
23
16
 
@@ -35,5 +28,5 @@ export async function init() {
35
28
  console.log('\nGenerating your .twin file...\n');
36
29
 
37
30
  const paired = answers.map((a) => `Q: ${a.question}\nA: ${a.answer}`).join('\n\n');
38
- await generateTwin(key, name, paired);
31
+ await generateTwin(name, paired);
39
32
  }
package/src/llm.js CHANGED
@@ -1,59 +1,52 @@
1
- export function requireKey() {
2
- const key = process.env.OPENROUTER_API_KEY;
3
- if (!key) {
4
- console.log('\nSet your OpenRouter API key first:');
5
- console.log(' export OPENROUTER_API_KEY="your-key-here"');
6
- console.log('\nGet one at https://openrouter.ai/keys\n');
7
- process.exit(1);
8
- }
9
- return key;
10
- }
1
+ import { spawn } from 'node:child_process';
11
2
 
12
- export async function checkKey(key) {
13
- try {
14
- const res = await fetch('https://openrouter.ai/api/v1/models', {
15
- headers: { 'Authorization': `Bearer ${key}` },
3
+ export async function callLLM(systemPrompt, userMessage) {
4
+ const prompt = `${systemPrompt}\n\n${userMessage}`;
5
+
6
+ const output = await new Promise((resolve, reject) => {
7
+ const claude = spawn('claude', ['--print'], {
8
+ stdio: ['pipe', 'pipe', 'pipe'],
9
+ env: { ...process.env },
16
10
  });
17
- if (!res.ok) {
18
- console.error(`\nAPI key check failed (${res.status}). Make sure your OPENROUTER_API_KEY is valid.`);
19
- console.log('Get one at https://openrouter.ai/keys\n');
20
- process.exit(1);
21
- }
22
- } catch (e) {
23
- console.error('\nCould not connect to OpenRouter. Check your internet connection.\n');
24
- process.exit(1);
25
- }
26
- }
27
11
 
28
- export async function callLLM(apiKey, systemPrompt, userMessage) {
29
- const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
30
- method: 'POST',
31
- headers: {
32
- 'Authorization': `Bearer ${apiKey}`,
33
- 'Content-Type': 'application/json',
34
- },
35
- body: JSON.stringify({
36
- model: 'google/gemini-3-flash-preview',
37
- messages: [
38
- { role: 'system', content: systemPrompt },
39
- { role: 'user', content: userMessage },
40
- ],
41
- }),
42
- });
12
+ let stdout = '';
13
+ let stderr = '';
43
14
 
44
- if (!res.ok) {
45
- const err = await res.text();
46
- console.error(`OpenRouter API error (${res.status}): ${err}`);
47
- process.exit(1);
48
- }
15
+ claude.stdout.on('data', (chunk) => {
16
+ stdout += chunk.toString();
17
+ });
18
+
19
+ claude.stderr.on('data', (chunk) => {
20
+ stderr += chunk.toString();
21
+ });
22
+
23
+ claude.on('close', (code) => {
24
+ if (code !== 0) {
25
+ console.error(`Claude Code exited with code ${code}.`);
26
+ if (stderr) console.error(stderr);
27
+ process.exit(1);
28
+ }
29
+ resolve(stdout.trim());
30
+ });
49
31
 
50
- const data = await res.json();
51
- const content = data.choices?.[0]?.message?.content;
32
+ claude.on('error', (err) => {
33
+ if (err.code === 'ENOENT') {
34
+ console.error('\nClaude Code is not installed or not in PATH.');
35
+ console.error('Install it: https://docs.anthropic.com/en/docs/claude-code\n');
36
+ process.exit(1);
37
+ }
38
+ console.error(`\nFailed to spawn Claude: ${err.message}\n`);
39
+ process.exit(1);
40
+ });
41
+
42
+ claude.stdin.write(prompt);
43
+ claude.stdin.end();
44
+ });
52
45
 
53
- if (!content) {
54
- console.error('No content returned from API.');
46
+ if (!output) {
47
+ console.error('No content returned from Claude Code.');
55
48
  process.exit(1);
56
49
  }
57
50
 
58
- return content;
51
+ return output;
59
52
  }
package/src/plan.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile, writeFile, readdir } from 'node:fs/promises';
2
2
  import { resolve, basename } from 'node:path';
3
- import { requireKey, checkKey, callLLM } from './llm.js';
3
+ import { callLLM } from './llm.js';
4
4
  import { createPrompter } from './prompt.js';
5
5
 
6
6
  const TASK_SYSTEM_PROMPT = `You are a taste-aware product planner. You receive:
@@ -72,12 +72,6 @@ function parseLLMJson(raw) {
72
72
  }
73
73
 
74
74
  export async function plan() {
75
- const key = requireKey();
76
-
77
- console.log('\nChecking API connection...');
78
- await checkKey(key);
79
- console.log('Connected.\n');
80
-
81
75
  // Find *.twin file — required
82
76
  const cwd = process.cwd();
83
77
  const files = await readdir(cwd);
@@ -119,7 +113,7 @@ export async function plan() {
119
113
  console.log('--- twin plan ---');
120
114
  console.log('Generating tasks that match your taste...\n');
121
115
 
122
- const raw = await callLLM(key, TASK_SYSTEM_PROMPT, userMessage);
116
+ const raw = await callLLM(TASK_SYSTEM_PROMPT, userMessage);
123
117
 
124
118
  // Parse structured output
125
119
  let prd;
@@ -136,6 +130,19 @@ export async function plan() {
136
130
  prd.project = basename(process.cwd());
137
131
  }
138
132
 
133
+ // Merge with existing stories if prd.json already exists
134
+ if (existingPrd) {
135
+ try {
136
+ const existing = JSON.parse(existingPrd);
137
+ const existingStories = existing.userStories || [];
138
+ prd.userStories = [...existingStories, ...prd.userStories];
139
+ if (!prd.project && existing.project) prd.project = existing.project;
140
+ if (!prd.description && existing.description) prd.description = existing.description;
141
+ } catch {
142
+ // If existing prd.json is malformed, just use the new one
143
+ }
144
+ }
145
+
139
146
  // Write prd.json
140
147
  await writeFile(prdPath, JSON.stringify(prd, null, 2) + '\n', 'utf-8');
141
148