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.
- package/lib/gateway/engine/claude-sdk.js +54 -14
- package/lib/gateway/engine/reflect.js +170 -0
- package/lib/gateway/router.js +76 -26
- package/lib/installer.js +11 -9
- package/package.json +1 -1
- package/skill/SKILL.md +17 -0
- package/skill/tasks/yuri-create-project.md +26 -0
- package/skill/tasks/yuri-handle-change.md +58 -20
- package/skill/tasks/yuri-plan-project.md +24 -8
- package/skill/tasks/yuri-test-project.md +18 -6
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
'-
|
|
109
|
-
'
|
|
110
|
-
'
|
|
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
|
|
338
|
-
* L1 context
|
|
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
|
|
341
|
-
|
|
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 };
|
package/lib/gateway/router.js
CHANGED
|
@@ -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
|
-
// ═══
|
|
92
|
-
const
|
|
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
|
|
110
|
-
//
|
|
111
|
-
|
|
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
|
|
204
|
+
* Detect signals from user message AND Claude's response.
|
|
205
|
+
* Uses word-boundary regex to avoid false positives.
|
|
175
206
|
*/
|
|
176
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
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"
|
|
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"
|
|
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\""
|
|
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"
|
|
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\""
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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}"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
|
109
|
+
**2.3.1** When answering agent questions or sending any text to agent windows:
|
|
100
110
|
```bash
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
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
|
-
)"
|
|
108
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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\""
|
|
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.
|