@open-code-review/cli 1.3.1 → 1.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/dist/index.js CHANGED
@@ -289,6 +289,28 @@ sessions/
289
289
  } catch {
290
290
  }
291
291
  }
292
+ const reviewersDir = join(ocrSkillsDest, "references", "reviewers");
293
+ const existingReviewers = /* @__PURE__ */ new Map();
294
+ const warnings = [];
295
+ if (existsSync(reviewersDir)) {
296
+ try {
297
+ const reviewerFiles = readdirSync(reviewersDir).filter(
298
+ (f) => f.endsWith(".md")
299
+ );
300
+ for (const file of reviewerFiles) {
301
+ const filePath = join(reviewersDir, file);
302
+ try {
303
+ existingReviewers.set(file, readFileSync(filePath));
304
+ } catch (err) {
305
+ const msg = err instanceof Error ? err.message : "unknown error";
306
+ warnings.push(`Could not read reviewer ${file}: ${msg}`);
307
+ }
308
+ }
309
+ } catch (err) {
310
+ const msg = err instanceof Error ? err.message : "unknown error";
311
+ warnings.push(`Could not read reviewers directory: ${msg}`);
312
+ }
313
+ }
292
314
  const skillsOk = copyDirSafe(ocrSkillsSource, ocrSkillsDest);
293
315
  if (!skillsOk) {
294
316
  return {
@@ -313,6 +335,17 @@ sessions/
313
335
  } catch {
314
336
  }
315
337
  }
338
+ if (existingReviewers.size > 0) {
339
+ ensureDir(reviewersDir);
340
+ for (const [file, content] of existingReviewers) {
341
+ try {
342
+ writeFileSync(join(reviewersDir, file), content);
343
+ } catch (err) {
344
+ const msg = err instanceof Error ? err.message : "unknown error";
345
+ warnings.push(`Could not restore reviewer ${file}: ${msg}`);
346
+ }
347
+ }
348
+ }
316
349
  const commandsOk = installCommandsForTool(tool, commandsSource, targetDir);
317
350
  if (!commandsOk) {
318
351
  return {
@@ -323,7 +356,8 @@ sessions/
323
356
  }
324
357
  return {
325
358
  tool,
326
- success: true
359
+ success: true,
360
+ warnings: warnings.length > 0 ? warnings : void 0
327
361
  };
328
362
  }
