orchestrix-yuri 4.6.3 โ†’ 4.6.5

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.
@@ -38,7 +38,7 @@ class PhaseOrchestrator {
38
38
  this.onError = opts.onError || (() => {});
39
39
  this.config = opts.config || {};
40
40
 
41
- this._phase = null; // 'plan' | 'develop' | null
41
+ this._phase = null; // 'plan' | 'develop' | 'test' | 'iterate' | 'change' | null
42
42
  this._step = 0; // current agent index
43
43
  this._session = null; // tmux session name
44
44
  this._projectRoot = null;
@@ -117,6 +117,62 @@ class PhaseOrchestrator {
117
117
  }
118
118
  }
119
119
  }
120
+
121
+ // Check phase4 (test)
122
+ const phase4Path = path.join(projectRoot, '.yuri', 'state', 'phase4.yaml');
123
+ if (fs.existsSync(phase4Path)) {
124
+ const phase4 = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
125
+ if (phase4.status === 'in_progress' && Array.isArray(phase4.epics)) {
126
+ // Find a live orchestrix tmux session
127
+ let session = null;
128
+ try {
129
+ const sessions = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8' }).trim();
130
+ session = sessions.split('\n').find((s) => s.startsWith('orchestrix-'));
131
+ } catch { /* no sessions */ }
132
+
133
+ if (session && tmx.hasSession(session)) {
134
+ const epicIds = phase4.epics.map((e) => String(e.id));
135
+ const firstPending = epicIds.findIndex((id) => {
136
+ const e = phase4.epics.find((ep) => String(ep.id) === id);
137
+ return !e || (e.status !== 'passed' && e.status !== 'failed');
138
+ });
139
+
140
+ if (firstPending >= 0) {
141
+ this._phase = 'test';
142
+ this._projectRoot = projectRoot;
143
+ this._session = session;
144
+ this._lastHash = '';
145
+ this._stableCount = 0;
146
+ this._testBusy = false;
147
+
148
+ // Rebuild results from already-tested epics
149
+ const results = {};
150
+ for (const e of phase4.epics) {
151
+ if (e.status === 'passed' || e.status === 'failed') {
152
+ results[e.id] = { status: e.status, rounds: e.rounds || 0 };
153
+ }
154
+ }
155
+
156
+ this._testContext = {
157
+ epicIds,
158
+ epicIdx: firstPending,
159
+ round: 0,
160
+ maxRounds: 3,
161
+ subPhase: null,
162
+ results,
163
+ };
164
+
165
+ this._loadAndTestEpic();
166
+
167
+ const pollInterval = this.config.phase_poll_interval || 30000;
168
+ this._timer = setInterval(() => this._pollTest(), pollInterval);
169
+
170
+ log.engine(`Recovered test phase: session=${session}, resuming at epic idx ${firstPending}`);
171
+ return;
172
+ }
173
+ }
174
+ }
175
+ }
120
176
  }
121
177
 
122
178
  /**
@@ -763,6 +819,29 @@ class PhaseOrchestrator {
763
819
  }
764
820
 
765
821
  this._phase = null;
822
+
823
+ // Update state files so auto-reporter stops
824
+ try {
825
+ const phase3Path = path.join(this._projectRoot, '.yuri', 'state', 'phase3.yaml');
826
+ if (fs.existsSync(phase3Path)) {
827
+ const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
828
+ phase3.status = 'complete';
829
+ phase3.completed_at = new Date().toISOString();
830
+ fs.writeFileSync(phase3Path, yaml.dump(phase3, { lineWidth: -1 }));
831
+ }
832
+
833
+ const focusPath = path.join(this._projectRoot, '.yuri', 'focus.yaml');
834
+ if (fs.existsSync(focusPath)) {
835
+ const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
836
+ focus.step = 'phase3.complete';
837
+ focus.pulse = 'Phase 3 complete, all stories done';
838
+ focus.updated_at = new Date().toISOString();
839
+ fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
840
+ }
841
+ } catch (err) {
842
+ log.warn(`Failed to update state files on dev complete: ${err.message}`);
843
+ }
844
+
766
845
  log.engine('Dev phase complete');
767
846
  this.onComplete('develop', '๐ŸŽ‰ Development complete! All stories finished.\n\nRun *test to start smoke testing.');
768
847
  }
@@ -772,6 +851,8 @@ class PhaseOrchestrator {
772
851
  /**
773
852
  * Start test phase: QA smoke test per epic, auto fix-retest loop.
774
853
  * Uses existing dev session's QA window (3) and Dev window (2).
854
+ *
855
+ * State machine subPhases: qa-loading โ†’ qa-testing โ†’ (on fail) dev-loading โ†’ dev-fixing โ†’ qa-loading ...
775
856
  */
