orchestrix-yuri 3.3.0 → 3.4.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 +113 -142
- package/bin/status.js +3 -0
- package/lib/gateway/channels/telegram.js +21 -5
- package/lib/gateway/engine/claude-sdk.js +23 -1
- package/lib/gateway/engine/phase-orchestrator.js +37 -0
- package/lib/gateway/router.js +104 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ User describes idea → Yuri drives: Create → Plan → Develop → Test → De
|
|
|
8
8
|
|
|
9
9
|
## How It Works
|
|
10
10
|
|
|
11
|
-
Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel Gateway.
|
|
11
|
+
Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel Gateway. Two ways to use:
|
|
12
12
|
|
|
13
13
|
1. **Terminal mode** — activate `/yuri` inside any Claude Code session
|
|
14
|
-
2. **Telegram mode** — chat with Yuri via Telegram bot
|
|
14
|
+
2. **Telegram mode** — chat with Yuri via Telegram bot
|
|
15
15
|
|
|
16
16
|
| Phase | What Yuri Does | Agents Involved |
|
|
17
17
|
|-------|---------------|-----------------|
|
|
@@ -34,35 +34,38 @@ Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel
|
|
|
34
34
|
### Method A: From npm (recommended)
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
# Install globally
|
|
38
37
|
npm install -g orchestrix-yuri
|
|
39
|
-
|
|
40
|
-
# Initialize skill + global memory
|
|
41
38
|
orchestrix-yuri install
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
orchestrix-yuri serve --telegram-token "YOUR_BOT_TOKEN"
|
|
39
|
+
orchestrix-yuri start --token "YOUR_BOT_TOKEN" # first time, saves token
|
|
40
|
+
orchestrix-yuri start # from now on
|
|
45
41
|
```
|
|
46
42
|
|
|
47
43
|
### Method B: From source
|
|
48
44
|
|
|
49
45
|
```bash
|
|
50
|
-
git clone https://github.com/
|
|
46
|
+
git clone https://github.com/dorayo/orchestrix-yuri.git
|
|
51
47
|
cd orchestrix-yuri
|
|
52
48
|
npm install
|
|
53
|
-
|
|
54
|
-
# Initialize skill + global memory
|
|
55
49
|
node bin/install.js install
|
|
56
|
-
|
|
57
|
-
# Start Telegram gateway
|
|
58
|
-
node bin/serve.js --telegram-token "YOUR_BOT_TOKEN"
|
|
50
|
+
node bin/serve.js --token "YOUR_BOT_TOKEN"
|
|
59
51
|
```
|
|
60
52
|
|
|
61
53
|
### What `install` does
|
|
62
54
|
|
|
63
55
|
- Copies the Yuri skill to `~/.claude/skills/yuri/`
|
|
64
|
-
- Initializes global memory at `~/.yuri/` (identity, boss profile, portfolio
|
|
65
|
-
-
|
|
56
|
+
- Initializes global memory at `~/.yuri/` (identity, boss profile, portfolio, focus, wisdom)
|
|
57
|
+
- Writes a bootstrap signal so Yuri greets you on first interaction
|
|
58
|
+
|
|
59
|
+
## CLI Commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
orchestrix-yuri install # Install Yuri skill + global memory
|
|
63
|
+
orchestrix-yuri start # Start the Channel Gateway
|
|
64
|
+
orchestrix-yuri start --token TOKEN # Start & save Telegram Bot token (first time)
|
|
65
|
+
orchestrix-yuri stop # Stop the running gateway
|
|
66
|
+
orchestrix-yuri status # Show gateway status + cost tracking
|
|
67
|
+
orchestrix-yuri --version # Show version
|
|
68
|
+
```
|
|
66
69
|
|
|
67
70
|
## Usage
|
|
68
71
|
|
|
@@ -74,168 +77,130 @@ In any Claude Code session:
|
|
|
74
77
|
/yuri
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
### Telegram Mode
|
|
78
81
|
|
|
79
82
|
| Command | Description |
|
|
80
83
|
|---------|-------------|
|
|
81
|
-
| `*create` | Create a new project
|
|
82
|
-
| `*plan` | Start
|
|
83
|
-
| `*develop` | Start
|
|
84
|
-
| `*test` |
|
|
85
|
-
| `*deploy` |
|
|
86
|
-
| `*status` | Show
|
|
87
|
-
| `*
|
|
88
|
-
| `*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
| `*create` | Create a new project |
|
|
85
|
+
| `*plan` | Start planning (runs in background, agents auto-chain) |
|
|
86
|
+
| `*develop` | Start development (4 agents run autonomously) |
|
|
87
|
+
| `*test` | Run smoke tests |
|
|
88
|
+
| `*deploy` | Deploy the project |
|
|
89
|
+
| `*status` | Show progress + cost tracking |
|
|
90
|
+
| `*projects` | List all registered projects |
|
|
91
|
+
| `*switch <name>` | Switch active project |
|
|
92
|
+
| `*cancel` | Stop running phase |
|
|
93
|
+
| `*resume` | Resume from last checkpoint |
|
|
94
|
+
| `*change "{desc}"` | Handle requirement change |
|
|
95
|
+
|
|
96
|
+
### Agent Interaction via Telegram
|
|
97
|
+
|
|
98
|
+
When a planning agent asks a question (e.g., PM asking to confirm features), Yuri bridges it to Telegram:
|
|
93
99
|
|
|
94
|
-
```bash
|
|
95
|
-
# With token as CLI argument
|
|
96
|
-
orchestrix-yuri serve --telegram-token "YOUR_BOT_TOKEN"
|
|
97
|
-
|
|
98
|
-
# Or configure in ~/.yuri/config/channels.yaml first
|
|
99
|
-
orchestrix-yuri serve
|
|
100
100
|
```
|
|
101
|
+
Yuri: "📋 PM is asking:
|
|
102
|
+
Which features to include?
|
|
103
|
+
1. User auth 2. Dashboard 3. API
|
|
104
|
+
↩️ Reply to this message to answer"
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Edit `~/.yuri/config/channels.yaml`:
|
|
105
|
-
|
|
106
|
-
```yaml
|
|
107
|
-
server:
|
|
108
|
-
port: 7890
|
|
109
|
-
|
|
110
|
-
channels:
|
|
111
|
-
telegram:
|
|
112
|
-
enabled: true
|
|
113
|
-
token: "YOUR_BOT_TOKEN"
|
|
114
|
-
mode: polling
|
|
115
|
-
owner_chat_id: "" # Auto-bound on first /start
|
|
116
|
-
|
|
117
|
-
engine:
|
|
118
|
-
skill: yuri
|
|
119
|
-
tmux_session: yuri-gateway
|
|
120
|
-
startup_timeout: 30000
|
|
121
|
-
poll_interval: 2000
|
|
122
|
-
timeout: 300000
|
|
123
|
-
autocompact_pct: 80
|
|
124
|
-
compact_every: 50
|
|
106
|
+
You reply: "1 and 2, skip 3" ← forwarded to PM agent
|
|
125
107
|
```
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
1. Start the gateway: `orchestrix-yuri serve --telegram-token "YOUR_TOKEN"`
|
|
130
|
-
2. Open Telegram, find your bot, send `/start`
|
|
131
|
-
3. The first user to send `/start` becomes the owner (all others are rejected)
|
|
132
|
-
4. Send any message to interact with Yuri
|
|
109
|
+
**Normal messages** (not reply-to) always go to Yuri directly.
|
|
133
110
|
|
|
134
111
|
## Architecture
|
|
135
112
|
|
|
136
|
-
###
|
|
137
|
-
|
|
138
|
-
The Telegram gateway runs Claude Code in a persistent tmux session (`yuri-gateway`), not as a one-shot subprocess. This means:
|
|
113
|
+
### Gateway Engine (claude-sdk)
|
|
139
114
|
|
|
140
|
-
|
|
141
|
-
- **Conversation context is preserved** natively by Claude Code
|
|
142
|
-
- **No cold-start per message** — only the first message incurs startup latency
|
|
115
|
+
Each Telegram message is processed via `claude -p --output-format json`:
|
|
143
116
|
|
|
144
|
-
|
|
117
|
+
- **First message**: `--system-prompt` injects Yuri persona (SKILL.md) + L1 memory + channel instructions
|
|
118
|
+
- **Subsequent**: `--resume SESSION_ID` preserves conversation context
|
|
119
|
+
- **Output**: Structured JSON with `result` (clean text), `session_id`, `total_cost_usd`
|
|
145
120
|
|
|
146
|
-
|
|
147
|
-
|--------|-------|-------------|
|
|
148
|
-
| `○` | Idle | Waiting for input |
|
|
149
|
-
| `●` | Processing | Generating a response |
|
|
150
|
-
| `◐` | Approval | Permission prompt (auto-approved) |
|
|
151
|
-
| `[Verb]ed for Ns` | Complete | Response finished (e.g. "Baked for 31s") |
|
|
121
|
+
### Async Phase Orchestration
|
|
152
122
|
|
|
153
|
-
|
|
123
|
+
`*plan` and `*develop` run as **Node.js background tasks** (not inside a `claude -p` call):
|
|
154
124
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
125
|
+
```
|
|
126
|
+
*plan → PhaseOrchestrator.startPlan() → instant reply
|
|
127
|
+
↓
|
|
128
|
+
setInterval(30s) polls tmux agent → detects completion
|
|
129
|
+
↓
|
|
130
|
+
starts next agent → proactive Telegram notification
|
|
131
|
+
↓
|
|
132
|
+
user asks anything → normal Claude response (not blocked)
|
|
133
|
+
```
|
|
158
134
|
|
|
159
|
-
|
|
135
|
+
tmux is used **only for multi-agent orchestration** (plan/develop), not for the gateway itself.
|
|
160
136
|
|
|
161
|
-
|
|
137
|
+
### Memory System (4 layers)
|
|
162
138
|
|
|
163
139
|
```
|
|
164
|
-
~/.yuri/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
│ └── preferences.yaml # Boss preferences
|
|
169
|
-
├── portfolio/
|
|
170
|
-
│ ├── registry.yaml # All projects (active/archived)
|
|
171
|
-
│ ├── priorities.yaml # Portfolio priorities
|
|
172
|
-
│ └── relationships.yaml # Project relationships
|
|
173
|
-
├── focus.yaml # Current focus & state
|
|
174
|
-
├── config/
|
|
175
|
-
│ └── channels.yaml # Gateway channel config
|
|
176
|
-
├── chat-history/ # JSONL per chat_id
|
|
177
|
-
├── inbox.jsonl # Observation signals
|
|
178
|
-
└── wisdom/ # Accumulated knowledge
|
|
140
|
+
L1 Global (~/.yuri/) — Yuri identity, boss profile, portfolio, focus
|
|
141
|
+
L2 Project ({project}/.yuri/) — Project identity, knowledge, decisions
|
|
142
|
+
L3 Phase State (state/) — Per-phase operational status
|
|
143
|
+
L4 Events (timeline/) — Append-only event history
|
|
179
144
|
```
|
|
180
145
|
|
|
181
|
-
|
|
146
|
+
**Reflect Engine**: Signals detected from user messages (preferences, identity, priorities) are written to `inbox.jsonl`, then processed by `reflect.js` into the corresponding YAML files before each Claude call.
|
|
182
147
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
148
|
+
### Context Management
|
|
149
|
+
|
|
150
|
+
- `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80` triggers auto-compact at 80%
|
|
151
|
+
- Proactive `/compact` every 50 messages
|
|
152
|
+
- L1 hash tracking: when memory changes, a `[CONTEXT UPDATE]` prefix is injected into the next message
|
|
188
153
|
|
|
189
154
|
### File Structure
|
|
190
155
|
|
|
191
156
|
```
|
|
192
157
|
orchestrix-yuri/
|
|
193
158
|
├── bin/
|
|
194
|
-
│ ├── install.js
|
|
195
|
-
│
|
|
159
|
+
│ ├── install.js # CLI entry (install/start/stop/status)
|
|
160
|
+
│ ├── serve.js # Gateway launcher
|
|
161
|
+
│ ├── stop.js # PID-based stop
|
|
162
|
+
│ └── status.js # Gateway + cost status
|
|
196
163
|
├── lib/
|
|
197
|
-
│ ├── installer.js
|
|
198
|
-
│ ├── migrate.js
|
|
164
|
+
│ ├── installer.js # Global install logic
|
|
165
|
+
│ ├── migrate.js # v1 → v2 memory migration
|
|
199
166
|
│ └── gateway/
|
|
200
|
-
│ ├── index.js
|
|
201
|
-
│ ├── config.js
|
|
202
|
-
│ ├── router.js
|
|
203
|
-
│ ├── binding.js
|
|
204
|
-
│ ├── history.js
|
|
167
|
+
│ ├── index.js # startGateway()
|
|
168
|
+
│ ├── config.js # Config loading + defaults
|
|
169
|
+
│ ├── router.js # Message routing, phase detection, signal detection
|
|
170
|
+
│ ├── binding.js # Owner authentication
|
|
171
|
+
│ ├── history.js # Chat history (JSONL)
|
|
172
|
+
│ ├── log.js # Colored terminal logging
|
|
205
173
|
│ ├── channels/
|
|
206
|
-
│ │ └── telegram.js
|
|
174
|
+
│ │ └── telegram.js # grammy adapter (placeholder + edit pattern)
|
|
207
175
|
│ └── engine/
|
|
208
|
-
│
|
|
176
|
+
│ ├── claude-sdk.js # Claude -p JSON engine + session management
|
|
177
|
+
│ ├── phase-orchestrator.js # Background plan/develop execution
|
|
178
|
+
│ ├── reflect.js # Inbox signal processing → YAML memory
|
|
179
|
+
│ └── tmux-utils.js # Shared tmux operations
|
|
209
180
|
└── skill/
|
|
210
|
-
├── SKILL.md
|
|
211
|
-
├── tasks/
|
|
212
|
-
├── scripts/
|
|
213
|
-
├── templates/
|
|
214
|
-
├── data/
|
|
215
|
-
└── resources/
|
|
181
|
+
├── SKILL.md # Yuri persona + tmux command rules
|
|
182
|
+
├── tasks/ # Phase workflow instructions
|
|
183
|
+
├── scripts/ # Shell scripts (tmux, monitoring)
|
|
184
|
+
├── templates/ # Memory schema
|
|
185
|
+
├── data/ # Decision rules, signal taxonomy
|
|
186
|
+
└── resources/ # MCP config, hooks, tmux scripts
|
|
216
187
|
```
|
|
217
188
|
|
|
218
|
-
|
|
189
|
+
### tmux Sessions (for agent orchestration)
|
|
190
|
+
|
|
191
|
+
| Session | Purpose | Windows |
|
|
192
|
+
|---------|---------|---------|
|
|
193
|
+
| `op-{project}` | Planning phase | One per agent |
|
|
194
|
+
| `orchestrix-{repo-id}` | Development phase | 4 fixed (Architect, SM, Dev, QA) |
|
|
219
195
|
|
|
220
|
-
|
|
196
|
+
## Change Management
|
|
221
197
|
|
|
222
198
|
| Scope | Action |
|
|
223
199
|
|-------|--------|
|
|
224
200
|
| Small (≤5 files) | Dev `*solo` directly |
|
|
225
201
|
| Medium | PO `*route-change` → standard workflow |
|
|
226
202
|
| Large | Pause dev, partial re-plan |
|
|
227
|
-
| New iteration | PM `*start-iteration` →
|
|
228
|
-
|
|
229
|
-
## Deployment Options
|
|
230
|
-
|
|
231
|
-
| Region | Provider | Best For |
|
|
232
|
-
|--------|----------|----------|
|
|
233
|
-
| China | Sealos | Prototype / MVP |
|
|
234
|
-
| China | Aliyun ECS + Docker | Production |
|
|
235
|
-
| China | Vercel (CN) | Frontend / SSR |
|
|
236
|
-
| Overseas | Vercel | Frontend / Full-stack |
|
|
237
|
-
| Overseas | Railway | Backend APIs |
|
|
238
|
-
| Overseas | AWS / GCP | Enterprise |
|
|
203
|
+
| New iteration | PM `*start-iteration` → plan → dev → test |
|
|
239
204
|
|
|
240
205
|
## Troubleshooting
|
|
241
206
|
|
|
@@ -243,17 +208,23 @@ Yuri handles mid-project changes based on scope:
|
|
|
243
208
|
# Check prerequisites
|
|
244
209
|
tmux -V && which claude && node -v
|
|
245
210
|
|
|
246
|
-
# View gateway logs
|
|
247
|
-
orchestrix-yuri
|
|
211
|
+
# View gateway logs
|
|
212
|
+
orchestrix-yuri start 2>&1 | tee gateway.log
|
|
213
|
+
|
|
214
|
+
# Check status + cost
|
|
215
|
+
orchestrix-yuri status
|
|
216
|
+
|
|
217
|
+
# Stop the gateway
|
|
218
|
+
orchestrix-yuri stop
|
|
248
219
|
|
|
249
|
-
# Check
|
|
220
|
+
# Check running tmux sessions (plan/develop phases)
|
|
250
221
|
tmux ls
|
|
251
222
|
|
|
252
|
-
# Peek into
|
|
253
|
-
tmux attach -t
|
|
223
|
+
# Peek into a planning agent
|
|
224
|
+
tmux attach -t op-myproject
|
|
254
225
|
|
|
255
|
-
#
|
|
256
|
-
tmux
|
|
226
|
+
# Peek into dev agents
|
|
227
|
+
tmux attach -t orchestrix-myproject
|
|
257
228
|
```
|
|
258
229
|
|
|
259
230
|
## License
|
package/bin/status.js
CHANGED
|
@@ -30,6 +30,9 @@ function status() {
|
|
|
30
30
|
const age = Date.now() - new Date(s.savedAt).getTime();
|
|
31
31
|
if (age < 24 * 3600_000) {
|
|
32
32
|
sessionInfo = `${s.sessionId.slice(0, 8)}... (${s.messageCount || 0} messages)`;
|
|
33
|
+
if (s.totalCost) {
|
|
34
|
+
sessionInfo += ` | $${s.totalCost.toFixed(4)}`;
|
|
35
|
+
}
|
|
33
36
|
} else {
|
|
34
37
|
sessionInfo = 'expired';
|
|
35
38
|
}
|
|
@@ -39,16 +39,32 @@ class TelegramAdapter {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
+
// Send placeholder immediately so user sees instant feedback
|
|
43
|
+
const placeholder = await ctx.reply('...').catch(() => null);
|
|
44
|
+
|
|
42
45
|
const reply = await this.onMessage(msg);
|
|
43
46
|
if (reply && reply.text) {
|
|
44
|
-
// Telegram has a 4096 char limit per message
|
|
45
47
|
const chunks = splitMessage(reply.text, 4000);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
if (placeholder && chunks.length === 1) {
|
|
50
|
+
// Single chunk: edit the placeholder message in-place
|
|
51
|
+
await ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, chunks[0], { parse_mode: 'Markdown' }).catch(() => {
|
|
52
|
+
return ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, chunks[0]).catch(() => {});
|
|
50
53
|
});
|
|
54
|
+
} else {
|
|
55
|
+
// Multi-chunk: delete placeholder, send chunks separately
|
|
56
|
+
if (placeholder) {
|
|
57
|
+
await ctx.api.deleteMessage(ctx.chat.id, placeholder.message_id).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
for (const chunk of chunks) {
|
|
60
|
+
await ctx.reply(chunk, { parse_mode: 'Markdown' }).catch(() => {
|
|
61
|
+
return ctx.reply(chunk);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
51
64
|
}
|
|
65
|
+
} else if (placeholder) {
|
|
66
|
+
// No reply: remove placeholder
|
|
67
|
+
await ctx.api.deleteMessage(ctx.chat.id, placeholder.message_id).catch(() => {});
|
|
52
68
|
}
|
|
53
69
|
} catch (err) {
|
|
54
70
|
log.error(`Message handling failed: ${err.message}`);
|
|
@@ -117,6 +117,8 @@ let _sessionId = null;
|
|
|
117
117
|
let _messageCount = 0;
|
|
118
118
|
let _messageQueue = Promise.resolve();
|
|
119
119
|
let _lastL1Hash = null;
|
|
120
|
+
let _totalCost = 0;
|
|
121
|
+
let _totalDuration = 0;
|
|
120
122
|
|
|
121
123
|
// ── System Prompt ──────────────────────────────────────────────────────────────
|
|
122
124
|
|
|
@@ -188,6 +190,8 @@ function saveSessionState() {
|
|
|
188
190
|
version: SESSION_VERSION,
|
|
189
191
|
sessionId: _sessionId,
|
|
190
192
|
messageCount: _messageCount,
|
|
193
|
+
totalCost: _totalCost,
|
|
194
|
+
totalDuration: _totalDuration,
|
|
191
195
|
savedAt: new Date().toISOString(),
|
|
192
196
|
}));
|
|
193
197
|
} catch { /* best effort */ }
|
|
@@ -290,6 +294,8 @@ async function callClaude(opts) {
|
|
|
290
294
|
log.engine(`Restoring session ${saved.sessionId.slice(0, 8)}...`);
|
|
291
295
|
_sessionId = saved.sessionId;
|
|
292
296
|
_messageCount = saved.messageCount || 0;
|
|
297
|
+
_totalCost = saved.totalCost || 0;
|
|
298
|
+
_totalDuration = saved.totalDuration || 0;
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
@@ -326,6 +332,8 @@ async function callClaude(opts) {
|
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
_messageCount++;
|
|
335
|
+
if (result.cost) _totalCost += result.cost;
|
|
336
|
+
if (result.duration) _totalDuration += result.duration;
|
|
329
337
|
saveSessionState();
|
|
330
338
|
|
|
331
339
|
// If --resume failed (session expired), retry with fresh session
|
|
@@ -385,10 +393,20 @@ function composePrompt(userMessage) {
|
|
|
385
393
|
* Destroy session state. Called on gateway shutdown.
|
|
386
394
|
*/
|
|
387
395
|
function destroySession() {
|
|
388
|
-
// Don't clear session file — allow restart to resume
|
|
389
396
|
log.engine('Gateway shutting down (session preserved for restart)');
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Get accumulated cost and usage stats.
|
|
401
|
+
*/
|
|
402
|
+
function getUsageStats() {
|
|
403
|
+
return {
|
|
404
|
+
messageCount: _messageCount,
|
|
405
|
+
totalCost: _totalCost,
|
|
406
|
+
totalDuration: _totalDuration,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
module.exports = {
|
|
393
411
|
callClaude,
|
|
394
412
|
composePrompt,
|
|
@@ -396,4 +414,8 @@ module.exports = {
|
|
|
396
414
|
resolveProjectRoot,
|
|
397
415
|
findClaudeBinary,
|
|
398
416
|
destroySession,
|
|
417
|
+
clearSessionState,
|
|
418
|
+
getUsageStats,
|
|
419
|
+
// Exported for testing
|
|
420
|
+
isEmptyTemplate,
|
|
399
421
|
};
|
|
@@ -112,6 +112,43 @@ class PhaseOrchestrator {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Capture the current agent's tmux pane content for Claude context injection.
|
|
117
|
+
* Returns a formatted string or null if no agent is active.
|
|
118
|
+
*/
|
|
119
|
+
captureCurrentAgentContext() {
|
|
120
|
+
if (!this._phase || !this._session || !tmx.hasSession(this._session)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this._phase === 'plan') {
|
|
125
|
+
const agent = PLAN_AGENTS[this._step];
|
|
126
|
+
if (!agent) return null;
|
|
127
|
+
|
|
128
|
+
const pane = tmx.capturePane(this._session, agent.window, 40);
|
|
129
|
+
if (!pane.trim()) return null;
|
|
130
|
+
|
|
131
|
+
const waiting = this._waitingForInput ? ' (WAITING FOR YOUR INPUT)' : '';
|
|
132
|
+
return `[LIVE AGENT CONTEXT] Phase: plan, Agent: ${agent.name} (${this._step + 1}/${PLAN_AGENTS.length})${waiting}\n` +
|
|
133
|
+
`tmux session: ${this._session}, window: ${agent.window}\n` +
|
|
134
|
+
`--- Agent output (last 40 lines) ---\n${pane}\n--- End agent output ---`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this._phase === 'develop') {
|
|
138
|
+
// Capture all 4 dev windows briefly
|
|
139
|
+
const windows = ['Architect', 'SM', 'Dev', 'QA'];
|
|
140
|
+
const summaries = [];
|
|
141
|
+
for (let w = 0; w < 4; w++) {
|
|
142
|
+
const tail = tmx.capturePane(this._session, w, 5);
|
|
143
|
+
const lastLine = tail.split('\n').filter((l) => l.trim()).pop() || '(empty)';
|
|
144
|
+
summaries.push(` Window ${w} (${windows[w]}): ${lastLine.trim().slice(0, 80)}`);
|
|
145
|
+
}
|
|
146
|
+
return `[LIVE AGENT CONTEXT] Phase: develop, Session: ${this._session}\n${summaries.join('\n')}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
getStatus() {
|
|
116
153
|
if (!this._phase) {
|
|
117
154
|
return { phase: null, message: 'No phase is running.' };
|
package/lib/gateway/router.js
CHANGED
|
@@ -24,6 +24,11 @@ const PHASE_COMMANDS = {
|
|
|
24
24
|
cancel: /^\*cancel\b/i,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const META_COMMANDS = {
|
|
28
|
+
projects: /^\*projects\b/i,
|
|
29
|
+
switch: /^\*switch\s+(.+)/i,
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
const STATUS_PATTERNS = [
|
|
28
33
|
/^\*status\b/i,
|
|
29
34
|
/进度|状态|怎么样了|到哪了/,
|
|
@@ -128,6 +133,15 @@ class Router {
|
|
|
128
133
|
return this._handleStatusQuery(msg);
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
// ═══ META COMMANDS — *projects, *switch (always allowed) ═══
|
|
137
|
+
if (META_COMMANDS.projects.test(msg.text.trim())) {
|
|
138
|
+
return this._handleProjects(msg);
|
|
139
|
+
}
|
|
140
|
+
const switchMatch = msg.text.trim().match(META_COMMANDS.switch);
|
|
141
|
+
if (switchMatch) {
|
|
142
|
+
return this._handleSwitch(switchMatch[1].trim(), msg);
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
// ═══ AGENT REPLY — user replied to an agent question notification ═══
|
|
132
146
|
if (msg.replyToMessageId && this.orchestrator.isWaitingForInput()) {
|
|
133
147
|
const waitingId = this.orchestrator.getWaitingMessageId();
|
|
@@ -252,8 +266,14 @@ class Router {
|
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
|
|
269
|
+
// Cost tracking
|
|
270
|
+
const usage = engine.getUsageStats();
|
|
271
|
+
if (usage.totalCost > 0) {
|
|
272
|
+
parts.push(`\n💰 Cost: $${usage.totalCost.toFixed(4)} | ${usage.messageCount} messages | ${(usage.totalDuration / 1000).toFixed(0)}s total`);
|
|
273
|
+
}
|
|
274
|
+
|
|
255
275
|
if (parts.length === 0) {
|
|
256
|
-
parts.push('No active phase. Available commands: *create, *plan, *develop, *test, *deploy');
|
|
276
|
+
parts.push('No active phase. Available commands: *create, *plan, *develop, *test, *deploy, *projects, *switch');
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
this.history.append(msg.chatId, 'user', msg.text);
|
|
@@ -270,7 +290,15 @@ class Router {
|
|
|
270
290
|
await this._runCatchUp();
|
|
271
291
|
|
|
272
292
|
const projectRoot = engine.resolveProjectRoot();
|
|
273
|
-
|
|
293
|
+
|
|
294
|
+
// If a phase is running, inject tmux pane context so Claude can see agent state
|
|
295
|
+
let prompt = engine.composePrompt(msg.text);
|
|
296
|
+
if (this.orchestrator.isRunning()) {
|
|
297
|
+
const agentContext = this.orchestrator.captureCurrentAgentContext();
|
|
298
|
+
if (agentContext) {
|
|
299
|
+
prompt = `${agentContext}\n\n---\n\nUser message: ${msg.text}`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
274
302
|
|
|
275
303
|
log.router(`Processing: "${msg.text.slice(0, 80)}..." → cwd: ${projectRoot || '~'}`);
|
|
276
304
|
const result = await engine.callClaude({
|
|
@@ -426,6 +454,80 @@ class Router {
|
|
|
426
454
|
}
|
|
427
455
|
}
|
|
428
456
|
|
|
457
|
+
// ── Multi-Project Commands ────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
_handleProjects(msg) {
|
|
460
|
+
const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
|
|
461
|
+
if (!fs.existsSync(registryPath)) {
|
|
462
|
+
return { text: 'No projects registered yet. Use *create to create one.' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
466
|
+
const projects = registry.projects || [];
|
|
467
|
+
if (projects.length === 0) {
|
|
468
|
+
return { text: 'No projects registered yet. Use *create to create one.' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Read active project
|
|
472
|
+
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
473
|
+
let activeId = '';
|
|
474
|
+
if (fs.existsSync(focusPath)) {
|
|
475
|
+
const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
|
|
476
|
+
activeId = focus.active_project || '';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const statusIcons = { active: '✅', paused: '💤', maintenance: '🔧', archived: '📦' };
|
|
480
|
+
const lines = projects.map((p, i) => {
|
|
481
|
+
const icon = statusIcons[p.status] || '❓';
|
|
482
|
+
const isCurrent = p.id === activeId ? ' ← current' : '';
|
|
483
|
+
return `${i + 1}. **${p.name || p.id}** ${icon} ${p.status} (Phase ${p.phase || '?'}: ${p.pulse || '?'})${isCurrent}`;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const reply = `📂 **Projects**\n\n${lines.join('\n')}\n\nUse \`*switch <name>\` to change active project.`;
|
|
487
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
488
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
489
|
+
return { text: reply };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_handleSwitch(query, msg) {
|
|
493
|
+
const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
|
|
494
|
+
if (!fs.existsSync(registryPath)) {
|
|
495
|
+
return { text: '❌ No projects registered.' };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
499
|
+
const projects = registry.projects || [];
|
|
500
|
+
const q = query.toLowerCase();
|
|
501
|
+
|
|
502
|
+
// Fuzzy match: exact id, exact name, prefix of id, prefix of name
|
|
503
|
+
const match = projects.find((p) => p.id === q || (p.name && p.name.toLowerCase() === q))
|
|
504
|
+
|| projects.find((p) => p.id.startsWith(q) || (p.name && p.name.toLowerCase().startsWith(q)));
|
|
505
|
+
|
|
506
|
+
if (!match) {
|
|
507
|
+
return { text: `❌ No project matching "${query}". Use *projects to see available projects.` };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update focus
|
|
511
|
+
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
512
|
+
let focus = {};
|
|
513
|
+
if (fs.existsSync(focusPath)) {
|
|
514
|
+
focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
|
|
515
|
+
}
|
|
516
|
+
focus.active_project = match.id;
|
|
517
|
+
focus.active_action = `switched to project: ${match.name || match.id}`;
|
|
518
|
+
focus.updated_at = new Date().toISOString();
|
|
519
|
+
fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
|
|
520
|
+
|
|
521
|
+
// Clear Claude session so next message gets fresh system prompt for new project
|
|
522
|
+
engine.clearSessionState();
|
|
523
|
+
|
|
524
|
+
const reply = `✅ Switched to **${match.name || match.id}** (Phase ${match.phase || '?'}: ${match.pulse || '?'})`;
|
|
525
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
526
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
527
|
+
log.router(`Project switched to: ${match.id}`);
|
|
528
|
+
return { text: reply };
|
|
529
|
+
}
|
|
530
|
+
|
|
429
531
|
_updateGlobalFocus(msg, projectRoot) {
|
|
430
532
|
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
431
533
|
if (!fs.existsSync(focusPath)) return;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrix-yuri",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
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": {
|
|
7
7
|
"orchestrix-yuri": "bin/install.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "node --test 'test/*.test.js'",
|
|
11
11
|
"serve": "node bin/serve.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|