@shmulikdav/solix 1.0.3 → 1.1.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
@@ -218,6 +218,19 @@ async function demoCmd(opts = {}) {
218
218
  console.log(` \u2022 1 planet at 87% context (orange flare)`);
219
219
  console.log(` \u2022 Compass pinned (always-on)`);
220
220
  console.log(`[solix demo] open ${BASE2} to see it.`);
221
+ console.log(
222
+ `[solix demo] in ~3s: Compass will be invoked on demo-a (watch for the toast + Audit tab row).`
223
+ );
224
+ await sleep(3e3);
225
+ await fetch(`${BASE2}/api/advisors/compass/invoke`, {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify({
229
+ targetSessionId: "demo-a",
230
+ prompt: "Review the orbital math refactor before merging"
231
+ })
232
+ });
233
+ console.log(`[solix demo] Compass invoked. Demo complete.`);
221
234
  }
222
235
 
223
236
  // src/doctor.ts
@@ -913,7 +926,9 @@ function getDb() {
913
926
  db.exec(SCHEMA);
914
927
  ensureColumn(db, "sessions", "kind", "kind TEXT NOT NULL DEFAULT 'user'");
915
928
  ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
929
+ ensureColumn(db, "sessions", "worktree_path", "worktree_path TEXT");
916
930
  ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
931
+ ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
917
932
  _db = db;
918
933
  return db;
919
934
  }
@@ -997,7 +1012,8 @@ function rowToSession(row) {
997
1012
  currentMissionId: row.current_mission_id ?? void 0,
998
1013
  lastCompletedMissionId: row.last_completed_mission_id ?? void 0,
999
1014
  orbitSlot: row.orbit_slot,
1000
- name: row.name ?? void 0
1015
+ name: row.name ?? void 0,
1016
+ worktreePath: row.worktree_path ?? void 0
1001
1017
  };
1002
1018
  }
