orchestrix-yuri 4.2.4 → 4.3.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.
|
@@ -753,6 +753,186 @@ class PhaseOrchestrator {
|
|
|
753
753
|
this.onComplete('develop', '🎉 Development complete! All stories finished.\n\nRun *test to start smoke testing.');
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
// ── Change Management ───────────────────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Execute a change request in background based on assessed scope.
|
|
760
|
+
*
|
|
761
|
+
* @param {string} projectRoot
|
|
762
|
+
* @param {'small'|'medium'|'large'} scope — assessed by Claude in step 1
|
|
763
|
+
* @param {string} description — the change description from user
|
|
764
|
+
* @returns {string} immediate status message
|
|
765
|
+
*/
|
|
766
|
+
startChange(projectRoot, scope, description) {
|
|
767
|
+
if (this._phase) {
|
|
768
|
+
return `⚠️ Phase "${this._phase}" is already running. Finish it first or *cancel.`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this._projectRoot = projectRoot;
|
|
772
|
+
this._phase = 'change';
|
|
773
|
+
this._lastHash = '';
|
|
774
|
+
this._stableCount = 0;
|
|
775
|
+
|
|
776
|
+
log.engine(`Change management: scope=${scope}, desc="${description.slice(0, 60)}..."`);
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
if (scope === 'small') {
|
|
780
|
+
return this._executeSmallChange(projectRoot, description);
|
|
781
|
+
} else if (scope === 'medium' || scope === 'large') {
|
|
782
|
+
return this._executeMediumChange(projectRoot, scope, description);
|
|
783
|
+
} else {
|
|
784
|
+
this._phase = null;
|
|
785
|
+
return `❌ Unknown scope: ${scope}. Expected small/medium/large.`;
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
this._phase = null;
|
|
789
|
+
return `❌ Change failed: ${err.message}`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Small change: send *solo to Dev window in existing dev session.
|
|
795
|
+
*/
|
|
796
|
+
_executeSmallChange(projectRoot, description) {
|
|
797
|
+
const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
|
|
798
|
+
const result = execSync(`bash "${scriptPath}" dev "${projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
|
|
799
|
+
const lines = result.split('\n');
|
|
800
|
+
this._session = lines[lines.length - 1].trim();
|
|
801
|
+
|
|
802
|
+
// Send to Dev window (window 2)
|
|
803
|
+
tmx.sendKeysWithEnter(this._session, 2, '/clear');
|
|
804
|
+
execSync('sleep 2');
|
|
805
|
+
tmx.sendKeysWithEnter(this._session, 2, '/o dev');
|
|
806
|
+
execSync('sleep 12');
|
|
807
|
+
tmx.sendKeysWithEnter(this._session, 2, `*solo "${description}"`);
|
|
808
|
+
|
|
809
|
+
// Poll for completion
|
|
810
|
+
const pollInterval = this.config.phase_poll_interval || 30000;
|
|
811
|
+
this._step = 0; // track which step we're on
|
|
812
|
+
this._changeContext = { scope: 'small', description };
|
|
813
|
+
this._timer = setInterval(() => this._pollChange(), pollInterval);
|
|
814
|
+
|
|
815
|
+
return `🔧 Small change started → Dev *solo\n\n"${description.slice(0, 100)}"\n\nI'll notify you when it's done.`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Medium/Large change: PO route-change in planning session, then apply in dev session.
|
|
820
|
+
*/
|
|
821
|
+
_executeMediumChange(projectRoot, scope, description) {
|
|
822
|
+
const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
|
|
823
|
+
|
|
824
|
+
// Step 1: Ensure planning session
|
|
825
|
+
const planResult = execSync(`bash "${scriptPath}" planning "${projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
|
|
826
|
+
const planLines = planResult.split('\n');
|
|
827
|
+
const planSession = planLines[planLines.length - 1].trim();
|
|
828
|
+
|
|
829
|
+
// Step 2: Activate PO and route change
|
|
830
|
+
tmx.sendKeysWithEnter(planSession, 0, '/o po');
|
|
831
|
+
execSync('sleep 15');
|
|
832
|
+
tmx.sendKeysWithEnter(planSession, 0, `*route-change "${description}"`);
|
|
833
|
+
|
|
834
|
+
// Poll for PO completion, then chain to next agent
|
|
835
|
+
const pollInterval = this.config.phase_poll_interval || 30000;
|
|
836
|
+
this._step = 0;
|
|
837
|
+
this._changeContext = {
|
|
838
|
+
scope,
|
|
839
|
+
description,
|
|
840
|
+
planSession,
|
|
841
|
+
// Steps: 0=PO route-change → 1=Architect/PM (based on PO output) → 2=SM apply-proposal
|
|
842
|
+
};
|
|
843
|
+
this._timer = setInterval(() => this._pollChange(), pollInterval);
|
|
844
|
+
|
|
845
|
+
return `🔧 ${scope === 'large' ? 'Large' : 'Medium'} change started → PO *route-change\n\n"${description.slice(0, 100)}"\n\nPO will assess and route to the right agent. I'll keep you updated.`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Poll change management progress.
|
|
850
|
+
*/
|
|
851
|
+
_pollChange() {
|
|
852
|
+
if (this._phase !== 'change') return;
|
|
853
|
+
if (this._waitingForInput) return;
|
|
854
|
+
|
|
855
|
+
const ctx = this._changeContext;
|
|
856
|
+
if (!ctx) return;
|
|
857
|
+
|
|
858
|
+
if (ctx.scope === 'small') {
|
|
859
|
+
// Polling Dev window for completion
|
|
860
|
+
if (!tmx.hasSession(this._session)) {
|
|
861
|
+
this._handleError('change', 'Dev tmux session died');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const result = tmx.checkCompletion(this._session, 2, this._lastHash);
|
|
865
|
+
if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
|
|
866
|
+
this._completeChange('Dev completed the change.');
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
|
|
870
|
+
else { this._lastHash = result.hash; }
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Medium/Large: multi-step
|
|
875
|
+
if (!tmx.hasSession(ctx.planSession)) {
|
|
876
|
+
this._handleError('change', 'Planning tmux session died');
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const result = tmx.checkCompletion(ctx.planSession, 0, this._lastHash);
|
|
881
|
+
|
|
882
|
+
if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
|
|
883
|
+
this._stableCount = 0;
|
|
884
|
+
this._lastHash = '';
|
|
885
|
+
this._step++;
|
|
886
|
+
|
|
887
|
+
if (this._step === 1) {
|
|
888
|
+
// PO finished routing. Now send to Architect for *resolve-change
|
|
889
|
+
this.onProgress('✅ PO routing complete. Sending to Architect...');
|
|
890
|
+
tmx.sendKeysWithEnter(ctx.planSession, 0, '/clear');
|
|
891
|
+
execSync('sleep 2');
|
|
892
|
+
tmx.sendKeysWithEnter(ctx.planSession, 0, '/o architect');
|
|
893
|
+
execSync('sleep 15');
|
|
894
|
+
tmx.sendKeysWithEnter(ctx.planSession, 0, '*resolve-change');
|
|
895
|
+
} else if (this._step === 2) {
|
|
896
|
+
// Architect finished. Apply in dev session via SM
|
|
897
|
+
this.onProgress('✅ Architect resolved. Applying change via SM...');
|
|
898
|
+
try {
|
|
899
|
+
const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
|
|
900
|
+
const devResult = execSync(`bash "${scriptPath}" dev "${this._projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
|
|
901
|
+
const devLines = devResult.split('\n');
|
|
902
|
+
this._session = devLines[devLines.length - 1].trim();
|
|
903
|
+
|
|
904
|
+
tmx.sendKeysWithEnter(this._session, 1, '/clear');
|
|
905
|
+
execSync('sleep 2');
|
|
906
|
+
tmx.sendKeysWithEnter(this._session, 1, '/o sm');
|
|
907
|
+
execSync('sleep 12');
|
|
908
|
+
tmx.sendKeysWithEnter(this._session, 1, '*draft');
|
|
909
|
+
|
|
910
|
+
// Now poll dev session SM window
|
|
911
|
+
this._changeContext._pollDevSM = true;
|
|
912
|
+
} catch (err) {
|
|
913
|
+
this._handleError('change', `Failed to apply in dev session: ${err.message}`);
|
|
914
|
+
}
|
|
915
|
+
} else if (this._step >= 3) {
|
|
916
|
+
this._completeChange('Change applied. SM started new stories from the change.');
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
|
|
922
|
+
else { this._lastHash = result.hash; }
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_completeChange(summary) {
|
|
926
|
+
if (this._timer) {
|
|
927
|
+
clearInterval(this._timer);
|
|
928
|
+
this._timer = null;
|
|
929
|
+
}
|
|
930
|
+
this._phase = null;
|
|
931
|
+
this._changeContext = null;
|
|
932
|
+
log.engine(`Change management complete: ${summary}`);
|
|
933
|
+
this.onComplete('change', `✅ Change management complete.\n\n${summary}`);
|
|
934
|
+
}
|
|
935
|
+
|
|
756
936
|
// ── Shared ─────────────────────────────────────────────────────────────────
|
|
757
937
|
|
|
758
938
|
_handleError(phase, message) {
|
package/lib/gateway/router.js
CHANGED
|
@@ -19,6 +19,7 @@ const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
|
|
|
19
19
|
const PHASE_COMMANDS = {
|
|
20
20
|
plan: /^\*plan\b/i,
|
|
21
21
|
develop: /^\*develop\b/i,
|
|
22
|
+
change: /^\*change\s+(.+)/i,
|
|
22
23
|
test: /^\*test\b/i,
|
|
23
24
|
deploy: /^\*deploy\b/i,
|
|
24
25
|
cancel: /^\*cancel\b/i,
|
|
@@ -266,7 +267,7 @@ class Router {
|
|
|
266
267
|
return null;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
_handlePhaseCommand(phase, msg) {
|
|
270
|
+
async _handlePhaseCommand(phase, msg) {
|
|
270
271
|
const projectRoot = engine.resolveProjectRoot();
|
|
271
272
|
if (!projectRoot) {
|
|
272
273
|
return { text: '❌ No active project found. Create one first with *create.' };
|
|
@@ -280,16 +281,15 @@ class Router {
|
|
|
280
281
|
case 'develop':
|
|
281
282
|
response = this.orchestrator.startDevelop(projectRoot);
|
|
282
283
|
break;
|
|
284
|
+
case 'change':
|
|
285
|
+
return this._handleChangeCommand(msg, projectRoot);
|
|
283
286
|
case 'test':
|
|
284
287
|
case 'deploy':
|
|
285
|
-
// These phases are simpler — let Claude handle them normally
|
|
286
|
-
// (they don't have the 30-minute orchestration problem)
|
|
287
288
|
return this._processMessageDirect(msg);
|
|
288
289
|
default:
|
|
289
290
|
response = `Unknown phase: ${phase}`;
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
// Save to chat history
|
|
293
293
|
this.history.append(msg.chatId, 'user', msg.text);
|
|
294
294
|
this.history.append(msg.chatId, 'assistant', response.slice(0, 2000));
|
|
295
295
|
this._updateGlobalFocus(msg, projectRoot);
|
|
@@ -297,6 +297,56 @@ class Router {
|
|
|
297
297
|
return { text: response };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Handle *change command in two steps:
|
|
302
|
+
* Step 1: Claude assesses the scope (small/medium/large) — quick claude -p call
|
|
303
|
+
* Step 2: Orchestrator executes the change in tmux background
|
|
304
|
+
*/
|
|
305
|
+
async _handleChangeCommand(msg, projectRoot) {
|
|
306
|
+
// Extract description from "*change description here"
|
|
307
|
+
const match = msg.text.trim().match(/^\*change\s+(.+)/i);
|
|
308
|
+
const description = match ? match[1].replace(/^["']|["']$/g, '') : '';
|
|
309
|
+
|
|
310
|
+
if (!description) {
|
|
311
|
+
return { text: '❌ Usage: *change "description of the change"\n\nExample: *change "Add dark mode toggle to settings page"' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Step 1: Ask Claude to assess scope
|
|
315
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
316
|
+
log.router(`Change request: "${description.slice(0, 80)}..." — assessing scope...`);
|
|
317
|
+
|
|
318
|
+
const scopePrompt = `You are assessing a change request for a software project.
|
|
319
|
+
|
|
320
|
+
Change description: "${description}"
|
|
321
|
+
|
|
322
|
+
Based on the description, classify the scope as one of:
|
|
323
|
+
- **small**: ≤5 files affected, no architectural changes, no new dependencies (e.g., UI tweak, bug fix, small feature)
|
|
324
|
+
- **medium**: Cross-component change, needs PO routing and possibly architect review (e.g., new API endpoint, refactoring a module)
|
|
325
|
+
- **large**: Cross-module/database/security change, needs full re-planning (e.g., auth system rewrite, new microservice)
|
|
326
|
+
|
|
327
|
+
Reply with ONLY one word: small, medium, or large. Nothing else.`;
|
|
328
|
+
|
|
329
|
+
const scopeResult = await engine.callClaude({
|
|
330
|
+
prompt: scopePrompt,
|
|
331
|
+
cwd: projectRoot,
|
|
332
|
+
engineConfig: this.config.engine,
|
|
333
|
+
timeout: 30000, // quick assessment
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const scopeRaw = (scopeResult.reply || '').trim().toLowerCase();
|
|
337
|
+
const scope = ['small', 'medium', 'large'].find((s) => scopeRaw.includes(s)) || 'medium';
|
|
338
|
+
|
|
339
|
+
log.router(`Change scope assessed: ${scope}`);
|
|
340
|
+
|
|
341
|
+
// Step 2: Execute via orchestrator
|
|
342
|
+
const response = this.orchestrator.startChange(projectRoot, scope, description);
|
|
343
|
+
|
|
344
|
+
this.history.append(msg.chatId, 'assistant', response.slice(0, 2000));
|
|
345
|
+
this._updateGlobalFocus(msg, projectRoot);
|
|
346
|
+
|
|
347
|
+
return { text: `📋 Scope: **${scope}**\n\n${response}` };
|
|
348
|
+
}
|
|
349
|
+
|
|
300
350
|
// ── Status Query ─────────────────────────────────────────────────────────────
|
|
301
351
|
|
|
302
352
|
_isStatusQuery(text) {
|
package/package.json
CHANGED
|
@@ -105,27 +105,63 @@ Read `{project}/.yuri/state/phase2.yaml`.
|
|
|
105
105
|
|
|
106
106
|
### IF Phase 3 (Develop) is in progress or complete:
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
**Generate a progress card** using these data sources:
|
|
109
109
|
|
|
110
|
-
Scan
|
|
110
|
+
**1. Scan story statuses:**
|
|
111
111
|
```bash
|
|
112
112
|
SCRIPT_DIR="${CLAUDE_SKILL_DIR}/scripts"
|
|
113
113
|
bash "$SCRIPT_DIR/scan-stories.sh" "$PROJECT_DIR"
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| In Progress | {in_progress count} |
|
|
124
|
-
| Blocked | {blocked count} |
|
|
125
|
-
| Remaining | {remaining count} |
|
|
116
|
+
The script outputs:
|
|
117
|
+
- `Total:{N}` — total planned stories from all `docs/prd/epic-*.yaml` definitions
|
|
118
|
+
- `Created:{N}` — story files in `docs/stories/`
|
|
119
|
+
- `StatusDone:{filename}` / `StatusInProgress:{filename}` / etc. — per-file status
|
|
120
|
+
- `Epics:{N}` — total epics (max N from `docs/prd/epic-N-*`)
|
|
121
|
+
- `CurrentEpic:{N}` — epic of current story
|
|
122
|
+
- `CurrentStory:{filename}` — InProgress or last non-Done story
|
|
126
123
|
|
|
127
|
-
|
|
128
|
-
```
|
|
124
|
+
**2. Detect active tmux agent:**
|
|
125
|
+
```bash
|
|
126
|
+
# Check which orchestrix-* session exists
|
|
127
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^orchestrix-"
|
|
128
|
+
|
|
129
|
+
# For each window (0=Architect, 1=SM, 2=Dev, 3=QA):
|
|
130
|
+
# - Window showing "Command | Description" table = IDLE
|
|
131
|
+
# - Window WITHOUT that table = actively executing
|
|
132
|
+
for w in 0 1 2 3; do
|
|
133
|
+
OUTPUT=$(tmux capture-pane -t "$DEV_SESSION:$w" -p -S -15)
|
|
134
|
+
if ! echo "$OUTPUT" | grep -q "Command.*Description"; then
|
|
135
|
+
echo "Window $w is ACTIVE"
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**3. Format the progress card:**
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
📊 Dev Progress Report
|
|
144
|
+
━━━━━━━━━━━━━━━━━━━━━
|
|
145
|
+
Epic: {CurrentEpic}/{Epics}
|
|
146
|
+
Story: {done}/{total} done ({pct}%) | {created} created
|
|
147
|
+
▓▓▓▓▓▓░░░░░░░░░░░░░░ {pct}%
|
|
148
|
+
━━━━━━━━━━━━━━━━━━━━━
|
|
149
|
+
✅ Done: {N}
|
|
150
|
+
🔄 InProgress: {N}
|
|
151
|
+
📋 Approved: {N}
|
|
152
|
+
📝 Draft: {N}
|
|
153
|
+
━━━━━━━━━━━━━━━━━━━━━
|
|
154
|
+
📝 Current: {CurrentStory}
|
|
155
|
+
🤖 Agent: {active_agent} (window {N})
|
|
156
|
+
⏱ Running for {elapsed}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Where:
|
|
160
|
+
- `{pct}` = round(done / total * 100)
|
|
161
|
+
- Progress bar: `▓` for filled, `░` for empty (20 chars total)
|
|
162
|
+
- Only show status categories with count > 0
|
|
163
|
+
- `{elapsed}` = time since `focus.updated_at` or `phase3.started_at`
|
|
164
|
+
- If no agent is active (all windows show Command table), show "All agents idle (waiting for handoff)"
|
|
129
165
|
|
|
130
166
|
### IF Phase 4 (Test) is in progress or complete:
|
|
131
167
|
|