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 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 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
-
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
- AI agents have velocity but no vector. They do what you tell them. When you stop telling them, they stop.
9
+ You give an agent a task list. It finishes. It stops. Now you have to decide what comes next.
12
10
 
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.
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 do not have your taste.
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 your taste
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
- Every twin project starts in its own folder. Create one, then run the commands from inside it.
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 (only need to do this once)
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
- Here is a full walkthrough from zero to working app.
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
- - **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
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
- This is the core cycle. Your twin drives the whole thing.
107
+ Your twin drives the whole cycle.
106
108
 
107
- 1. **`twin init`** — create your taste profile (once)
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 plans what comes next
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 atomic capabilities that match your taste. Writes `prd.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.
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 taste
134
+ - Reads your twin file for how you think
131
135
  - Reads `prd.json` for open stories
132
- - Picks the next story to build (the model decides, guided by your taste)
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 people would disagree with?
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twin-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Your twin builds while you sleep. Not human in the loop. Twin in the loop.",
5
5
  "type": "module",
6
6
  "bin": {
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
 
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