1003
1019
  function nextOrbitSlot(db, projectId) {
@@ -1032,9 +1048,9 @@ function upsertSession(db, input) {
1032
1048
  `INSERT INTO sessions (
1033
1049
  id, pid, project_id, parent_session_id, origin, model, status,
1034
1050
  context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
1035
- created_at, updated_at
1051
+ worktree_path, created_at, updated_at
1036
1052
  )
1037
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?)`
1053
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?)`
1038
1054
  ).run(
1039
1055
  input.id,
1040
1056
  input.pid,
@@ -1047,6 +1063,7 @@ function upsertSession(db, input) {
1047
1063
  input.cwd,
1048
1064
  kind,
1049
1065
  input.advisorRole ?? null,
1066
+ input.worktreePath ?? null,
1050
1067
  ts2,
1051
1068
  ts2
1052
1069
  );
@@ -1064,7 +1081,8 @@ function upsertSession(db, input) {
1064
1081
  advisorRole: input.advisorRole,
1065
1082
  parentSessionId: input.parentSessionId,
1066
1083
  contextUsagePct: 0,
1067
- orbitSlot
1084
+ orbitSlot,
1085
+ worktreePath: input.worktreePath
1068
1086
  };
1069
1087
  }
1070
1088
  function setSessionStatus(db, sessionId, status) {
@@ -1142,9 +1160,16 @@ function rowToMission(row) {
1142
1160
  subagentCount: row.subagent_count,
1143
1161
  toolCallCount: row.tool_call_count
1144
1162
  },
1145
- filesTouched
1163
+ filesTouched,
1164
+ errorSummary: row.error_summary ?? void 0
1146
1165
  };
1147
1166
  }
1167
+ function setMissionError(db, missionId, errorSummary) {
1168
+ db.prepare(
1169
+ `UPDATE missions SET error_summary = ? WHERE id = ?`
1170
+ ).run(errorSummary, missionId);
1171
+ return getMission(db, missionId);
1172
+ }
1148
1173
  function shortNameFromPrompt(prompt) {
1149
1174
  const words = prompt.trim().split(/\s+/).slice(0, 3);
1150
1175
  if (!words.length) return "New Mission";
@@ -2388,9 +2413,51 @@ function mimeFor(filePath) {
2388
2413
  }
2389
2414
 
2390
2415
  // ../server/src/launcher.ts
2391
- import { spawn } from "child_process";
2392
- import { existsSync as existsSync7 } from "fs";
2416
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
2417
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
2418
+ import { homedir as homedir5 } from "os";
2419
+ import { basename as basename2, join as join9 } from "path";
2393
2420
  import { nanoid as nanoid4 } from "nanoid";
2421
+ function ensureWorktree(opts) {
2422
+ const repoRoot = (() => {
2423
+ const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
2424
+ cwd: opts.repoCwd,
2425
+ encoding: "utf8"
2426
+ });
2427
+ if (r.status !== 0) {
2428
+ throw new Error(`not a git repository: ${opts.repoCwd}`);
2429
+ }
2430
+ return (r.stdout ?? "").trim();
2431
+ })();
2432
+ const repoName = basename2(repoRoot);
2433
+ const safeBranch = opts.branch.replace(/[^a-zA-Z0-9._-]+/g, "-");
2434
+ const worktreesDir = join9(homedir5(), ".solix", "worktrees");
2435
+ const path = join9(worktreesDir, `${repoName}-${safeBranch}`);
2436
+ const list = spawnSync2("git", ["worktree", "list", "--porcelain"], {
2437
+ cwd: repoRoot,
2438
+ encoding: "utf8"
2439
+ });
2440
+ if (list.status === 0 && (list.stdout ?? "").includes(`worktree ${path}`)) {
2441
+ return { path, created: false };
2442
+ }
2443
+ mkdirSync3(worktreesDir, { recursive: true });
2444
+ const branchProbe = spawnSync2(
2445
+ "git",
2446
+ ["rev-parse", "--verify", "--quiet", `refs/heads/${opts.branch}`],
2447
+ { cwd: repoRoot, encoding: "utf8" }
2448
+ );
2449
+ const args = branchProbe.status === 0 ? ["worktree", "add", path, opts.branch] : ["worktree", "add", path, "-b", opts.branch, opts.baseRef ?? "HEAD"];
2450
+ const add = spawnSync2("git", args, {
2451
+ cwd: repoRoot,
2452
+ encoding: "utf8"
2453
+ });
2454
+ if (add.status !== 0) {
2455
+ throw new Error(
2456
+ `git worktree add failed: ${(add.stderr ?? "").slice(0, 280)}`
2457
+ );
2458
+ }
2459
+ return { path, created: true };
2460
+ }
2394
2461
  var FAKE_CLAUDE = process.env.SOLIX_FAKE_CLAUDE === "1";
2395
2462
  var Launcher = class {
2396
2463
  constructor(db, broadcaster) {
@@ -2544,14 +2611,44 @@ var Launcher = class {
2544
2611
  */
2545
2612
  launch(opts) {
2546
2613
  if (!opts.initialPrompt.trim()) return { ok: false };
2614
+ let spawnCwd = opts.cwd;
2615
+ let worktreePath;
2616
+ if (opts.worktreeBranch?.trim()) {
2617
+ try {
2618
+ const wt = ensureWorktree({
2619
+ repoCwd: opts.cwd,
2620
+ branch: opts.worktreeBranch.trim(),
2621
+ baseRef: opts.worktreeBaseRef?.trim() || void 0
2622
+ });
2623
+ spawnCwd = wt.path;
2624
+ worktreePath = wt.path;
2625
+ this.broadcaster.broadcast({
2626
+ type: "toast",
2627
+ level: "info",
2628
+ message: wt.created ? `Worktree created at ${wt.path}` : `Reusing worktree ${wt.path}`
2629
+ });
2630
+ } catch (err) {
2631
+ this.broadcaster.broadcast({
2632
+ type: "toast",
2633
+ level: "error",
2634
+ message: `Worktree setup failed: ${err.message}`
2635
+ });
2636
+ return { ok: false };
2637
+ }
2638
+ }
2547
2639
  if (FAKE_CLAUDE) {
2548
- return this.launchSynthetic(opts);
2640
+ return this.launchSynthetic({
2641
+ cwd: spawnCwd,
2642
+ model: opts.model,
2643
+ initialPrompt: opts.initialPrompt,
2644
+ worktreePath
2645
+ });
2549
2646
  }
2550
- if (!existsSync7(opts.cwd)) {
2647
+ if (!existsSync7(spawnCwd)) {
2551
2648
  this.broadcaster.broadcast({
2552
2649
  type: "toast",
2553
2650
  level: "error",
2554
- message: `Launch failed: cwd does not exist (${opts.cwd})`
2651
+ message: `Launch failed: cwd does not exist (${spawnCwd})`
2555
2652
  });
2556
2653
  return { ok: false };
2557
2654
  }
@@ -2561,9 +2658,10 @@ var Launcher = class {
2561
2658
  const sessionId = `task-${nanoid4(8)}`;
2562
2659
  return this.spawnPrint({
2563
2660
  sessionId,
2564
- cwd: opts.cwd,
2661
+ cwd: spawnCwd,
2565
2662
  args,
2566
- isFollowUp: false
2663
+ isFollowUp: false,
2664
+ worktreePath
2567
2665
  });
2568
2666
  }
2569
2667
  sendPromptToInternal(sessionId, text) {
@@ -2609,6 +2707,15 @@ var Launcher = class {
2609
2707
  isFollowUp: true
2610
2708
  }).ok;
2611
2709
  }
2710
+ /** Returns the worktree path the launcher resolved for an internal task,
2711
+ * if any. Used by router.onSessionStart to persist worktree_path on the
2712
+ * session row when claude reports its session_start hook. */
2713
+ worktreePathForInternalCwd(cwd) {
2714
+ for (const rec of this.internalTasks.values()) {
2715
+ if (rec.cwd === cwd && rec.worktreePath) return rec.worktreePath;
2716
+ }
2717
+ return void 0;
2718
+ }
2612
2719
  spawnPrint(opts) {
2613
2720
  let child;
2614
2721
  try {
@@ -2628,7 +2735,10 @@ var Launcher = class {
2628
2735
  }
2629
2736
  const pid = child.pid ?? 0;
2630
2737
  if (!opts.isFollowUp) {
2631
- this.internalTasks.set(opts.sessionId, { cwd: opts.cwd });
2738
+ this.internalTasks.set(opts.sessionId, {
2739
+ cwd: opts.cwd,
2740
+ worktreePath: opts.worktreePath
2741
+ });
2632
2742
  }
2633
2743
  let stdout = "";
2634
2744
  let stderr = "";
@@ -2681,7 +2791,8 @@ var Launcher = class {
2681
2791
  projectId: project.id,
2682
2792
  cwd: opts.cwd,
2683
2793
  origin: "internal",
2684
- model: opts.model ?? "sonnet"
2794
+ model: opts.model ?? "sonnet",
2795
+ worktreePath: opts.worktreePath
2685
2796
  });
2686
2797
  const active = setSessionStatus(this.db, sessionId, "active");
2687
2798
  if (active)
@@ -2824,6 +2935,7 @@ var EventRouter = class {
2824
2935
  const project = ensureProject(this.db, event.cwd);
2825
2936
  const sessionId = this.extractSessionId(event);
2826
2937
  const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
2938
+ const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
2827
2939
  const session = upsertSession(this.db, {
2828
2940
  id: sessionId,
2829
2941
  pid: event.pid,
@@ -2833,7 +2945,8 @@ var EventRouter = class {
2833
2945
  model: this.extractModel(event),
2834
2946
  parentSessionId: this.extractParentSessionId(event),
2835
2947
  kind: advisorRole ? "advisor" : "user",
2836
- advisorRole
2948
+ advisorRole,
2949
+ worktreePath
2837
2950
  });
2838
2951
  this.broadcaster.broadcast({ type: "session_upsert", session });
2839
2952
  if (!session.parentSessionId) {
@@ -2959,9 +3072,18 @@ var EventRouter = class {
2959
3072
  const session = getSession(this.db, sessionId);
2960
3073
  if (!session?.currentMissionId) return;
2961
3074
  bumpToolCallCount(this.db, session.currentMissionId);
2962
- const mission = getMission(this.db, session.currentMissionId);
2963
- if (mission)
2964
- this.broadcaster.broadcast({ type: "mission_upsert", mission });
3075
+ const payload = event.payload;
3076
+ const toolResponse = payload.tool_response;
3077
+ let updated = getMission(this.db, session.currentMissionId);
3078
+ if (toolResponse?.is_error) {
3079
+ const raw = typeof toolResponse.content === "string" ? toolResponse.content : JSON.stringify(toolResponse.content ?? "");
3080
+ const summary = raw.slice(0, 280).replace(/\s+/g, " ").trim();
3081
+ if (summary) {
3082
+ updated = setMissionError(this.db, session.currentMissionId, summary);
3083
+ }
3084
+ }
3085
+ if (updated)
3086
+ this.broadcaster.broadcast({ type: "mission_upsert", mission: updated });
2965
3087
  }
2966
3088
  onNotification(event) {
2967
3089
  const sessionId = this.extractSessionId(event);
@@ -3102,7 +3224,9 @@ var EventRouter = class {
3102
3224
  return this.launcher.launch({
3103
3225
  cwd: opts.cwd,
3104
3226
  model: opts.model,
3105
- initialPrompt: opts.initialPrompt
3227
+ initialPrompt: opts.initialPrompt,
3228
+ worktreeBranch: opts.worktreeBranch,
3229
+ worktreeBaseRef: opts.worktreeBaseRef
3106
3230
  });
3107
3231
  }
3108
3232
  sendPromptToSession(sessionId, text) {
@@ -3214,7 +3338,9 @@ function handleClientMessage(ctx, _ws, msg) {
3214
3338
  ctx.router.launchInternalSession({
3215
3339
  cwd: msg.cwd,
3216
3340
  model: msg.model,
3217
- initialPrompt: msg.initialPrompt
3341
+ initialPrompt: msg.initialPrompt,
3342
+ worktreeBranch: msg.worktreeBranch,
3343
+ worktreeBaseRef: msg.worktreeBaseRef
3218
3344
  });
3219
3345
  break;
3220
3346
  case "invoke_advisor":
@@ -3244,9 +3370,9 @@ import {
3244
3370
  statSync as statSync5,
3245
3371
  watch
3246
3372
  } from "fs";
3247
- import { homedir as homedir5 } from "os";
3248
- import { join as join9 } from "path";
3249
- var TRANSCRIPT_BASE = join9(homedir5(), ".claude", "projects");
3373
+ import { homedir as homedir6 } from "os";
3374
+ import { join as join10 } from "path";
3375
+ var TRANSCRIPT_BASE = join10(homedir6(), ".claude", "projects");
3250
3376
  var CONTEXT_BUDGETS_BY_MODEL = {
3251
3377
  "claude-opus-4-7": 2e5,
3252
3378
  "claude-opus-4-6": 2e5,
@@ -3259,7 +3385,7 @@ function encodeProjectPath(cwd) {
3259
3385
  return cwd.replace(/[/\\]/g, "-");
3260
3386
  }
3261
3387
  function transcriptPathFor(cwd, sessionId) {
3262
- return join9(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
3388
+ return join10(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
3263
3389
  }
3264
3390
  var TranscriptWatcherManager = class {
3265
3391
  constructor(db, broadcaster) {