twin-cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,85 +1,135 @@
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.
5
+ A `.twin` file is your decision-making source code. Your taste. Your biases. Your heuristics. Drop it into any project and your AI tools make decisions the way you would.
6
6
 
7
- The twin is not a PRD. It's not a project spec. It's **you**.
7
+ The twin is not a PRD. It is not a project spec. It is you.
8
8
 
9
9
  ## The Problem
10
10
 
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.
11
+ AI agents have velocity but no vector. They do what you tell them. When you stop telling them, they stop.
12
12
 
13
- **We call this Vampire Coding.** Technically hands-off. Practically consumed.
13
+ You are not writing code. But you are checking at 2am. Writing the next batch of tasks. Going back to sleep. Waking at 5am to check again.
14
14
 
15
- The root cause: agents don't know what you *would* build next. They don't have your taste.
15
+ This is Vampire Coding. Hands-off in theory. Consumed in practice.
16
16
 
17
- ## The Solution
17
+ The root cause: agents do not know what you would build next. They do not have your taste.
18
18
 
19
- Three commands. That's it.
19
+ ## The Fix
20
20
 
21
- ```bash
21
+ Three commands.
22
+
23
+ ```
22
24
  twin init # encode your taste
23
25
  twin plan # your twin decides what to build
24
26
  twin build # your twin builds it
25
27
  ```
26
28
 
27
- ## Prerequisites
29
+ ## Before You Start
28
30
 
29
31
  - **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`)
32
+ - **Claude Code** — [docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code) (powers all three commands)
32
33
 
33
34
  ## Quick Start
34
35
 
36
+ Every twin project starts in its own folder. Create one, then run the commands from inside it.
37
+
35
38
  ```bash
36
- # 1. Set your OpenRouter API key
37
- export OPENROUTER_API_KEY="your-key-here"
39
+ # 0. Create a project folder
40
+ mkdir my-app && cd my-app
38
41
 
39
- # 2. Create your twin
42
+ # 1. Create your twin (only need to do this once)
40
43
  npx twin-cli init
41
44
  # → Asks your name, then 5 questions about how you build
42
- # → Generates dru.twin (or whatever your name is)
45
+ # → Generates yourname.twin
43
46
 
44
- # 3. Generate your first plan
47
+ # 2. Generate your first plan
45
48
  npx twin-cli plan
46
49
  # → Reads your twin, asks about your product, writes prd.json
47
50
 
48
- # 4. Let your twin build it
51
+ # 3. Let your twin build
52
+ npx twin-cli build
53
+ # → Spawns Claude Code in a loop
54
+ # → Builds each story, updates prd.json as it goes
55
+ ```
56
+
57
+ ## Example: Building a Habit Tracker
58
+
59
+ Here is a full walkthrough from zero to working app.
60
+
61
+ ```bash
62
+ # Create the project
63
+ mkdir habit-tracker && cd habit-tracker
64
+
65
+ # Create your twin
66
+ npx twin-cli init
67
+ # → "What should we call you?" → Dru
68
+ # → Answer 5 questions about how you build
69
+ # → Generates dru.twin
70
+
71
+ # Generate the plan
72
+ npx twin-cli plan
73
+ # → "What are you building?" → A habit app with GitHub-style tracking and timers
74
+ # → "Who is it for?" → People who want to build daily habits
75
+ # → Writes prd.json with 3-5 user stories based on YOUR taste
76
+
77
+ # Build it
78
+ npx twin-cli build
79
+ # → Claude picks the first story, builds it, marks it done
80
+ # → Picks the next story, builds it, marks it done
81
+ # → You watch it happen in real time
82
+
83
+ # Want more features? Plan again.
84
+ npx twin-cli plan
85
+ # → Sees what is done, generates the next batch
49
86
  npx twin-cli build
50
- # → Spawns Claude Code in a loop, builds each story, updates prd.json as it goes
51
87
  ```
52
88
 
