santree 0.7.4 → 0.7.6

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.
Files changed (114) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/config.d.ts +12 -0
  3. package/dist/commands/config.js +444 -0
  4. package/dist/commands/dashboard.js +187 -80
  5. package/dist/commands/helpers/statusline.js +0 -30
  6. package/dist/commands/issue/index.d.ts +1 -1
  7. package/dist/commands/issue/index.js +1 -1
  8. package/dist/commands/pr/context.d.ts +7 -0
  9. package/dist/commands/pr/context.js +36 -0
  10. package/dist/commands/pr/fix.d.ts +11 -2
  11. package/dist/commands/pr/fix.js +64 -12
  12. package/dist/commands/worktree/create.js +17 -11
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/commands/worktree/switch.js +3 -11
  15. package/dist/lib/ai.d.ts +11 -0
  16. package/dist/lib/ai.js +71 -2
  17. package/dist/lib/cd-hint.d.ts +18 -0
  18. package/dist/lib/cd-hint.js +27 -0
  19. package/dist/lib/claude-config.d.ts +26 -0
  20. package/dist/lib/claude-config.js +121 -0
  21. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  22. package/dist/lib/config/TrackerPicker.js +113 -0
  23. package/dist/lib/config/diagnostics.d.ts +37 -0
  24. package/dist/lib/config/diagnostics.js +190 -0
  25. package/dist/lib/config-store.d.ts +23 -0
  26. package/dist/lib/config-store.js +68 -0
  27. package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
  28. package/dist/lib/dashboard/DetailPanel.js +54 -37
  29. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  30. package/dist/lib/dashboard/IssueList.js +24 -5
  31. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  32. package/dist/lib/dashboard/Overlays.js +3 -4
  33. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  34. package/dist/lib/dashboard/ReviewList.js +6 -3
  35. package/dist/lib/dashboard/data.js +7 -34
  36. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  37. package/dist/lib/dashboard/external-editor.js +3 -2
  38. package/dist/lib/dashboard/types.d.ts +19 -2
  39. package/dist/lib/dashboard/types.js +68 -2
  40. package/dist/lib/exec.d.ts +13 -0
  41. package/dist/lib/exec.js +11 -1
  42. package/dist/lib/fix-loop.d.ts +45 -0
  43. package/dist/lib/fix-loop.js +83 -0
  44. package/dist/lib/git.d.ts +3 -29
  45. package/dist/lib/git.js +6 -62
  46. package/dist/lib/github.d.ts +70 -0
  47. package/dist/lib/github.js +128 -0
  48. package/dist/lib/multiplexer/cmux.js +143 -17
  49. package/dist/lib/multiplexer/none.js +3 -3
  50. package/dist/lib/multiplexer/tmux.js +5 -29
  51. package/dist/lib/multiplexer/types.d.ts +34 -1
  52. package/dist/lib/open-url.d.ts +3 -2
  53. package/dist/lib/open-url.js +5 -4
  54. package/dist/lib/prompts.d.ts +35 -1
  55. package/dist/lib/prompts.js +7 -0
  56. package/dist/lib/setup/apply.d.ts +16 -0
  57. package/dist/lib/setup/apply.js +35 -0
  58. package/dist/lib/setup/gitignore.d.ts +24 -0
  59. package/dist/lib/setup/gitignore.js +95 -0
  60. package/dist/lib/setup/steps.d.ts +39 -0
  61. package/dist/lib/setup/steps.js +333 -0
  62. package/dist/lib/setup/tools.d.ts +23 -0
  63. package/dist/lib/setup/tools.js +47 -0
  64. package/dist/lib/squirrel-loader.d.ts +3 -1
  65. package/dist/lib/squirrel-loader.js +2 -2
  66. package/dist/lib/trackers/github/index.js +2 -2
  67. package/dist/lib/trackers/local/store.js +1 -1
  68. package/package.json +1 -2
  69. package/prompts/fix-context.njk +79 -0
  70. package/prompts/fix-loop.njk +18 -0
  71. package/prompts/pr.njk +1 -1
  72. package/dist/commands/doctor.d.ts +0 -2
  73. package/dist/commands/doctor.js +0 -618
  74. package/dist/commands/github/auth.d.ts +0 -2
  75. package/dist/commands/github/auth.js +0 -56
  76. package/dist/commands/github/index.d.ts +0 -1
  77. package/dist/commands/github/index.js +0 -1
  78. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  79. package/dist/commands/helpers/english-tutor/index.js +0 -1
  80. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  81. package/dist/commands/helpers/english-tutor/install.js +0 -24
  82. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  83. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  84. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  85. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  86. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  87. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  88. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  89. package/dist/commands/helpers/session-signal/end.js +0 -13
  90. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  91. package/dist/commands/helpers/session-signal/index.js +0 -1
  92. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  93. package/dist/commands/helpers/session-signal/install.js +0 -24
  94. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  95. package/dist/commands/helpers/session-signal/notification.js +0 -13
  96. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  97. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  98. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  99. package/dist/commands/helpers/session-signal/stop.js +0 -13
  100. package/dist/commands/helpers/shell-init.d.ts +0 -11
  101. package/dist/commands/helpers/shell-init.js +0 -122
  102. package/dist/commands/issue/setup.d.ts +0 -2
  103. package/dist/commands/issue/setup.js +0 -108
  104. package/dist/commands/issue/switch.d.ts +0 -12
  105. package/dist/commands/issue/switch.js +0 -38
  106. package/dist/commands/linear/switch.d.ts +0 -2
  107. package/dist/commands/linear/switch.js +0 -75
  108. package/dist/lib/english-tutor.d.ts +0 -13
  109. package/dist/lib/english-tutor.js +0 -125
  110. package/dist/lib/session-signal.d.ts +0 -16
  111. package/dist/lib/session-signal.js +0 -104
  112. package/prompts/english-tutor-prompt.njk +0 -15
  113. package/shell/init.bash.njk +0 -129
  114. package/shell/init.zsh.njk +0 -204
