twin-cli 0.2.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 +29 -25
- package/package.json +1 -1
- package/src/build.js +61 -27
- package/src/plan.js +13 -0
package/README.md
CHANGED
|
@@ -2,30 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Old TDD meant write the tests first. New TDD means write the twin first.
|
|
4
4
|
|
|
5
|
-
A `.twin` file
|
|
6
|
-
|
|
7
|
-
The twin is not a PRD. It is not a project spec. It is 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
|
-
|
|
9
|
+
You give an agent a task list. It finishes. It stops. Now you have to decide what comes next.
|
|
12
10
|
|
|
13
|
-
|
|
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
13
|
This is Vampire Coding. Hands-off in theory. Consumed in practice.
|
|
16
14
|
|
|
17
|
-
The root cause: agents do not know what you would build next. They
|
|
15
|
+
The root cause: agents do not know what you would build next. They can execute. They cannot decide.
|
|
18
16
|
|
|
19
17
|
## The Fix
|
|
20
18
|
|
|
21
19
|
Three commands.
|
|
22
20
|
|
|
23
21
|
```
|
|
24
|
-
twin init # encode
|
|
25
|
-
twin plan # your twin decides what to build
|
|
26
|
-
twin build # your twin builds it
|
|
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
|
|
27
25
|
```
|
|
28
26
|
|
|
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
|
+
|
|
29
31
|
## Before You Start
|
|
30
32
|
|
|
31
33
|
- **Node.js 18+** — [nodejs.org](https://nodejs.org)
|
|
@@ -33,13 +35,13 @@ twin build # your twin builds it
|
|
|
33
35
|
|
|
34
36
|
## Quick Start
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
Each twin project starts in its own folder. Create one, then run from inside it.
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
41
|
# 0. Create a project folder
|
|
40
42
|
mkdir my-app && cd my-app
|
|
41
43
|
|
|
42
|
-
# 1. Create your twin (
|
|
44
|
+
# 1. Create your twin (once)
|
|
43
45
|
npx twin-cli init
|
|
44
46
|
# → Asks your name, then 5 questions about how you build
|
|
45
47
|
# → Generates yourname.twin
|
|
@@ -56,7 +58,7 @@ npx twin-cli build
|
|
|
56
58
|
|
|
57
59
|
## Example: Building a Habit Tracker
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
A full walkthrough from zero to working app.
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
64
|
# Create the project
|
|
@@ -92,22 +94,24 @@ Your `.twin` file is portable. Copy it into any new project and run `twin plan`
|
|
|
92
94
|
|
|
93
95
|
Twin works best on new projects. Some things to try:
|
|
94
96
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
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
|
|
100
102
|
|
|
101
103
|
## The Loop
|
|
102
104
|
|
|
103
105
|
`init → plan → build → plan`
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
Your twin drives the whole cycle.
|
|
106
108
|
|
|
107
|
-
1. **`twin init`** —
|
|
109
|
+
1. **`twin init`** — encode how you think (once)
|
|
108
110
|
2. **`twin plan`** — your twin generates tasks that match how you prioritize
|
|
109
111
|
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
|
|
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.
|
|
111
115
|
|
|
112
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.
|
|
113
117
|
|
|
@@ -119,7 +123,7 @@ Asks your name, then 5 questions about how you build things. Generates `yourname
|
|
|
119
123
|
|
|
120
124
|
### `twin plan`
|
|
121
125
|
|
|
122
|
-
Reads your `.twin` file and project context. Generates 3-5
|
|
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.
|
|
123
127
|
|
|
124
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.
|
|
125
129
|
|
|
@@ -127,9 +131,9 @@ If no `product.md` exists, `twin plan` asks 2 quick questions to set up your pro
|
|
|
127
131
|
|
|
128
132
|
Runs an autonomous build loop using Claude Code. Each iteration:
|
|
129
133
|
|
|
130
|
-
- Reads your twin file for
|
|
134
|
+
- Reads your twin file for how you think
|
|
131
135
|
- Reads `prd.json` for open stories
|
|
132
|
-
- Picks the next story to build (
|
|
136
|
+
- Picks the next story to build (guided by your taste)
|
|
133
137
|
- Builds it, commits, and marks it done in `prd.json`
|
|
134
138
|
- Appends learnings to `progress.md`
|
|
135
139
|
- Repeats until all stories are done or max iterations hit
|
|
@@ -146,7 +150,7 @@ Requires Claude Code installed and available in your PATH.
|
|
|
146
150
|
- **Execution bias** — do you plan first or build first? Ship rough or wait for polish?
|
|
147
151
|
- **Quality compass** — how do you define "done"? What does "good" look like to you?
|
|
148
152
|
- **Decision-making style** — how do you get unstuck? How do you weigh trade-offs?
|
|
149
|
-
- **Strong beliefs** — what do you believe that most
|
|
153
|
+
- **Strong beliefs** — what do you believe that most would disagree with?
|
|
150
154
|
- **Anti-patterns** — what do you refuse to do?
|
|
151
155
|
|
|
152
156
|
## What Does NOT Go in a `.twin` File
|
|
@@ -159,7 +163,7 @@ The twin encodes how you think. The project files encode what you are building.
|
|
|
159
163
|
|
|
160
164
|
## What Comes Next
|
|
161
165
|
|
|
162
|
-
`twin tweak` for natural language updates. `twin share` for publishing your taste.
|
|
166
|
+
`twin tweak` for natural language updates. `twin share` for publishing your taste publicly.
|
|
163
167
|
|
|
164
168
|
## License
|
|
165
169
|
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -56,27 +56,33 @@ ${progressContent ? `\n## progress.md\n${progressContent}` : ''}
|
|
|
56
56
|
`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
if (event.type === '
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
137
|
+
const idle = Date.now() - lastActivity;
|
|
138
|
+
if (idle > 5_000) {
|
|
139
|
+
showStatus('Working...');
|
|
109
140
|
}
|
|
110
|
-
},
|
|
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
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
package/src/plan.js
CHANGED
|
@@ -130,6 +130,19 @@ export async function plan() {
|
|
|
130
130
|
prd.project = basename(process.cwd());
|
|
131
131
|
}
|
|
132
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
|
+
|
|
133
146
|
// Write prd.json
|
|
134
147
|
await writeFile(prdPath, JSON.stringify(prd, null, 2) + '\n', 'utf-8');
|
|
135
148
|
|