santree 0.7.5 → 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 (109) 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 +165 -61
  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 +6 -1
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/lib/ai.d.ts +11 -0
  15. package/dist/lib/ai.js +71 -2
  16. package/dist/lib/claude-config.d.ts +18 -4
  17. package/dist/lib/claude-config.js +58 -23
  18. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  19. package/dist/lib/config/TrackerPicker.js +113 -0
  20. package/dist/lib/config/diagnostics.d.ts +37 -0
  21. package/dist/lib/config/diagnostics.js +190 -0
  22. package/dist/lib/config-store.d.ts +23 -0
  23. package/dist/lib/config-store.js +68 -0
  24. package/dist/lib/dashboard/DetailPanel.d.ts +4 -2
  25. package/dist/lib/dashboard/DetailPanel.js +44 -37
  26. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  27. package/dist/lib/dashboard/IssueList.js +24 -5
  28. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  29. package/dist/lib/dashboard/Overlays.js +3 -4
  30. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  31. package/dist/lib/dashboard/ReviewList.js +6 -3
  32. package/dist/lib/dashboard/data.js +7 -34
  33. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  34. package/dist/lib/dashboard/external-editor.js +3 -2
  35. package/dist/lib/dashboard/types.d.ts +13 -2
  36. package/dist/lib/dashboard/types.js +33 -2
  37. package/dist/lib/exec.d.ts +13 -0
  38. package/dist/lib/exec.js +11 -1
  39. package/dist/lib/fix-loop.d.ts +45 -0
  40. package/dist/lib/fix-loop.js +83 -0
  41. package/dist/lib/git.d.ts +3 -29
  42. package/dist/lib/git.js +6 -62
  43. package/dist/lib/github.d.ts +70 -0
  44. package/dist/lib/github.js +128 -0
  45. package/dist/lib/multiplexer/cmux.js +143 -17
  46. package/dist/lib/multiplexer/none.js +3 -3
  47. package/dist/lib/multiplexer/tmux.js +5 -29
  48. package/dist/lib/multiplexer/types.d.ts +34 -1
  49. package/dist/lib/open-url.d.ts +3 -2
  50. package/dist/lib/open-url.js +5 -4
  51. package/dist/lib/prompts.d.ts +35 -1
  52. package/dist/lib/prompts.js +7 -0
  53. package/dist/lib/setup/apply.d.ts +2 -2
  54. package/dist/lib/setup/apply.js +2 -2
  55. package/dist/lib/setup/gitignore.d.ts +7 -0
  56. package/dist/lib/setup/gitignore.js +39 -1
  57. package/dist/lib/setup/steps.d.ts +6 -2
  58. package/dist/lib/setup/steps.js +65 -60
  59. package/dist/lib/setup/tools.d.ts +1 -1
  60. package/dist/lib/setup/tools.js +1 -1
  61. package/dist/lib/trackers/github/index.js +2 -2
  62. package/dist/lib/trackers/local/store.js +1 -1
  63. package/package.json +1 -1
  64. package/prompts/fix-context.njk +79 -0
  65. package/prompts/fix-loop.njk +18 -0
  66. package/prompts/pr.njk +1 -1
  67. package/dist/commands/doctor.d.ts +0 -2
  68. package/dist/commands/doctor.js +0 -545
  69. package/dist/commands/github/auth.d.ts +0 -2
  70. package/dist/commands/github/auth.js +0 -56
  71. package/dist/commands/github/index.d.ts +0 -1
  72. package/dist/commands/github/index.js +0 -1
  73. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  74. package/dist/commands/helpers/english-tutor/index.js +0 -1
  75. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  76. package/dist/commands/helpers/english-tutor/install.js +0 -24
  77. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  78. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  79. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  80. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  81. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  82. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  83. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  84. package/dist/commands/helpers/session-signal/end.js +0 -13
  85. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  86. package/dist/commands/helpers/session-signal/index.js +0 -1
  87. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  88. package/dist/commands/helpers/session-signal/install.js +0 -24
  89. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  90. package/dist/commands/helpers/session-signal/notification.js +0 -13
  91. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  92. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  93. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  94. package/dist/commands/helpers/session-signal/stop.js +0 -13
  95. package/dist/commands/issue/setup.d.ts +0 -2
  96. package/dist/commands/issue/setup.js +0 -108
  97. package/dist/commands/issue/switch.d.ts +0 -12
  98. package/dist/commands/issue/switch.js +0 -38
  99. package/dist/commands/linear/switch.d.ts +0 -2
  100. package/dist/commands/linear/switch.js +0 -75
  101. package/dist/commands/setup.d.ts +0 -11
  102. package/dist/commands/setup.js +0 -194
  103. package/dist/lib/english-tutor.d.ts +0 -13
  104. package/dist/lib/english-tutor.js +0 -125
  105. package/dist/lib/session-signal.d.ts +0 -16
  106. package/dist/lib/session-signal.js +0 -104
  107. package/dist/lib/setup/shell-config.d.ts +0 -38
  108. package/dist/lib/setup/shell-config.js +0 -131
  109. package/prompts/english-tutor-prompt.njk +0 -15