@@ -9,11 +9,15 @@ import * as path from "path";
9
9
  const require = createRequire(import.meta.url);
10
10
  const { version } = require("../../package.json");
11
11
  import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, getDiffTool, getWorktreeStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, } from "../lib/git.js";
12
- import { run, spawnAsync } from "../lib/exec.js";
12
+ import { run, spawnAsync, execFileAsync } from "../lib/exec.js";
13
+ import { getConfiguredEditor } from "../lib/config-store.js";
14
+ import { printCdHint } from "../lib/cd-hint.js";
13
15
  import { resolveAgentBinary, resolveClaudeBinary, fillCommitMessage, askTicketQuestion, } from "../lib/ai.js";
14
16
  import { readTriageInvestigateConfig, isTriageInvestigateConfigured, buildInvestigatePrompt, buildInvestigateCommand, } from "../lib/triage-config.js";
15
17
  import { getInstalledClaudeVersion } from "../lib/version.js";
16
18
  import { extractTicketId, getStagedDiffContent } from "../lib/git.js";
19
+ import { santreeSelfArgv } from "../lib/setup/apply.js";
20
+ import { startFixLoop } from "../lib/fix-loop.js";
17
21
  import { getMultiplexer } from "../lib/multiplexer/index.js";
18
22
  import { shellEscape } from "../lib/multiplexer/types.js";
19
23
  import Spinner from "ink-spinner";
@@ -293,6 +297,32 @@ function Tab({ active, label, mode }) {
293
297
  }
294
298
  return _jsx(Text, { children: ` ${label} ` });
295
299
  }