776
857
  startTest(projectRoot) {
777
858
  if (this._phase) {
@@ -780,11 +861,11 @@ class PhaseOrchestrator {
780
861
 
781
862
  this._projectRoot = projectRoot;
782
863
  this._phase = 'test';
783
- this._step = 0;
784
864
  this._lastHash = '';
785
865
  this._stableCount = 0;
866
+ this._testBusy = false;
786
867
 
787
- // Ensure dev session exists (QA is window 3)
868
+ // Ensure dev session exists (QA is window 3, Dev is window 2)
788
869
  try {
789
870
  const scriptPath = path.join(SKILL_DIR, 'scripts', 'ensure-session.sh');
790
871
  const result = execSync(`bash "${scriptPath}" dev "${projectRoot}"`, {
@@ -797,44 +878,350 @@ class PhaseOrchestrator {
797
878
  return `โŒ Failed to ensure dev session: ${err.message}`;
798
879
  }
799
880
 
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');
881
+ // Collect epic list from PRD files
882
+ const prdDir = path.join(projectRoot, 'docs', 'prd');
883
+ let epicIds = [];
884
+ try {
885
+ if (fs.existsSync(prdDir)) {
886
+ epicIds = fs.readdirSync(prdDir)
887
+ .filter((f) => /^epic-\d+/.test(f) && f.endsWith('.yaml'))
888
+ .map((f) => f.replace(/^epic-/, '').replace(/\.yaml$/, ''))
889
+ .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
890
+ }
891
+ } catch { /* ignore */ }
892
+
893
+ if (epicIds.length === 0) {
894
+ this._phase = null;
895
+ return 'โŒ No epic files found in docs/prd/. Nothing to test.';
896
+ }
897
+
898
+ // Initialize or resume phase4.yaml state
899
+ const phase4Path = path.join(projectRoot, '.yuri', 'state', 'phase4.yaml');
900
+ let startIdx = 0;
901
+ try {
902
+ if (fs.existsSync(phase4Path)) {
903
+ const existing = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
904
+ if (existing.status === 'in_progress' && Array.isArray(existing.epics)) {
905
+ // Resume: find first non-passed epic
906
+ const idx = epicIds.findIndex((id) => {
907
+ const e = existing.epics.find((ep) => String(ep.id) === String(id));
908
+ return !e || e.status !== 'passed';
909
+ });
910
+ if (idx >= 0) startIdx = idx;
911
+ }
912
+ }
913
+
914
+ // Write initial state
915
+ const epicsState = epicIds.map((id, i) => ({
916
+ id, status: i < startIdx ? 'passed' : 'pending', rounds: 0, last_tested_at: '',
917
+ }));
918
+ const state = { status: 'in_progress', started_at: new Date().toISOString(), completed_at: '', epics: epicsState, regression_rounds: 0 };
919
+ fs.writeFileSync(phase4Path, yaml.dump(state, { lineWidth: -1 }));
920
+ } catch (err) {
921
+ log.warn(`Failed to init phase4.yaml: ${err.message}`);
922
+ }
923
+
924
+ // Update focus.yaml
925
+ try {
926
+ const focusPath = path.join(projectRoot, '.yuri', 'focus.yaml');
927
+ if (fs.existsSync(focusPath)) {
928
+ const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
929
+ focus.phase = 4;
930
+ focus.step = 'testing';
931
+ focus.action = 'starting smoke tests';
932
+ focus.pulse = `Phase 4: testing 0/${epicIds.length} epics`;
933
+ focus.updated_at = new Date().toISOString();
934
+ fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
935
+ }
936
+ } catch { /* ignore */ }
937
+
938
+ // Initialize test context
939
+ this._testContext = {
940
+ epicIds,
941
+ epicIdx: startIdx,
942
+ round: 0,
943
+ maxRounds: 3,
944
+ subPhase: null, // set by _loadAndTestEpic
945
+ results: {},
946
+ };
947
+
948
+ // Start first epic test
949
+ this._loadAndTestEpic();
806
950
 
807
951
  const pollInterval = this.config.phase_poll_interval || 30000;
808
952
  this._timer = setInterval(() => this._pollTest(), pollInterval);
809
953
 
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.';
954
+ log.engine(`Test phase started: session=${this._session}, epics=${epicIds.join(',')}, startIdx=${startIdx}`);
955
+ return `๐Ÿงช Testing started! ${epicIds.length} epic(s) to test.\n\nQA will smoke-test each epic. Failed tests auto-trigger Dev fixes (max 3 rounds).\nI'll notify you of results.`;
812
956
  }
813
957
 
958
+ /**
959
+ * Reload QA agent and send *smoke-test for current epic.
960
+ */
961
+ _loadAndTestEpic() {
962
+ const ctx = this._testContext;
963
+ const epicId = ctx.epicIds[ctx.epicIdx];
964
+
965
+ this._testBusy = true;
966
+ this._lastHash = '';
967
+ this._stableCount = 0;
968
+
969
+ try {
970
+ tmx.sendKeysWithEnter(this._session, 3, '/clear');
971
+ execSync('sleep 2');
972
+ tmx.sendKeysWithEnter(this._session, 3, '/o qa');
973
+ execSync('sleep 12');
974
+ tmx.sendKeysWithEnter(this._session, 3, `*smoke-test ${epicId}`);
975
+ } catch (err) {
976
+ this._testBusy = false;
977
+ this._handleError('test', `Failed to start QA for epic ${epicId}: ${err.message}`);
978
+ return;
979
+ }
980
+
981
+ ctx.subPhase = 'qa-testing';
982
+ this._testBusy = false;
983
+ log.engine(`QA testing epic ${epicId} (round ${ctx.round + 1}/${ctx.maxRounds})`);
984
+ }
985
+
986
+ /**
987
+ * Reload Dev agent and send *quick-fix for the detected bug.
988
+ */
989
+ _loadAndFixEpic(bugDesc) {
990
+ this._testBusy = true;
991
+ this._lastHash = '';
992
+ this._stableCount = 0;
993
+
994
+ try {
995
+ tmx.sendKeysWithEnter(this._session, 2, '/clear');
996
+ execSync('sleep 2');
997
+ tmx.sendKeysWithEnter(this._session, 2, '/o dev');
998
+ execSync('sleep 12');
999
+
1000
+ // Escape quotes in bug description
1001
+ const safeBug = (bugDesc || 'smoke test failure').replace(/"/g, '\\"');
1002
+ tmx.sendKeysWithEnter(this._session, 2, `*quick-fix "${safeBug}"`);
1003
+ } catch (err) {
1004
+ this._testBusy = false;
1005
+ this._handleError('test', `Failed to start Dev fix: ${err.message}`);
1006
+ return;
1007
+ }
1008
+
1009
+ this._testContext.subPhase = 'dev-fixing';
1010
+ this._testBusy = false;
1011
+ log.engine(`Dev fixing: ${bugDesc}`);
1012
+ }
1013
+
1014
+ /**
1015
+ * Capture QA pane output and determine PASS/FAIL.
1016
+ */
1017
+ _evaluateTestResult() {
1018
+ const output = tmx.capturePane(this._session, 3, 200);
1019
+ const lines = output.split('\n');
1020
+ const tail = lines.slice(-50).join('\n');
1021
+
1022
+ // PASS indicators
1023
+ if (/all\s+tests?\s+passed/i.test(tail) || /โœ….*pass/i.test(tail) || /smoke.*pass/i.test(tail) || /PASS/i.test(tail)) {
1024
+ return { passed: true, bugDesc: null };
1025
+ }
1026
+
1027
+ // FAIL: extract bug description near failure markers
1028
+ let bugDesc = 'smoke test failure';
1029
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 50); i--) {
1030
+ if (/fail|โŒ|error|FAIL/i.test(lines[i])) {
1031
+ // Take up to 3 lines around the failure for context
1032
+ const start = Math.max(0, i - 1);
1033
+ const end = Math.min(lines.length, i + 2);
1034
+ bugDesc = lines.slice(start, end).join(' ').trim().substring(0, 200);
1035
+ break;
1036
+ }
1037
+ }
1038
+
1039
+ return { passed: false, bugDesc };
1040
+ }
1041
+
1042
+ /**
1043
+ * State machine poll for test phase.
1044
+ */
814
1045
  _pollTest() {
815
1046
  if (this._phase !== 'test') return;
1047
+ if (this._testBusy) return;
1048
+
1049
+ const ctx = this._testContext;
1050
+ if (!ctx) return;
816
1051
 
817
1052
  if (!tmx.hasSession(this._session)) {
818
1053
  this._handleError('test', 'tmux session died');
819
1054
  return;
820
1055
  }
821
1056
 
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 = '';
1057
+ // Determine which window to watch
1058
+ const window = ctx.subPhase === 'dev-fixing' ? 2 : 3;
1059
+ const result = tmx.checkCompletion(this._session, window, this._lastHash);
1060
+
1061
+ // Not done yet
1062
+ if (result.status !== 'complete' && !(result.status === 'stable' && ++this._stableCount >= 3)) {
1063
+ if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
1064
+ else { this._lastHash = result.hash; }
1065
+ return;
1066
+ }
1067
+
1068
+ // Agent finished โ€” reset counters
1069
+ this._stableCount = 0;
1070
+ this._lastHash = '';
1071
+
1072
+ const epicId = ctx.epicIds[ctx.epicIdx];
1073
+
1074
+ if (ctx.subPhase === 'qa-testing') {
1075
+ const testResult = this._evaluateTestResult();
1076
+
1077
+ if (testResult.passed) {
1078
+ ctx.results[epicId] = { status: 'passed', rounds: ctx.round + 1 };
1079
+ this._updateTestState(epicId, 'passed', ctx.round + 1);
1080
+ this.onProgress(`โœ… Epic ${epicId} passed (${ctx.round + 1} round(s))`);
1081
+ log.engine(`Epic ${epicId} PASSED after ${ctx.round + 1} round(s)`);
1082
+ this._advanceToNextEpic();
1083
+ } else if (ctx.round + 1 >= ctx.maxRounds) {
1084
+ ctx.results[epicId] = { status: 'failed', rounds: ctx.round + 1 };
1085
+ this._updateTestState(epicId, 'failed', ctx.round + 1);
1086
+ this.onProgress(`โŒ Epic ${epicId} failed after ${ctx.round + 1} rounds.\nLast error: ${testResult.bugDesc}`);
1087
+ log.engine(`Epic ${epicId} FAILED after ${ctx.round + 1} rounds`);
1088
+ this._advanceToNextEpic();
1089
+ } else {
1090
+ ctx.round++;
1091
+ this.onProgress(`๏ฟฝ๏ฟฝ๏ฟฝ๏ธ Epic ${epicId} failed (round ${ctx.round}/${ctx.maxRounds}). Sending to Dev for fix...`);
1092
+ log.engine(`Epic ${epicId} failed round ${ctx.round}, sending to Dev`);
1093
+ this._loadAndFixEpic(testResult.bugDesc);
1094
+ }
1095
+ } else if (ctx.subPhase === 'dev-fixing') {
1096
+ this.onProgress(`๐Ÿ”ง Dev fix done for epic ${epicId}. Retesting...`);
1097
+ log.engine(`Dev fix done for epic ${epicId}, retesting`);
1098
+ this._loadAndTestEpic();
1099
+ }
1100
+ }
1101
+
1102
+ /**
1103
+ * Advance to next epic or complete test phase.
1104
+ */
1105
+ _advanceToNextEpic() {
1106
+ const ctx = this._testContext;
1107
+ ctx.epicIdx++;
1108
+ ctx.round = 0;
1109
+
1110
+ const tested = ctx.epicIdx;
1111
+ const total = ctx.epicIds.length;
1112
+
1113
+ // Update focus pulse
1114
+ try {
1115
+ const focusPath = path.join(this._projectRoot, '.yuri', 'focus.yaml');
1116
+ if (fs.existsSync(focusPath)) {
1117
+ const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
1118
+ focus.pulse = `Phase 4: ${tested}/${total} epics tested`;
1119
+ focus.updated_at = new Date().toISOString();
1120
+ fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
1121
+ }
1122
+ } catch { /* ignore */ }
1123
+
1124
+ if (ctx.epicIdx >= ctx.epicIds.length) {
826
1125
  this._completeTest();
827
1126
  return;
828
1127
  }
829
- if (result.status !== 'stable') { this._stableCount = 0; this._lastHash = result.hash || ''; }
830
- else { this._lastHash = result.hash; }
1128
+
1129
+ // Start next epic
1130
+ this._loadAndTestEpic();
831
1131
  }
832
1132
 
1133
+ /**
1134
+ * Update a single epic's status in phase4.yaml.
1135
+ */
1136
+ _updateTestState(epicId, status, rounds) {
1137
+ try {
1138
+ const phase4Path = path.join(this._projectRoot, '.yuri', 'state', 'phase4.yaml');
1139
+ if (!fs.existsSync(phase4Path)) return;
1140
+
1141
+ const state = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
1142
+ if (!Array.isArray(state.epics)) return;
1143
+
1144
+ const epic = state.epics.find((e) => String(e.id) === String(epicId));
1145
+ if (epic) {
1146
+ epic.status = status;
1147
+ epic.rounds = rounds;
1148
+ epic.last_tested_at = new Date().toISOString();
1149
+ }
1150
+ state.regression_rounds = (state.regression_rounds || 0) + rounds;
1151
+ fs.writeFileSync(phase4Path, yaml.dump(state, { lineWidth: -1 }));
1152
+ } catch (err) {
1153
+ log.warn(`Failed to update phase4.yaml for epic ${epicId}: ${err.message}`);
1154
+ }
1155
+ }
1156
+
1157
+ /**
1158
+ * Complete test phase: aggregate results, update state files, send summary.
1159
+ */
833
1160
  _completeTest() {
834
1161
  if (this._timer) { clearInterval(this._timer); this._timer = null; }
1162
+
1163
+ const ctx = this._testContext;
1164
+ const passed = Object.values(ctx.results).filter((r) => r.status === 'passed').length;
1165
+ const failed = Object.values(ctx.results).filter((r) => r.status === 'failed').length;
1166
+ const total = ctx.epicIds.length;
1167
+
1168
+ // Update phase4.yaml
1169
+ try {
1170
+ const phase4Path = path.join(this._projectRoot, '.yuri', 'state', 'phase4.yaml');
1171
+ if (fs.existsSync(phase4Path)) {
1172
+ const state = yaml.load(fs.readFileSync(phase4Path, 'utf8')) || {};
1173
+ state.status = failed > 0 ? 'complete_with_failures' : 'complete';
1174
+ state.completed_at = new Date().toISOString();
1175
+ fs.writeFileSync(phase4Path, yaml.dump(state, { lineWidth: -1 }));
1176
+ }
1177
+ } catch (err) {
1178
+ log.warn(`Failed to finalize phase4.yaml: ${err.message}`);
1179
+ }
1180
+
1181
+ // Update focus.yaml
1182
+ try {
1183
+ const focusPath = path.join(this._projectRoot, '.yuri', 'focus.yaml');
1184
+ if (fs.existsSync(focusPath)) {
1185
+ const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
1186
+ focus.step = 'phase4.complete';
1187
+ focus.pulse = `Phase 4 complete: ${passed}/${total} passed`;
1188
+ focus.updated_at = new Date().toISOString();
1189
+ fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
1190
+ }
1191
+ } catch { /* ignore */ }
1192
+
1193
+ // Append timeline event
1194
+ try {
1195
+ const timelinePath = path.join(this._projectRoot, '.yuri', 'timeline', 'events.jsonl');
1196
+ const dir = path.dirname(timelinePath);
1197
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1198
+ const event = JSON.stringify({ ts: new Date().toISOString(), type: 'phase_completed', phase: 4, passed, failed, total });
1199
+ fs.appendFileSync(timelinePath, event + '\n');
1200
+ } catch { /* ignore */ }
1201
+
1202
+ // Build summary
1203
+ const lines = [`๐Ÿงช Testing complete! ${passed}/${total} epics passed.\n`];
1204
+ for (const epicId of ctx.epicIds) {
1205
+ const r = ctx.results[epicId];
1206
+ if (r && r.status === 'passed') {
1207
+ lines.push(` โœ… Epic ${epicId} โ€” passed (${r.rounds} round(s))`);
1208
+ } else if (r && r.status === 'failed') {
1209
+ lines.push(` โŒ Epic ${epicId} โ€” failed after ${r.rounds} rounds`);
1210
+ } else {
1211
+ lines.push(` โญ๏ธ Epic ${epicId} โ€” skipped`);
1212
+ }
1213
+ }
1214
+
1215
+ if (failed === 0) {
1216
+ lines.push('\n๐Ÿš€ All tests passed! Run *deploy when ready.');
1217
+ } else {
1218
+ lines.push(`\nโš ๏ธ ${failed} epic(s) failed. Review and fix manually, or run *test again.`);
1219
+ }
1220
+
835
1221
  this._phase = null;
836
- log.engine('Test phase complete');
837
- this.onComplete('test', '๐Ÿงช Testing complete! Check QA results.\n\nRun *deploy when ready.');
1222
+ this._testContext = null;
1223
+ log.engine(`Test phase complete: ${passed}/${total} passed, ${failed} failed`);
1224
+ this.onComplete('test', lines.join('\n'));
838
1225
  }
839
1226
 
840
1227
  // โ”€โ”€ Iterate (New Iteration) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -101,6 +101,13 @@ class Router {
101
101
  const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
102
102
  if (parseInt(focus.phase, 10) !== 3) return;
103
103
 
104
+ // Skip if dev phase already complete
105
+ const phase3Path = path.join(projectRoot, '.yuri', 'state', 'phase3.yaml');
106
+ if (fs.existsSync(phase3Path)) {
107
+ const phase3 = yaml.load(fs.readFileSync(phase3Path, 'utf8')) || {};
108
+ if (phase3.status === 'complete') return;
109
+ }
110
+
104
111
  // Check tmux session is alive
105
112
  const { execSync } = require('child_process');
106
113
  const sessions = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8' }).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "4.6.3",
3
+ "version": "4.6.5",
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": {
@@ -55,9 +55,9 @@ for f in "$STORIES_DIR"/*.md; do
55
55
  # Try heading format: "## Status" then next non-empty line
56
56
  status=$(awk '/^## Status/{found=1; next} found && /^[[:space:]]*$/{next} found{print; exit}' "$f" 2>/dev/null)
57
57
 
58
- # Fallback: inline "Status: XXX"
58
+ # Fallback: inline "status: Done" or "Status: Done" (case-insensitive)
59
59
  if [ -z "$status" ]; then
60
- status=$(grep -m1 -oE 'Status:\s*\S+' "$f" 2>/dev/null | sed 's/Status:\s*//')
60
+ status=$(grep -m1 -oiE 'status:\s*\S+' "$f" 2>/dev/null | sed 's/[Ss]tatus:\s*//')
61
61
  fi
62
62
 
63
63
  # Normalize to lowercase, strip whitespace