@@ -12,7 +12,7 @@ import { run } from "../exec.js";
12
12
  export const SANTREE_IGNORE_ENTRIES = [
13
13
  ".santree/worktrees/",
14
14
  ".santree/metadata.json",
15
- ".santree/session-states/",
15
+ ".santree/fix-loops/",
16
16
  ];
17
17
  export function ignoreTargetPath(repoRoot, target) {
18
18
  if (target === "exclude") {
@@ -55,3 +55,41 @@ export function addIgnoreEntries(repoRoot, target) {
55
55
  fs.appendFileSync(filePath, prefix + header + toAdd.join("\n") + "\n");
56
56
  return toAdd;
57
57
  }
58
+ /**
59
+ * Remove santree's ignore entries (and an orphaned `# santree` header) from both
60
+ * candidate files. Returns the entries actually removed. Leaves every other line
61
+ * — including user-authored ignores — untouched, and collapses the blank lines
62
+ * left behind so the file doesn't accumulate gaps across enable/disable cycles.
63
+ */
64
+ export function removeIgnoreEntries(repoRoot) {
65
+ const removed = [];
66
+ for (const target of ["gitignore", "exclude"]) {
67
+ const filePath = ignoreTargetPath(repoRoot, target);
68
+ if (!fs.existsSync(filePath))
69
+ continue;
70
+ const original = fs.readFileSync(filePath, "utf-8");
71
+ let changed = false;
72
+ const kept = original.split("\n").filter((line) => {
73
+ const t = line.trim();
74
+ if (SANTREE_IGNORE_ENTRIES.includes(t)) {
75
+ removed.push(t);
76
+ changed = true;
77
+ return false;
78
+ }
79
+ if (t === "# santree") {
80
+ // santree's header only ever sits above its own (now-removed) entries.
81
+ changed = true;
82
+ return false;
83
+ }
84
+ return true;
85
+ });
86
+ if (!changed)
87
+ continue;
88
+ const cleaned = kept
89
+ .join("\n")
90
+ .replace(/\n{3,}/g, "\n\n")
91
+ .replace(/^\n+/, "");
92
+ fs.writeFileSync(filePath, cleaned.length === 0 ? "" : cleaned.replace(/\n*$/, "\n"));
93
+ }
94
+ return removed;
95
+ }
@@ -1,4 +1,3 @@
1
- import { type ShellConfig } from "./shell-config.js";
2
1
  export type DetectState = "ok" | "actionable" | "unavailable";
3
2
  export interface StepOption {
4
3
  value: string;
@@ -21,10 +20,15 @@ export interface SetupStep {
21
20
  options?: StepOption[];
22
21
  optionPrompt?: string;
23
22
  apply: (choice: string | undefined) => StepResult | Promise<StepResult>;
23
+ /**
24
+ * Reverse `apply`, returning the step to its un-configured state. Present only
25
+ * on steps santree fully owns and can cleanly undo (statusline, remote control,
26
+ * gitignore). Steps without it have no "off" in the panel.
27
+ */
28
+ unapply?: (choice: string | undefined) => StepResult | Promise<StepResult>;
24
29
  }
25
30
  export interface SetupContext {
26
31
  repoRoot: string | null;
27
- shell: ShellConfig;
28
32
  dryRun: boolean;
29
33
  }
30
34
  export declare function buildContext(dryRun: boolean): SetupContext;
@@ -5,16 +5,14 @@ import { isRepoTrackerConfigured } from "../trackers/index.js";
5
5
  import { getMultiplexer } from "../multiplexer/index.js";
6
6
  import { resolveClaudeBinary } from "../ai.js";
7
7
  import { CLAUDE_CODE_PACKAGE, detectPackageManager, getInstallCommandFor } from "../version.js";
8
- import { installHooks as installSessionSignalHooks } from "../session-signal.js";
9
- import { isStatuslineConfigured, configureStatusline, isRemoteControlEnabled, enableRemoteControl, isSessionSignalConfigured, } from "../claude-config.js";
10
- import { resolveShellConfig, upsertManagedLine, isEnvVarSet, envKey, } from "./shell-config.js";
8
+ import { isStatuslineConfigured, configureStatusline, removeStatusline, isRemoteControlEnabled, enableRemoteControl, disableRemoteControl, } from "../claude-config.js";
9
+ import { setConfigValue, getConfiguredEditor, getConfiguredDiffTool, configStorePath, } from "../config-store.js";
11
10
  import { detectDiffPagers, detectEditors, getInstaller, which } from "./tools.js";
12
- import { missingIgnoreEntries, addIgnoreEntries, SANTREE_IGNORE_ENTRIES, } from "./gitignore.js";
13
- import { spawnTTY, santreeSelfArgv } from "./apply.js";
11
+ import { missingIgnoreEntries, addIgnoreEntries, removeIgnoreEntries, SANTREE_IGNORE_ENTRIES, } from "./gitignore.js";
12
+ import { spawnTTY } from "./apply.js";
14
13
  export function buildContext(dryRun) {
15
14
  return {
16
15
  repoRoot: findMainRepoRoot(),
17
- shell: resolveShellConfig(),
18
16
  dryRun,
19
17
  };
20
18
  }
@@ -24,17 +22,20 @@ export function buildContext(dryRun) {
24
22
  */
25
23
  export function buildSteps(ctx) {
26
24
  const steps = [];
27
- const { shell, repoRoot, dryRun } = ctx;
25
+ const { repoRoot, dryRun } = ctx;
28
26
  const installer = getInstaller();
29
- // ── Editor (SANTREE_EDITOR) ─────────────────────────────────────────────────
27
+ // ── Editor ──────────────────────────────────────────────────────────────────
30
28
  {
31
29
  const editors = detectEditors();
32
- const set = isEnvVarSet("SANTREE_EDITOR", shell);
33
- const detect = set ? "ok" : editors.length > 0 ? "actionable" : "unavailable";
30
+ const detect = getConfiguredEditor()
31
+ ? "ok"
32
+ : editors.length > 0
33
+ ? "actionable"
34
+ : "unavailable";
34
35
  steps.push({
35
36
  id: "editor",
36
- title: "Default editor (SANTREE_EDITOR)",
37
- detail: "Editor opened by the dashboard's [e] action",
37
+ title: "Default editor",
38
+ detail: "Editor opened by the dashboard's [e] action (stored in santree's config)",
38
39
  scope: "global",
39
40
  kind: "file",
40
41
  recommended: true,
@@ -44,13 +45,13 @@ export function buildSteps(ctx) {
44
45
  apply: (choice) => {
45
46
  const editor = choice || editors[0]?.command || "vim";
46
47
  if (dryRun)
47
- return { ok: true, message: `Would set SANTREE_EDITOR=${editor}` };
48
- upsertManagedLine(shell.rcPath, envKey("SANTREE_EDITOR"), `export SANTREE_EDITOR="${editor}"`);
49
- return { ok: true, message: `Set SANTREE_EDITOR=${editor} in ${shell.rcPath}` };
48
+ return { ok: true, message: `Would set editor=${editor}` };
49
+ setConfigValue("editor", editor);
50
+ return { ok: true, message: `Set editor to ${editor} in ${configStorePath()}` };
50
51
  },
51
52
  });
52
53
  }
53
- // ── Diff tool (SANTREE_DIFF_TOOL) ───────────────────────────────────────────
54
+ // ── Diff tool ───────────────────────────────────────────────────────────────
54
55
  {
55
56
  const pagers = detectDiffPagers();
56
57
  const hasDelta = pagers.some((p) => p.command === "delta");
@@ -61,12 +62,15 @@ export function buildSteps(ctx) {
61
62
  label: `git-delta — install via ${installer.name}`,
62
63
  });
63
64
  }
64
- const set = isEnvVarSet("SANTREE_DIFF_TOOL", shell);
65
- const detect = set ? "ok" : options.length > 0 ? "actionable" : "unavailable";
65
+ const detect = getConfiguredDiffTool()
66
+ ? "ok"
67
+ : options.length > 0
68
+ ? "actionable"
69
+ : "unavailable";
66
70
  steps.push({
67
71
  id: "diff-tool",
68
- title: "Diff tool (SANTREE_DIFF_TOOL)",
69
- detail: "Pretty diffs in `worktree diff` and the dashboard [v] overlay",
72
+ title: "Diff tool",
73
+ detail: "Pretty diffs in `worktree diff` and the dashboard [v] overlay (stored in santree's config)",
70
74
  scope: "global",
71
75
  kind: installer && !hasDelta ? "spawn" : "file",
72
76
  recommended: true,
@@ -80,29 +84,33 @@ export function buildSteps(ctx) {
80
84
  let tool = pick;
81
85
  if (pick.startsWith("install:")) {
82
86
  tool = pick.slice("install:".length);
87
+ // `install:` options are only offered when an installer exists, but
88
+ // guard explicitly so a broken invariant degrades instead of NPEs.
89
+ if (!installer)
90
+ return { ok: false, message: "No installer available for git-delta" };
83
91
  if (dryRun) {
84
92
  return {
85
93
  ok: true,
86
- message: `Would ${installer.installArgv("git-delta").join(" ")} then set SANTREE_DIFF_TOOL=${tool}`,
94
+ message: `Would ${installer.installArgv("git-delta").join(" ")} then set diffTool=${tool}`,
87
95
  };
88
96
  }
89
97
  const argv = installer.installArgv("git-delta");
90
98
  const code = spawnTTY(argv[0], argv.slice(1));
91
99
  if (code !== 0)
92
- return { ok: false, message: "git-delta install failed — SANTREE_DIFF_TOOL not set" };
100
+ return { ok: false, message: "git-delta install failed — diff tool not set" };
93
101
  }
94
102
  if (dryRun)
95
- return { ok: true, message: `Would set SANTREE_DIFF_TOOL=${tool}` };
96
- upsertManagedLine(shell.rcPath, envKey("SANTREE_DIFF_TOOL"), `export SANTREE_DIFF_TOOL="${tool}"`);
97
- return { ok: true, message: `Set SANTREE_DIFF_TOOL=${tool} in ${shell.rcPath}` };
103
+ return { ok: true, message: `Would set diffTool=${tool}` };
104
+ setConfigValue("diffTool", tool);
105
+ return { ok: true, message: `Set diff tool to ${tool} in ${configStorePath()}` };
98
106
  },
99
107
  });
100
108
  }
101
109
  // ── Statusline ──────────────────────────────────────────────────────────────
102
110
  steps.push({
103
111
  id: "statusline",
104
- title: "Claude Code statusline",
105
- detail: "Worktree-aware statusline (branch, context usage, session state)",
112
+ title: "Statusline",
113
+ detail: "Worktree-aware statusline (branch, git changes, context usage)",
106
114
  scope: "global",
107
115
  kind: "file",
108
116
  recommended: true,
@@ -113,30 +121,17 @@ export function buildSteps(ctx) {
113
121
  const p = configureStatusline();
114
122
  return { ok: true, message: `Configured statusline in ${p}` };
115
123
  },
116
- });
117
- // ── Session-signal hooks ────────────────────────────────────────────────────
118
- steps.push({
119
- id: "session-signal",
120
- title: "Session-state signal hooks",
121
- detail: "Surface Claude session state (waiting/active/idle) in the dashboard",
122
- scope: "global",
123
- kind: "file",
124
- recommended: true,
125
- detect: isSessionSignalConfigured() ? "ok" : "actionable",
126
- apply: () => {
124
+ unapply: () => {
127
125
  if (dryRun)
128
- return {
129
- ok: true,
130
- message: "Would install session-signal hooks in ~/.claude/settings.json",
131
- };
132
- const p = installSessionSignalHooks();
133
- return { ok: true, message: `Installed session-signal hooks in ${p}` };
126
+ return { ok: true, message: "Would remove statusLine from ~/.claude/settings.json" };
127
+ const p = removeStatusline();
128
+ return { ok: true, message: `Removed statusline from ${p}` };
134
129
  },
135
130
  });
136
131
  // ── Remote control ──────────────────────────────────────────────────────────
137
132
  steps.push({
138
133
  id: "remote-control",
139
- title: "Claude Code remote control",
134
+ title: "Remote control",
140
135
  detail: "Enable remote control at startup (drive sessions from the dashboard)",
141
136
  scope: "global",
142
137
  kind: "file",
@@ -148,6 +143,12 @@ export function buildSteps(ctx) {
148
143
  const p = enableRemoteControl();
149
144
  return { ok: true, message: `Enabled remote control in ${p}` };
150
145
  },
146
+ unapply: () => {
147
+ if (dryRun)
148
+ return { ok: true, message: "Would unset remoteControlAtStartup in ~/.claude.json" };
149
+ const p = disableRemoteControl();
150
+ return { ok: true, message: `Disabled remote control in ${p}` };
151
+ },
151
152
  });
152
153
  // ── GitHub CLI install + auth ───────────────────────────────────────────────
153
154
  {
@@ -161,7 +162,7 @@ export function buildSteps(ctx) {
161
162
  : "unavailable";
162
163
  steps.push({
163
164
  id: "gh",
164
- title: "GitHub CLI (gh) auth",
165
+ title: "GitHub CLI",
165
166
  detail: "Required for PR create / review / checks",
166
167
  scope: "global",
167
168
  kind: "spawn",
@@ -192,7 +193,7 @@ export function buildSteps(ctx) {
192
193
  const installed = !!resolveClaudeBinary();
193
194
  steps.push({
194
195
  id: "claude",
195
- title: "Claude Code CLI",
196
+ title: "Claude Code",
196
197
  detail: "Powers `worktree work`, `pr fix`, `pr review`",
197
198
  scope: "global",
198
199
  kind: "spawn",
@@ -215,7 +216,7 @@ export function buildSteps(ctx) {
215
216
  const detect = active ? "ok" : installer ? "actionable" : "unavailable";
216
217
  steps.push({
217
218
  id: "tmux",
218
- title: "tmux (terminal multiplexer)",
219
+ title: "tmux",
219
220
  detail: "Enables new-window flows: work / fix / review / investigate",
220
221
  scope: "global",
221
222
  kind: "spawn",
@@ -252,7 +253,7 @@ export function buildSteps(ctx) {
252
253
  }
253
254
  steps.push({
254
255
  id: "scaffold",
255
- title: ".santree scaffold",
256
+ title: "Repo scaffold",
256
257
  detail: "Create .santree/ and an executable init.sh (runs on worktree create)",
257
258
  scope: "repo",
258
259
  kind: "file",
@@ -275,7 +276,7 @@ export function buildSteps(ctx) {
275
276
  const missing = missingIgnoreEntries(repoRoot);
276
277
  steps.push({
277
278
  id: "gitignore",
278
- title: "Ignore santree's machine files",
279
+ title: "Gitignore entries",
279
280
  detail: `Ignore ${SANTREE_IGNORE_ENTRIES.join(", ")} (keeps .santree/issues/ tracked)`,
280
281
  scope: "repo",
281
282
  kind: "file",
@@ -300,6 +301,17 @@ export function buildSteps(ctx) {
300
301
  message: `Added ${added.length} entr${added.length === 1 ? "y" : "ies"} to ${where}`,
301
302
  };
302
303
  },
304
+ unapply: () => {
305
+ if (dryRun)
306
+ return { ok: true, message: "Would remove santree's ignore entries" };
307
+ const removed = removeIgnoreEntries(repoRoot);
308
+ return {
309
+ ok: true,
310
+ message: removed.length
311
+ ? `Removed ${removed.length} entr${removed.length === 1 ? "y" : "ies"}`
312
+ : "No santree ignore entries to remove",
313
+ };
314
+ },
303
315
  });
304
316
  }
305
317
  // Issue tracker
@@ -309,18 +321,11 @@ export function buildSteps(ctx) {
309
321
  title: "Issue tracker",
310
322
  detail: "Pick + authenticate Linear / GitHub / Local for this repo",
311
323
  scope: "repo",
312
- kind: "spawn",
324
+ kind: "file",
313
325
  recommended: true,
314
326
  detect: isRepoTrackerConfigured(repoRoot) ? "ok" : "actionable",
315
- apply: () => {
316
- if (dryRun)
317
- return { ok: true, message: "Would run: santree issue setup" };
318
- const { cmd, args } = santreeSelfArgv(["issue", "setup"]);
319
- const code = spawnTTY(cmd, args, { cwd: repoRoot });
320
- return code === 0
321
- ? { ok: true, message: "Issue tracker configured" }
322
- : { ok: false, message: "Tracker setup did not complete" };
323
- },
327
+ // Driven by the config panel's inline TrackerPicker; apply is unused.
328
+ apply: () => ({ ok: true, message: "Pick a tracker from the config panel" }),
324
329
  });
325
330
  }
326
331
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tool detection + installation for `santree setup`.
2
+ * Tool detection + installation for `santree config`.
3
3
  *
4
4
  * macOS (Homebrew) is the only auto-install path today. The `PlatformInstaller`
5
5
  * seam keeps the door open for apt/dnf/etc. without touching call sites — add a
@@ -1,7 +1,7 @@
1
1
  import * as os from "os";
2
2
  import { run } from "../exec.js";
3
3
  /**
4
- * Tool detection + installation for `santree setup`.
4
+ * Tool detection + installation for `santree config`.
5
5
  *
6
6
  * macOS (Homebrew) is the only auto-install path today. The `PlatformInstaller`
7
7
  * seam keeps the door open for apt/dnf/etc. without touching call sites — add a
@@ -4,7 +4,7 @@ import { cleanupGithubImages, rewriteGithubImages } from "./images.js";
4
4
  async function getAuthStatus(_repoRoot) {
5
5
  const user = await getAuthenticatedUser();
6
6
  if (!user) {
7
- return { authenticated: false, hint: "Run: santree github auth (or: gh auth login)" };
7
+ return { authenticated: false, hint: "GitHub CLI not authenticated. Run: gh auth login" };
8
8
  }
9
9
  return {
10
10
  authenticated: true,
@@ -33,7 +33,7 @@ async function listAssigned(repoRoot) {
33
33
  return {
34
34
  ok: false,
35
35
  reason: "unauthenticated",
36
- message: "Run: santree github auth (or: gh auth login)",
36
+ message: "GitHub CLI not authenticated. Run: gh auth login",
37
37
  };
38
38
  }
39
39
  const nwo = await getCurrentRepoNwo(repoRoot);
@@ -4,7 +4,7 @@ import { getSantreeDir, readAllMetadata, writeAllMetadata } from "../../metadata
4
4
  import { parseFrontmatter, serializeFrontmatter } from "./frontmatter.js";
5
5
  // On-disk layout: one Markdown file per issue under `.santree/issues/`,
6
6
  // named `<ID>.md` (e.g. `LOCAL-1.md`). `.santree/issues/` is NOT in
7
- // .gitignore (which only excludes worktrees/metadata.json/session-states),
7
+ // .gitignore (which only excludes worktrees/metadata.json/fix-loops),
8
8
  // so issue files are version-controlled by default — the whole point of the
9
9
  // built-in tracker.
10
10
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",
@@ -0,0 +1,79 @@
1
+ ## PR #{{ pr_number }} — fix-loop iteration brief
2
+ Branch: `{{ branch }}` → base: `{{ base_branch }}`
3
+ {% if pr_url %}{{ pr_url }}{% endif %}
4
+
5
+ ### Mergeability
6
+ {% if conflicts -%}
7
+ ⚠️ **CONFLICTS** with base `{{ base_branch }}` (mergeable={{ mergeable }}, state={{ merge_state }}).
8
+ {%- elif mergeable == "UNKNOWN" -%}
9
+ Mergeability not yet computed by GitHub (UNKNOWN) — do **not** assume conflicts.
10
+ {%- else -%}
11
+ No conflicts (mergeable={{ mergeable }}, state={{ merge_state }}).
12
+ {%- endif %}
13
+
14
+ ### Failing checks — {{ fixable_count }} fixable, {{ manual_count }} manual{% if pending_count %}, {{ pending_count }} still pending{% endif %}
15
+ {% if failed_checks | length -%}
16
+ {% for c in failed_checks %}
17
+ - **[{{ "FIXABLE" if c.fixable else "MANUAL" }}]** {{ c.name }}{% if c.workflow %} ({{ c.workflow }}){% endif %}{% if c.failed_step %} — failed step: `{{ c.failed_step }}`{% endif %}
18
+ {%- if c.fixable and c.log %}
19
+ ```
20
+ {{ c.log }}
21
+ ```
22
+ {%- endif %}
23
+ {%- endfor %}
24
+ {%- else -%}
25
+ No failing checks.
26
+ {%- endif %}
27
+
28
+ {% if reviews and reviews | length -%}
29
+ ### Reviews
30
+ {% for r in reviews %}- **{{ r.author.login }}**: {{ r.state }}{% if r.body %} — {{ r.body }}{% endif %}
31
+ {% endfor %}
32
+ {%- endif %}
33
+ ### Approved review comments — {{ approved_comments | length }}
34
+ {% if approved_comments | length -%}
35
+ Unresolved threads you've 👍-approved (these — and only these — are in scope):
36
+ {% for t in approved_comments %}
37
+ - **Thread** `{{ t.id }}` on `{{ t.path }}{% if t.line %}:{{ t.line }}{% endif %}`{% if t.isOutdated %} *(outdated — the diff moved; find the equivalent code)*{% endif %}
38
+ {% for c in t.comments %} - **{{ c.author }}**: {{ c.body }}
39
+ {% endfor %}{% endfor %}
40
+ {%- else -%}
41
+ None approved. Only unresolved threads whose **last comment you've 👍-reacted to** are applied — leave a 👍 to approve one.
42
+ {%- endif %}
43
+
44
+ ## ➡️ Do this now
45
+ {% if directive == "merge" -%}
46
+ The PR **conflicts** with `{{ base_branch }}` — resolve before anything else:
47
+ 1. `{{ santree_cmd }} pr fix --signal merging`
48
+ 2. `git fetch origin` then `git merge origin/{{ base_branch }}`
49
+ 3. Resolve every conflict (preserve both sides' intent) and commit the merge.
50
+ 4. `git push`, then `{{ santree_cmd }} pr fix --signal waiting-ci`.
51
+
52
+ → Schedule the next iteration ~5 min out and end your turn.
53
+ {%- elif directive == "work" -%}
54
+ Run `{{ santree_cmd }} pr fix --signal fixing`, then:
55
+ {% if fixable_count -%}
56
+ - Fix the **[FIXABLE]** checks above. Run the matching local command (test / lint / typecheck / formatter) to confirm each passes **locally** before pushing. Never touch **[MANUAL]** checks.
57
+ {% endif -%}
58
+ {% if approved_comments | length -%}
59
+ - Apply each **approved review thread** above (read the whole thread for intent). After pushing, resolve each one so it won't reappear:
60
+ ```
61
+ gh api graphql -f query='mutation($t:ID!){resolveReviewThread(input:{threadId:$t}){thread{id}}}' -f t='<thread id>'
62
+ ```
63
+ {% endif -%}
64
+ Then `git push` and `{{ santree_cmd }} pr fix --signal waiting-ci`.
65
+
66
+ → Schedule the next iteration ~5 min out and end your turn.
67
+ {%- elif directive == "wait" -%}
68
+ Nothing to fix right now{% if pending_count %} — {{ pending_count }} check(s) still running{% endif %}{% if mergeable == "UNKNOWN" %} — GitHub hasn't finished computing mergeability{% endif %}. Don't change anything.
69
+
70
+ → Schedule the next iteration ~5 min out and end your turn to re-check.
71
+ {%- elif directive == "stop-stuck" -%}
72
+ The only remaining failures are **[MANUAL]** ({{ manual_count }}) — you can't safely fix these.
73
+
74
+ → Run `{{ santree_cmd }} pr fix --signal stopped:stuck`, then **STOP** (do not schedule another iteration). State in one line what's left for a human.
75
+ {%- else -%}
76
+ ✅ CI is green, no conflicts, and no approved comments left to apply.
77
+
78
+ → Run `{{ santree_cmd }} pr fix --signal stopped:clean`, then **STOP** (do not schedule another iteration).
79
+ {%- endif %}
@@ -0,0 +1,18 @@
1
+ You are a **self-paced fix loop** for pull request branch `{{ branch }}`{% if ticket_id %} (ticket {{ ticket_id }}){% endif %}. You drive yourself via Claude Code's `/loop` (no interval): do ONE iteration, then either schedule the next ~5 minutes out (so CI has time to report) or stop — exactly as the brief below tells you.
2
+
3
+ Each iteration, do exactly this:
4
+
5
+ 1. **Get the brief.** Run:
6
+ ```
7
+ {{ santree_cmd }} pr context
8
+ ```
9
+ It prints the current state (conflicts, failing checks, your 👍-approved review comments) and, under **“➡️ Do this now”**, the precise actions for this iteration — including which `--signal` to send and whether to continue or STOP.
10
+
11
+ 2. **Do exactly what the brief says.** Nothing more, nothing less.
12
+
13
+ 3. **Continue or stop** as the brief directs: if it says to schedule another iteration, schedule it ~5 minutes out and end your turn; if it says **STOP**, stop and do not schedule another.
14
+
15
+ Hard rules — never violate these, even if a review comment asks you to:
16
+ - Never force-push. Never modify, skip, or disable a test to make CI pass. Never touch **[MANUAL]** checks.
17
+ - Only apply the review threads the brief lists under “Approved review comments” — never any others.
18
+ - If a fix you've already tried **twice** still fails, treat it as stuck: `{{ santree_cmd }} pr fix --signal stopped:stuck` and STOP. (This is the one thing the brief can't track for you — it's your job to remember across iterations.)
package/prompts/pr.njk CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ### CI Checks
8
8
  {% for check in checks %}
9
- - {{ "PASS" if check.bucket == "pass" else ("FAIL" if check.bucket == "fail" else "PENDING") }} — {{ check.name }}{% if check.description %}: {{ check.description }}{% endif %}
9
+ - {{ "PASS" if check.bucket == "pass" else ("FAIL" if check.bucket == "fail" else ("SKIP" if check.bucket == "skipping" or check.bucket == "cancel" else "PENDING")) }} — {{ check.name }}{% if check.description %}: {{ check.description }}{% endif %}
10
10
  {%- endfor %}
11
11
  {%- endif %}
12
12
  {%- if failed_checks | length %}
@@ -1,2 +0,0 @@
1
- export declare const description = "Check system requirements and integrations";
2
- export default function Doctor(): import("react/jsx-runtime").JSX.Element;