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.
- package/lib/gateway/engine/phase-orchestrator.js +375 -0
- package/lib/gateway/router.js +110 -10
- package/package.json +1 -1
|
@@ -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) {
|
package/lib/gateway/router.js
CHANGED
|
@@ -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:
|
|
21
|
-
develop:
|
|
22
|
-
test:
|
|
23
|
-
|
|
24
|
-
|
|
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;
|