300
+ const TAB_NAMES = {
301
+ triage: "Triage",
302
+ issues: "Issues",
303
+ trees: "Trees",
304
+ reviews: "Reviews",
305
+ };
306
+ /**
307
+ * Column ranges (1-based, absolute) of each tab pill in the tab strip, so a
308
+ * row-2 click maps to the right tab. Must mirror the renderer exactly: the strip
309
+ * Box has `paddingX={1}` (first pill at col 2), each `<Tab>` pill is `" label "`
310
+ * (label + 2), and a 1-space separator sits between pills. Labels embed the live
311
+ * counts, so widths shift as data refreshes — always pass current counts.
312
+ */
313
+ function computeTabHitboxes(order, counts) {
314
+ const boxes = [];
315
+ let col = 2; // paddingX={1} on the row-2 Box → first pill starts at col 2
316
+ order.forEach((tab, i) => {
317
+ if (i > 0)
318
+ col += 1; // 1-space separator between pills
319
+ const label = `${i + 1} ${TAB_NAMES[tab]} (${counts[tab]})`;
320
+ const width = label.length + 2; // pill renders ` label `
321
+ boxes.push({ tab, start: col, end: col + width - 1 });
322
+ col += width;
323
+ });
324
+ return boxes;
325
+ }
296
326
  /**
297
327
  * Single-line global keymap shown at the bottom-left of the dashboard. The
298
328
  * `E workspace` hint only appears when the action is meaningful
@@ -335,6 +365,10 @@ export default function Dashboard() {
335
365
  const repoRootRef = useRef(null);
336
366
  const stateRef = useRef(state);
337
367
  stateRef.current = state;
368
+ // Kept in sync so the long-lived mouse handler can rebuild the tab order
369
+ // (which depends on whether Triage is present) for click-to-switch.
370
+ const supportsTriageRef = useRef(supportsTriage);
371
+ supportsTriageRef.current = supportsTriage;
338
372
  const draggingRef = useRef(null);
339
373
  const [termSize, setTermSize] = useState({
340
374
  columns: stdout?.columns ?? 80,
@@ -410,10 +444,11 @@ export default function Dashboard() {
410
444
  return;
411
445
  }
412
446
  try {
413
- // Re-detect terminal theme alongside data fetch so light↔dark
414
- // switches propagate within one refresh cycle (≤5min, or sooner
415
- // on a manual `R`). Skip the
416
- // OSC 11 query when a text-input overlay is active — the
447
+ // Refresh the colorscheme FIRST it's the most visible change, so on a
448
+ // manual `R` (or a light↔dark switch) the theme should flip immediately
449
+ // rather than waiting on the slower gh-backed data fetch below. Detect +
450
+ // apply it, then load everything else.
451
+ // Skip the OSC 11 query when a text-input overlay is active — the
417
452
  // terminal's response would otherwise leak into the user's
418
453
  // commit/PR/context message via Ink's stdin handler.
419
454
  const overlay = stateRef.current.overlay;
@@ -421,18 +456,17 @@ export default function Dashboard() {
421
456
  (overlay === "triage-ask" && stateRef.current.triageAskPhase === "input") ||
422
457
  (overlay === "pr-create" && stateRef.current.prCreatePhase === "review") ||
423
458
  (overlay === "commit" && stateRef.current.commitPhase === "awaiting-message");
424
- const themeP = inTextInput ? Promise.resolve(null) : detectTerminalTheme();
425
- const [data, reviewData, themeMode] = await Promise.all([
459
+ const themeMode = inTextInput ? null : await detectTerminalTheme();
460
+ if (themeMode !== null)
461
+ setTheme(getThemeForMode(themeMode));
462
+ const [data, reviewData] = await Promise.all([
426
463
  loadDashboardData(repoRoot),
427
464
  loadReviewsData(repoRoot),
428
- themeP,
429
465
  ]);
430
- if (themeMode !== null)
431
- setTheme(getThemeForMode(themeMode));
432
466
  // Workspace file presence — only meaningful when the editor consumes
433
467
  // `.code-workspace` files. Cheap directory read; recomputed each cycle
434
468
  // in case the user adds/removes one.
435
- const editor = (process.env.SANTREE_EDITOR ?? "code").toLowerCase();
469
+ const editor = (getConfiguredEditor() ?? "code").toLowerCase();
436
470
  const editorAcceptsWorkspace = editor === "code" || editor === "cursor";
437
471
  let hasWs = false;
438
472
  if (editorAcceptsWorkspace) {
@@ -598,6 +632,28 @@ export default function Dashboard() {
598
632
  }
599
633
  if (!isPress)
600
634
  return;
635
+ // Tab strip click (row 2) — switch tabs by clicking, like the 1–N keys.
636
+ // Ignored while any overlay is up or data isn't ready.
637
+ {
638
+ const s = stateRef.current;
639
+ if (row === 2 && s.overlay === null && !s.loading && !s.error) {
640
+ const order = supportsTriageRef.current
641
+ ? ["triage", "issues", "trees", "reviews"]
642
+ : ["issues", "trees", "reviews"];
643
+ const counts = {
644
+ triage: s.flatTriage.length,
645
+ issues: s.flatIssues.length,
646
+ trees: s.flatTrees.length,
647
+ reviews: s.flatReviews.length,
648
+ };
649
+ const hit = computeTabHitboxes(order, counts).find((b) => col >= b.start && col <= b.end);
650
+ if (hit) {
651
+ if (hit.tab !== s.activeTab)
652
+ dispatch({ type: "SET_TAB", tab: hit.tab });
653
+ return;
654
+ }
655
+ }
656
+ }
601
657
  // Diff overlay click: drag divider, or select file row in left pane
602
658
  {
603
659
  const s = stateRef.current;
@@ -1049,7 +1105,9 @@ export default function Dashboard() {
1049
1105
  ? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
1050
1106
  : null;
1051
1107
  const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
1052
- const workCmd = mode === "plan" ? `st worktree work --plan${contextArg}` : `st worktree work${contextArg}`;
1108
+ const workCmd = mode === "plan"
1109
+ ? `santree worktree work --plan${contextArg}`
1110
+ : `santree worktree work${contextArg}`;
1053
1111
  const cmd = resumeCmd ?? workCmd;
1054
1112
  const mux = getMultiplexer();
1055
1113
  const selected = await mux.selectWindow(windowName);
@@ -1075,6 +1133,8 @@ export default function Dashboard() {
1075
1133
  name: windowName,
1076
1134
  cwd: worktreePath,
1077
1135
  command: cmd,
1136
+ group: di.issue.projectName ?? undefined,
1137
+ tabName: "work",
1078
1138
  });
1079
1139
  if (created.ok) {
1080
1140
  dispatch({
@@ -1091,21 +1151,23 @@ export default function Dashboard() {
1091
1151
  });
1092
1152
  }
1093
1153
  }
1094
- // Delayed refresh to pick up session ID created by `st worktree work`
1154
+ // Delayed refresh to pick up session ID created by `santree worktree work`
1095
1155
  setTimeout(() => refresh(), 3000);
1096
1156
  }, [refresh]);
1097
- const launchAfterCreation = useCallback(async (mode, worktreePath, ticketId, contextFile) => {
1157
+ const launchAfterCreation = useCallback(async (mode, worktreePath, ticketId, contextFile, group) => {
1098
1158
  const mux = getMultiplexer();
1099
1159
  if (mux.isActive()) {
1100
1160
  const windowName = ticketId;
1101
1161
  const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
1102
1162
  const workCmd = mode === "plan"
1103
- ? `st worktree work --plan${contextArg}`
1104
- : `st worktree work${contextArg}`;
1163
+ ? `santree worktree work --plan${contextArg}`
1164
+ : `santree worktree work${contextArg}`;
1105
1165
  const created = await mux.createWindow({
1106
1166
  name: windowName,
1107
1167
  cwd: worktreePath,
1108
1168
  command: workCmd,
1169
+ group,
1170
+ tabName: "work",
1109
1171
  });
1110
1172
  if (created.ok) {
1111
1173
  dispatch({
@@ -1123,10 +1185,10 @@ export default function Dashboard() {
1123
1185
  }
1124
1186
  else {
1125
1187
  leaveAltScreen();
1126
- console.log(`SANTREE_CD:${worktreePath}`);
1127
- console.log(`SANTREE_WORK:${mode}`);
1128
- if (contextFile)
1129
- console.log(`SANTREE_WORK_CONTEXT:${contextFile}`);
1188
+ printCdHint({
1189
+ path: worktreePath,
1190
+ work: { mode, contextFile: contextFile || undefined },
1191
+ });
1130
1192
  exit();
1131
1193
  }
1132
1194
  }, [exit, refresh]);
@@ -1241,7 +1303,7 @@ export default function Dashboard() {
1241
1303
  }
1242
1304
  // 4. Done — launch work
1243
1305
  dispatch({ type: "CREATION_DONE" });
1244
- launchAfterCreation(mode, result.path, ticketId, contextFile);
1306
+ launchAfterCreation(mode, result.path, ticketId, contextFile, di.issue.projectName ?? undefined);
1245
1307
  }, [launchAfterCreation]);
1246
1308
  // Holds the context file path through multi-step flows (mode-select → base-select → confirm-setup → create)
1247
1309
  const pendingContextFileRef = useRef(null);
@@ -1277,10 +1339,10 @@ export default function Dashboard() {
1277
1339
  }
1278
1340
  else {
1279
1341
  leaveAltScreen();
1280
- console.log(`SANTREE_CD:${di.worktree.path}`);
1281
- console.log(`SANTREE_WORK:${mode}`);
1282
- if (contextFile)
1283
- console.log(`SANTREE_WORK_CONTEXT:${contextFile}`);
1342
+ printCdHint({
1343
+ path: di.worktree.path,
1344
+ work: { mode, contextFile: contextFile || undefined },
1345
+ });
1284
1346
  exit();
1285
1347
  }
1286
1348
  }
@@ -1411,7 +1473,7 @@ export default function Dashboard() {
1411
1473
  if (!user) {
1412
1474
  dispatch({
1413
1475
  type: "TRACKER_SELECT_MESSAGE",
1414
- message: "GitHub CLI not authenticated. Run: santree github auth",
1476
+ message: "GitHub CLI not authenticated. Run: gh auth login",
1415
1477
  });
1416
1478
  return;
1417
1479
  }
@@ -1578,7 +1640,9 @@ export default function Dashboard() {
1578
1640
  const msg = tid && !trimmed.includes(`[${tid}]`) ? `[${tid}] ${trimmed}` : trimmed;
1579
1641
  dispatch({ type: "COMMIT_PHASE", phase: "committing" });
1580
1642
  try {
1581
- await execAsync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, {
1643
+ // Array args (no shell) — the message is user-typed and may contain
1644
+ // backticks, `$(…)`, quotes, or backslashes.
1645
+ await execFileAsync("git", ["commit", "-m", msg], {
1582
1646
  cwd: s.commitWorktreePath,
1583
1647
  });
1584
1648
  }
@@ -1591,7 +1655,9 @@ export default function Dashboard() {
1591
1655
  }
1592
1656
  dispatch({ type: "COMMIT_PHASE", phase: "pushing" });
1593
1657
  try {
1594
- await execAsync(`git push -u origin "${s.commitBranch}"`, { cwd: s.commitWorktreePath });
1658
+ await execFileAsync("git", ["push", "-u", "origin", s.commitBranch], {
1659
+ cwd: s.commitWorktreePath,
1660
+ });
1595
1661
  }
1596
1662
  catch (e) {
1597
1663
  dispatch({ type: "COMMIT_ERROR", error: e?.stderr?.trim() || e?.message || "Push failed" });
@@ -1605,7 +1671,7 @@ export default function Dashboard() {
1605
1671
  }, [refresh]);
1606
1672
  // ── Editor actions ───────────────────────────────────────────────
1607
1673
  const openInEditor = useCallback((wtPath) => {
1608
- const editor = process.env.SANTREE_EDITOR || "code";
1674
+ const editor = getConfiguredEditor() || "code";
1609
1675
  spawn(editor, [wtPath], { detached: true, stdio: "ignore" }).unref();
1610
1676
  dispatch({
1611
1677
  type: "SET_ACTION_MESSAGE",
@@ -1616,7 +1682,7 @@ export default function Dashboard() {
1616
1682
  const repoRoot = repoRootRef.current;
1617
1683
  if (!repoRoot)
1618
1684
  return;
1619
- const editor = process.env.SANTREE_EDITOR || "code";
1685
+ const editor = getConfiguredEditor() || "code";
1620
1686
  try {
1621
1687
  const entries = fs.readdirSync(repoRoot);
1622
1688
  const wsFile = entries.find((f) => f.endsWith(".code-workspace"));
@@ -2483,7 +2549,9 @@ export default function Dashboard() {
2483
2549
  (async () => {
2484
2550
  try {
2485
2551
  dispatch({ type: "CREATION_LOG", logs: `Fetching ${ri.branch}...\n` });
2486
- await execAsync(`git fetch origin ${ri.branch}`, { cwd: repoRoot });
2552
+ // Array args (no shell) foreign PR branch names are not
2553
+ // locally slugified and could otherwise inject.
2554
+ await execFileAsync("git", ["fetch", "origin", ri.branch], { cwd: repoRoot });
2487
2555
  dispatch({ type: "CREATION_LOG", logs: `Creating worktree...\n` });
2488
2556
  const result = await createWorktree(ri.branch, ri.baseBranch ?? getDefaultBranch(), repoRoot);
2489
2557
  if (!result.success || !result.path) {
@@ -2534,7 +2602,7 @@ export default function Dashboard() {
2534
2602
  dispatch({ type: "CREATION_DONE" });
2535
2603
  dispatch({ type: "SET_ACTION_MESSAGE", message: `Worktree created for ${ticketId}` });
2536
2604
  // Open in editor automatically
2537
- const editor = process.env.SANTREE_EDITOR || "code";
2605
+ const editor = getConfiguredEditor() || "code";
2538
2606
  spawn(editor, [result.path], { detached: true, stdio: "ignore" }).unref();
2539
2607
  refresh();
2540
2608
  }
@@ -2550,7 +2618,7 @@ export default function Dashboard() {
2550
2618
  dispatch({ type: "SET_ACTION_MESSAGE", message: "No worktree (press w to checkout)" });
2551
2619
  return;
2552
2620
  }
2553
- const editor = process.env.SANTREE_EDITOR || "code";
2621
+ const editor = getConfiguredEditor() || "code";
2554
2622
  spawn(editor, [ri.worktree.path], { detached: true, stdio: "ignore" }).unref();
2555
2623
  dispatch({ type: "SET_ACTION_MESSAGE", message: `Opened in ${editor}` });
2556
2624
  return;
@@ -2566,25 +2634,29 @@ export default function Dashboard() {
2566
2634
  }
2567
2635
  const mux = getMultiplexer();
2568
2636
  if (mux.isActive()) {
2569
- const windowName = `review-${extractTicketId(ri.branch ?? "") ?? ri.pr.number}`;
2637
+ const ticketId = extractTicketId(ri.branch ?? "") ?? `pr-${ri.pr.number}`;
2570
2638
  const cwd = ri.worktree.path;
2571
2639
  void (async () => {
2572
- const created = await mux.createWindow({
2573
- name: windowName,
2640
+ // Open review as a `review` tab inside the ticket's workspace
2641
+ // when one exists; addTab falls back to a new window otherwise.
2642
+ const created = await mux.addTab({
2643
+ windowName: ticketId,
2644
+ tabName: "review",
2574
2645
  cwd,
2575
- command: "st pr review",
2646
+ command: "santree pr review",
2647
+ group: ri.ticket?.projectName ?? undefined,
2576
2648
  });
2577
2649
  dispatch({
2578
2650
  type: "SET_ACTION_MESSAGE",
2579
2651
  message: created.ok
2580
- ? "Launched AI review in new window"
2652
+ ? "Launched AI review in review tab"
2581
2653
  : `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
2582
2654
  });
2583
2655
  })();
2584
2656
  }
2585
2657
  else {
2586
2658
  leaveAltScreen();
2587
- console.log(`SANTREE_CD:${ri.worktree.path}`);
2659
+ printCdHint({ path: ri.worktree.path });
2588
2660
  exit();
2589
2661
  }
2590
2662
  return;
@@ -2653,7 +2725,7 @@ export default function Dashboard() {
2653
2725
  if (di.worktree?.sessionId) {
2654
2726
  dispatch({
2655
2727
  type: "SET_ACTION_MESSAGE",
2656
- message: "Session active — switch to the Trees tab to resume.",
2728
+ message: "A Claude session exists — switch to the Trees tab to resume.",
2657
2729
  });
2658
2730
  return;
2659
2731
  }
@@ -2692,7 +2764,12 @@ export default function Dashboard() {
2692
2764
  const command = buildInvestigateCommand(resolveClaudeBinary() ?? "claude", prompt);
2693
2765
  const windowName = `investigate-${di.issue.identifier}`;
2694
2766
  void (async () => {
2695
- const created = await mux.createWindow({ name: windowName, cwd: root, command });
2767
+ const created = await mux.createWindow({
2768
+ name: windowName,
2769
+ cwd: root,
2770
+ command,
2771
+ group: di.issue.projectName ?? undefined,
2772
+ });
2696
2773
  dispatch({
2697
2774
  type: "SET_ACTION_MESSAGE",
2698
2775
  message: created.ok
@@ -2755,7 +2832,7 @@ export default function Dashboard() {
2755
2832
  if (di.worktree?.sessionId) {
2756
2833
  dispatch({
2757
2834
  type: "SET_ACTION_MESSAGE",
2758
- message: "Session active — switch to the Trees tab to resume.",
2835
+ message: "A Claude session exists — switch to the Trees tab to resume.",
2759
2836
  });
2760
2837
  return;
2761
2838
  }
@@ -2822,7 +2899,7 @@ export default function Dashboard() {
2822
2899
  if (di.worktree?.sessionId) {
2823
2900
  dispatch({
2824
2901
  type: "SET_ACTION_MESSAGE",
2825
- message: "Session active. Press Enter to resume.",
2902
+ message: "A Claude session exists. Press Enter to resume.",
2826
2903
  });
2827
2904
  return;
2828
2905
  }
@@ -2849,11 +2926,13 @@ export default function Dashboard() {
2849
2926
  const selected = await mux.selectWindow(windowName);
2850
2927
  if (selected.ok)
2851
2928
  return;
2852
- const cmd = resumeCmd ?? "st worktree work";
2929
+ const cmd = resumeCmd ?? "santree worktree work";
2853
2930
  const created = await mux.createWindow({
2854
2931
  name: windowName,
2855
2932
  cwd: worktreePath,
2856
2933
  command: cmd,
2934
+ group: di.issue.projectName ?? undefined,
2935
+ tabName: "work",
2857
2936
  });
2858
2937
  if (!created.ok) {
2859
2938
  dispatch({
@@ -2865,7 +2944,7 @@ export default function Dashboard() {
2865
2944
  }
2866
2945
  else {
2867
2946
  leaveAltScreen();
2868
- console.log(`SANTREE_CD:${di.worktree.path}`);
2947
+ printCdHint({ path: di.worktree.path });
2869
2948
  exit();
2870
2949
  }
2871
2950
  return;
@@ -2918,25 +2997,28 @@ export default function Dashboard() {
2918
2997
  }
2919
2998
  const mux = getMultiplexer();
2920
2999
  if (mux.isActive()) {
2921
- const windowName = `review-${di.issue.identifier}`;
2922
3000
  const cwd = di.worktree.path;
2923
3001
  void (async () => {
2924
- const created = await mux.createWindow({
2925
- name: windowName,
3002
+ // Open review as a `review` tab inside the ticket's workspace
3003
+ // (next to `work`), not as a separate window.
3004
+ const created = await mux.addTab({
3005
+ windowName: di.issue.identifier,
3006
+ tabName: "review",
2926
3007
  cwd,
2927
- command: "st pr review",
3008
+ command: "santree pr review",
3009
+ group: di.issue.projectName ?? undefined,
2928
3010
  });
2929
3011
  dispatch({
2930
3012
  type: "SET_ACTION_MESSAGE",
2931
3013
  message: created.ok
2932
- ? "Launched review in new window"
3014
+ ? "Launched review in review tab"
2933
3015
  : `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
2934
3016
  });
2935
3017
  })();
2936
3018
  }
2937
3019
  else {
2938
3020
  leaveAltScreen();
2939
- console.log(`SANTREE_CD:${di.worktree.path}`);
3021
+ printCdHint({ path: di.worktree.path });
2940
3022
  exit();
2941
3023
  }
2942
3024
  return;
@@ -2979,35 +3061,50 @@ export default function Dashboard() {
2979
3061
  });
2980
3062
  return;
2981
3063
  }
2982
- // Fix PR
3064
+ // Fix PR — launch a self-driving fix loop (re-checks CI + conflicts every 5 min)
2983
3065
  if (input === "f") {
2984
3066
  if (!di.pr || !di.worktree) {
2985
3067
  dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to fix" });
2986
3068
  return;
2987
3069
  }
2988
3070
  const mux = getMultiplexer();
2989
- if (mux.isActive()) {
2990
- const windowName = `fix-${di.issue.identifier}`;
2991
- const cwd = di.worktree.path;
2992
- void (async () => {
2993
- const created = await mux.createWindow({
2994
- name: windowName,
2995
- cwd,
2996
- command: "st pr fix",
2997
- });
2998
- dispatch({
2999
- type: "SET_ACTION_MESSAGE",
3000
- message: created.ok
3001
- ? "Launched PR fix in new window"
3002
- : `Failed to launch PR fix${created.message ? `: ${created.message}` : ""}`,
3003
- });
3004
- })();
3005
- }
3006
- else {
3007
- leaveAltScreen();
3008
- console.log(`SANTREE_CD:${di.worktree.path}`);
3009
- exit();
3071
+ if (!mux.isActive()) {
3072
+ dispatch({
3073
+ type: "SET_ACTION_MESSAGE",
3074
+ message: "Fix loop needs tmux or cmux — run `santree pr fix --loop` in the worktree",
3075
+ });
3076
+ return;
3010
3077
  }
3078
+ const ticketId = di.issue.identifier;
3079
+ const cwd = di.worktree.path;
3080
+ // Absolute santree invocation: the new window's shell may not have
3081
+ // `santree` on PATH (cmux workspaces start a fresh login shell).
3082
+ const self = santreeSelfArgv(["pr", "fix", "--loop"]);
3083
+ const command = [self.cmd, ...self.args].join(" ");
3084
+ // Seed the marker + optimistic badge so the loop shows immediately,
3085
+ // before the launched process writes its own marker.
3086
+ const root = repoRootRef.current ?? findMainRepoRoot();
3087
+ if (root)
3088
+ startFixLoop(root, ticketId, 5);
3089
+ dispatch({ type: "FIXLOOP_START", ticketId });
3090
+ void (async () => {
3091
+ // Add the loop as a `fix-loop` tab inside the ticket's workspace
3092
+ // (next to its `work` tab). On tmux this falls back to a separate
3093
+ // `fix-loop-<ticket>` window.
3094
+ const created = await mux.addTab({
3095
+ windowName: ticketId,
3096
+ tabName: "fix-loop",
3097
+ cwd,
3098
+ command,
3099
+ group: di.issue.projectName ?? undefined,
3100
+ });
3101
+ dispatch({
3102
+ type: "SET_ACTION_MESSAGE",
3103
+ message: created.ok
3104
+ ? "Started fix loop (re-checks every 5 min) in fix-loop tab"
3105
+ : `Failed to start fix loop${created.message ? `: ${created.message}` : ""}`,
3106
+ });
3107
+ })();
3011
3108
  return;
3012
3109
  }
3013
3110
  // View diff (inline overlay)
@@ -3052,7 +3149,7 @@ export default function Dashboard() {
3052
3149
  });
3053
3150
  // ── Render ─────────────────────────────────────────────────────────
3054
3151
  if (state.loading) {
3055
- return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: _jsx(SquirrelLoader, { text: "Loading dashboard..." }) }) }));
3152
+ return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: _jsx(SquirrelLoader, { text: "Loading dashboard...", version: version }) }) }));
3056
3153
  }
3057
3154
  if (state.error) {
3058
3155
  return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press R to retry or q to quit" })] }) }));
@@ -3065,6 +3162,11 @@ export default function Dashboard() {
3065
3162
  : state.flatIssues[state.selectedIndex]) ?? null;
3066
3163
  const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
3067
3164
  const activeTracker = getIssueTracker(repoRootRef.current);
3165
+ // The tracker actually in use right now — null until the repo is configured,
3166
+ // so the `t` overlay doesn't mark a tracker the user never picked.
3167
+ const configuredTrackerKind = isRepoTrackerConfigured(repoRootRef.current)
3168
+ ? activeTracker.kind
3169
+ : null;
3068
3170
  // Comments for the selected triage issue (lazily loaded; undefined = still
3069
3171
  // fetching, which the detail panel renders as "loading…").
3070
3172
  const triageComments = state.activeTab === "triage" && selectedIssue
@@ -3104,12 +3206,17 @@ export default function Dashboard() {
3104
3206
  const sel = i === state.trackerSelectIndex;
3105
3207
  return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
3106
3208
  }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to link, ESC to go back" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select an issue tracker for this repo:" }), _jsx(Text, { children: " " }), [
3107
- { label: "Local", hint: "built-in, file-based — no account needed" },
3108
- { label: "Linear", hint: "OAuth workspace" },
3109
- { label: "GitHub", hint: "GitHub Issues via gh CLI" },
3209
+ {
3210
+ kind: "local",
3211
+ label: "Local",
3212
+ hint: "built-in, file-based — no account needed",
3213
+ },
3214
+ { kind: "linear", label: "Linear", hint: "OAuth workspace" },
3215
+ { kind: "github", label: "GitHub", hint: "GitHub Issues via gh CLI" },
3110
3216
  ].map((t, i) => {
3111
3217
  const sel = i === state.trackerSelectIndex;
3112
- return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", t.label] }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", t.hint] })] }, t.label));
3218
+ const active = t.kind === configuredTrackerKind;
3219
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", t.label] }), active ? _jsx(Text, { color: "green", children: " \u25CF in use" }) : null, _jsxs(Text, { dimColor: true, children: [" \u2014 ", t.hint] })] }, t.label));
3113
3220
  }), _jsx(Text, { children: " " }), state.trackerSelectMessage ? (_jsx(Text, { color: "yellow", children: state.trackerSelectMessage })) : null, _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, q to quit" })] })) }) })) : state.overlay === "confirm-delete-issue" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Delete issue?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [selectedIssue?.issue.identifier, " ", selectedIssue?.issue.title] }), _jsx(Text, { dimColor: true, children: "This removes the issue file from .santree/issues/" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "issue-form" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.issueFormMode === "edit" ? `Edit ${state.issueFormId}` : "New issue", " · ", state.issueFormPhase === "title"
3114
3221
  ? "title"
3115
3222
  : state.issueFormPhase === "description"
@@ -3141,7 +3248,7 @@ export default function Dashboard() {
3141
3248
  })(), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u21E7\u2191\u2193/j/k" }), " scroll · ", _jsx(Text, { color: "cyan", bold: true, children: "a" }), " ask again · ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), " close"] })] }))] })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.activeTab === "triage" ? (state.flatTriage.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "Triage inbox empty" }) })) : (_jsx(IssueList, { groups: state.triageGroups, flatIssues: state.flatTriage, selectedIndex: state.triageSelectedIndex, scrollOffset: state.triageListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg, variant: "triage" }))) : (state.activeTab === "trees" ? state.flatTrees : state.flatIssues).length ===
3142
3249
  0 ? (_jsxs(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: state.activeTab === "trees" ? "No worktrees yet" : "No active issues" }), state.activeTab === "issues" && activeTracker.canMutate ? (_jsx(Text, { dimColor: true, children: "Press n to create one" })) : null] })) : (_jsx(IssueList, { groups: state.activeTab === "trees" ? state.treeGroups : state.groups, flatIssues: state.activeTab === "trees" ? state.flatTrees : state.flatIssues, selectedIndex: state.activeTab === "trees" ? state.treeSelectedIndex : state.selectedIndex, scrollOffset: state.activeTab === "trees"
3143
3250
  ? state.treeListScrollOffset
3144
- : state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg, deletingIds: state.activeTab === "trees" ? deletingIds : undefined, variant: state.activeTab === "issues" ? "issues" : "default" })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
3251
+ : state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg, deletingIds: state.activeTab === "trees" ? deletingIds : undefined, fixLoops: state.activeTab === "trees" ? state.fixLoopTickets : undefined, variant: state.activeTab === "issues" ? "issues" : "default" })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
3145
3252
  .split("\n")
3146
3253
  .slice(-(contentHeight - 1))
3147
3254
  .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, draft: state.prCreateDraft, dispatch: dispatch })) : state.activeTab === "triage" && !selectedIssue ? (
@@ -3152,7 +3259,7 @@ export default function Dashboard() {
3152
3259
  ? state.treeDetailScrollOffset
3153
3260
  : state.activeTab === "triage"
3154
3261
  ? state.triageDetailScrollOffset
3155
- : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true, triageInvestigateConfigured: triageInvestigateConfigured }) })] })) })] })] }));
3262
+ : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, prSyncing: !!selectedIssue && !!state.pendingPrs[selectedIssue.issue.identifier], fixLoop: selectedIssue ? state.fixLoopTickets[selectedIssue.issue.identifier] : undefined, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true, triageInvestigateConfigured: triageInvestigateConfigured }) })] })) })] })] }));
3156
3263
  }
3157
3264
  /**
3158
3265
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
@@ -83,28 +83,6 @@ function isWorktree(cwd) {
83
83
  function isSantreeWorktree(cwd) {
84
84
  return cwd.includes("/.santree/worktrees/");
85
85
  }
86
- // Read session state from per-ticket state file
87
- function readSessionStateFile(cwd) {
88
- const marker = "/.santree/worktrees/";
89
- const idx = cwd.indexOf(marker);
90
- if (idx === -1)
91
- return null;
92
- const repoRoot = cwd.slice(0, idx);
93
- const rest = cwd.slice(idx + marker.length);
94
- const ticketId = rest.split("/")[0];
95
- if (!ticketId)
96
- return null;
97
- const filePath = path.join(repoRoot, ".santree", "session-states", `${ticketId}.json`);
98
- try {
99
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
100
- if (data.state === "exited")
101
- return null;
102
- return data.state;
103
- }
104
- catch {
105
- return null;
106
- }
107
- }
108
86
  // Extract ticket ID from branch name (e.g., feature/TEAM-123-desc -> TEAM-123)
109
87
  function extractTicketId(branch) {
110
88
  const match = branch.match(/([a-zA-Z]+)-(\d+)/);
@@ -181,14 +159,6 @@ function buildSantreeStatusline(cwd, model, usedPercentage) {
181
159
  // Git changes
182
160
  const changes = getGitChanges(cwd);
183
161
  parts.push(formatChanges(changes));
184
- // Session state
185
- const sessState = readSessionStateFile(cwd);
186
- if (sessState === "waiting") {
187
- parts.push(`${c.red}WAITING${c.reset}`);
188
- }
189
- else if (sessState === "idle") {
190
- parts.push(`${c.yellow}idle${c.reset}`);
191
- }
192
162
  // Model
193
163
  if (model) {
194
164
  parts.push(`${c.blue}${model}${c.reset}`);
@@ -1 +1 @@
1
- export declare const description = "Issue tracker commands (Linear / GitHub)";
1
+ export declare const description = "Issue actions (open the current branch's issue)";
@@ -1 +1 @@
1
- export const description = "Issue tracker commands (Linear / GitHub)";
1
+ export const description = "Issue actions (open the current branch's issue)";
@@ -0,0 +1,7 @@
1
+ export declare const description = "Print the fix-loop iteration brief (state + the exact actions to take)";
2
+ /**
3
+ * Read-only command the auto-fix `/loop` runs each iteration to refresh state.
4
+ * No Ink UI — it writes the rendered brief straight to stdout and exits (same
5
+ * pattern as the statusline helper) so the agent can read it cleanly.
6
+ */
7
+ export default function PRContext(): null;