329
363
  function detectInstalledTools(targetDir, tools) {
@@ -357,9 +391,11 @@ Always open \`.ocr/skills/SKILL.md\` when the request:
357
391
  - Asks for code review, PR review, or feedback on changes
358
392
  - Mentions "review my code" or similar phrases
359
393
  - Wants multi-perspective analysis of code quality
394
+ - Asks to map, organize, or navigate a large changeset
360
395
 
361
396
  Use \`.ocr/skills/SKILL.md\` to learn:
362
397
  - How to run the 8-phase review workflow
398
+ - How to generate a Code Review Map for large changesets
363
399
  - Available reviewer personas and their focus areas
364
400
  - Session management and output format
365
401
 
@@ -545,6 +581,14 @@ var initCommand = new Command("init").description("Set up OCR for AI coding envi
545
581
  console.log(` ${chalk2.red("\u2717")} ${result.tool.name}: ${result.error}`);
546
582
  }
547
583
  }
584
+ const allWarnings = results.flatMap((r) => r.warnings ?? []);
585
+ if (allWarnings.length > 0) {
586
+ console.log();
587
+ console.log(chalk2.yellow("\u26A0 Warnings:"));
588
+ for (const warning of allWarnings) {
589
+ console.log(` ${chalk2.yellow("\u26A0")} ${warning}`);
590
+ }
591
+ }
548
592
  if (options.inject && successful.length > 0) {
549
593
  console.log();
550
594
  const injectSpinner = ora(
@@ -582,11 +626,11 @@ var initCommand = new Command("init").description("Set up OCR for AI coding envi
582
626
 
583
627
  // packages/cli/src/commands/progress.ts
584
628
  import { Command as Command2 } from "commander";
585
- import chalk4 from "chalk";
629
+ import chalk7 from "chalk";
586
630
  import { watch } from "chokidar";
587
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "node:fs";
588
- import { join as join5, basename } from "node:path";
589
- import logUpdate from "log-update";
631
+ import { existsSync as existsSync8, readdirSync as readdirSync5, statSync } from "node:fs";
632
+ import { join as join8, basename as basename3 } from "node:path";
633
+ import logUpdate4 from "log-update";
590
634
 
591
635
  // packages/cli/src/lib/guards.ts
592
636
  import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
@@ -641,30 +685,66 @@ function ensureSessionsDir(targetDir) {
641
685
  return sessionsDir;
642
686
  }
643
687
 
644
- // packages/cli/src/commands/progress.ts
645
- function debounce(fn, delay) {
646
- let timeoutId = null;
647
- return (...args) => {
648
- if (timeoutId) {
649
- clearTimeout(timeoutId);
688
+ // packages/cli/src/lib/progress/strategy.ts
689
+ var strategies = /* @__PURE__ */ new Map();
690
+ function registerStrategy(strategy) {
691
+ strategies.set(strategy.workflowType, strategy);
692
+ }
693
+ function getStrategy(workflowType) {
694
+ return strategies.get(workflowType);
695
+ }
696
+
697
+ // packages/cli/src/lib/progress/detector.ts
698
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "node:fs";
699
+ import { join as join5 } from "node:path";
700
+ function detectWorkflowType(sessionPath, explicitType) {
701
+ if (explicitType) {
702
+ return explicitType;
703
+ }
704
+ const statePath = join5(sessionPath, "state.json");
705
+ if (existsSync5(statePath)) {
706
+ try {
707
+ const content = readFileSync4(statePath, "utf-8");
708
+ const state = JSON.parse(content);
709
+ if (state.workflow_type) {
710
+ return state.workflow_type;
711
+ }
712
+ } catch {
650
713
  }
651
- timeoutId = setTimeout(() => {
652
- fn(...args);
653
- timeoutId = null;
654
- }, delay);
655
- };
714
+ }
715
+ const hasMapDir = existsSync5(join5(sessionPath, "map"));
716
+ const hasRoundsDir = existsSync5(join5(sessionPath, "rounds"));
717
+ if (hasMapDir && !hasRoundsDir) {
718
+ return "map";
719
+ }
720
+ if (hasRoundsDir && !hasMapDir) {
721
+ return "review";
722
+ }
723
+ if (hasMapDir && hasRoundsDir) {
724
+ if (existsSync5(statePath)) {
725
+ try {
726
+ const content = readFileSync4(statePath, "utf-8");
727
+ const state = JSON.parse(content);
728
+ const phase = state.current_phase;
729
+ if (phase.startsWith("map-") || phase === "topology" || phase === "flow-analysis" || phase === "requirements-mapping") {
730
+ return "map";
731
+ }
732
+ return "review";
733
+ } catch {
734
+ return "review";
735
+ }
736
+ }
737
+ }
738
+ return "review";
656
739
  }
657
- var lastRenderType = null;
658
- var lastLineCount = 0;
659
- var TOTAL_PHASES = 8;
660
740
  function isSessionActive(sessionPath) {
661
741
  const statePath = join5(sessionPath, "state.json");
662
742
  if (!existsSync5(statePath)) {
663
743
  return true;
664
744
  }
665
745
  try {
666
- const stateContent = readFileSync4(statePath, "utf-8");
667
- const state = JSON.parse(stateContent);
746
+ const content = readFileSync4(statePath, "utf-8");
747
+ const state = JSON.parse(content);
668
748
  if (state.status === "closed" || state.current_phase === "complete") {
669
749
  return false;
670
750
  }
@@ -673,27 +753,119 @@ function isSessionActive(sessionPath) {
673
753
  return true;
674
754
  }
675
755
  }
676
- function findLatestActiveSession(sessionsDir) {
677
- if (!existsSync5(sessionsDir)) {
678
- return null;
756
+ function detectActiveWorkflows(sessionPath) {
757
+ const activeWorkflows = [];
758
+ const hasRoundsDir = existsSync5(join5(sessionPath, "rounds"));
759
+ if (hasRoundsDir) {
760
+ const roundsDir = join5(sessionPath, "rounds");
761
+ const rounds = existsSync5(roundsDir) ? readdirSync2(roundsDir).filter((d) => d.match(/^round-\d+$/)).sort() : [];
762
+ if (rounds.length > 0) {
763
+ const latestRound = rounds[rounds.length - 1];
764
+ const finalPath = join5(roundsDir, latestRound, "final.md");
765
+ if (!existsSync5(finalPath)) {
766
+ activeWorkflows.push("review");
767
+ }
768
+ } else {
769
+ activeWorkflows.push("review");
770
+ }
679
771
  }
680
- const sessions = readdirSync2(sessionsDir).filter((name) => {
681
- const sessionPath = join5(sessionsDir, name);
682
- return statSync(sessionPath).isDirectory();
683
- }).sort().reverse();
684
- for (const session of sessions) {
685
- const sessionPath = join5(sessionsDir, session);
686
- if (isSessionActive(sessionPath)) {
687
- return session;
772
+ const hasMapDir = existsSync5(join5(sessionPath, "map"));
773
+ if (hasMapDir) {
774
+ const runsDir = join5(sessionPath, "map", "runs");
775
+ const runs = existsSync5(runsDir) ? readdirSync2(runsDir).filter((d) => d.match(/^run-\d+$/)).sort() : [];
776
+ if (runs.length > 0) {
777
+ const latestRun = runs[runs.length - 1];
778
+ const mapPath = join5(runsDir, latestRun, "map.md");
779
+ if (!existsSync5(mapPath)) {
780
+ activeWorkflows.push("map");
781
+ }
782
+ } else {
783
+ activeWorkflows.push("map");
688
784
  }
689
785
  }
690
- return null;
786
+ if (activeWorkflows.length === 0) {
787
+ const statePath = join5(sessionPath, "state.json");
788
+ if (existsSync5(statePath)) {
789
+ try {
790
+ const content = readFileSync4(statePath, "utf-8");
791
+ const state = JSON.parse(content);
792
+ if (state.workflow_type && state.current_phase !== "complete") {
793
+ activeWorkflows.push(state.workflow_type);
794
+ }
795
+ } catch {
796
+ }
797
+ }
798
+ }
799
+ return activeWorkflows;
800
+ }
801
+
802
+ // packages/cli/src/lib/progress/render-utils.ts
803
+ import chalk4 from "chalk";
804
+ import logUpdate from "log-update";
805
+ var lastRenderType = null;
806
+ var lastLineCount = 0;
807
+ function formatDuration(ms) {
808
+ const clampedMs = Math.max(0, ms);
809
+ const totalSeconds = Math.floor(clampedMs / 1e3);
810
+ const hours = Math.floor(totalSeconds / 3600);
811
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
812
+ const seconds = totalSeconds % 60;
813
+ if (hours > 0) {
814
+ return `${hours}h ${minutes}m ${seconds}s`;
815
+ } else if (minutes > 0) {
816
+ return `${minutes}m ${seconds}s`;
817
+ } else {
818
+ return `${seconds}s`;
819
+ }
820
+ }
821
+ function renderProgressBar(current, total, label) {
822
+ const width = 24;
823
+ const filled = Math.round(current / total * width);
824
+ const empty = width - filled;
825
+ const bar = chalk4.green("\u2501".repeat(filled)) + chalk4.dim("\u2500".repeat(empty));
826
+ const percent = Math.round(current / total * 100);
827
+ const percentStr = chalk4.bold.white(`${percent}%`);
828
+ return label ? `${bar} ${percentStr} ${chalk4.dim("\xB7")} ${chalk4.cyan(label)}` : `${bar} ${percentStr}`;
829
+ }
830
+ function getPhaseStatus(isComplete, isCurrent) {
831
+ if (isComplete) return chalk4.green("\u2713");
832
+ if (isCurrent) return chalk4.cyan("\u25B8");
833
+ return chalk4.dim("\xB7");
834
+ }
835
+ function clearForRenderType(renderType) {
836
+ if (lastRenderType !== renderType) {
837
+ logUpdate.clear();
838
+ }
839
+ lastRenderType = renderType;
840
+ }
841
+ function padLines(lines) {
842
+ while (lines.length < lastLineCount) {
843
+ lines.push("");
844
+ }
845
+ lastLineCount = lines.length;
846
+ return lines;
691
847
  }
848
+
849
+ // packages/cli/src/lib/progress/review-strategy.ts
850
+ import chalk5 from "chalk";
851
+ import logUpdate2 from "log-update";
852
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
853
+ import { join as join6, basename } from "node:path";
854
+ var REVIEW_PHASES = [
855
+ { key: "context", label: "Context Discovery" },
856
+ { key: "change-context", label: "Change Context" },
857
+ { key: "analysis", label: "Tech Lead Analysis" },
858
+ { key: "reviews", label: "Parallel Reviews" },
859
+ { key: "aggregation", label: "Aggregate Findings" },
860
+ { key: "discourse", label: "Reviewer Discourse" },
861
+ { key: "synthesis", label: "Final Synthesis" },
862
+ { key: "complete", label: "Complete" }
863
+ ];
692
864
  function countFindings(filePath) {
693
- if (!existsSync5(filePath)) {
865
+ if (!existsSync6(filePath)) {
694
866
  return 0;
695
867
  }
696
- const content = readFileSync4(filePath, "utf-8");
868
+ const content = readFileSync5(filePath, "utf-8");
697
869
  const findingMatches = content.match(/^##\s+(Finding|Issue|Suggestion)/gm);
698
870
  return findingMatches?.length ?? 0;
699
871
  }
@@ -706,346 +878,622 @@ function formatReviewerName(filename) {
706
878
  }
707
879
  return base.charAt(0).toUpperCase() + base.slice(1);
708
880
  }
709
- function parseSessionState(sessionPath, preservedStartTime) {
710
- const session = basename(sessionPath);
711
- const statePath = join5(sessionPath, "state.json");
712
- if (!existsSync5(statePath)) {
713
- return null;
714
- }
715
- try {
716
- const stateContent = readFileSync4(statePath, "utf-8");
717
- const state = JSON.parse(stateContent);
718
- return parseFromStateJson(session, state, sessionPath, preservedStartTime);
719
- } catch {
720
- return null;
721
- }
722
- }
723
- function parseFromStateJson(session, state, sessionPath, preservedStartTime) {
724
- const effectiveStartTime = state.round_started_at ?? state.started_at;
725
- const startTime = preservedStartTime ?? (effectiveStartTime ? new Date(effectiveStartTime).getTime() : Date.now());
726
- const roundsDir = join5(sessionPath, "rounds");
727
- const rounds = deriveRoundsFromFilesystem(roundsDir);
728
- const highestExistingRound = rounds.length > 0 ? Math.max(...rounds.map((r) => r.round)) : 1;
729
- const stateRound = state.current_round ?? 1;
730
- const currentRound = Math.min(stateRound, highestExistingRound);
731
- const currentRoundDir = join5(roundsDir, `round-${currentRound}`);
732
- const reviewsDir = join5(currentRoundDir, "reviews");
733
- const reviewers = [];
734
- if (existsSync5(reviewsDir)) {
735
- const entries = readdirSync2(reviewsDir);
736
- const reviewFiles = entries.filter((f) => f.endsWith(".md"));
737
- for (const file of reviewFiles) {
738
- const reviewPath = join5(reviewsDir, file);
739
- const findings = countFindings(reviewPath);
740
- reviewers.push({
741
- name: file.replace(".md", ""),
742
- displayName: formatReviewerName(file),
743
- status: "complete",
744
- findings
745
- });
746
- }
747
- }
748
- const contextComplete = existsSync5(
749
- join5(sessionPath, "discovered-standards.md")
750
- );
751
- const changeContextComplete = existsSync5(join5(sessionPath, "context.md"));
752
- const analysisComplete = changeContextComplete;
753
- const reviewsComplete = state.phase_number > 4;
754
- const discourseComplete = existsSync5(join5(currentRoundDir, "discourse.md"));
755
- const synthesisComplete = existsSync5(join5(currentRoundDir, "final.md"));
756
- return {
757
- session,
758
- phase: state.current_phase,
759
- phaseNumber: state.phase_number,
760
- totalPhases: TOTAL_PHASES,
761
- contextComplete,
762
- changeContextComplete,
763
- analysisComplete,
764
- reviewsComplete,
765
- aggregationComplete: reviewsComplete,
766
- // Aggregation is inline
767
- discourseComplete,
768
- synthesisComplete,
769
- currentRound,
770
- rounds,
771
- reviewers,
772
- startTime,
773
- complete: state.current_phase === "complete"
774
- };
775
- }
776
881
  function deriveRoundsFromFilesystem(roundsDir) {
777
- if (!existsSync5(roundsDir)) {
882
+ if (!existsSync6(roundsDir)) {
778
883
  return [];
779
884
  }
780
- const roundDirs = readdirSync2(roundsDir).filter((d) => d.match(/^round-\d+$/)).sort((a, b) => {
885
+ const roundDirs = readdirSync3(roundsDir).filter((d) => d.match(/^round-\d+$/)).sort((a, b) => {
781
886
  const numA = parseInt(a.replace("round-", ""));
782
887
  const numB = parseInt(b.replace("round-", ""));
783
888
  return numA - numB;
784
889
  });
785
890
  return roundDirs.map((dir) => {
786
891
  const roundNum = parseInt(dir.replace("round-", ""));
787
- const roundPath = join5(roundsDir, dir);
788
- const reviewsPath = join5(roundPath, "reviews");
789
- const finalPath = join5(roundPath, "final.md");
892
+ const roundPath = join6(roundsDir, dir);
893
+ const reviewsPath = join6(roundPath, "reviews");
894
+ const finalPath = join6(roundPath, "final.md");
790
895
  const reviewers = [];
791
- if (existsSync5(reviewsPath)) {
792
- const files = readdirSync2(reviewsPath).filter((f) => f.endsWith(".md"));
896
+ if (existsSync6(reviewsPath)) {
897
+ const files = readdirSync3(reviewsPath).filter((f) => f.endsWith(".md"));
793
898
  reviewers.push(...files.map((f) => f.replace(".md", "")));
794
899
  }
795
900
  return {
796
901
  round: roundNum,
797
- isComplete: existsSync5(finalPath),
902
+ isComplete: existsSync6(finalPath),
798
903
  reviewers
799
904
  };
800
905
  });
801
906
  }
802
- function formatDuration(ms) {
803
- const absMs = Math.abs(ms);
804
- const totalSeconds = Math.floor(absMs / 1e3);
805
- const hours = Math.floor(totalSeconds / 3600);
806
- const minutes = Math.floor(totalSeconds % 3600 / 60);
807
- const seconds = totalSeconds % 60;
808
- let duration;
809
- if (hours > 0) {
810
- duration = `${hours}h ${minutes}m ${seconds}s`;
811
- } else if (minutes > 0) {
812
- duration = `${minutes}m ${seconds}s`;
813
- } else {
814
- duration = `${seconds}s`;
907
+ var ReviewProgressStrategy = class {
908
+ workflowType = "review";
909
+ phases = REVIEW_PHASES;
910
+ totalPhases = 8;
911
+ parseState(sessionPath, preservedStartTime) {
912
+ const session = basename(sessionPath);
913
+ const statePath = join6(sessionPath, "state.json");
914
+ if (!existsSync6(statePath)) {
915
+ return null;
916
+ }
917
+ try {
918
+ const stateContent = readFileSync5(statePath, "utf-8");
919
+ const state = JSON.parse(stateContent);
920
+ return this.parseFromStateJson(
921
+ session,
922
+ state,
923
+ sessionPath,
924
+ preservedStartTime
925
+ );
926
+ } catch {
927
+ return null;
928
+ }
815
929
  }
816
- return duration;
817
- }
818
- var PHASE_INFO = [
819
- { key: "context", label: "Context Discovery" },
820
- { key: "change-context", label: "Change Context" },
821
- { key: "analysis", label: "Tech Lead Analysis" },
822
- { key: "reviews", label: "Parallel Reviews" },
823
- { key: "aggregation", label: "Aggregate Findings" },
824
- { key: "discourse", label: "Reviewer Discourse" },
825
- { key: "synthesis", label: "Final Synthesis" },
930
+ parseFromStateJson(session, state, sessionPath, preservedStartTime) {
931
+ const effectiveStartTime = state.round_started_at ?? state.started_at;
932
+ const startTime = preservedStartTime ?? (effectiveStartTime ? new Date(effectiveStartTime).getTime() : Date.now());
933
+ const roundsDir = join6(sessionPath, "rounds");
934
+ const rounds = deriveRoundsFromFilesystem(roundsDir);
935
+ const highestExistingRound = rounds.length > 0 ? Math.max(...rounds.map((r) => r.round)) : 1;
936
+ const stateRound = state.current_round ?? 1;
937
+ const currentRound = Math.min(stateRound, highestExistingRound);
938
+ const currentRoundDir = join6(roundsDir, `round-${currentRound}`);
939
+ const reviewsDir = join6(currentRoundDir, "reviews");
940
+ const reviewers = [];
941
+ if (existsSync6(reviewsDir)) {
942
+ const entries = readdirSync3(reviewsDir);
943
+ const reviewFiles = entries.filter((f) => f.endsWith(".md"));
944
+ for (const file of reviewFiles) {
945
+ const reviewPath = join6(reviewsDir, file);
946
+ const findings = countFindings(reviewPath);
947
+ reviewers.push({
948
+ name: file.replace(".md", ""),
949
+ displayName: formatReviewerName(file),
950
+ status: "complete",
951
+ findings
952
+ });
953
+ }
954
+ }
955
+ const contextComplete = existsSync6(
956
+ join6(sessionPath, "discovered-standards.md")
957
+ );
958
+ const changeContextComplete = existsSync6(join6(sessionPath, "context.md"));
959
+ const analysisComplete = changeContextComplete;
960
+ const reviewsComplete = state.phase_number > 4;
961
+ const discourseComplete = existsSync6(join6(currentRoundDir, "discourse.md"));
962
+ const synthesisComplete = existsSync6(join6(currentRoundDir, "final.md"));
963
+ return {
964
+ workflowType: "review",
965
+ session,
966
+ phase: state.current_phase,
967
+ phaseNumber: state.phase_number,
968
+ totalPhases: this.totalPhases,
969
+ contextComplete,
970
+ changeContextComplete,
971
+ analysisComplete,
972
+ reviewsComplete,
973
+ aggregationComplete: reviewsComplete,
974
+ discourseComplete,
975
+ synthesisComplete,
976
+ currentRound,
977
+ rounds,
978
+ reviewers,
979
+ startTime,
980
+ complete: state.current_phase === "complete"
981
+ };
982
+ }
983
+ render(state) {
984
+ const lines = [];
985
+ const log = (line = "") => lines.push(line);
986
+ log();
987
+ log(chalk5.bold.white(" Open Code Review"));
988
+ log();
989
+ const elapsed = Math.max(0, Date.now() - state.startTime);
990
+ const roundInfo = state.currentRound > 1 ? chalk5.cyan(` Round ${state.currentRound}`) + chalk5.dim(" \xB7 ") : "";
991
+ log(
992
+ chalk5.dim(" ") + chalk5.white(state.session) + chalk5.dim(" \xB7 ") + roundInfo + chalk5.white(formatDuration(elapsed))
993
+ );
994
+ log();
995
+ const progressPhases = state.complete ? 8 : state.phaseNumber;
996
+ const currentPhase = this.phases.find((p) => p.key === state.phase);
997
+ const phaseLabel = state.complete ? "Done" : currentPhase?.label;
998
+ log(` ${renderProgressBar(progressPhases, 8, phaseLabel)}`);
999
+ log();
1000
+ const phaseCompletion = {
1001
+ context: state.contextComplete,
1002
+ "change-context": state.changeContextComplete,
1003
+ analysis: state.analysisComplete,
1004
+ reviews: state.reviewsComplete,
1005
+ aggregation: state.aggregationComplete,
1006
+ discourse: state.discourseComplete,
1007
+ synthesis: state.synthesisComplete,
1008
+ complete: state.complete
1009
+ };
1010
+ for (const phase of this.phases) {
1011
+ const isComplete = phaseCompletion[phase.key] ?? false;
1012
+ const isCurrent = state.phase === phase.key && !state.complete;
1013
+ const status = getPhaseStatus(isComplete, isCurrent);
1014
+ let label;
1015
+ if (isCurrent) {
1016
+ label = chalk5.cyan.bold(phase.label);
1017
+ } else if (isComplete) {
1018
+ label = chalk5.white(phase.label);
1019
+ } else {
1020
+ label = chalk5.dim(phase.label);
1021
+ }
1022
+ log(` ${status} ${label}`);
1023
+ if (phase.key === "reviews" && state.reviewers.length > 0) {
1024
+ if (state.currentRound > 1) {
1025
+ log(chalk5.dim(" ") + chalk5.cyan(`Round ${state.currentRound}`));
1026
+ }
1027
+ const reviewerLine = state.reviewers.map((r) => {
1028
+ const icon = r.status === "complete" ? chalk5.green("\u2713") : chalk5.dim("\u25CB");
1029
+ const name = chalk5.dim(r.displayName);
1030
+ const count = r.findings > 0 ? chalk5.cyan(` ${r.findings}`) : chalk5.dim(" 0");
1031
+ return `${icon} ${name}${count}`;
1032
+ }).join(chalk5.dim(" \u2502 "));
1033
+ log(chalk5.dim(" ") + reviewerLine);
1034
+ if (state.rounds.length > 1) {
1035
+ const prevRounds = state.rounds.slice(0, -1);
1036
+ for (const round of prevRounds) {
1037
+ const roundLabel = chalk5.dim(` Round ${round.round}`);
1038
+ const reviewerCount = round.reviewers.length;
1039
+ const rstatus = round.isComplete ? chalk5.green("\u2713") : chalk5.dim("\u25CB");
1040
+ log(
1041
+ `${roundLabel} ${rstatus} ${chalk5.dim(`${reviewerCount} reviewers`)}`
1042
+ );
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ log();
1048
+ if (state.complete) {
1049
+ const totalFindings = state.reviewers.reduce(
1050
+ (sum, r) => sum + r.findings,
1051
+ 0
1052
+ );
1053
+ log(
1054
+ chalk5.green.bold(" \u2713 Complete") + chalk5.dim(" \xB7 ") + chalk5.white(
1055
+ `${totalFindings} finding${totalFindings !== 1 ? "s" : ""}`
1056
+ )
1057
+ );
1058
+ log(
1059
+ chalk5.dim(" ") + chalk5.dim("\u2192 ") + chalk5.white(
1060
+ `.ocr/sessions/${state.session}/rounds/round-${state.currentRound}/final.md`
1061
+ )
1062
+ );
1063
+ } else {
1064
+ log(chalk5.dim(" Ctrl+C to exit"));
1065
+ }
1066
+ log();
1067
+ clearForRenderType("review-progress");
1068
+ logUpdate2(padLines(lines).join("\n"));
1069
+ }
1070
+ renderWaiting() {
1071
+ const lines = [];
1072
+ const log = (line = "") => lines.push(line);
1073
+ log();
1074
+ log(chalk5.bold.white(" Open Code Review"));
1075
+ log();
1076
+ log(chalk5.dim(" Waiting for session..."));
1077
+ log();
1078
+ const bar = chalk5.dim("\u2500".repeat(24));
1079
+ log(` ${bar} ${chalk5.dim("0%")}`);
1080
+ log();
1081
+ log(
1082
+ chalk5.dim(" Run ") + chalk5.white("/ocr-review") + chalk5.dim(" to start")
1083
+ );
1084
+ log();
1085
+ log(chalk5.dim(" Ctrl+C to exit"));
1086
+ log();
1087
+ clearForRenderType("review-waiting");
1088
+ logUpdate2(padLines(lines).join("\n"));
1089
+ }
1090
+ };
1091
+ var reviewStrategy = new ReviewProgressStrategy();
1092
+
1093
+ // packages/cli/src/lib/progress/map-strategy.ts
1094
+ import chalk6 from "chalk";
1095
+ import logUpdate3 from "log-update";
1096
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
1097
+ import { join as join7, basename as basename2 } from "node:path";
1098
+ var MAP_PHASES = [
1099
+ { key: "map-context", label: "Context Discovery" },
1100
+ { key: "topology", label: "Topology Analysis" },
1101
+ { key: "flow-analysis", label: "Flow Tracing" },
1102
+ { key: "requirements-mapping", label: "Requirements Mapping" },
1103
+ { key: "synthesis", label: "Map Synthesis" },
826
1104
  { key: "complete", label: "Complete" }
827
1105
  ];
828
- function getPhaseStatus(isComplete, isCurrent) {
829
- if (isComplete) return chalk4.green("\u2713");
830
- if (isCurrent) return chalk4.cyan("\u25B8");
831
- return chalk4.dim("\xB7");
832
- }
833
- function renderProgressBar(current, total, label) {
834
- const width = 24;
835
- const filled = Math.round(current / total * width);
836
- const empty = width - filled;
837
- const bar = chalk4.green("\u2501".repeat(filled)) + chalk4.dim("\u2500".repeat(empty));
838
- const percent = Math.round(current / total * 100);
839
- const percentStr = chalk4.bold.white(`${percent}%`);
840
- return label ? `${bar} ${percentStr} ${chalk4.dim("\xB7")} ${chalk4.cyan(label)}` : `${bar} ${percentStr}`;
1106
+ function deriveRunsFromFilesystem(mapDir) {
1107
+ const runsDir = join7(mapDir, "runs");
1108
+ if (!existsSync7(runsDir)) {
1109
+ return [];
1110
+ }
1111
+ const runDirs = readdirSync4(runsDir).filter((d) => d.match(/^run-\d+$/)).sort((a, b) => {
1112
+ const numA = parseInt(a.replace("run-", ""));
1113
+ const numB = parseInt(b.replace("run-", ""));
1114
+ return numA - numB;
1115
+ });
1116
+ return runDirs.map((dir) => {
1117
+ const runNum = parseInt(dir.replace("run-", ""));
1118
+ const runPath = join7(runsDir, dir);
1119
+ const mapPath = join7(runPath, "map.md");
1120
+ let fileCount = 0;
1121
+ const topologyPath = join7(runPath, "topology.md");
1122
+ if (existsSync7(topologyPath)) {
1123
+ const content = readFileSync6(topologyPath, "utf-8");
1124
+ const fileListMatch = content.match(
1125
+ /## Canonical File List[\s\S]*?```([\s\S]*?)```/
1126
+ );
1127
+ if (fileListMatch && fileListMatch[1]) {
1128
+ fileCount = fileListMatch[1].trim().split("\n").filter(Boolean).length;
1129
+ }
1130
+ }
1131
+ return {
1132
+ run: runNum,
1133
+ isComplete: existsSync7(mapPath),
1134
+ fileCount
1135
+ };
1136
+ });
841
1137
  }
842
- function renderProgress(state) {
843
- const lines = [];
844
- const log = (line = "") => lines.push(line);
845
- log();
846
- log(chalk4.bold.white(" Open Code Review"));
847
- log();
848
- const elapsed = Date.now() - state.startTime;
849
- const roundInfo = state.currentRound > 1 ? chalk4.cyan(` Round ${state.currentRound}`) + chalk4.dim(" \xB7 ") : "";
850
- log(
851
- chalk4.dim(" ") + chalk4.white(state.session) + chalk4.dim(" \xB7 ") + roundInfo + chalk4.white(formatDuration(elapsed))
852
- );
853
- log();
854
- const progressPhases = state.complete ? 8 : state.phaseNumber;
855
- const currentPhase = PHASE_INFO.find((p) => p.key === state.phase);
856
- const phaseLabel = state.complete ? "Done" : currentPhase?.label;
857
- log(` ${renderProgressBar(progressPhases, 8, phaseLabel)}`);
858
- log();
859
- const phaseCompletion = {
860
- waiting: false,
861
- context: state.contextComplete,
862
- "change-context": state.changeContextComplete,
863
- analysis: state.analysisComplete,
864
- reviews: state.reviewsComplete,
865
- aggregation: state.aggregationComplete,
866
- discourse: state.discourseComplete,
867
- synthesis: state.synthesisComplete,
868
- complete: state.complete
869
- };
870
- for (const phase of PHASE_INFO) {
871
- const isComplete = phaseCompletion[phase.key];
872
- const isCurrent = state.phase === phase.key && !state.complete;
873
- const status = getPhaseStatus(isComplete, isCurrent);
874
- let label;
875
- if (isCurrent) {
876
- label = chalk4.cyan.bold(phase.label);
877
- } else if (isComplete) {
878
- label = chalk4.white(phase.label);
879
- } else {
880
- label = chalk4.dim(phase.label);
1138
+ var MapProgressStrategy = class {
1139
+ workflowType = "map";
1140
+ phases = MAP_PHASES;
1141
+ totalPhases = 6;
1142
+ parseState(sessionPath, preservedStartTime) {
1143
+ const session = basename2(sessionPath);
1144
+ const statePath = join7(sessionPath, "state.json");
1145
+ if (!existsSync7(statePath)) {
1146
+ return null;
881
1147
  }
882
- log(` ${status} ${label}`);
883
- if (phase.key === "reviews" && state.reviewers.length > 0) {
884
- if (state.currentRound > 1) {
885
- log(chalk4.dim(" ") + chalk4.cyan(`Round ${state.currentRound}`));
886
- }
887
- const reviewerLine = state.reviewers.map((r) => {
888
- const icon = r.status === "complete" ? chalk4.green("\u2713") : chalk4.dim("\u25CB");
889
- const name = chalk4.dim(r.displayName);
890
- const count = r.findings > 0 ? chalk4.cyan(` ${r.findings}`) : chalk4.dim(" 0");
891
- return `${icon} ${name}${count}`;
892
- }).join(chalk4.dim(" \u2502 "));
893
- log(chalk4.dim(" ") + reviewerLine);
894
- if (state.rounds.length > 1) {
895
- const prevRounds = state.rounds.slice(0, -1);
896
- for (const round of prevRounds) {
897
- const roundLabel = chalk4.dim(` Round ${round.round}`);
898
- const reviewerCount = round.reviewers.length;
899
- const status2 = round.isComplete ? chalk4.green("\u2713") : chalk4.dim("\u25CB");
900
- log(
901
- `${roundLabel} ${status2} ${chalk4.dim(`${reviewerCount} reviewers`)}`
902
- );
903
- }
1148
+ try {
1149
+ const stateContent = readFileSync6(statePath, "utf-8");
1150
+ const state = JSON.parse(stateContent);
1151
+ if (state.workflow_type !== "map") {
1152
+ return null;
904
1153
  }
1154
+ return this.parseFromStateJson(
1155
+ session,
1156
+ state,
1157
+ sessionPath,
1158
+ preservedStartTime
1159
+ );
1160
+ } catch {
1161
+ return null;
905
1162
  }
906
1163
  }
907
- log();
908
- if (state.complete) {
909
- const totalFindings = state.reviewers.reduce(
910
- (sum, r) => sum + r.findings,
911
- 0
1164
+ parseFromStateJson(session, state, sessionPath, preservedStartTime) {
1165
+ const effectiveStartTime = state.map_started_at ?? state.started_at;
1166
+ const startTime = preservedStartTime ?? (effectiveStartTime ? new Date(effectiveStartTime).getTime() : Date.now());
1167
+ const mapDir = join7(sessionPath, "map");
1168
+ const runs = deriveRunsFromFilesystem(mapDir);
1169
+ const highestExistingRun = runs.length > 0 ? Math.max(...runs.map((r) => r.run)) : 1;
1170
+ const stateRun = state.current_map_run ?? 1;
1171
+ const currentRun = Math.min(stateRun, highestExistingRun);
1172
+ const currentRunDir = join7(mapDir, "runs", `run-${currentRun}`);
1173
+ const contextComplete = existsSync7(
1174
+ join7(sessionPath, "discovered-standards.md")
912
1175
  );
913
- log(
914
- chalk4.green.bold(" \u2713 Complete") + chalk4.dim(" \xB7 ") + chalk4.white(
915
- `${totalFindings} finding${totalFindings !== 1 ? "s" : ""}`
916
- )
1176
+ const topologyComplete = existsSync7(join7(currentRunDir, "topology.md"));
1177
+ const flowAnalysisComplete = existsSync7(
1178
+ join7(currentRunDir, "flow-analysis.md")
917
1179
  );
918
- log(
919
- chalk4.dim(" ") + chalk4.dim("\u2192 ") + chalk4.white(
920
- `.ocr/sessions/${state.session}/rounds/round-${state.currentRound}/final.md`
921
- )
1180
+ const requirementsMappingComplete = existsSync7(
1181
+ join7(currentRunDir, "requirements-mapping.md")
922
1182
  );
923
- } else {
924
- log(chalk4.dim(" Ctrl+C to exit"));
1183
+ const synthesisComplete = existsSync7(join7(currentRunDir, "map.md"));
1184
+ const hasRequirements = existsSync7(join7(sessionPath, "requirements.md"));
1185
+ const flowAnalysts = flowAnalysisComplete ? [
1186
+ {
1187
+ name: "flow-analyst",
1188
+ displayName: "Flow Analysts",
1189
+ status: "complete"
1190
+ }
1191
+ ] : [];
1192
+ const requirementsMappers = requirementsMappingComplete && hasRequirements ? [
1193
+ {
1194
+ name: "req-mapper",
1195
+ displayName: "Requirements Mappers",
1196
+ status: "complete"
1197
+ }
1198
+ ] : [];
1199
+ return {
1200
+ workflowType: "map",
1201
+ session,
1202
+ phase: state.current_phase,
1203
+ phaseNumber: state.phase_number,
1204
+ totalPhases: this.totalPhases,
1205
+ contextComplete,
1206
+ topologyComplete,
1207
+ flowAnalysisComplete,
1208
+ requirementsMappingComplete,
1209
+ synthesisComplete,
1210
+ currentRun,
1211
+ runs,
1212
+ flowAnalysts,
1213
+ requirementsMappers,
1214
+ hasRequirements,
1215
+ startTime,
1216
+ complete: state.current_phase === "complete"
1217
+ };
925
1218
  }
926
- log();
927
- if (lastRenderType !== "progress") {
928
- logUpdate.clear();
1219
+ render(state) {
1220
+ const lines = [];
1221
+ const log = (line = "") => lines.push(line);
1222
+ log();
1223
+ log(chalk6.bold.white(" Open Code Review") + chalk6.cyan(" \xB7 Map"));
1224
+ log();
1225
+ const elapsed = Math.max(0, Date.now() - state.startTime);
1226
+ const runInfo = state.currentRun > 1 ? chalk6.cyan(` Run ${state.currentRun}`) + chalk6.dim(" \xB7 ") : "";
1227
+ log(
1228
+ chalk6.dim(" ") + chalk6.white(state.session) + chalk6.dim(" \xB7 ") + runInfo + chalk6.white(formatDuration(elapsed))
1229
+ );
1230
+ log();
1231
+ const currentRunInfo = state.runs.find((r) => r.run === state.currentRun);
1232
+ if (currentRunInfo && currentRunInfo.fileCount > 0) {
1233
+ log(
1234
+ chalk6.dim(" ") + chalk6.white(`${currentRunInfo.fileCount} files`) + chalk6.dim(" in changeset")
1235
+ );
1236
+ log();
1237
+ }
1238
+ const progressPhases = state.complete ? 6 : state.phaseNumber;
1239
+ const currentPhase = this.phases.find((p) => p.key === state.phase);
1240
+ const phaseLabel = state.complete ? "Done" : currentPhase?.label;
1241
+ log(` ${renderProgressBar(progressPhases, 6, phaseLabel)}`);
1242
+ log();
1243
+ const phaseCompletion = {
1244
+ "map-context": state.contextComplete,
1245
+ topology: state.topologyComplete,
1246
+ "flow-analysis": state.flowAnalysisComplete,
1247
+ "requirements-mapping": state.requirementsMappingComplete,
1248
+ synthesis: state.synthesisComplete,
1249
+ complete: state.complete
1250
+ };
1251
+ for (const phase of this.phases) {
1252
+ if (phase.key === "requirements-mapping" && !state.hasRequirements) {
1253
+ continue;
1254
+ }
1255
+ const isComplete = phaseCompletion[phase.key] ?? false;
1256
+ const isCurrent = state.phase === phase.key && !state.complete;
1257
+ const status = getPhaseStatus(isComplete, isCurrent);
1258
+ let label;
1259
+ if (isCurrent) {
1260
+ label = chalk6.cyan.bold(phase.label);
1261
+ } else if (isComplete) {
1262
+ label = chalk6.white(phase.label);
1263
+ } else {
1264
+ label = chalk6.dim(phase.label);
1265
+ }
1266
+ log(` ${status} ${label}`);
1267
+ if (phase.key === "flow-analysis" && state.flowAnalysts.length > 0) {
1268
+ const agentLine = state.flowAnalysts.map((a) => {
1269
+ const icon = a.status === "complete" ? chalk6.green("\u2713") : chalk6.dim("\u25CB");
1270
+ return `${icon} ${chalk6.dim(a.displayName)}`;
1271
+ }).join(chalk6.dim(" \u2502 "));
1272
+ log(chalk6.dim(" ") + agentLine);
1273
+ }
1274
+ if (phase.key === "requirements-mapping" && state.requirementsMappers.length > 0) {
1275
+ const agentLine = state.requirementsMappers.map((a) => {
1276
+ const icon = a.status === "complete" ? chalk6.green("\u2713") : chalk6.dim("\u25CB");
1277
+ return `${icon} ${chalk6.dim(a.displayName)}`;
1278
+ }).join(chalk6.dim(" \u2502 "));
1279
+ log(chalk6.dim(" ") + agentLine);
1280
+ }
1281
+ }
1282
+ log();
1283
+ if (state.complete) {
1284
+ log(chalk6.green.bold(" \u2713 Map Complete"));
1285
+ log(
1286
+ chalk6.dim(" ") + chalk6.dim("\u2192 ") + chalk6.white(
1287
+ `.ocr/sessions/${state.session}/map/runs/run-${state.currentRun}/map.md`
1288
+ )
1289
+ );
1290
+ } else {
1291
+ log(chalk6.dim(" Ctrl+C to exit"));
1292
+ }
1293
+ log();
1294
+ clearForRenderType("map-progress");
1295
+ logUpdate3(padLines(lines).join("\n"));
929
1296
  }
930
- lastRenderType = "progress";
931
- while (lines.length < lastLineCount) {
932
- lines.push("");
1297
+ renderWaiting() {
1298
+ const lines = [];
1299
+ const log = (line = "") => lines.push(line);
1300
+ log();
1301
+ log(chalk6.bold.white(" Open Code Review") + chalk6.cyan(" \xB7 Map"));
1302
+ log();
1303
+ log(chalk6.dim(" Waiting for session..."));
1304
+ log();
1305
+ const bar = chalk6.dim("\u2500".repeat(24));
1306
+ log(` ${bar} ${chalk6.dim("0%")}`);
1307
+ log();
1308
+ log(chalk6.dim(" Run ") + chalk6.white("/ocr-map") + chalk6.dim(" to start"));
1309
+ log();
1310
+ log(chalk6.dim(" Ctrl+C to exit"));
1311
+ log();
1312
+ clearForRenderType("map-waiting");
1313
+ logUpdate3(padLines(lines).join("\n"));
933
1314
  }
934
- lastLineCount = lines.length;
935
- logUpdate(lines.join("\n"));
1315
+ };
1316
+ var mapStrategy = new MapProgressStrategy();
1317
+
1318
+ // packages/cli/src/lib/progress/index.ts
1319
+ registerStrategy(reviewStrategy);
1320
+ registerStrategy(mapStrategy);
1321
+
1322
+ // packages/cli/src/commands/progress.ts
1323
+ function debounce(fn, delay) {
1324
+ let timeoutId = null;
1325
+ return (...args) => {
1326
+ if (timeoutId) {
1327
+ clearTimeout(timeoutId);
1328
+ }
1329
+ timeoutId = setTimeout(() => {
1330
+ fn(...args);
1331
+ timeoutId = null;
1332
+ }, delay);
1333
+ };
936
1334
  }
937
- function renderWaiting() {
938
- const lines = [];
939
- const log = (line = "") => lines.push(line);
940
- log();
941
- log(chalk4.bold.white(" Open Code Review"));
942
- log();
943
- log(chalk4.dim(" Waiting for session..."));
944
- log();
945
- const bar = chalk4.dim("\u2500".repeat(24));
946
- log(` ${bar} ${chalk4.dim("0%")}`);
947
- log();
948
- log(
949
- chalk4.dim(" Run ") + chalk4.white("/ocr-review") + chalk4.dim(" to start")
950
- );
951
- log();
952
- log(chalk4.dim(" Ctrl+C to exit"));
953
- log();
954
- if (lastRenderType !== "waiting") {
955
- logUpdate.clear();
1335
+ function findLatestActiveSession(sessionsDir) {
1336
+ if (!existsSync8(sessionsDir)) {
1337
+ return null;
956
1338
  }
957
- lastRenderType = "waiting";
958
- while (lines.length < lastLineCount) {
959
- lines.push("");
1339
+ const sessions = readdirSync5(sessionsDir).filter((name) => {
1340
+ const sessionPath = join8(sessionsDir, name);
1341
+ return statSync(sessionPath).isDirectory();
1342
+ }).sort().reverse();
1343
+ for (const session of sessions) {
1344
+ const sessionPath = join8(sessionsDir, session);
1345
+ if (isSessionActive(sessionPath)) {
1346
+ return session;
1347
+ }
960
1348
  }
961
- lastLineCount = lines.length;
962
- logUpdate(lines.join("\n"));
1349
+ return null;
963
1350
  }
964
- var progressCommand = new Command2("progress").description("Watch real-time progress of a code review session").option("-s, --session <name>", "Specify session name").action(async (options) => {
1351
+ function getStrategyForSession(sessionPath, explicitWorkflow) {
1352
+ const workflowType = detectWorkflowType(sessionPath, explicitWorkflow);
1353
+ if (!workflowType) {
1354
+ return null;
1355
+ }
1356
+ return getStrategy(workflowType) ?? null;
1357
+ }
1358
+ var progressCommand = new Command2("progress").description("Watch real-time progress of a code review or map session").option("-s, --session <name>", "Specify session name").option(
1359
+ "-w, --workflow <type>",
1360
+ "Specify workflow type (review or map)",
1361
+ (value) => {
1362
+ if (value !== "review" && value !== "map") {
1363
+ throw new Error(
1364
+ `Invalid workflow type: ${value}. Use 'review' or 'map'.`
1365
+ );
1366
+ }
1367
+ return value;
1368
+ }
1369
+ ).action(async (options) => {
965
1370
  const targetDir = process.cwd();
966
1371
  requireOcrSetup(targetDir);
967
1372
  const sessionsDir = ensureSessionsDir(targetDir);
968
- const ocrDir = join5(targetDir, ".ocr");
1373
+ const ocrDir = join8(targetDir, ".ocr");
969
1374
  if (options.session) {
970
- const sessionPath = join5(sessionsDir, options.session);
971
- if (!existsSync5(sessionPath)) {
972
- console.log(chalk4.red(`Session not found: ${options.session}`));
1375
+ const sessionPath = join8(sessionsDir, options.session);
1376
+ if (!existsSync8(sessionPath)) {
1377
+ console.log(chalk7.red(`Session not found: ${options.session}`));
1378
+ process.exit(1);
1379
+ }
1380
+ const strategy = getStrategyForSession(sessionPath, options.workflow);
1381
+ if (!strategy) {
1382
+ console.log(
1383
+ chalk7.red(
1384
+ `Cannot determine workflow type for session ${options.session}`
1385
+ )
1386
+ );
1387
+ console.log(
1388
+ chalk7.dim(`Try specifying --workflow review or --workflow map`)
1389
+ );
973
1390
  process.exit(1);
974
1391
  }
975
- let state = parseSessionState(sessionPath);
1392
+ let state = strategy.parseState(sessionPath);
976
1393
  if (!state) {
977
1394
  console.log(
978
- chalk4.red(
1395
+ chalk7.red(
979
1396
  `Session ${options.session} has no state.json - cannot track progress`
980
1397
  )
981
1398
  );
982
1399
  console.log(
983
- chalk4.dim(
1400
+ chalk7.dim(
984
1401
  `The orchestrating agent must create state.json for progress tracking.`
985
1402
  )
986
1403
  );
987
1404
  process.exit(1);
988
1405
  }
989
- let preservedStartTime2 = state.startTime;
990
- renderProgress(state);
1406
+ let preservedStartTime = state.startTime;
1407
+ strategy.render(state);
991
1408
  const timerInterval2 = setInterval(() => {
992
- const newState = parseSessionState(sessionPath, preservedStartTime2);
1409
+ const newState = strategy.parseState(sessionPath, preservedStartTime);
993
1410
  if (newState) {
994
1411
  state = newState;
995
- renderProgress(state);
1412
+ strategy.render(state);
996
1413
  }
997
1414
  }, 1e3);
998
1415
  const watcher = watch(sessionPath, {
999
1416
  persistent: true,
1000
1417
  ignoreInitial: true,
1001
- depth: 3
1418
+ depth: 4
1419
+ // map/runs/run-{n}/*.md
1002
1420
  });
1003
1421
  watcher.on("all", () => {
1004
- const newState = parseSessionState(sessionPath, preservedStartTime2);
1422
+ const newState = strategy.parseState(sessionPath, preservedStartTime);
1005
1423
  if (newState) {
1006
1424
  state = newState;
1007
- renderProgress(state);
1425
+ strategy.render(state);
1008
1426
  }
1009
1427
  });
1010
1428
  process.on("SIGINT", () => {
1011
1429
  clearInterval(timerInterval2);
1012
1430
  watcher.close();
1013
- logUpdate.done();
1431
+ logUpdate4.done();
1014
1432
  process.exit(0);
1015
1433
  });
1016
1434
  return;
1017
1435
  }
1018
1436
  let currentSession = findLatestActiveSession(sessionsDir);
1019
- let currentSessionPath = currentSession ? join5(sessionsDir, currentSession) : null;
1437
+ let currentSessionPath = currentSession ? join8(sessionsDir, currentSession) : null;
1020
1438
  let sessionWatcher = null;
1021
- let preservedStartTime;
1439
+ const preservedStartTimes = {
1440
+ review: void 0,
1441
+ map: void 0
1442
+ };
1443
+ let currentStrategy = null;
1022
1444
  const updateDisplayImpl = () => {
1023
- if (!currentSessionPath || !existsSync5(currentSessionPath) || !isSessionActive(currentSessionPath)) {
1445
+ if (!currentSessionPath || !existsSync8(currentSessionPath) || !isSessionActive(currentSessionPath)) {
1024
1446
  const latestActive = findLatestActiveSession(sessionsDir);
1025
1447
  if (latestActive && latestActive !== currentSession) {
1026
1448
  currentSession = latestActive;
1027
- currentSessionPath = join5(sessionsDir, latestActive);
1028
- preservedStartTime = void 0;
1449
+ currentSessionPath = join8(sessionsDir, latestActive);
1450
+ preservedStartTimes.review = void 0;
1451
+ preservedStartTimes.map = void 0;
1452
+ currentStrategy = null;
1029
1453
  watchSession(currentSessionPath);
1030
1454
  } else if (!latestActive) {
1031
1455
  currentSession = null;
1032
1456
  currentSessionPath = null;
1033
- preservedStartTime = void 0;
1457
+ preservedStartTimes.review = void 0;
1458
+ preservedStartTimes.map = void 0;
1459
+ currentStrategy = null;
1034
1460
  }
1035
1461
  }
1036
- if (currentSessionPath && existsSync5(currentSessionPath)) {
1037
- const state = parseSessionState(currentSessionPath, preservedStartTime);
1038
- if (state) {
1039
- if (!preservedStartTime) {
1040
- preservedStartTime = state.startTime;
1462
+ if (currentSessionPath && existsSync8(currentSessionPath)) {
1463
+ if (!options.workflow) {
1464
+ const activeWorkflows = detectActiveWorkflows(currentSessionPath);
1465
+ if (activeWorkflows.length > 1) {
1466
+ renderCombinedProgress(currentSessionPath, preservedStartTimes);
1467
+ return;
1468
+ }
1469
+ }
1470
+ if (!currentStrategy) {
1471
+ currentStrategy = getStrategyForSession(
1472
+ currentSessionPath,
1473
+ options.workflow
1474
+ );
1475
+ }
1476
+ if (currentStrategy) {
1477
+ const workflowType = currentStrategy.workflowType;
1478
+ const state = currentStrategy.parseState(
1479
+ currentSessionPath,
1480
+ preservedStartTimes[workflowType]
1481
+ );
1482
+ if (state) {
1483
+ if (!preservedStartTimes[workflowType]) {
1484
+ preservedStartTimes[workflowType] = state.startTime;
1485
+ }
1486
+ currentStrategy.render(state);
1487
+ } else {
1488
+ currentStrategy.renderWaiting();
1041
1489
  }
1042
- renderProgress(state);
1043
1490
  } else {
1044
- renderWaiting();
1491
+ renderGenericWaiting();
1045
1492
  }
1046
1493
  } else {
1047
- preservedStartTime = void 0;
1048
- renderWaiting();
1494
+ preservedStartTimes.review = void 0;
1495
+ preservedStartTimes.map = void 0;
1496
+ renderGenericWaiting();
1049
1497
  }
1050
1498
  };
1051
1499
  const updateDisplay = debounce(updateDisplayImpl, 50);
@@ -1056,7 +1504,7 @@ var progressCommand = new Command2("progress").description("Watch real-time prog
1056
1504
  sessionWatcher = watch(sessionPath, {
1057
1505
  persistent: true,
1058
1506
  ignoreInitial: true,
1059
- depth: 3
1507
+ depth: 4
1060
1508
  });
1061
1509
  sessionWatcher.on("all", updateDisplay);
1062
1510
  };
@@ -1065,20 +1513,22 @@ var progressCommand = new Command2("progress").description("Watch real-time prog
1065
1513
  watchSession(currentSessionPath);
1066
1514
  }
1067
1515
  const timerInterval = setInterval(updateDisplay, 1e3);
1068
- const watchDir = existsSync5(ocrDir) ? ocrDir : targetDir;
1516
+ const watchDir = existsSync8(ocrDir) ? ocrDir : targetDir;
1069
1517
  const dirWatcher = watch(watchDir, {
1070
1518
  persistent: true,
1071
1519
  ignoreInitial: true,
1072
1520
  depth: 3
1073
1521
  });
1074
1522
  dirWatcher.on("addDir", (dirPath) => {
1075
- const parentDir = join5(dirPath, "..");
1076
- const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join5(".ocr", "sessions"));
1523
+ const parentDir = join8(dirPath, "..");
1524
+ const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join8(".ocr", "sessions"));
1077
1525
  if (isDirectChild && !dirPath.endsWith("sessions")) {
1078
- const newSession = basename(dirPath);
1526
+ const newSession = basename3(dirPath);
1079
1527
  currentSession = newSession;
1080
1528
  currentSessionPath = dirPath;
1081
- preservedStartTime = void 0;
1529
+ preservedStartTimes.review = void 0;
1530
+ preservedStartTimes.map = void 0;
1531
+ currentStrategy = null;
1082
1532
  watchSession(dirPath);
1083
1533
  updateDisplay();
1084
1534
  }
@@ -1089,25 +1539,103 @@ var progressCommand = new Command2("progress").description("Watch real-time prog
1089
1539
  clearInterval(timerInterval);
1090
1540
  dirWatcher.close();
1091
1541
  if (sessionWatcher) sessionWatcher.close();
1092
- logUpdate.done();
1542
+ logUpdate4.done();
1093
1543
  process.exit(0);
1094
1544
  });
1095
1545
  });
1546
+ function renderGenericWaiting() {
1547
+ const lines = [];
1548
+ lines.push("");
1549
+ lines.push(chalk7.bold.white(" Open Code Review"));
1550
+ lines.push("");
1551
+ lines.push(chalk7.dim(" Waiting for session..."));
1552
+ lines.push("");
1553
+ lines.push(` ${chalk7.dim("\u2500".repeat(24))} ${chalk7.dim("0%")}`);
1554
+ lines.push("");
1555
+ lines.push(
1556
+ chalk7.dim(" Run ") + chalk7.white("/ocr-review") + chalk7.dim(" or ") + chalk7.white("/ocr-map") + chalk7.dim(" to start")
1557
+ );
1558
+ lines.push("");
1559
+ lines.push(chalk7.dim(" Ctrl+C to exit"));
1560
+ lines.push("");
1561
+ logUpdate4(lines.join("\n"));
1562
+ }
1563
+ function renderCombinedProgress(sessionPath, preservedStartTimes) {
1564
+ const lines = [];
1565
+ const session = basename3(sessionPath);
1566
+ lines.push("");
1567
+ lines.push(
1568
+ chalk7.bold.white(" Open Code Review") + chalk7.yellow(" \xB7 Parallel Workflows")
1569
+ );
1570
+ lines.push("");
1571
+ lines.push(chalk7.dim(" ") + chalk7.white(session));
1572
+ lines.push("");
1573
+ const reviewStrategy2 = getStrategy("review");
1574
+ const mapStrategy2 = getStrategy("map");
1575
+ if (reviewStrategy2) {
1576
+ const reviewState = reviewStrategy2.parseState(
1577
+ sessionPath,
1578
+ preservedStartTimes.review
1579
+ );
1580
+ if (reviewState) {
1581
+ const reviewPercent = Math.round(
1582
+ reviewState.phaseNumber / reviewStrategy2.totalPhases * 100
1583
+ );
1584
+ const reviewBar = chalk7.blue("\u2501".repeat(Math.round(reviewPercent / 10))) + chalk7.dim("\u2500".repeat(10 - Math.round(reviewPercent / 10)));
1585
+ const currentPhase = reviewStrategy2.phases.find(
1586
+ (p) => p.key === reviewState.phase
1587
+ );
1588
+ lines.push(
1589
+ chalk7.blue(" \u25C9 Review") + chalk7.dim(" ") + reviewBar + chalk7.dim(" ") + chalk7.white(`${reviewPercent}%`) + chalk7.dim(" \xB7 ") + chalk7.cyan(currentPhase?.label ?? reviewState.phase)
1590
+ );
1591
+ } else {
1592
+ lines.push(chalk7.blue(" \u25C9 Review") + chalk7.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 0%"));
1593
+ }
1594
+ }
1595
+ if (mapStrategy2) {
1596
+ const mapState = mapStrategy2.parseState(
1597
+ sessionPath,
1598
+ preservedStartTimes.map
1599
+ );
1600
+ if (mapState) {
1601
+ const mapPercent = Math.round(
1602
+ mapState.phaseNumber / mapStrategy2.totalPhases * 100
1603
+ );
1604
+ const mapBar = chalk7.green("\u2501".repeat(Math.round(mapPercent / 10))) + chalk7.dim("\u2500".repeat(10 - Math.round(mapPercent / 10)));
1605
+ const currentPhase = mapStrategy2.phases.find(
1606
+ (p) => p.key === mapState.phase
1607
+ );
1608
+ lines.push(
1609
+ chalk7.green(" \u25C9 Map") + chalk7.dim(" ") + mapBar + chalk7.dim(" ") + chalk7.white(`${mapPercent}%`) + chalk7.dim(" \xB7 ") + chalk7.cyan(currentPhase?.label ?? mapState.phase)
1610
+ );
1611
+ } else {
1612
+ lines.push(chalk7.green(" \u25C9 Map") + chalk7.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 0%"));
1613
+ }
1614
+ }
1615
+ lines.push("");
1616
+ lines.push(
1617
+ chalk7.dim(" Use ") + chalk7.white("--workflow review") + chalk7.dim(" or ") + chalk7.white("--workflow map") + chalk7.dim(" for details")
1618
+ );
1619
+ lines.push("");
1620
+ lines.push(chalk7.dim(" Ctrl+C to exit"));
1621
+ lines.push("");
1622
+ logUpdate4(lines.join("\n"));
1623
+ }
1096
1624
 
1097
1625
  // packages/cli/src/commands/update.ts
1098
1626
  import { Command as Command3 } from "commander";
1099
- import chalk5 from "chalk";
1627
+ import chalk8 from "chalk";
1100
1628
  import ora2 from "ora";
1101
- import { existsSync as existsSync6 } from "node:fs";
1102
- import { join as join6 } from "node:path";
1629
+ import { existsSync as existsSync9 } from "node:fs";
1630
+ import { join as join9 } from "node:path";
1103
1631
  function detectConfiguredTools(targetDir) {
1104
1632
  return AI_TOOLS.filter((tool) => {
1105
1633
  if (tool.commandStrategy === "subdirectory") {
1106
- const ocrDir = join6(targetDir, tool.commandsDir, "ocr");
1107
- return existsSync6(ocrDir);
1634
+ const ocrDir = join9(targetDir, tool.commandsDir, "ocr");
1635
+ return existsSync9(ocrDir);
1108
1636
  } else {
1109
- const reviewCmd = join6(targetDir, tool.commandsDir, "ocr-review.md");
1110
- return existsSync6(reviewCmd);
1637
+ const reviewCmd = join9(targetDir, tool.commandsDir, "ocr-review.md");
1638
+ return existsSync9(reviewCmd);
1111
1639
  }
1112
1640
  });
1113
1641
  }
@@ -1118,7 +1646,7 @@ var updateCommand = new Command3("update").description("Update OCR assets after
1118
1646
  const targetDir = process.cwd();
1119
1647
  requireOcrSetup(targetDir);
1120
1648
  console.log();
1121
- console.log(chalk5.bold.cyan(" Open Code Review - Update"));
1649
+ console.log(chalk8.bold.cyan(" Open Code Review - Update"));
1122
1650
  console.log();
1123
1651
  const savedToolIds = getConfiguredToolIds(targetDir);
1124
1652
  const configuredTools = detectConfiguredTools(targetDir);
@@ -1132,8 +1660,8 @@ var updateCommand = new Command3("update").description("Update OCR assets after
1132
1660
  );
1133
1661
  }
1134
1662
  if (toolsToUpdate.length === 0) {
1135
- console.log(chalk5.yellow(" No configured AI tools found."));
1136
- console.log(chalk5.dim(" Run `ocr init` to set up OCR first."));
1663
+ console.log(chalk8.yellow(" No configured AI tools found."));
1664
+ console.log(chalk8.dim(" Run `ocr init` to set up OCR first."));
1137
1665
  console.log();
1138
1666
  process.exit(1);
1139
1667
  }
@@ -1142,31 +1670,33 @@ var updateCommand = new Command3("update").description("Update OCR assets after
1142
1670
  const updateSkills = options.skills || !hasSpecificFlag;
1143
1671
  const updateInject = options.inject || !hasSpecificFlag;
1144
1672
  if (options.dryRun) {
1145
- console.log(chalk5.yellow(" Dry run mode - no files will be modified"));
1673
+ console.log(chalk8.yellow(" Dry run mode - no files will be modified"));
1146
1674
  console.log();
1147
1675
  }
1148
- console.log(chalk5.dim(" Detected tools:"));
1676
+ console.log(chalk8.dim(" Detected tools:"));
1149
1677
  for (const tool of toolsToUpdate) {
1150
1678
  console.log(` \u2022 ${tool.name}`);
1151
1679
  }
1152
1680
  console.log();
1153
1681
  if (updateCommands || updateSkills) {
1154
1682
  if (options.dryRun) {
1155
- console.log(chalk5.dim(" Would update:"));
1156
- console.log(chalk5.dim(" \u2022 .ocr/skills/SKILL.md (main skill)"));
1683
+ console.log(chalk8.dim(" Would update:"));
1684
+ console.log(chalk8.dim(" \u2022 .ocr/skills/SKILL.md (main skill)"));
1157
1685
  console.log(
1158
- chalk5.dim(" \u2022 .ocr/skills/references/ (workflow, reviewers)")
1686
+ chalk8.dim(" \u2022 .ocr/skills/references/ (except reviewers/)")
1159
1687
  );
1160
- console.log(chalk5.dim(" \u2022 .ocr/skills/assets/reviewer-template.md"));
1688
+ console.log(chalk8.dim(" \u2022 .ocr/skills/assets/reviewer-template.md"));
1689
+ console.log(chalk8.dim(" Preserved (not modified):"));
1690
+ console.log(chalk8.dim(" \u2022 .ocr/config.yaml"));
1161
1691
  console.log(
1162
- chalk5.dim(" \u2022 .ocr/config.yaml (preserved if customized)")
1692
+ chalk8.dim(" \u2022 .ocr/skills/references/reviewers/ (all reviewers)")
1163
1693
  );
1164
1694
  for (const tool of toolsToUpdate) {
1165
1695
  if (tool.commandStrategy === "subdirectory") {
1166
- console.log(chalk5.dim(` \u2022 ${tool.commandsDir}/ocr/ (commands)`));
1696
+ console.log(chalk8.dim(` \u2022 ${tool.commandsDir}/ocr/ (commands)`));
1167
1697
  } else {
1168
1698
  console.log(
1169
- chalk5.dim(` \u2022 ${tool.commandsDir}/ocr-*.md (commands)`)
1699
+ chalk8.dim(` \u2022 ${tool.commandsDir}/ocr-*.md (commands)`)
1170
1700
  );
1171
1701
  }
1172
1702
  }
@@ -1183,34 +1713,42 @@ var updateCommand = new Command3("update").description("Update OCR assets after
1183
1713
  const successful = results.filter((r) => r.success);
1184
1714
  const failed = results.filter((r) => !r.success);
1185
1715
  if (successful.length > 0) {
1186
- console.log(chalk5.green(" \u2713 Commands and skills updated"));
1716
+ console.log(chalk8.green(" \u2713 Commands and skills updated"));
1187
1717
  console.log(
1188
- chalk5.dim(" Including: SKILL.md, references/, assets/")
1718
+ chalk8.dim(" Including: SKILL.md, references/, assets/")
1189
1719
  );
1190
1720
  for (const result of successful) {
1191
- console.log(` ${chalk5.green("\u2713")} ${result.tool.name}`);
1721
+ console.log(` ${chalk8.green("\u2713")} ${result.tool.name}`);
1192
1722
  }
1193
1723
  }
1194
1724
  if (failed.length > 0) {
1195
1725
  console.log();
1196
- console.log(chalk5.red(" \u2717 Some updates failed:"));
1726
+ console.log(chalk8.red(" \u2717 Some updates failed:"));
1197
1727
  for (const result of failed) {
1198
1728
  console.log(
1199
- ` ${chalk5.red("\u2717")} ${result.tool.name}: ${result.error}`
1729
+ ` ${chalk8.red("\u2717")} ${result.tool.name}: ${result.error}`
1200
1730
  );
1201
1731
  }
1202
1732
  }
1733
+ const allWarnings = results.flatMap((r) => r.warnings ?? []);
1734
+ if (allWarnings.length > 0) {
1735
+ console.log();
1736
+ console.log(chalk8.yellow(" \u26A0 Warnings:"));
1737
+ for (const warning of allWarnings) {
1738
+ console.log(` ${chalk8.yellow("\u26A0")} ${warning}`);
1739
+ }
1740
+ }
1203
1741
  console.log();
1204
1742
  }
1205
1743
  }
1206
1744
  if (updateInject) {
1207
1745
  if (options.dryRun) {
1208
- console.log(chalk5.dim(" Would update:"));
1209
- if (existsSync6(join6(targetDir, "AGENTS.md"))) {
1210
- console.log(chalk5.dim(" \u2022 AGENTS.md (OCR managed block)"));
1746
+ console.log(chalk8.dim(" Would update:"));
1747
+ if (existsSync9(join9(targetDir, "AGENTS.md"))) {
1748
+ console.log(chalk8.dim(" \u2022 AGENTS.md (OCR managed block)"));
1211
1749
  }
1212
- if (existsSync6(join6(targetDir, "CLAUDE.md"))) {
1213
- console.log(chalk5.dim(" \u2022 CLAUDE.md (OCR managed block)"));
1750
+ if (existsSync9(join9(targetDir, "CLAUDE.md"))) {
1751
+ console.log(chalk8.dim(" \u2022 CLAUDE.md (OCR managed block)"));
1214
1752
  }
1215
1753
  console.log();
1216
1754
  } else {
@@ -1218,23 +1756,23 @@ var updateCommand = new Command3("update").description("Update OCR assets after
1218
1756
  const injectResults = injectIntoProjectFiles(targetDir);
1219
1757
  spinner.stop();
1220
1758
  if (injectResults.agentsMd || injectResults.claudeMd) {
1221
- console.log(chalk5.green(" \u2713 Instructions updated"));
1759
+ console.log(chalk8.green(" \u2713 Instructions updated"));
1222
1760
  if (injectResults.agentsMd) {
1223
- console.log(` ${chalk5.green("\u2713")} AGENTS.md`);
1761
+ console.log(` ${chalk8.green("\u2713")} AGENTS.md`);
1224
1762
  }
1225
1763
  if (injectResults.claudeMd) {
1226
- console.log(` ${chalk5.green("\u2713")} CLAUDE.md`);
1764
+ console.log(` ${chalk8.green("\u2713")} CLAUDE.md`);
1227
1765
  }
1228
1766
  } else {
1229
- console.log(chalk5.dim(" No instruction files to update"));
1767
+ console.log(chalk8.dim(" No instruction files to update"));
1230
1768
  }
1231
1769
  console.log();
1232
1770
  }
1233
1771
  }
1234
1772
  if (options.dryRun) {
1235
- console.log(chalk5.dim(" Run without --dry-run to apply changes."));
1773
+ console.log(chalk8.dim(" Run without --dry-run to apply changes."));
1236
1774
  } else {
1237
- console.log(chalk5.green(" \u2713 Update complete"));
1775
+ console.log(chalk8.green(" \u2713 Update complete"));
1238
1776
  }
1239
1777
  console.log();
1240
1778
  });