orchestrix-yuri 3.0.1 → 3.1.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.
@@ -12,6 +12,30 @@ const { log } = require('../log');
12
12
 
13
13
  // ── Shared Utilities ───────────────────────────────────────────────────────────
14
14
 
15
+ /**
16
+ * Check if a YAML file is still an empty template (all leaf values are "", [], null).
17
+ */
18
+ function isEmptyTemplate(yamlContent) {
19
+ try {
20
+ const parsed = yaml.load(yamlContent);
21
+ if (!parsed || typeof parsed !== 'object') return true;
22
+ return checkAllEmpty(parsed);
23
+ } catch { return true; }
24
+ }
25
+
26
+ function checkAllEmpty(obj) {
27
+ for (const val of Object.values(obj)) {
28
+ if (val === null || val === undefined || val === '') continue;
29
+ if (Array.isArray(val) && val.length === 0) continue;
30
+ if (typeof val === 'object' && !Array.isArray(val)) {
31
+ if (!checkAllEmpty(val)) return false;
32
+ continue;
33
+ }
34
+ return false; // non-empty leaf found
35
+ }
36
+ return true;
37
+ }
38
+
15
39
  function loadL1Context() {
16
40
  const files = [
17
41
  { label: 'Yuri Identity', path: path.join(YURI_GLOBAL, 'self.yaml') },
@@ -23,12 +47,11 @@ function loadL1Context() {
23
47
 
24
48
  const sections = [];
25
49
  for (const f of files) {
26
- if (fs.existsSync(f.path)) {
27
- const content = fs.readFileSync(f.path, 'utf8').trim();
28
- if (content) {
29
- sections.push(`### ${f.label}\n\`\`\`yaml\n${content}\n\`\`\``);
30
- }
31
- }
50
+ if (!fs.existsSync(f.path)) continue;
51
+ const content = fs.readFileSync(f.path, 'utf8').trim();
52
+ // Skip empty template files — they waste tokens and confuse Claude with name: ""
53
+ if (!content || isEmptyTemplate(content)) continue;
54
+ sections.push(`### ${f.label}\n\`\`\`yaml\n${content}\n\`\`\``);
32
55
  }
33
56
 
34
57
  return sections.length > 0
@@ -93,6 +116,7 @@ function getClaudeBinary() {
93
116
  let _sessionId = null;
94
117
  let _messageCount = 0;
95
118
  let _messageQueue = Promise.resolve();
119
+ let _lastL1Hash = null;
96
120
 
97
121
  // ── System Prompt ──────────────────────────────────────────────────────────────
98
122
 
@@ -105,10 +129,9 @@ const CHANNEL_MODE_INSTRUCTIONS = [
105
129
  '- Keep responses concise and mobile-friendly.',
106
130
  '- Use markdown formatting sparingly (Telegram supports basic markdown).',
107
131
  '- If you need to perform operations, do so and report the result.',
108
- '- At the end of your response, if you observed any memory-worthy signals',
109
- ' (user preferences, priority changes, tech lessons, corrections),',
110
- ' write them to ~/.yuri/inbox.jsonl.',
111
- '- Update ~/.yuri/focus.yaml and the project\'s focus.yaml after any operation.',
132
+ '- Memory signals (preferences, identity, priorities) are detected and',
133
+ ' processed automatically by the gateway. You do not need to write to',
134
+ ' inbox.jsonl or manage memory files manually.',
112
135
  ].join('\n');
113
136
 
114
137
  /**
@@ -334,11 +357,28 @@ async function callClaude(opts) {
334
357
  }
335
358
 
336
359
  /**
337
- * Compose prompt just returns the raw user message.
338
- * L1 context and channel instructions are handled by --system-prompt.
360
+ * Compose prompt for the user message.
361
+ * If L1 context has changed since the last call (e.g., reflect engine updated
362
+ * boss/preferences.yaml), prepend a context refresh block so the resumed
363
+ * session gets the latest memory without needing a new system prompt.
339
364
  */
340
- function composePrompt(userMessage, _chatHistory) {
341
- return userMessage;
365
+ function composePrompt(userMessage) {
366
+ const crypto = require('crypto');
367
+ const l1 = loadL1Context();
368
+ const l1Hash = crypto.createHash('md5').update(l1 || '').digest('hex');
369
+
370
+ // First call or L1 unchanged: just the user message
371
+ if (!_lastL1Hash || l1Hash === _lastL1Hash) {
372
+ _lastL1Hash = l1Hash;
373
+ return userMessage;
374
+ }
375
+
376
+ // L1 changed: prepend context refresh
377
+ _lastL1Hash = l1Hash;
378
+ if (!l1) return userMessage;
379
+
380
+ log.engine('L1 context changed, injecting refresh into prompt');
381
+ return `[CONTEXT UPDATE — Your global memory has been updated]\n${l1}\n[END CONTEXT UPDATE]\n\n${userMessage}`;
342
382
  }
343
383
 
344
384
  /**
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const yaml = require('js-yaml');
7
+ const { log } = require('../log');
8
+
9
+ const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
10
+ const INBOX_PATH = path.join(YURI_GLOBAL, 'inbox.jsonl');
11
+
12
+ // Signal → target file mapping (mirrors observe-signals.yaml)
13
+ const SIGNAL_TARGETS = {
14
+ boss_preference: path.join(YURI_GLOBAL, 'boss', 'preferences.yaml'),
15
+ boss_identity: path.join(YURI_GLOBAL, 'boss', 'profile.yaml'),
16
+ priority_change: path.join(YURI_GLOBAL, 'portfolio', 'priorities.yaml'),
17
+ // tech_lesson and correction are project-specific, handled separately
18
+ // emotion stays in inbox for context
19
+ };
20
+
21
+ const MAX_INBOX_LINES = 100;
22
+ const KEEP_INBOX_LINES = 50;
23
+
24
+ /**
25
+ * Reflect Engine (F.1) — Process unprocessed inbox signals.
26
+ *
27
+ * Reads inbox.jsonl, groups entries by signal type, appends raw observations
28
+ * to target YAML files under an `observed:` array. Claude interprets these
29
+ * observations contextually on the next system prompt load.
30
+ *
31
+ * @returns {number} Number of entries processed
32
+ */
33
+ function runReflect() {
34
+ if (!fs.existsSync(INBOX_PATH)) return 0;
35
+
36
+ const raw = fs.readFileSync(INBOX_PATH, 'utf8').trim();
37
+ if (!raw) return 0;
38
+
39
+ // Parse all entries
40
+ const entries = [];
41
+ for (const line of raw.split('\n')) {
42
+ if (!line.trim()) continue;
43
+ try {
44
+ entries.push(JSON.parse(line));
45
+ } catch {
46
+ // Skip malformed lines
47
+ }
48
+ }
49
+
50
+ // Filter unprocessed
51
+ const unprocessed = entries.filter((e) => !e.processed);
52
+ if (unprocessed.length === 0) return 0;
53
+
54
+ // Group by signal type
55
+ const groups = {};
56
+ for (const entry of unprocessed) {
57
+ const sig = entry.signal || 'unknown';
58
+ if (!groups[sig]) groups[sig] = [];
59
+ groups[sig].push(entry);
60
+ }
61
+
62
+ // Process each group
63
+ let processedCount = 0;
64
+
65
+ for (const [signal, items] of Object.entries(groups)) {
66
+ const targetPath = SIGNAL_TARGETS[signal];
67
+ if (!targetPath) {
68
+ // No target file for this signal type (e.g., emotion, correction)
69
+ // Just mark as processed
70
+ for (const item of items) item.processed = true;
71
+ processedCount += items.length;
72
+ continue;
73
+ }
74
+
75
+ try {
76
+ appendObservations(targetPath, signal, items);
77
+ for (const item of items) item.processed = true;
78
+ processedCount += items.length;
79
+ } catch (err) {
80
+ log.warn(`Reflect: failed to write ${signal} to ${targetPath}: ${err.message}`);
81
+ }
82
+ }
83
+
84
+ if (processedCount > 0) {
85
+ log.engine(`Reflect: processed ${processedCount} inbox signals`);
86
+ }
87
+
88
+ // Rewrite inbox with processed markers (atomic write)
89
+ rewriteInbox(entries);
90
+
91
+ return processedCount;
92
+ }
93
+
94
+ /**
95
+ * Append raw observations to a target YAML file's `observed:` array.
96
+ * Preserves existing content and structured fields.
97
+ */
98
+ function appendObservations(targetPath, signal, items) {
99
+ let doc = {};
100
+
101
+ if (fs.existsSync(targetPath)) {
102
+ const content = fs.readFileSync(targetPath, 'utf8');
103
+ doc = yaml.load(content) || {};
104
+ }
105
+
106
+ // Initialize observed array if missing
107
+ if (!Array.isArray(doc.observed)) {
108
+ doc.observed = [];
109
+ }
110
+
111
+ // Append new observations
112
+ for (const item of items) {
113
+ doc.observed.push({
114
+ ts: item.ts,
115
+ signal,
116
+ raw: item.raw,
117
+ context: item.context || '',
118
+ });
119
+ }
120
+
121
+ // Keep only last 30 observations to prevent unbounded growth
122
+ if (doc.observed.length > 30) {
123
+ doc.observed = doc.observed.slice(-30);
124
+ }
125
+
126
+ // Write atomically: temp file then rename
127
+ const tmpPath = targetPath + '.tmp';
128
+ const header = getFileHeader(signal);
129
+ const yamlContent = yaml.dump(doc, { lineWidth: -1 });
130
+ fs.writeFileSync(tmpPath, header + yamlContent);
131
+ fs.renameSync(tmpPath, targetPath);
132
+ }
133
+
134
+ /**
135
+ * Get the header comment for a target file (preserves documentation).
136
+ */
137
+ function getFileHeader(signal) {
138
+ switch (signal) {
139
+ case 'boss_preference':
140
+ return '# Boss Preferences — accumulated understanding of user preferences\n' +
141
+ '# Location: ~/.yuri/boss/preferences.yaml\n\n';
142
+ case 'boss_identity':
143
+ return '# Boss Profile — accumulated understanding of the user\n' +
144
+ '# Location: ~/.yuri/boss/profile.yaml\n\n';
145
+ case 'priority_change':
146
+ return '# Portfolio Priorities — project priority signals\n' +
147
+ '# Location: ~/.yuri/portfolio/priorities.yaml\n\n';
148
+ default:
149
+ return '';
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Atomically rewrite inbox.jsonl with updated entries.
155
+ * Truncates if too many lines.
156
+ */
157
+ function rewriteInbox(entries) {
158
+ // Truncate: keep only recent entries if exceeding max
159
+ let toWrite = entries;
160
+ if (toWrite.length > MAX_INBOX_LINES) {
161
+ toWrite = toWrite.slice(-KEEP_INBOX_LINES);
162
+ }
163
+
164
+ const content = toWrite.map((e) => JSON.stringify(e)).join('\n') + '\n';
165
+ const tmpPath = INBOX_PATH + '.tmp';
166
+ fs.writeFileSync(tmpPath, content);
167
+ fs.renameSync(tmpPath, INBOX_PATH);
168
+ }
169
+
170
+ module.exports = { runReflect };
@@ -8,6 +8,7 @@ const yaml = require('js-yaml');
8
8
  const { ChatHistory } = require('./history');
9
9
  const { OwnerBinding } = require('./binding');
10
10
  const engine = require('./engine/claude-sdk');
11
+ const { runReflect } = require('./engine/reflect');
11
12
  const { log } = require('./log');
12
13
 
13
14
  const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
@@ -78,21 +79,19 @@ class Router {
78
79
  }
79
80
 
80
81
  async _processMessage(msg) {
82
+ // ═══ ENGINE: Reflect (code-enforced) ═══
83
+ // Process any unprocessed inbox signals BEFORE the Claude call,
84
+ // so the updated memory is available in the system prompt.
85
+ try { runReflect(); } catch (err) { log.warn(`Reflect failed: ${err.message}`); }
86
+
81
87
  // ═══ ENGINE: Catch-up (code-enforced) ═══
82
88
  await this._runCatchUp();
83
89
 
84
- // ═══ ENGINE: Load L1 (code pre-loads into prompt) ═══
85
- // L1 is loaded inside composePrompt() — injected into the prompt automatically.
86
- // Claude does not need to "remember" to read these files.
87
-
88
90
  // ═══ Resolve project context ═══
89
91
  const projectRoot = engine.resolveProjectRoot();
90
92
 
91
- // ═══ Get chat history for conversation continuity ═══
92
- const chatHistory = this.history.getRecent(msg.chatId);
93
-
94
- // ═══ Compose prompt: L1 context + chat history + user message ═══
95
- const prompt = engine.composePrompt(msg.text, chatHistory);
93
+ // ═══ Compose prompt ═══
94
+ const prompt = engine.composePrompt(msg.text);
96
95
 
97
96
  // ═══ WORK: Call Claude engine ═══
98
97
  log.router(`Processing: "${msg.text.slice(0, 80)}..." → cwd: ${projectRoot || '~'}`);
@@ -106,11 +105,9 @@ class Router {
106
105
  this.history.append(msg.chatId, 'user', msg.text);
107
106
  this.history.append(msg.chatId, 'assistant', result.reply.slice(0, 2000));
108
107
 
109
- // ═══ ENGINE: Observe (code writes inbox) ═══
110
- // In channel mode, observations are extracted from Claude's response.
111
- // Claude is instructed to write to inbox.jsonl directly via the Channel Mode Instructions.
112
- // Additionally, we detect priority signals from the user's message here.
113
- this._detectBasicSignals(msg);
108
+ // ═══ ENGINE: Observe (code-enforced signal detection) ═══
109
+ // Detect signals from BOTH user message and Claude's response.
110
+ this._detectSignals(msg, result.reply);
114
111
 
115
112
  // ═══ ENGINE: Update Focus (code-enforced) ═══
116
113
  this._updateGlobalFocus(msg, projectRoot);
@@ -170,27 +167,76 @@ class Router {
170
167
  }
171
168
  }
172
169
 
170
+ // ── Signal Detection Patterns (word-boundary aware) ──
171
+
172
+ static PRIORITY_PATTERNS = [
173
+ /\b(focus\s+on|switch\s+to|prioritize)\b/i,
174
+ /\b(pause|stop|halt)\s+(this|the|project|work|development)/i,
175
+ /\b(urgent|deadline|asap)\b/i,
176
+ /先搞|暂停\s*(这个|项目|开发)|不做了/,
177
+ ];
178
+
179
+ static PREFERENCE_PATTERNS = [
180
+ /\b(from\s+now\s+on|going\s+forward|always|never)\b/i,
181
+ /\b(don't|do\s+not|stop)\s+(use|do|write|send|make|add)/i,
182
+ /\b(I\s+prefer|I\s+like|I\s+want\s+you\s+to)\b/i,
183
+ /(别|不要)\s*\S+/,
184
+ /以后/,
185
+ ];
186
+
187
+ static IDENTITY_PATTERNS = [
188
+ /\b(I\s+am\s+a|I'm\s+a|my\s+role|my\s+job|I\s+work\s+(as|in|at|on))\b/i,
189
+ /\b(my\s+name\s+is|I'm\s+called|call\s+me)\b/i,
190
+ /我是|我叫|我的角色|我负责/,
191
+ ];
192
+
193
+ static RESPONSE_PREFERENCE_HINTS = [
194
+ /I'll remember|noted|preference saved|got it/i,
195
+ /记住了|已记录|偏好已/,
196
+ ];
197
+
198
+ static RESPONSE_IDENTITY_HINTS = [
199
+ /your role|you mentioned you|your expertise|your name/i,
200
+ /你的角色|你提到/,
201
+ ];
202
+
173
203
  /**
174
- * Detect basic signals from the user's message (code-level observation).
204
+ * Detect signals from user message AND Claude's response.
205
+ * Uses word-boundary regex to avoid false positives.
175
206
  */
176
- _detectBasicSignals(msg) {
177
- const text = msg.text.toLowerCase();
207
+ _detectSignals(msg, claudeReply) {
178
208
  const inboxPath = path.join(YURI_GLOBAL, 'inbox.jsonl');
179
-
180
209
  const signals = [];
181
210
 
182
- // Priority change signals
183
- const priorityPatterns = ['先搞', '暂停', '不做了', 'urgent', 'deadline', 'focus on', 'pause', 'stop'];
184
- if (priorityPatterns.some((p) => text.includes(p))) {
185
- signals.push({ signal: 'priority_change', raw: msg.text });
211
+ const text = msg.text;
212
+
213
+ // Detect from user message
214
+ if (Router.PRIORITY_PATTERNS.some((re) => re.test(text))) {
215
+ signals.push({ signal: 'priority_change', raw: text });
216
+ }
217
+ if (Router.PREFERENCE_PATTERNS.some((re) => re.test(text))) {
218
+ signals.push({ signal: 'boss_preference', raw: text });
219
+ }
220
+ if (Router.IDENTITY_PATTERNS.some((re) => re.test(text))) {
221
+ signals.push({ signal: 'boss_identity', raw: text });
186
222
  }
187
223
 
188
- // Preference signals
189
- const prefPatterns = ['别', '不要', 'don\'t', 'stop doing', 'from now on', '以后'];
190
- if (prefPatterns.some((p) => text.includes(p))) {
191
- signals.push({ signal: 'boss_preference', raw: msg.text });
224
+ // Detect from Claude's response (confirms Claude recognized a signal)
225
+ if (claudeReply) {
226
+ if (Router.RESPONSE_PREFERENCE_HINTS.some((re) => re.test(claudeReply))) {
227
+ // Only add if we didn't already detect from user message
228
+ if (!signals.some((s) => s.signal === 'boss_preference')) {
229
+ signals.push({ signal: 'boss_preference', raw: text });
230
+ }
231
+ }
232
+ if (Router.RESPONSE_IDENTITY_HINTS.some((re) => re.test(claudeReply))) {
233
+ if (!signals.some((s) => s.signal === 'boss_identity')) {
234
+ signals.push({ signal: 'boss_identity', raw: text });
235
+ }
236
+ }
192
237
  }
193
238
 
239
+ // Write to inbox
194
240
  for (const sig of signals) {
195
241
  const entry = {
196
242
  ts: new Date().toISOString(),
@@ -201,6 +247,10 @@ class Router {
201
247
  };
202
248
  fs.appendFileSync(inboxPath, JSON.stringify(entry) + '\n');
203
249
  }
250
+
251
+ if (signals.length > 0) {
252
+ log.router(`Detected ${signals.length} signal(s): ${signals.map((s) => s.signal).join(', ')}`);
253
+ }
204
254
  }
205
255
 
206
256
  /**
package/lib/installer.js CHANGED
@@ -130,15 +130,17 @@ function initGlobalMemory() {
130
130
  }
131
131
  }
132
132
 
133
- // Empty files: only create if not already present
134
- const emptyFiles = [
135
- path.join(YURI_GLOBAL, 'inbox.jsonl'),
136
- ];
137
-
138
- for (const filePath of emptyFiles) {
139
- if (!fs.existsSync(filePath)) {
140
- fs.writeFileSync(filePath, '');
141
- }
133
+ // Inbox file: create if not present, add bootstrap signal for first interaction
134
+ const inboxPath = path.join(YURI_GLOBAL, 'inbox.jsonl');
135
+ if (!fs.existsSync(inboxPath)) {
136
+ const bootstrap = JSON.stringify({
137
+ ts: new Date().toISOString(),
138
+ signal: 'boss_identity',
139
+ raw: '(bootstrap) User just installed Yuri. On first interaction, greet them warmly as Yuri and ask for their name and role so you can personalize the experience.',
140
+ context: 'installer',
141
+ processed: false,
142
+ });
143
+ fs.writeFileSync(inboxPath, bootstrap + '\n');
142
144
  }
143
145
 
144
146
  // Wisdom files: create with header comment if not present
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "Yuri — Meta-Orchestrator for Orchestrix. Drive your entire project lifecycle with natural language.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -22,6 +22,23 @@ delivering complete projects from natural language descriptions.
22
22
  4. **Proactive reporting** at phase boundaries and every 5 minutes during monitoring.
23
23
  5. **Default language is English.** Switch to Chinese only if user explicitly requests it.
24
24
 
25
+ ## tmux Command Rules (MANDATORY)
26
+
27
+ When sending ANY content to Claude Code via tmux, you MUST follow this exact 3-step pattern:
28
+
29
+ ```bash
30
+ # Step 1: Send content (pastes into Claude Code's input box)
31
+ tmux send-keys -t "$SESSION:$WINDOW" "your content here"
32
+ # Step 2: Wait for TUI to process the paste
33
+ sleep 1
34
+ # Step 3: Submit the input
35
+ tmux send-keys -t "$SESSION:$WINDOW" Enter
36
+ ```
37
+
38
+ **NEVER** combine content and Enter in one `send-keys` call (e.g., `send-keys "text" Enter`).
39
+ Claude Code's TUI needs the 1-second pause to process pasted text before receiving Enter.
40
+ Without it, the Enter may arrive before the TUI is ready, leaving content stuck in the input box.
41
+
25
42
  ## Available Commands
26
43
 
27
44
  | # | Command | Description |
@@ -126,6 +126,32 @@ cp "$RESOURCE_DIR/start-orchestrix.sh" "$PROJECT_DIR/.orchestrix-core/script
126
126
  chmod +x "$PROJECT_DIR/.orchestrix-core/scripts/start-orchestrix.sh"
127
127
  ```
128
128
 
129
+ ### Step 6.1: Register Trusted Directory
130
+
131
+ Claude Code shows a "trust this directory?" dialog when entering a new project for the first time.
132
+ This blocks automated tmux-based agent operations. Pre-register the project directory as trusted:
133
+
134
+ ```bash
135
+ # Add project to Claude Code's trusted directories list
136
+ CLAUDE_SETTINGS="$HOME/.claude/settings.local.json"
137
+ if [ -f "$CLAUDE_SETTINGS" ]; then
138
+ # Use node to safely merge into JSON (avoids jq dependency)
139
+ node -e "
140
+ const fs = require('fs');
141
+ const s = JSON.parse(fs.readFileSync('$CLAUDE_SETTINGS', 'utf8'));
142
+ if (!s.trustedDirectories) s.trustedDirectories = [];
143
+ if (!s.trustedDirectories.includes('$PROJECT_DIR')) {
144
+ s.trustedDirectories.push('$PROJECT_DIR');
145
+ fs.writeFileSync('$CLAUDE_SETTINGS', JSON.stringify(s, null, 2));
146
+ }
147
+ "
148
+ else
149
+ echo '{"trustedDirectories":["'"$PROJECT_DIR"'"]}' > "$CLAUDE_SETTINGS"
150
+ fi
151
+ ```
152
+
153
+ This ensures all tmux-based agents (planning, development) can start without manual trust confirmation.
154
+
129
155
  ---
130
156
 
131
157
  ## Step 7: Generate core-config.yaml
@@ -70,12 +70,18 @@ SESSION="{from focus.yaml → tmux.dev_session}"
70
70
  SCRIPT_DIR="${CLAUDE_SKILL_DIR}/scripts"
71
71
  SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" dev "$PROJECT_DIR")
72
72
 
73
- # Send to Dev
74
- tmux send-keys -t "$SESSION:2" "/clear" Enter
73
+ # Send to Dev (3-step pattern: content → sleep → Enter)
74
+ tmux send-keys -t "$SESSION:2" "/clear"
75
+ sleep 1
76
+ tmux send-keys -t "$SESSION:2" Enter
75
77
  sleep 2
76
- tmux send-keys -t "$SESSION:2" "/o dev" Enter
78
+ tmux send-keys -t "$SESSION:2" "/o dev"
79
+ sleep 1
80
+ tmux send-keys -t "$SESSION:2" Enter
77
81
  sleep 12
78
- tmux send-keys -t "$SESSION:2" "*solo \"$CHANGE_DESCRIPTION\"" Enter
82
+ tmux send-keys -t "$SESSION:2" "*solo \"$CHANGE_DESCRIPTION\""
83
+ sleep 1
84
+ tmux send-keys -t "$SESSION:2" Enter
79
85
  ```
80
86
 
81
87
  Monitor Dev completion, then resume normal Phase 3 monitoring.
@@ -91,29 +97,45 @@ SCRIPT_DIR="${CLAUDE_SKILL_DIR}/scripts"
91
97
  PLAN_SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" planning "$PROJECT_DIR")
92
98
 
93
99
  # 2. Activate PO and route change
94
- tmux send-keys -t "$PLAN_SESSION:0" "/o po" Enter
100
+ tmux send-keys -t "$PLAN_SESSION:0" "/o po"
101
+ sleep 1
102
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
95
103
  sleep 15
96
- tmux send-keys -t "$PLAN_SESSION:0" "*route-change \"$CHANGE_DESCRIPTION\"" Enter
104
+ tmux send-keys -t "$PLAN_SESSION:0" "*route-change \"$CHANGE_DESCRIPTION\""
105
+ sleep 1
106
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
97
107
  ```
98
108
 
99
109
  Wait for PO routing result. Then:
100
110
 
101
111
  - IF routes to **Architect**:
102
112
  ```bash
103
- tmux send-keys -t "$PLAN_SESSION:0" "/clear" Enter
113
+ tmux send-keys -t "$PLAN_SESSION:0" "/clear"
114
+ sleep 1
115
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
104
116
  sleep 2
105
- tmux send-keys -t "$PLAN_SESSION:0" "/o architect" Enter
117
+ tmux send-keys -t "$PLAN_SESSION:0" "/o architect"
118
+ sleep 1
119
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
106
120
  sleep 15
107
- tmux send-keys -t "$PLAN_SESSION:0" "*resolve-change" Enter
121
+ tmux send-keys -t "$PLAN_SESSION:0" "*resolve-change"
122
+ sleep 1
123
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
108
124
  ```
109
125
 
110
126
  - IF routes to **PM**:
111
127
  ```bash
112
- tmux send-keys -t "$PLAN_SESSION:0" "/clear" Enter
128
+ tmux send-keys -t "$PLAN_SESSION:0" "/clear"
129
+ sleep 1
130
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
113
131
  sleep 2
114
- tmux send-keys -t "$PLAN_SESSION:0" "/o pm" Enter
132
+ tmux send-keys -t "$PLAN_SESSION:0" "/o pm"
133
+ sleep 1
134
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
115
135
  sleep 15
116
- tmux send-keys -t "$PLAN_SESSION:0" "*revise-prd" Enter
136
+ tmux send-keys -t "$PLAN_SESSION:0" "*revise-prd"
137
+ sleep 1
138
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
117
139
  ```
118
140
 
119
141
  Wait for Proposal output (PCP/TCP).
@@ -123,11 +145,17 @@ Then switch to dev session:
123
145
  DEV_SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" dev "$PROJECT_DIR")
124
146
 
125
147
  # In SM window: apply proposal
126
- tmux send-keys -t "$DEV_SESSION:1" "/clear" Enter
148
+ tmux send-keys -t "$DEV_SESSION:1" "/clear"
149
+ sleep 1
150
+ tmux send-keys -t "$DEV_SESSION:1" Enter
127
151
  sleep 2
128
- tmux send-keys -t "$DEV_SESSION:1" "/o sm" Enter
152
+ tmux send-keys -t "$DEV_SESSION:1" "/o sm"
153
+ sleep 1
154
+ tmux send-keys -t "$DEV_SESSION:1" Enter
129
155
  sleep 12
130
- tmux send-keys -t "$DEV_SESSION:1" "*apply-proposal {proposal_id}" Enter
156
+ tmux send-keys -t "$DEV_SESSION:1" "*apply-proposal {proposal_id}"
157
+ sleep 1
158
+ tmux send-keys -t "$DEV_SESSION:1" Enter
131
159
  ```
132
160
 
133
161
  SM → Architect → Dev → QA auto-loop resumes.
@@ -160,9 +188,13 @@ SCRIPT_DIR="${CLAUDE_SKILL_DIR}/scripts"
160
188
 
161
189
  # Step 1: PM generates next-steps.md
162
190
  PLAN_SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" planning "$PROJECT_DIR")
163
- tmux send-keys -t "$PLAN_SESSION:0" "/o pm" Enter
191
+ tmux send-keys -t "$PLAN_SESSION:0" "/o pm"
192
+ sleep 1
193
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
164
194
  sleep 15
165
- tmux send-keys -t "$PLAN_SESSION:0" "*start-iteration" Enter
195
+ tmux send-keys -t "$PLAN_SESSION:0" "*start-iteration"
196
+ sleep 1
197
+ tmux send-keys -t "$PLAN_SESSION:0" Enter
166
198
  ```
167
199
 
168
200
  Monitor PM completion. PM creates: `docs/prd/epic-*.yaml` + `docs/prd/*next-steps.md`
@@ -198,11 +230,17 @@ DEV_SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" dev "$PROJECT_DIR")
198
230
  ```
199
231
  3. In SM window (window 1) of dev session:
200
232
  ```bash
201
- tmux send-keys -t "$DEV_SESSION:1" "/clear" Enter
233
+ tmux send-keys -t "$DEV_SESSION:1" "/clear"
234
+ sleep 1
235
+ tmux send-keys -t "$DEV_SESSION:1" Enter
202
236
  sleep 2
203
- tmux send-keys -t "$DEV_SESSION:1" "/o sm" Enter
237
+ tmux send-keys -t "$DEV_SESSION:1" "/o sm"
238
+ sleep 1
239
+ tmux send-keys -t "$DEV_SESSION:1" Enter
204
240
  sleep 12
205
- tmux send-keys -t "$DEV_SESSION:1" "*draft" Enter
241
+ tmux send-keys -t "$DEV_SESSION:1" "*draft"
242
+ sleep 1
243
+ tmux send-keys -t "$DEV_SESSION:1" Enter
206
244
  ```
207
245
  SM creates first story → handoff chain auto-starts (SM → Arch → Dev → QA)
208
246
 
@@ -91,21 +91,37 @@ sleep 2 # Wait for Claude Code to start
91
91
 
92
92
  **2.3** Activate agent and send command:
93
93
  ```bash
94
- tmux send-keys -t "$SESSION:$WINDOW_IDX" "/o {agent}" Enter
94
+ # CRITICAL: Every tmux send-keys MUST follow the pattern:
95
+ # send-keys "content" → sleep 1 → send-keys Enter
96
+ # Claude Code TUI needs time to process pasted text before Enter.
97
+ # Sending content and Enter in one call can cause the Enter to be lost.
98
+
99
+ tmux send-keys -t "$SESSION:$WINDOW_IDX" "/o {agent}"
100
+ sleep 1
101
+ tmux send-keys -t "$SESSION:$WINDOW_IDX" Enter
95
102
  sleep 10 # Wait for agent to load
96
- tmux send-keys -t "$SESSION:$WINDOW_IDX" "{command}" Enter
103
+
104
+ tmux send-keys -t "$SESSION:$WINDOW_IDX" "{command}"
105
+ sleep 1
106
+ tmux send-keys -t "$SESSION:$WINDOW_IDX" Enter
97
107
  ```
98
108
 
99
- **2.3.1** When answering agent questions (multi-line text):
109
+ **2.3.1** When answering agent questions or sending any text to agent windows:
100
110
  ```bash
101
- # Multi-line content is treated as a "paste" by Claude Code TUI.
102
- # It lands in the input buffer but does NOT auto-submit.
103
- # You MUST send Enter immediately after the content.
111
+ # ALWAYS use this 3-step pattern for sending content to Claude Code:
112
+ # Step 1: send-keys "content" (pastes text into input box)
113
+ # Step 2: sleep 1 (let TUI process the paste)
114
+ # Step 3: send-keys Enter (submit the input)
115
+ #
116
+ # NEVER combine content and Enter in a single send-keys call.
117
+ # NEVER skip the sleep — without it, Enter may arrive before TUI is ready.
118
+
104
119
  tmux send-keys -t "$SESSION:$WINDOW_IDX" "$(cat <<'EOF'
105
120
  your multi-line answer here
106
121
  EOF
107
- )" Enter
108
- # ^ Enter is CRITICAL — without it, content stays in input box
122
+ )"
123
+ sleep 1
124
+ tmux send-keys -t "$SESSION:$WINDOW_IDX" Enter
109
125
  ```
110
126
 
111
127
  **IMPORTANT:** Do NOT use `/clear` within the planning phase. Each agent has its own
@@ -54,9 +54,13 @@ SESSION=$(bash "$SCRIPT_DIR/ensure-session.sh" dev "$PROJECT_DIR")
54
54
 
55
55
  Reload QA agent in clean state:
56
56
  ```bash
57
- tmux send-keys -t "$SESSION:3" "/clear" Enter
57
+ tmux send-keys -t "$SESSION:3" "/clear"
58
+ sleep 1
59
+ tmux send-keys -t "$SESSION:3" Enter
58
60
  sleep 2
59
- tmux send-keys -t "$SESSION:3" "/o qa" Enter
61
+ tmux send-keys -t "$SESSION:3" "/o qa"
62
+ sleep 1
63
+ tmux send-keys -t "$SESSION:3" Enter
60
64
  sleep 12
61
65
  ```
62
66
 
@@ -71,7 +75,9 @@ FOR EACH `EPIC_ID` in the epic list:
71
75
  ### 2.1 Run Smoke Test
72
76
 
73
77
  ```bash
74
- tmux send-keys -t "$SESSION:3" "*smoke-test $EPIC_ID" Enter
78
+ tmux send-keys -t "$SESSION:3" "*smoke-test $EPIC_ID"
79
+ sleep 1
80
+ tmux send-keys -t "$SESSION:3" Enter
75
81
  ```
76
82
 
77
83
  Monitor completion:
@@ -95,15 +101,21 @@ IF test failed, extract bug descriptions from QA output. Then:
95
101
 
96
102
  1. Reload Dev agent:
97
103
  ```bash
98
- tmux send-keys -t "$SESSION:2" "/clear" Enter
104
+ tmux send-keys -t "$SESSION:2" "/clear"
105
+ sleep 1
106
+ tmux send-keys -t "$SESSION:2" Enter
99
107
  sleep 2
100
- tmux send-keys -t "$SESSION:2" "/o dev" Enter
108
+ tmux send-keys -t "$SESSION:2" "/o dev"
109
+ sleep 1
110
+ tmux send-keys -t "$SESSION:2" Enter
101
111
  sleep 12
102
112
  ```
103
113
 
104
114
  2. Send quick-fix command:
105
115
  ```bash
106
- tmux send-keys -t "$SESSION:2" "*quick-fix \"$BUG_DESC\"" Enter
116
+ tmux send-keys -t "$SESSION:2" "*quick-fix \"$BUG_DESC\""
117
+ sleep 1
118
+ tmux send-keys -t "$SESSION:2" Enter
107
119
  ```
108
120
 
109
121
  3. Monitor Dev completion.