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 +93 -45
- package/package.json +2 -2
- package/src/build.js +6 -2
- package/src/generate.js +3 -3
- package/src/init.js +2 -9
- package/src/llm.js +42 -49
- package/src/plan.js +2 -8
package/README.md
CHANGED
|
@@ -1,85 +1,135 @@
|
|
|
1
1
|
# Twin-Driven Development
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Old TDD meant write the tests first. New TDD means write the twin first.
|
|
4
4
|
|
|
5
|
-
A `.twin` file
|
|
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
|
|
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
|
|
11
|
+
AI agents have velocity but no vector. They do what you tell them. When you stop telling them, they stop.
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
15
|
+
This is Vampire Coding. Hands-off in theory. Consumed in practice.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The root cause: agents do not know what you would build next. They do not have your taste.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## The Fix
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
##
|
|
29
|
+
## Before You Start
|
|
28
30
|
|
|
29
31
|
- **Node.js 18+** — [nodejs.org](https://nodejs.org)
|
|
30
|
-
- **
|
|
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
|
-
#
|
|
37
|
-
|
|
39
|
+
# 0. Create a project folder
|
|
40
|
+
mkdir my-app && cd my-app
|
|
38
41
|
|
|
39
|
-
#
|
|
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
|
|
45
|
+
# → Generates yourname.twin
|
|
43
46
|
|
|
44
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
3. **`twin build`** — your twin builds
|
|
62
|
-
4. **`twin plan
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
142
|
+
Requires Claude Code installed and available in your PATH.
|
|
93
143
|
|
|
94
144
|
## What Goes in a `.twin` File
|
|
95
145
|
|
|
96
|
-
- **Execution
|
|
97
|
-
- **Quality
|
|
98
|
-
- **Decision-
|
|
99
|
-
- **
|
|
100
|
-
- **Anti-
|
|
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
|
|
105
|
-
- Your tech stack (that
|
|
106
|
-
- Your roadmap (that
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
## What Comes Next
|
|
113
161
|
|
|
114
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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)}
|
|
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)}
|
|
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(
|
|
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('\
|
|
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
|
-
|
|
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(
|
|
31
|
+
await generateTwin(name, paired);
|
|
39
32
|
}
|
package/src/llm.js
CHANGED
|
@@ -1,59 +1,52 @@
|
|
|
1
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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 (!
|
|
54
|
-
console.error('No content returned from
|
|
46
|
+
if (!output) {
|
|
47
|
+
console.error('No content returned from Claude Code.');
|
|
55
48
|
process.exit(1);
|
|
56
49
|
}
|
|
57
50
|
|
|
58
|
-
return
|
|
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 {
|
|
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(
|
|
116
|
+
const raw = await callLLM(TASK_SYSTEM_PROMPT, userMessage);
|
|
123
117
|
|
|
124
118
|
// Parse structured output
|
|
125
119
|
let prd;
|