53
- Get an OpenRouter key at [openrouter.ai/keys](https://openrouter.ai/keys).
89
+ Your `.twin` file is portable. Copy it into any new project and run `twin plan` to start.
90
+
91
+ ## Project Ideas
92
+
93
+ Twin works best on new projects. Some things to try:
54
94
 
55
- ## The Loop: init plan build → plan
95
+ - **Habit tracker** with streaks and daily timers
96
+ - **Personal landing page** with email capture and a changelog
97
+ - **Micro-SaaS dashboard** for tracking one metric
98
+ - **CLI tool** that solves a problem you keep solving manually
99
+ - **PWA** that replaces a spreadsheet you use every day
56
100
 
57
- This is the core workflow. Your twin drives the whole cycle:
101
+ ## The Loop
102
+
103
+ `init → plan → build → plan`
104
+
105
+ This is the core cycle. Your twin drives the whole thing.
58
106
 
59
107
  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
108
+ 2. **`twin plan`** — your twin generates tasks that match how you prioritize
109
+ 3. **`twin build`** — your twin builds on its own, updating `prd.json` as stories complete
110
+ 4. **`twin plan` again** — your twin reads what shipped and plans what comes next
63
111
 
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.
112
+ 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
113
 
66
114
  ## Commands
67
115
 
68
116
  ### `twin init`
69
- Asks your name, then 5 questions about how you build things. Generates `yourname.twin` — your decision-making DNA.
117
+
118
+ Asks your name, then 5 questions about how you build things. Generates `yourname.twin`.
70
119
 
71
120
  ### `twin plan`
72
- Reads your `.twin` file + project context, generates 3-5 atomic capabilities that match your taste.
73
121
 
74
- Writes **`prd.json`** — structured JSON with user stories and status tracking.
122
+ Reads your `.twin` file and project context. Generates 3-5 atomic capabilities that match your taste. Writes `prd.json` with user stories and status tracking.
75
123
 
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.
124
+ 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
125
 
78
126
  ### `twin build`
127
+
79
128
  Runs an autonomous build loop using Claude Code. Each iteration:
129
+
80
130
  - Reads your twin file for taste
81
131
  - Reads `prd.json` for open stories
82
- - Picks the next story to build (the model decides, based on your taste)
132
+ - Picks the next story to build (the model decides, guided by your taste)
83
133
  - Builds it, commits, and marks it done in `prd.json`
84
134
  - Appends learnings to `progress.md`
85
135
  - Repeats until all stories are done or max iterations hit
@@ -89,29 +139,27 @@ twin build # default: 5 iterations
89
139
  twin build --max-iterations 10 # custom limit
90
140
  ```
91
141
 
92
- Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and available in your PATH.
142
+ Requires Claude Code installed and available in your PATH.
93
143
 
94
144
  ## What Goes in a `.twin` File
95
145
 
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?
146
+ - **Execution bias** — do you plan first or build first? Ship rough or wait for polish?
147
+ - **Quality compass** — how do you define "done"? What does "good" look like to you?
148
+ - **Decision-making style** — how do you get unstuck? How do you weigh trade-offs?
149
+ - **Strong beliefs** — what do you believe that most people would disagree with?
150
+ - **Anti-patterns** — what do you refuse to do?
101
151
 
102
152
  ## What Does NOT Go in a `.twin` File
103
153
 
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.**
154
+ - Your product's target user (that goes in a product doc)
155
+ - Your tech stack (that goes in project config)
156
+ - Your roadmap (that goes in a PRD)
109
157
 
110
- ## Philosophy
158
+ 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
159
 
112
- This project follows the skateboard-to-car model of iterative delivery.
160
+ ## What Comes Next
113
161
 
114
- What comes next: `twin tweak` for natural language updates, `twin share` for publishing your taste publicly.
162
+ `twin tweak` for natural language updates. `twin share` for publishing your taste.
115
163
 
116
164
  ## License
117
165
 
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.0",
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
@@ -214,7 +214,9 @@ export async function build(maxIterations = 5) {
214
214
  if (output.includes(ALL_DONE_SIGNAL)) {
215
215
  console.log(`\n${'='.repeat(60)}`);
216
216
  console.log(' All stories complete!');
217
- console.log(`${'='.repeat(60)}\n`);
217
+ console.log(`${'='.repeat(60)}`);
218
+ console.log('\nNext step — plan more features:');
219
+ console.log(' twin plan\n');
218
220
  break;
219
221
  }
220
222
 
@@ -225,7 +227,9 @@ export async function build(maxIterations = 5) {
225
227
  if (remaining.length === 0) {
226
228
  console.log(`\n${'='.repeat(60)}`);
227
229
  console.log(' All stories complete!');
228
- console.log(`${'='.repeat(60)}\n`);
230
+ console.log(`${'='.repeat(60)}`);
231
+ console.log('\nNext step — plan more features:');
232
+ console.log(' twin plan\n');
229
233
  break;
230
234
  }
231
235
  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;