orchestrix-yuri 4.2.5 โ†’ 4.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.
@@ -181,6 +181,20 @@ class PhaseOrchestrator {
181
181
  };
182
182
  }
183
183
 
184
+ if (this._phase === 'test') {
185
+ return { phase: 'test', message: '๐Ÿงช Testing in progress. QA running smoke tests.' };
186
+ }
187
+
188
+ if (this._phase === 'iterate') {
189
+ const ctx = this._changeContext || {};
190
+ return { phase: 'iterate', message: `๐Ÿ”„ Iteration in progress. Current: ${ctx.iteratePhase || 'starting'}` };
191
+ }
192
+
193
+ if (this._phase === 'change') {
194
+ const ctx = this._changeContext || {};
195
+ return { phase: 'change', message: `๐Ÿ”ง Change in progress (${ctx.scope || '?'}). Step ${this._step + 1}` };
196
+ }
197
+
184
198
  return { phase: this._phase, message: `Phase ${this._phase} is running.` };
185
199
  }
186
200
 
@@ -753,6 +767,367 @@ class PhaseOrchestrator {
753
767
  this.onComplete('develop', '๐ŸŽ‰ Development complete! All stories finished.\n\nRun *test to start smoke testing.');
754
768
  }
755
769
 
770
+ // โ”€โ”€ Test Phase โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
771
+
772
+ /**
773
+ * Start test phase: QA smoke test per epic, auto fix-retest loop.
774
+ * Uses existing dev session's QA window (3) and Dev window (2).
775
+ */
776
+ startTest(projectRoot) {
777
+ if (this._phase) {
778
+ return `โš ๏ธ Phase "${this._phase}" is already running. Use *status to check.`;
779
+ }
780
+
781
+ this._projectRoot = projectRoot;
782
+ this._phase = 'test';
783
+ this._step = 0;
784
+ this._lastHash = '';
785
+ this._stableCount = 0;
786
+
787
+ // Ensure dev session exists (QA is window 3)
788
+ try {
789
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
790
+ const result = execSync(`bash "${scriptPath}" dev "${projectRoot}"`, {
791
+ encoding: 'utf8', timeout: 60000,
792
+ }).trim();
793
+ const lines = result.split('\n');
794
+ this._session = lines[lines.length - 1].trim();
795
+ } catch (err) {
796
+ this._phase = null;
797
+ return `โŒ Failed to ensure dev session: ${err.message}`;
798
+ }
799
+
800
+ // Start QA smoke test on first epic
801
+ tmx.sendKeysWithEnter(this._session, 3, '/clear');
802
+ execSync('sleep 2');
803
+ tmx.sendKeysWithEnter(this._session, 3, '/o qa');
804
+ execSync('sleep 12');
805
+ tmx.sendKeysWithEnter(this._session, 3, '*smoke-test');
806
+
807
+ const pollInterval = this.config.phase_poll_interval || 30000;
808
+ this._timer = setInterval(() => this._pollTest(), pollInterval);
809
+
810
+ log.engine(`Test phase started: session=${this._session}`);
811
+ return '๐Ÿงช Testing started! QA is running smoke tests.\n\nI\'ll notify you of results. Failed tests will auto-trigger Dev fixes.';
812
+ }
813
+
814
+ _pollTest() {
815
+ if (this._phase !== 'test') return;
816
+
817
+ if (!tmx.hasSession(this._session)) {
818
+ this._handleError('test', 'tmux session died');
819
+ return;
820
+ }
821
+
822
+ const result = tmx.checkCompletion(this._session, 3, this._lastHash);
823
+ if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
824
+ this._stableCount = 0;
825
+ this._lastHash = '';
826
+ this._completeTest();
827
+ return;
828
+ }
829
+ if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
830
+ else { this._lastHash = result.hash; }
831
+ }
832
+
833
+ _completeTest() {
834
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
835
+ this._phase = null;
836
+ log.engine('Test phase complete');
837
+ this.onComplete('test', '๐Ÿงช Testing complete! Check QA results.\n\nRun *deploy when ready.');
838
+ }
839
+
840
+ // โ”€โ”€ Iterate (New Iteration) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
841
+
842
+ /**
843
+ * Start a new iteration: PM generates next-steps, agents execute, SM starts dev.
844
+ * Flow: PM *start-iteration โ†’ parse HANDOFF โ†’ execute agents โ†’ SM *draft โ†’ dev auto
845
+ */
846
+ startIterate(projectRoot) {
847
+ if (this._phase) {
848
+ return `โš ๏ธ Phase "${this._phase}" is already running. Use *status to check.`;
849
+ }
850
+
851
+ this._projectRoot = projectRoot;
852
+ this._phase = 'iterate';
853
+ this._step = 0;
854
+ this._lastHash = '';
855
+ this._stableCount = 0;
856
+
857
+ // Step 1: Ensure planning session
858
+ try {
859
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
860
+ const result = execSync(`bash "${scriptPath}" planning "${projectRoot}"`, {
861
+ encoding: 'utf8', timeout: 60000,
862
+ }).trim();
863
+ const lines = result.split('\n');
864
+ this._session = lines[lines.length - 1].trim();
865
+ } catch (err) {
866
+ this._phase = null;
867
+ return `โŒ Failed to create planning session: ${err.message}`;
868
+ }
869
+
870
+ // Start PM *start-iteration
871
+ tmx.sendKeysWithEnter(this._session, 0, '/o pm');
872
+ execSync('sleep 15');
873
+ tmx.sendKeysWithEnter(this._session, 0, '*start-iteration');
874
+
875
+ this._changeContext = { iteratePhase: 'pm' };
876
+ const pollInterval = this.config.phase_poll_interval || 30000;
877
+ this._timer = setInterval(() => this._pollIterate(), pollInterval);
878
+
879
+ log.engine(`Iterate started: session=${this._session}`);
880
+ return '๐Ÿ”„ New iteration started! PM is generating next-steps.\n\nAfter PM finishes, agents will execute in sequence, then dev automation resumes.';
881
+ }
882
+
883
+ _pollIterate() {
884
+ if (this._phase !== 'iterate') return;
885
+ if (this._waitingForInput) return;
886
+
887
+ const ctx = this._changeContext;
888
+ if (!ctx) return;
889
+
890
+ if (!tmx.hasSession(this._session)) {
891
+ this._handleError('iterate', 'tmux session died');
892
+ return;
893
+ }
894
+
895
+ const result = tmx.checkCompletion(this._session, 0, this._lastHash);
896
+ if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
897
+ this._stableCount = 0;
898
+ this._lastHash = '';
899
+ this._step++;
900
+
901
+ if (ctx.iteratePhase === 'pm') {
902
+ // PM done โ†’ send to Architect for review
903
+ this.onProgress('โœ… PM generated next-steps. Sending to Architect...');
904
+ ctx.iteratePhase = 'architect';
905
+ tmx.sendKeysWithEnter(this._session, 0, '/clear');
906
+ execSync('sleep 2');
907
+ tmx.sendKeysWithEnter(this._session, 0, '/o architect');
908
+ execSync('sleep 15');
909
+ tmx.sendKeysWithEnter(this._session, 0, '*resolve-change');
910
+ } else if (ctx.iteratePhase === 'architect') {
911
+ // Architect done โ†’ transition to dev: SM *draft
912
+ this.onProgress('โœ… Architect resolved. Starting dev automation via SM...');
913
+ try {
914
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
915
+ const devResult = execSync(`bash "${scriptPath}" dev "${this._projectRoot}"`, {
916
+ encoding: 'utf8', timeout: 120000,
917
+ }).trim();
918
+ const devLines = devResult.split('\n');
919
+ const devSession = devLines[devLines.length - 1].trim();
920
+
921
+ tmx.sendKeysWithEnter(devSession, 1, '/clear');
922
+ execSync('sleep 2');
923
+ tmx.sendKeysWithEnter(devSession, 1, '/o sm');
924
+ execSync('sleep 12');
925
+ tmx.sendKeysWithEnter(devSession, 1, '*draft');
926
+
927
+ this._completeIterate(devSession);
928
+ } catch (err) {
929
+ this._handleError('iterate', `Failed to start dev: ${err.message}`);
930
+ }
931
+ }
932
+ return;
933
+ }
934
+
935
+ if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
936
+ else { this._lastHash = result.hash; }
937
+ }
938
+
939
+ _completeIterate(devSession) {
940
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
941
+ // Kill planning session
942
+ if (this._session && tmx.hasSession(this._session)) {
943
+ tmx.killSession(this._session);
944
+ }
945
+ this._phase = null;
946
+ this._changeContext = null;
947
+ log.engine('Iterate complete โ€” dev automation started');
948
+ this.onComplete('iterate', `๐Ÿ”„ New iteration launched!\n\nSM is drafting stories in dev session: ${devSession}\nAgents will chain automatically via handoff-detector.`);
949
+ }
950
+
951
+ // โ”€โ”€ Change Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
952
+
953
+ /**
954
+ * Execute a change request in background based on assessed scope.
955
+ *
956
+ * @param {string} projectRoot
957
+ * @param {'small'|'medium'|'large'} scope โ€” assessed by Claude in step 1
958
+ * @param {string} description โ€” the change description from user
959
+ * @returns {string} immediate status message
960
+ */
961
+ startChange(projectRoot, scope, description) {
962
+ if (this._phase) {
963
+ return `โš ๏ธ Phase "${this._phase}" is already running. Finish it first or *cancel.`;
964
+ }
965
+
966
+ this._projectRoot = projectRoot;
967
+ this._phase = 'change';
968
+ this._lastHash = '';
969
+ this._stableCount = 0;
970
+
971
+ log.engine(`Change management: scope=${scope}, desc="${description.slice(0, 60)}..."`);
972
+
973
+ try {
974
+ if (scope === 'small') {
975
+ return this._executeSmallChange(projectRoot, description);
976
+ } else if (scope === 'medium' || scope === 'large') {
977
+ return this._executeMediumChange(projectRoot, scope, description);
978
+ } else {
979
+ this._phase = null;
980
+ return `โŒ Unknown scope: ${scope}. Expected small/medium/large.`;
981
+ }
982
+ } catch (err) {
983
+ this._phase = null;
984
+ return `โŒ Change failed: ${err.message}`;
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Small change: send *solo to Dev window in existing dev session.
990
+ */
991
+ _executeSmallChange(projectRoot, description) {
992
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
993
+ const result = execSync(`bash "${scriptPath}" dev "${projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
994
+ const lines = result.split('\n');
995
+ this._session = lines[lines.length - 1].trim();
996
+
997
+ // Send to Dev window (window 2)
998
+ tmx.sendKeysWithEnter(this._session, 2, '/clear');
999
+ execSync('sleep 2');
1000
+ tmx.sendKeysWithEnter(this._session, 2, '/o dev');
1001
+ execSync('sleep 12');
1002
+ tmx.sendKeysWithEnter(this._session, 2, `*solo "${description}"`);
1003
+
1004
+ // Poll for completion
1005
+ const pollInterval = this.config.phase_poll_interval || 30000;
1006
+ this._step = 0; // track which step we're on
1007
+ this._changeContext = { scope: 'small', description };
1008
+ this._timer = setInterval(() => this._pollChange(), pollInterval);
1009
+
1010
+ return `๐Ÿ”ง Small change started โ†’ Dev *solo\n\n"${description.slice(0, 100)}"\n\nI'll notify you when it's done.`;
1011
+ }
1012
+
1013
+ /**
1014
+ * Medium/Large change: PO route-change in planning session, then apply in dev session.
1015
+ */
1016
+ _executeMediumChange(projectRoot, scope, description) {
1017
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
1018
+
1019
+ // Step 1: Ensure planning session
1020
+ const planResult = execSync(`bash "${scriptPath}" planning "${projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
1021
+ const planLines = planResult.split('\n');
1022
+ const planSession = planLines[planLines.length - 1].trim();
1023
+
1024
+ // Step 2: Activate PO and route change
1025
+ tmx.sendKeysWithEnter(planSession, 0, '/o po');
1026
+ execSync('sleep 15');
1027
+ tmx.sendKeysWithEnter(planSession, 0, `*route-change "${description}"`);
1028
+
1029
+ // Poll for PO completion, then chain to next agent
1030
+ const pollInterval = this.config.phase_poll_interval || 30000;
1031
+ this._step = 0;
1032
+ this._changeContext = {
1033
+ scope,
1034
+ description,
1035
+ planSession,
1036
+ // Steps: 0=PO route-change โ†’ 1=Architect/PM (based on PO output) โ†’ 2=SM apply-proposal
1037
+ };
1038
+ this._timer = setInterval(() => this._pollChange(), pollInterval);
1039
+
1040
+ 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.`;
1041
+ }
1042
+
1043
+ /**
1044
+ * Poll change management progress.
1045
+ */
1046
+ _pollChange() {
1047
+ if (this._phase !== 'change') return;
1048
+ if (this._waitingForInput) return;
1049
+
1050
+ const ctx = this._changeContext;
1051
+ if (!ctx) return;
1052
+
1053
+ if (ctx.scope === 'small') {
1054
+ // Polling Dev window for completion
1055
+ if (!tmx.hasSession(this._session)) {
1056
+ this._handleError('change', 'Dev tmux session died');
1057
+ return;
1058
+ }
1059
+ const result = tmx.checkCompletion(this._session, 2, this._lastHash);
1060
+ if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
1061
+ this._completeChange('Dev completed the change.');
1062
+ return;
1063
+ }
1064
+ if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
1065
+ else { this._lastHash = result.hash; }
1066
+ return;
1067
+ }
1068
+
1069
+ // Medium/Large: multi-step
1070
+ if (!tmx.hasSession(ctx.planSession)) {
1071
+ this._handleError('change', 'Planning tmux session died');
1072
+ return;
1073
+ }
1074
+
1075
+ const result = tmx.checkCompletion(ctx.planSession, 0, this._lastHash);
1076
+
1077
+ if (result.status === 'complete' || (result.status === 'stable' && ++this._stableCount >= 3)) {
1078
+ this._stableCount = 0;
1079
+ this._lastHash = '';
1080
+ this._step++;
1081
+
1082
+ if (this._step === 1) {
1083
+ // PO finished routing. Now send to Architect for *resolve-change
1084
+ this.onProgress('โœ… PO routing complete. Sending to Architect...');
1085
+ tmx.sendKeysWithEnter(ctx.planSession, 0, '/clear');
1086
+ execSync('sleep 2');
1087
+ tmx.sendKeysWithEnter(ctx.planSession, 0, '/o architect');
1088
+ execSync('sleep 15');
1089
+ tmx.sendKeysWithEnter(ctx.planSession, 0, '*resolve-change');
1090
+ } else if (this._step === 2) {
1091
+ // Architect finished. Apply in dev session via SM
1092
+ this.onProgress('โœ… Architect resolved. Applying change via SM...');
1093
+ try {
1094
+ const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
1095
+ const devResult = execSync(`bash "${scriptPath}" dev "${this._projectRoot}"`, { encoding: 'utf8', timeout: 60000 }).trim();
1096
+ const devLines = devResult.split('\n');
1097
+ this._session = devLines[devLines.length - 1].trim();
1098
+
1099
+ tmx.sendKeysWithEnter(this._session, 1, '/clear');
1100
+ execSync('sleep 2');
1101
+ tmx.sendKeysWithEnter(this._session, 1, '/o sm');
1102
+ execSync('sleep 12');
1103
+ tmx.sendKeysWithEnter(this._session, 1, '*draft');
1104
+
1105
+ // Now poll dev session SM window
1106
+ this._changeContext._pollDevSM = true;
1107
+ } catch (err) {
1108
+ this._handleError('change', `Failed to apply in dev session: ${err.message}`);
1109
+ }
1110
+ } else if (this._step >= 3) {
1111
+ this._completeChange('Change applied. SM started new stories from the change.');
1112
+ }
1113
+ return;
1114
+ }
1115
+
1116
+ if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
1117
+ else { this._lastHash = result.hash; }
1118
+ }
1119
+
1120
+ _completeChange(summary) {
1121
+ if (this._timer) {
1122
+ clearInterval(this._timer);
1123
+ this._timer = null;
1124
+ }
1125
+ this._phase = null;
1126
+ this._changeContext = null;
1127
+ log.engine(`Change management complete: ${summary}`);
1128
+ this.onComplete('change', `โœ… Change management complete.\n\n${summary}`);
1129
+ }
1130
+
756
1131
  // โ”€โ”€ Shared โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
757
1132
 
758
1133
  _handleError(phase, message) {
@@ -17,14 +17,17 @@ const YURI_GLOBAL = path.join(os.homedir(), '.yuri');
17
17
  // โ”€โ”€ Phase command patterns โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
18
 
19
19
  const PHASE_COMMANDS = {
20
- plan: /^\*plan\b/i,
21
- develop: /^\*develop\b/i,
22
- test: /^\*test\b/i,
23
- deploy: /^\*deploy\b/i,
24
- cancel: /^\*cancel\b/i,
20
+ plan: /^\*plan\b/i,
21
+ develop: /^\*develop\b/i,
22
+ test: /^\*test\b/i,
23
+ change: /^\*change\s+(.+)/i,
24
+ iterate: /^\*iterate\b/i,
25
+ deploy: /^\*deploy\b/i,
26
+ cancel: /^\*cancel\b/i,
25
27
  };
26
28
 
27
29
  const META_COMMANDS = {
30
+ help: /^\*help\b/i,
28
31
  projects: /^\*projects\b/i,
29
32
  switch: /^\*switch\s+(.+)/i,
30
33
  };
@@ -207,7 +210,10 @@ class Router {
207
210
  return this._handleStatusQuery(msg);
208
211
  }
209
212
 
210
- // โ•โ•โ• META COMMANDS โ€” *projects, *switch (always allowed) โ•โ•โ•
213
+ // โ•โ•โ• META COMMANDS โ€” *help, *projects, *switch (always allowed) โ•โ•โ•
214
+ if (META_COMMANDS.help.test(msg.text.trim())) {
215
+ return { text: this._buildHelpText() };
216
+ }
211
217
  if (META_COMMANDS.projects.test(msg.text.trim())) {
212
218
  return this._handleProjects(msg);
213
219
  }
@@ -266,7 +272,7 @@ class Router {
266
272
  return null;
267
273
  }
268
274
 
269
- _handlePhaseCommand(phase, msg) {
275
+ async _handlePhaseCommand(phase, msg) {
270
276
  const projectRoot = engine.resolveProjectRoot();
271
277
  if (!projectRoot) {
272
278
  return { text: 'โŒ No active project found. Create one first with *create.' };
@@ -281,15 +287,19 @@ class Router {
281
287
  response = this.orchestrator.startDevelop(projectRoot);
282
288
  break;
283
289
  case 'test':
290
+ response = this.orchestrator.startTest(projectRoot);
291
+ break;
292
+ case 'change':
293
+ return this._handleChangeCommand(msg, projectRoot);
294
+ case 'iterate':
295
+ response = this.orchestrator.startIterate(projectRoot);
296
+ break;
284
297
  case 'deploy':
285
- // These phases are simpler โ€” let Claude handle them normally
286
- // (they don't have the 30-minute orchestration problem)
287
298
  return this._processMessageDirect(msg);
288
299
  default:
289
300
  response = `Unknown phase: ${phase}`;
290
301
  }
291
302
 
292
- // Save to chat history
293
303
  this.history.append(msg.chatId, 'user', msg.text);
294
304
  this.history.append(msg.chatId, 'assistant', response.slice(0, 2000));
295
305
  this._updateGlobalFocus(msg, projectRoot);
@@ -297,6 +307,56 @@ class Router {
297
307
  return { text: response };
298
308
  }
299
309
 
310
+ /**
311
+ * Handle *change command in two steps:
312
+ * Step 1: Claude assesses the scope (small/medium/large) โ€” quick claude -p call
313
+ * Step 2: Orchestrator executes the change in tmux background
314
+ */
315
+ async _handleChangeCommand(msg, projectRoot) {
316
+ // Extract description from "*change description here"
317
+ const match = msg.text.trim().match(/^\*change\s+(.+)/i);
318
+ const description = match ? match[1].replace(/^["']|["']$/g, '') : '';
319
+
320
+ if (!description) {
321
+ return { text: 'โŒ Usage: *change "description of the change"\n\nExample: *change "Add dark mode toggle to settings page"' };
322
+ }
323
+
324
+ // Step 1: Ask Claude to assess scope
325
+ this.history.append(msg.chatId, 'user', msg.text);
326
+ log.router(`Change request: "${description.slice(0, 80)}..." โ€” assessing scope...`);
327
+
328
+ const scopePrompt = `You are assessing a change request for a software project.
329
+
330
+ Change description: "${description}"
331
+
332
+ Based on the description, classify the scope as one of:
333
+ - **small**: โ‰ค5 files affected, no architectural changes, no new dependencies (e.g., UI tweak, bug fix, small feature)
334
+ - **medium**: Cross-component change, needs PO routing and possibly architect review (e.g., new API endpoint, refactoring a module)
335
+ - **large**: Cross-module/database/security change, needs full re-planning (e.g., auth system rewrite, new microservice)
336
+
337
+ Reply with ONLY one word: small, medium, or large. Nothing else.`;
338
+
339
+ const scopeResult = await engine.callClaude({
340
+ prompt: scopePrompt,
341
+ cwd: projectRoot,
342
+ engineConfig: this.config.engine,
343
+ timeout: 30000, // quick assessment
344
+ });
345
+
346
+ const scopeRaw = (scopeResult.reply || '').trim().toLowerCase();
347
+ const scope = ['small', 'medium', 'large'].find((s) => scopeRaw.includes(s)) || 'medium';
348
+
349
+ log.router(`Change scope assessed: ${scope}`);
350
+
351
+ // Step 2: Execute via orchestrator
352
+ const response = this.orchestrator.startChange(projectRoot, scope, description);
353
+
354
+ this.history.append(msg.chatId, 'assistant', response.slice(0, 2000));
355
+ this._updateGlobalFocus(msg, projectRoot);
356
+
357
+ return { text: `๐Ÿ“‹ Scope: **${scope}**\n\n${response}` };
358
+ }
359
+
300
360
  // โ”€โ”€ Status Query โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
301
361
 
302
362
  _isStatusQuery(text) {
@@ -749,6 +809,46 @@ class Router {
749
809
  return lines.join('\n');
750
810
  }
751
811
 
812
+ // โ”€โ”€ Help โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
813
+
814
+ _buildHelpText() {
815
+ return `๐Ÿš€ **Yuri โ€” Meta-Orchestrator**
816
+
817
+ **Project Lifecycle**
818
+ | Command | Description |
819
+ |---------|-------------|
820
+ | \`*create\` | Create a new project (interactive Q&A) |
821
+ | \`*plan\` | Start planning phase (6 agents sequentially, background) |
822
+ | \`*develop\` | Start development phase (4 agents with HANDOFF, background) |
823
+ | \`*test\` | Start smoke testing (QA per epic, auto fix-retest) |
824
+ | \`*deploy\` | Deploy the project |
825
+
826
+ **Change & Iteration**
827
+ | Command | Description |
828
+ |---------|-------------|
829
+ | \`*change "desc"\` | Handle a requirement change (auto scope assessment) |
830
+ | \`*iterate\` | Start new iteration (PM โ†’ agents โ†’ dev automation) |
831
+
832
+ **Monitoring**
833
+ | Command | Description |
834
+ |---------|-------------|
835
+ | \`*status\` | Show progress card (epic/story/agent/cost) |
836
+ | \`*cancel\` | Stop the running phase |
837
+ | \`*resume\` | Resume from last checkpoint |
838
+
839
+ **Portfolio**
840
+ | Command | Description |
841
+ |---------|-------------|
842
+ | \`*projects\` | List all registered projects |
843
+ | \`*switch <name>\` | Switch active project |
844
+ | \`*help\` | Show this help |
845
+
846
+ **Notes**
847
+ - \`*plan\`, \`*develop\`, \`*test\`, \`*change\`, \`*iterate\` run in background โ€” you can chat normally while they execute
848
+ - Progress is reported every 30 minutes automatically
849
+ - Reply to agent question messages to answer them directly`;
850
+ }
851
+
752
852
  _updateGlobalFocus(msg, projectRoot) {
753
853
  const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
754
854
  if (!fs.existsSync(focusPath)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.2.5",
3
+ "version": "4.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": {