@shmulikdav/solix 1.4.2 → 1.8.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
@@ -245,6 +245,7 @@ import { fileURLToPath } from "url";
245
245
  var SOLIX_HOME = process.env.SOLIX_HOME ?? join2(homedir2(), ".solix");
246
246
  var HOOKS_DIR = join2(SOLIX_HOME, "hooks");
247
247
  var SOLIX_SKILLS_DIR = join2(SOLIX_HOME, "skills");
248
+ var SOLIX_TOKEN_FILE = join2(SOLIX_HOME, "token");
248
249
  var CLAUDE_DIR = join2(homedir2(), ".claude");
249
250
  var CLAUDE_SETTINGS = join2(CLAUDE_DIR, "settings.json");
250
251
  var CLAUDE_BACKUP = join2(CLAUDE_DIR, "settings.solix.backup.json");
@@ -343,6 +344,46 @@ async function probeWrappers(port) {
343
344
  };
344
345
  }
345
346
  }
347
+ async function probeAgentView(port) {
348
+ try {
349
+ const res = await fetch(`http://127.0.0.1:${port}/api/system/preflight`, {
350
+ signal: AbortSignal.timeout(1500)
351
+ });
352
+ if (!res.ok) {
353
+ return {
354
+ ok: true,
355
+ label: "Agent View available",
356
+ detail: "server too old to report"
357
+ };
358
+ }
359
+ const data = await res.json();
360
+ if (!data.claudeAvailable) {
361
+ return {
362
+ ok: false,
363
+ label: "Agent View available",
364
+ detail: "claude not on PATH"
365
+ };
366
+ }
367
+ if (data.agentViewAvailable) {
368
+ return {
369
+ ok: true,
370
+ label: "Agent View available",
371
+ detail: `yes (${data.version ?? "unknown version"})`
372
+ };
373
+ }
374
+ return {
375
+ ok: true,
376
+ label: "Agent View available",
377
+ detail: `no \u2014 need Claude Code 2.1.139+ (have ${data.version ?? "?"})`
378
+ };
379
+ } catch {
380
+ return {
381
+ ok: true,
382
+ label: "Agent View available",
383
+ detail: "unknown \u2014 server unreachable"
384
+ };
385
+ }
386
+ }
346
387
  async function doctor() {
347
388
  const port = Number(process.env.SOLIX_PORT ?? 4242);
348
389
  const checks = [];
@@ -425,6 +466,7 @@ async function doctor() {
425
466
  });
426
467
  checks.push(await probeHealth(port));
427
468
  checks.push(await probeWrappers(port));
469
+ checks.push(await probeAgentView(port));
428
470
  console.log("\nSolix Diagnostics\n");
429
471
  let allOk = true;
430
472
  for (const c of checks) {
@@ -560,6 +602,7 @@ import {
560
602
  writeFileSync as writeFileSync2,
561
603
  chmodSync
562
604
  } from "fs";
605
+ import { randomBytes } from "crypto";
563
606
  import { join as join4 } from "path";
564
607
  function readSettings() {
565
608
  if (!existsSync3(CLAUDE_SETTINGS)) return {};
@@ -613,6 +656,15 @@ function mergeHooks(existing, solix) {
613
656
  }
614
657
  return merged;
615
658
  }
659
+ function ensureToken() {
660
+ if (existsSync3(SOLIX_TOKEN_FILE)) return;
661
+ const token = randomBytes(24).toString("hex");
662
+ writeFileSync2(SOLIX_TOKEN_FILE, token, { mode: 384 });
663
+ try {
664
+ chmodSync(SOLIX_TOKEN_FILE, 384);
665
+ } catch {
666
+ }
667
+ }
616
668
  function installHookScripts() {
617
669
  mkdirSync(HOOKS_DIR, { recursive: true });
618
670
  const src = packagedHooksDir();
@@ -678,6 +730,7 @@ function install(opts = {}) {
678
730
  } else if (opts.force && existsSync3(CLAUDE_SETTINGS)) {
679
731
  copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
680
732
  }
733
+ ensureToken();
681
734
  installHookScripts();
682
735
  console.log(`[solix] installed hook scripts in ${HOOKS_DIR}`);
683
736
  const advisorsCopied = installAdvisorAgents();
@@ -979,8 +1032,145 @@ async function installSkillCmd(id, projectId) {
979
1032
  }
980
1033
  }
981
1034
 
1035
+ // src/schedule.ts
1036
+ var PORT6 = process.env.SOLIX_PORT ?? "4242";
1037
+ var BASE6 = `http://127.0.0.1:${PORT6}`;
1038
+ async function api4(path, init) {
1039
+ const res = await fetch(`${BASE6}${path}`, {
1040
+ ...init,
1041
+ headers: { "content-type": "application/json", ...init?.headers ?? {} }
1042
+ });
1043
+ if (!res.ok) {
1044
+ const text = await res.text().catch(() => "");
1045
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
1046
+ }
1047
+ return await res.json();
1048
+ }
1049
+ function unreachable(err) {
1050
+ console.error(`[solix] could not reach server at ${BASE6}: ${String(err)}`);
1051
+ console.error("[solix] is `solix start` running?");
1052
+ process.exitCode = 1;
1053
+ }
1054
+ async function listSchedulesCmd() {
1055
+ try {
1056
+ const list = await api4("/api/schedules");
1057
+ if (!list.length) {
1058
+ console.log("No schedules. Add one with `solix schedule add`.");
1059
+ return;
1060
+ }
1061
+ console.log("id every state next run prompt");
1062
+ for (const s of list) {
1063
+ const next = new Date(s.nextRunAt).toLocaleString();
1064
+ const state = s.enabled ? "on " : "off";
1065
+ console.log(
1066
+ ` ${s.id.padEnd(8)} ${s.cron.padEnd(5)} [${state}] ${next.padEnd(20)} ${s.prompt.slice(0, 40)}`
1067
+ );
1068
+ }
1069
+ } catch (err) {
1070
+ unreachable(err);
1071
+ }
1072
+ }
1073
+ async function addScheduleCmd(prompt, opts) {
1074
+ const cwd = opts.cwd ?? process.cwd();
1075
+ const cadence = opts.every ?? "1h";
1076
+ try {
1077
+ const s = await api4("/api/schedules", {
1078
+ method: "POST",
1079
+ body: JSON.stringify({ cwd, prompt, cadence, name: opts.name })
1080
+ });
1081
+ console.log(
1082
+ `[solix] scheduled ${s.id} \u2014 every ${s.cron} in ${cwd}
1083
+ next run: ${new Date(s.nextRunAt).toLocaleString()}`
1084
+ );
1085
+ } catch (err) {
1086
+ unreachable(err);
1087
+ }
1088
+ }
1089
+ async function toggle(id, enabled) {
1090
+ try {
1091
+ await api4(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
1092
+ method: "POST",
1093
+ body: JSON.stringify({ enabled })
1094
+ });
1095
+ console.log(`[solix] schedule ${id} \u2192 ${enabled ? "enabled" : "disabled"}`);
1096
+ } catch (err) {
1097
+ unreachable(err);
1098
+ }
1099
+ }
1100
+ var enableScheduleCmd = (id) => toggle(id, true);
1101
+ var disableScheduleCmd = (id) => toggle(id, false);
1102
+ async function removeScheduleCmd(id) {
1103
+ try {
1104
+ await api4(`/api/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
1105
+ console.log(`[solix] removed schedule ${id}`);
1106
+ } catch (err) {
1107
+ unreachable(err);
1108
+ }
1109
+ }
1110
+
1111
+ // src/goals.ts
1112
+ var PORT7 = process.env.SOLIX_PORT ?? "4242";
1113
+ var BASE7 = `http://127.0.0.1:${PORT7}`;
1114
+ async function api5(path, init) {
1115
+ const res = await fetch(`${BASE7}${path}`, {
1116
+ ...init,
1117
+ headers: { "content-type": "application/json", ...init?.headers ?? {} }
1118
+ });
1119
+ if (!res.ok) {
1120
+ const text = await res.text().catch(() => "");
1121
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
1122
+ }
1123
+ return await res.json();
1124
+ }
1125
+ function unreachable2(err) {
1126
+ console.error(`[solix] could not reach server at ${BASE7}: ${String(err)}`);
1127
+ console.error("[solix] is `solix start` running?");
1128
+ process.exitCode = 1;
1129
+ }
1130
+ async function listGoalsCmd() {
1131
+ try {
1132
+ const goals = await api5("/api/goals");
1133
+ if (!goals.length) {
1134
+ console.log('No goals. Add one with `solix goal add "<name>"`.');
1135
+ return;
1136
+ }
1137
+ console.log("id color name");
1138
+ for (const g of goals) {
1139
+ console.log(` ${g.id.padEnd(8)} ${g.color.padEnd(8)} ${g.name}`);
1140
+ }
1141
+ } catch (err) {
1142
+ unreachable2(err);
1143
+ }
1144
+ }
1145
+ async function addGoalCmd(name, opts) {
1146
+ try {
1147
+ const g = await api5("/api/goals", {
1148
+ method: "POST",
1149
+ body: JSON.stringify({
1150
+ name,
1151
+ description: opts.description,
1152
+ color: opts.color
1153
+ })
1154
+ });
1155
+ console.log(`[solix] created goal ${g.id} \u2014 "${g.name}" (${g.color})`);
1156
+ } catch (err) {
1157
+ unreachable2(err);
1158
+ }
1159
+ }
1160
+ async function removeGoalCmd(id) {
1161
+ try {
1162
+ await api5(`/api/goals/${encodeURIComponent(id)}`, { method: "DELETE" });
1163
+ console.log(`[solix] removed goal ${id}`);
1164
+ } catch (err) {
1165
+ unreachable2(err);
1166
+ }
1167
+ }
1168
+
982
1169
  // ../server/src/create.ts
983
1170
  import { serve } from "@hono/node-server";
1171
+ import { readFileSync as readFileSync8 } from "fs";
1172
+ import { homedir as homedir11 } from "os";
1173
+ import { join as join15 } from "path";
984
1174
 
985
1175
  // ../server/src/broadcaster.ts
986
1176
  var Broadcaster = class {
@@ -1156,6 +1346,14 @@ CREATE TABLE IF NOT EXISTS scheduled_tasks (
1156
1346
  last_run_at INTEGER,
1157
1347
  next_run_at INTEGER NOT NULL
1158
1348
  );
1349
+
1350
+ CREATE TABLE IF NOT EXISTS goals (
1351
+ id TEXT PRIMARY KEY,
1352
+ name TEXT NOT NULL,
1353
+ description TEXT,
1354
+ color TEXT NOT NULL,
1355
+ created_at INTEGER NOT NULL
1356
+ );
1159
1357
  `;
1160
1358
  var _db = null;
1161
1359
  function ensureColumn(db, table, column, ddl) {
@@ -1175,8 +1373,18 @@ function getDb() {
1175
1373
  ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
1176
1374
  ensureColumn(db, "sessions", "worktree_path", "worktree_path TEXT");
1177
1375
  ensureColumn(db, "sessions", "wrapper_socket_path", "wrapper_socket_path TEXT");
1376
+ ensureColumn(db, "sessions", "agent_view_id", "agent_view_id TEXT");
1377
+ ensureColumn(db, "sessions", "agent_view_summary", "agent_view_summary TEXT");
1378
+ ensureColumn(db, "sessions", "pr_url", "pr_url TEXT");
1379
+ ensureColumn(db, "sessions", "pr_check_status", "pr_check_status TEXT");
1178
1380
  ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
1179
1381
  ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
1382
+ ensureColumn(db, "sessions", "cost_usd", "cost_usd REAL DEFAULT 0");
1383
+ ensureColumn(db, "sessions", "budget_usd", "budget_usd REAL");
1384
+ ensureColumn(db, "sessions", "current_goal_id", "current_goal_id TEXT");
1385
+ ensureColumn(db, "missions", "goal_id", "goal_id TEXT");
1386
+ ensureColumn(db, "scheduled_tasks", "cwd", "cwd TEXT");
1387
+ ensureColumn(db, "scheduled_tasks", "name", "name TEXT");
1180
1388
  _db = db;
1181
1389
  return db;
1182
1390
  }
@@ -1262,7 +1470,14 @@ function rowToSession(row) {
1262
1470
  orbitSlot: row.orbit_slot,
1263
1471
  name: row.name ?? void 0,
1264
1472
  worktreePath: row.worktree_path ?? void 0,
1265
- wrapperSocketPath: row.wrapper_socket_path ?? void 0
1473
+ wrapperSocketPath: row.wrapper_socket_path ?? void 0,
1474
+ agentViewId: row.agent_view_id ?? void 0,
1475
+ agentViewSummary: row.agent_view_summary ?? void 0,
1476
+ prUrl: row.pr_url ?? void 0,
1477
+ prCheckStatus: row.pr_check_status ?? void 0,
1478
+ costUsd: row.cost_usd ?? 0,
1479
+ budgetUsd: row.budget_usd ?? void 0,
1480
+ currentGoalId: row.current_goal_id ?? void 0
1266
1481
  };
1267
1482
  }
1268
1483
  function nextOrbitSlot(db, projectId) {
@@ -1297,9 +1512,11 @@ function upsertSession(db, input) {
1297
1512
  `INSERT INTO sessions (
1298
1513
  id, pid, project_id, parent_session_id, origin, model, status,
1299
1514
  context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
1300
- worktree_path, wrapper_socket_path, created_at, updated_at
1515
+ worktree_path, wrapper_socket_path,
1516
+ agent_view_id, agent_view_summary, pr_url, pr_check_status,
1517
+ created_at, updated_at
1301
1518
  )
1302
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?)`
1519
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1303
1520
  ).run(
1304
1521
  input.id,
1305
1522
  input.pid,
@@ -1314,6 +1531,10 @@ function upsertSession(db, input) {
1314
1531
  input.advisorRole ?? null,
1315
1532
  input.worktreePath ?? null,
1316
1533
  input.wrapperSocketPath ?? null,
1534
+ input.agentViewId ?? null,
1535
+ input.agentViewSummary ?? null,
1536
+ input.prUrl ?? null,
1537
+ input.prCheckStatus ?? null,
1317
1538
  ts2,
1318
1539
  ts2
1319
1540
  );
@@ -1333,9 +1554,40 @@ function upsertSession(db, input) {
1333
1554
  contextUsagePct: 0,
1334
1555
  orbitSlot,
1335
1556
  worktreePath: input.worktreePath,
1336
- wrapperSocketPath: input.wrapperSocketPath
1557
+ wrapperSocketPath: input.wrapperSocketPath,
1558
+ agentViewId: input.agentViewId,
1559
+ agentViewSummary: input.agentViewSummary,
1560
+ prUrl: input.prUrl,
1561
+ prCheckStatus: input.prCheckStatus,
1562
+ costUsd: 0
1337
1563
  };
1338
1564
  }
1565
+ function setAgentViewFields(db, sessionId, fields) {
1566
+ const ts2 = now();
1567
+ const updates = ["updated_at = ?"];
1568
+ const values = [ts2];
1569
+ if (fields.status !== void 0) {
1570
+ updates.push("status = ?");
1571
+ values.push(fields.status);
1572
+ }
1573
+ if (fields.agentViewSummary !== void 0) {
1574
+ updates.push("agent_view_summary = ?");
1575
+ values.push(fields.agentViewSummary);
1576
+ }
1577
+ if (fields.prUrl !== void 0) {
1578
+ updates.push("pr_url = ?");
1579
+ values.push(fields.prUrl);
1580
+ }
1581
+ if (fields.prCheckStatus !== void 0) {
1582
+ updates.push("pr_check_status = ?");
1583
+ values.push(fields.prCheckStatus);
1584
+ }
1585
+ values.push(sessionId);
1586
+ db.prepare(
1587
+ `UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`
1588
+ ).run(...values);
1589
+ return getSession(db, sessionId);
1590
+ }
1339
1591
  function setSessionStatus(db, sessionId, status) {
1340
1592
  const ts2 = now();
1341
1593
  if (status === "terminated") {
@@ -1371,6 +1623,27 @@ function setSessionContextUsage(db, sessionId, pct) {
1371
1623
  ).run(clamped, ts2, sessionId);
1372
1624
  return getSession(db, sessionId);
1373
1625
  }
1626
+ function setSessionCost(db, sessionId, costUsd) {
1627
+ const ts2 = now();
1628
+ db.prepare(
1629
+ `UPDATE sessions SET cost_usd = ?, updated_at = ? WHERE id = ?`
1630
+ ).run(Math.max(0, costUsd), ts2, sessionId);
1631
+ return getSession(db, sessionId);
1632
+ }
1633
+ function setSessionBudget(db, sessionId, budgetUsd) {
1634
+ const ts2 = now();
1635
+ db.prepare(
1636
+ `UPDATE sessions SET budget_usd = ?, updated_at = ? WHERE id = ?`
1637
+ ).run(budgetUsd, ts2, sessionId);
1638
+ return getSession(db, sessionId);
1639
+ }
1640
+ function setSessionGoal(db, sessionId, goalId) {
1641
+ const ts2 = now();
1642
+ db.prepare(
1643
+ `UPDATE sessions SET current_goal_id = ?, updated_at = ? WHERE id = ?`
1644
+ ).run(goalId, ts2, sessionId);
1645
+ return getSession(db, sessionId);
1646
+ }
1374
1647
  function getSession(db, sessionId) {
1375
1648
  const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1376
1649
  return row ? rowToSession(row) : null;
@@ -1419,7 +1692,8 @@ function rowToMission(row) {
1419
1692
  toolCallCount: row.tool_call_count
1420
1693
  },
1421
1694
  filesTouched,
1422
- errorSummary: row.error_summary ?? void 0
1695
+ errorSummary: row.error_summary ?? void 0,
1696
+ goalId: row.goal_id ?? void 0
1423
1697
  };
1424
1698
  }
1425
1699
  function setMissionError(db, missionId, errorSummary) {
@@ -1435,14 +1709,14 @@ function shortNameFromPrompt(prompt) {
1435
1709
  (w) => w.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/^./, (c) => c.toUpperCase())
1436
1710
  ).filter(Boolean).join(" ") || "New Mission";
1437
1711
  }
1438
- function startMission(db, sessionId, prompt) {
1712
+ function startMission(db, sessionId, prompt, goalId) {
1439
1713
  const id = nanoid2();
1440
1714
  const ts2 = now();
1441
1715
  const shortName = shortNameFromPrompt(prompt);
1442
1716
  db.prepare(
1443
- `INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json)
1444
- VALUES (?, ?, ?, ?, 'active', ?, '[]')`
1445
- ).run(id, sessionId, prompt, shortName, ts2);
1717
+ `INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json, goal_id)
1718
+ VALUES (?, ?, ?, ?, 'active', ?, '[]', ?)`
1719
+ ).run(id, sessionId, prompt, shortName, ts2, goalId ?? null);
1446
1720
  return {
1447
1721
  id,
1448
1722
  sessionId,
@@ -1451,9 +1725,16 @@ function startMission(db, sessionId, prompt) {
1451
1725
  shortName,
1452
1726
  status: "active",
1453
1727
  metrics: { subagentCount: 0, toolCallCount: 0 },
1454
- filesTouched: []
1728
+ filesTouched: [],
1729
+ goalId
1455
1730
  };
1456
1731
  }
1732
+ function addMissionTokens(db, missionId, tokens) {
1733
+ if (tokens <= 0) return;
1734
+ db.prepare(
1735
+ `UPDATE missions SET total_tokens = COALESCE(total_tokens, 0) + ? WHERE id = ?`
1736
+ ).run(Math.round(tokens), missionId);
1737
+ }
1457
1738
  function completeMission(db, missionId, status = "completed") {
1458
1739
  const ts2 = now();
1459
1740
  const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
@@ -1895,6 +2176,138 @@ function writeToWrapperSocket(socketPath, text) {
1895
2176
  }
1896
2177
  }
1897
2178
 
2179
+ // ../server/src/state/schedules.ts
2180
+ import { nanoid as nanoid4 } from "nanoid";
2181
+ function rowToSchedule(row) {
2182
+ return {
2183
+ id: row.id,
2184
+ projectId: row.project_id,
2185
+ cwd: row.cwd ?? "",
2186
+ name: row.name ?? void 0,
2187
+ prompt: row.prompt,
2188
+ cron: row.cron,
2189
+ enabled: row.enabled !== 0,
2190
+ lastRunAt: row.last_run_at ?? void 0,
2191
+ nextRunAt: row.next_run_at
2192
+ };
2193
+ }
2194
+ function cadenceToMs(cadence) {
2195
+ const m = cadence.trim().match(/^(\d+)\s*([mhd])$/i);
2196
+ if (!m) return null;
2197
+ const n = Number(m[1]);
2198
+ if (!Number.isFinite(n) || n <= 0) return null;
2199
+ const unit = m[2].toLowerCase();
2200
+ const mult = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2201
+ return n * mult;
2202
+ }
2203
+ function nextRunFrom(fromMs, cadence) {
2204
+ const ms = cadenceToMs(cadence);
2205
+ return fromMs + (ms ?? 36e5);
2206
+ }
2207
+ function createSchedule(db, input) {
2208
+ const project = ensureProject(db, input.cwd);
2209
+ const id = nanoid4(8);
2210
+ const ts2 = now();
2211
+ const nextRun = nextRunFrom(ts2, input.cadence);
2212
+ db.prepare(
2213
+ `INSERT INTO scheduled_tasks
2214
+ (id, project_id, cwd, name, prompt, cron, enabled, last_run_at, next_run_at)
2215
+ VALUES (?, ?, ?, ?, ?, ?, 1, NULL, ?)`
2216
+ ).run(id, project.id, input.cwd, input.name ?? null, input.prompt, input.cadence, nextRun);
2217
+ return getSchedule(db, id);
2218
+ }
2219
+ function getSchedule(db, id) {
2220
+ const row = db.prepare("SELECT * FROM scheduled_tasks WHERE id = ?").get(id);
2221
+ return row ? rowToSchedule(row) : null;
2222
+ }
2223
+ function listSchedules(db) {
2224
+ const rows = db.prepare("SELECT * FROM scheduled_tasks ORDER BY next_run_at ASC").all();
2225
+ return rows.map(rowToSchedule);
2226
+ }
2227
+ function listDueSchedules(db, asOf) {
2228
+ const rows = db.prepare(
2229
+ `SELECT * FROM scheduled_tasks WHERE enabled = 1 AND next_run_at <= ?`
2230
+ ).all(asOf);
2231
+ return rows.map(rowToSchedule);
2232
+ }
2233
+ function setScheduleEnabled(db, id, enabled) {
2234
+ db.prepare("UPDATE scheduled_tasks SET enabled = ? WHERE id = ?").run(
2235
+ enabled ? 1 : 0,
2236
+ id
2237
+ );
2238
+ return getSchedule(db, id);
2239
+ }
2240
+ function markScheduleRun(db, id) {
2241
+ const sched = getSchedule(db, id);
2242
+ if (!sched) return null;
2243
+ const ts2 = now();
2244
+ const next = nextRunFrom(ts2, sched.cron);
2245
+ db.prepare(
2246
+ "UPDATE scheduled_tasks SET last_run_at = ?, next_run_at = ? WHERE id = ?"
2247
+ ).run(ts2, next, id);
2248
+ return getSchedule(db, id);
2249
+ }
2250
+ function deleteSchedule(db, id) {
2251
+ const res = db.prepare("DELETE FROM scheduled_tasks WHERE id = ?").run(id);
2252
+ return res.changes > 0;
2253
+ }
2254
+
2255
+ // ../server/src/state/goals.ts
2256
+ import { nanoid as nanoid5 } from "nanoid";
2257
+ var PALETTE = [
2258
+ "#38bdf8",
2259
+ // sky
2260
+ "#a78bfa",
2261
+ // violet
2262
+ "#34d399",
2263
+ // emerald
2264
+ "#fbbf24",
2265
+ // amber
2266
+ "#f472b6",
2267
+ // pink
2268
+ "#f87171",
2269
+ // red
2270
+ "#22d3ee",
2271
+ // cyan
2272
+ "#c084fc"
2273
+ // purple
2274
+ ];
2275
+ function rowToGoal(row) {
2276
+ return {
2277
+ id: row.id,
2278
+ name: row.name,
2279
+ description: row.description ?? void 0,
2280
+ color: row.color,
2281
+ createdAt: row.created_at
2282
+ };
2283
+ }
2284
+ function nextColor(db) {
2285
+ const count = db.prepare("SELECT COUNT(*) AS n FROM goals").get().n;
2286
+ return PALETTE[count % PALETTE.length];
2287
+ }
2288
+ function createGoal(db, input) {
2289
+ const id = nanoid5(8);
2290
+ const ts2 = now();
2291
+ const color = input.color ?? nextColor(db);
2292
+ db.prepare(
2293
+ `INSERT INTO goals (id, name, description, color, created_at)
2294
+ VALUES (?, ?, ?, ?, ?)`
2295
+ ).run(id, input.name, input.description ?? null, color, ts2);
2296
+ return { id, name: input.name, description: input.description, color, createdAt: ts2 };
2297
+ }
2298
+ function listGoals(db) {
2299
+ const rows = db.prepare("SELECT * FROM goals ORDER BY created_at ASC").all();
2300
+ return rows.map(rowToGoal);
2301
+ }
2302
+ function deleteGoal(db, id) {
2303
+ db.prepare(
2304
+ `UPDATE sessions SET current_goal_id = NULL WHERE current_goal_id = ?`
2305
+ ).run(id);
2306
+ db.prepare(`UPDATE missions SET goal_id = NULL WHERE goal_id = ?`).run(id);
2307
+ const res = db.prepare("DELETE FROM goals WHERE id = ?").run(id);
2308
+ return res.changes > 0;
2309
+ }
2310
+
1898
2311
  // ../server/src/state/context.ts
1899
2312
  var MISSIONS_FOR_HANDOFF = 3;
1900
2313
  var DEFAULT_ASKS = {
@@ -2101,7 +2514,7 @@ function recordSkillInstall(db, skillId, projectId) {
2101
2514
  }
2102
2515
 
2103
2516
  // ../server/src/state/galaxy.ts
2104
- import { nanoid as nanoid4 } from "nanoid";
2517
+ import { nanoid as nanoid6 } from "nanoid";
2105
2518
  function exportManifest(db, opts = {}) {
2106
2519
  const advisors2 = listAdvisors(db);
2107
2520
  const skills2 = listSkills(db);
@@ -2149,7 +2562,7 @@ function importManifest(db, manifest, sourceUrl) {
2149
2562
  db.prepare(
2150
2563
  `INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
2151
2564
  VALUES (?, ?, ?, ?)`
2152
- ).run(nanoid4(), sourceUrl ?? null, JSON.stringify(manifest), now());
2565
+ ).run(nanoid6(), sourceUrl ?? null, JSON.stringify(manifest), now());
2153
2566
  return {
2154
2567
  advisorsEnabled: enabled,
2155
2568
  advisorsDisabled: disabled,
@@ -2210,7 +2623,7 @@ function snapshotExport(db, manifest) {
2210
2623
  return rowToVersion(existing);
2211
2624
  }
2212
2625
  }
2213
- const id = nanoid4();
2626
+ const id = nanoid6();
2214
2627
  const ts2 = now();
2215
2628
  const ordinal = (last?.ordinal ?? 0) + 1;
2216
2629
  db.prepare(
@@ -2288,6 +2701,29 @@ function diffManifests(a, b) {
2288
2701
  };
2289
2702
  }
2290
2703
 
2704
+ // ../shared/src/pricing.ts
2705
+ var MODEL_PRICING = {
2706
+ opus: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
2707
+ sonnet: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
2708
+ haiku: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }
2709
+ };
2710
+ function pricingFor(model) {
2711
+ const m = (model ?? "").toLowerCase();
2712
+ if (m.includes("opus")) return MODEL_PRICING.opus;
2713
+ if (m.includes("haiku")) return MODEL_PRICING.haiku;
2714
+ return MODEL_PRICING.sonnet;
2715
+ }
2716
+ function costForUsage(model, usage) {
2717
+ if (!usage) return 0;
2718
+ const p = pricingFor(model);
2719
+ const cost = ((usage.input_tokens ?? 0) * p.input + (usage.output_tokens ?? 0) * p.output + (usage.cache_read_input_tokens ?? 0) * p.cacheRead + (usage.cache_creation_input_tokens ?? 0) * p.cacheWrite) / 1e6;
2720
+ return cost;
2721
+ }
2722
+ function totalTokens(usage) {
2723
+ if (!usage) return 0;
2724
+ return (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
2725
+ }
2726
+
2291
2727
  // ../server/src/cloud.ts
2292
2728
  var RegistryClient = class {
2293
2729
  constructor(baseUrl = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
@@ -2362,9 +2798,44 @@ var RegistryClient = class {
2362
2798
  };
2363
2799
 
2364
2800
  // ../server/src/http.ts
2801
+ function isAgentViewVersion(version) {
2802
+ if (!version) return false;
2803
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
2804
+ if (!match) return false;
2805
+ const major = Number(match[1]);
2806
+ const minor = Number(match[2]);
2807
+ const patch = Number(match[3]);
2808
+ if (major > 2) return true;
2809
+ if (major < 2) return false;
2810
+ if (minor > 1) return true;
2811
+ if (minor < 1) return false;
2812
+ return patch >= 139;
2813
+ }
2365
2814
  function createHttpApp(opts) {
2366
2815
  const app = new Hono();
2367
- app.use("*", cors());
2816
+ app.use(
2817
+ "*",
2818
+ cors({
2819
+ origin: [
2820
+ "http://127.0.0.1:4242",
2821
+ "http://localhost:4242",
2822
+ "http://127.0.0.1:4243",
2823
+ "http://localhost:4243"
2824
+ ]
2825
+ })
2826
+ );
2827
+ if (opts.token) {
2828
+ const expected = opts.token;
2829
+ const paths = ["/events", "/events/permission"];
2830
+ for (const p of paths) {
2831
+ app.use(p, async (c, next) => {
2832
+ if (c.req.header("x-solix-token") !== expected) {
2833
+ return c.json({ error: "unauthorized" }, 401);
2834
+ }
2835
+ await next();
2836
+ });
2837
+ }
2838
+ }
2368
2839
  const registry = new RegistryClient();
2369
2840
  app.get(
2370
2841
  "/api/health",
@@ -2388,6 +2859,18 @@ function createHttpApp(opts) {
2388
2859
  }
2389
2860
  return c.json({ ok: true });
2390
2861
  });
2862
+ app.post("/events/permission", async (c) => {
2863
+ let body = null;
2864
+ try {
2865
+ body = await c.req.json();
2866
+ } catch {
2867
+ return c.json({ decision: "allow" });
2868
+ }
2869
+ if (!body || !body.event) return c.json({ decision: "allow" });
2870
+ const result = await opts.router.requestPermission(body);
2871
+ const decision = result.timedOut ? "timeout" : result.approved ? "allow" : "deny";
2872
+ return c.json({ decision });
2873
+ });
2391
2874
  app.get("/api/projects", (c) => c.json(listProjects(opts.db)));
2392
2875
  app.get("/api/projects/:id/sessions", (c) => {
2393
2876
  const id = c.req.param("id");
@@ -2468,11 +2951,11 @@ function createHttpApp(opts) {
2468
2951
  return c.json({ ...a, agentMd: readAdvisorAgentMd(a) });
2469
2952
  });
2470
2953
  app.post("/api/advisors/:id/enable", (c) => {
2471
- const a = setAdvisorEnabled(opts.db, c.req.param("id"), true);
2954
+ const a = opts.router.setAdvisorEnabled(c.req.param("id"), true);
2472
2955
  return c.json({ ok: Boolean(a), advisor: a });
2473
2956
  });
2474
2957
  app.post("/api/advisors/:id/disable", (c) => {
2475
- const a = setAdvisorEnabled(opts.db, c.req.param("id"), false);
2958
+ const a = opts.router.setAdvisorEnabled(c.req.param("id"), false);
2476
2959
  return c.json({ ok: Boolean(a), advisor: a });
2477
2960
  });
2478
2961
  app.post("/api/advisors/:id/pin", (c) => {
@@ -2628,6 +3111,52 @@ function createHttpApp(opts) {
2628
3111
  return c.json({ ok: true });
2629
3112
  });
2630
3113
  app.get("/api/wrappers", (c) => c.json(listWrappers()));
3114
+ app.get("/api/schedules", (c) => c.json(listSchedules(opts.db)));
3115
+ app.post("/api/schedules", async (c) => {
3116
+ const body = await c.req.json().catch(() => ({}));
3117
+ if (!body.cwd || !body.prompt || !body.cadence) {
3118
+ return c.json({ error: "cwd, prompt, cadence required" }, 400);
3119
+ }
3120
+ const schedule2 = createSchedule(opts.db, {
3121
+ cwd: body.cwd,
3122
+ prompt: body.prompt,
3123
+ cadence: body.cadence,
3124
+ name: body.name
3125
+ });
3126
+ opts.router.broadcastScheduleUpsert(schedule2);
3127
+ return c.json(schedule2);
3128
+ });
3129
+ app.post("/api/schedules/:id/toggle", async (c) => {
3130
+ const body = await c.req.json().catch(() => ({}));
3131
+ const s = setScheduleEnabled(opts.db, c.req.param("id"), Boolean(body.enabled));
3132
+ if (!s) return c.json({ error: "not found" }, 404);
3133
+ opts.router.broadcastScheduleUpsert(s);
3134
+ return c.json(s);
3135
+ });
3136
+ app.delete("/api/schedules/:id", (c) => {
3137
+ const id = c.req.param("id");
3138
+ const ok = deleteSchedule(opts.db, id);
3139
+ if (ok) opts.router.broadcastScheduleRemove(id);
3140
+ return c.json({ ok });
3141
+ });
3142
+ app.get("/api/goals", (c) => c.json(listGoals(opts.db)));
3143
+ app.post("/api/goals", async (c) => {
3144
+ const body = await c.req.json().catch(() => ({}));
3145
+ if (!body.name) return c.json({ error: "name required" }, 400);
3146
+ const goal2 = createGoal(opts.db, {
3147
+ name: body.name,
3148
+ description: body.description,
3149
+ color: body.color
3150
+ });
3151
+ opts.router.broadcastGoalUpsert(goal2);
3152
+ return c.json(goal2);
3153
+ });
3154
+ app.delete("/api/goals/:id", (c) => {
3155
+ const id = c.req.param("id");
3156
+ const ok = deleteGoal(opts.db, id);
3157
+ if (ok) opts.router.broadcastGoalRemove(id);
3158
+ return c.json({ ok });
3159
+ });
2631
3160
  let preflightCache = null;
2632
3161
  app.get("/api/system/preflight", (c) => {
2633
3162
  if (preflightCache) return c.json(preflightCache);
@@ -2637,15 +3166,17 @@ function createHttpApp(opts) {
2637
3166
  encoding: "utf8"
2638
3167
  });
2639
3168
  if (res.status === 0) {
3169
+ const version = (res.stdout ?? "").trim() || void 0;
2640
3170
  preflightCache = {
2641
3171
  claudeAvailable: true,
2642
- version: (res.stdout ?? "").trim() || void 0
3172
+ version,
3173
+ agentViewAvailable: isAgentViewVersion(version)
2643
3174
  };
2644
3175
  } else {
2645
- preflightCache = { claudeAvailable: false };
3176
+ preflightCache = { claudeAvailable: false, agentViewAvailable: false };
2646
3177
  }
2647
3178
  } catch {
2648
- preflightCache = { claudeAvailable: false };
3179
+ preflightCache = { claudeAvailable: false, agentViewAvailable: false };
2649
3180
  }
2650
3181
  return c.json(preflightCache);
2651
3182
  });
@@ -2767,7 +3298,7 @@ import { spawn, spawnSync as spawnSync2 } from "child_process";
2767
3298
  import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2768
3299
  import { homedir as homedir8 } from "os";
2769
3300
  import { basename as basename3, join as join12 } from "path";
2770
- import { nanoid as nanoid5 } from "nanoid";
3301
+ import { nanoid as nanoid7 } from "nanoid";
2771
3302
  function ensureWorktree(opts) {
2772
3303
  const repoRoot = (() => {
2773
3304
  const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
@@ -2809,6 +3340,53 @@ function ensureWorktree(opts) {
2809
3340
  return { path, created: true };
2810
3341
  }
2811
3342
  var FAKE_CLAUDE = process.env.SOLIX_FAKE_CLAUDE === "1";
3343
+ function buildSpawnEnv() {
3344
+ const scrub = process.env.SOLIX_ENV_SCRUB === "1" || (process.env.SOLIX_SANDBOX_CMD ?? "").trim() !== "";
3345
+ if (!scrub) return void 0;
3346
+ const allow = [
3347
+ "PATH",
3348
+ "HOME",
3349
+ "USER",
3350
+ "LOGNAME",
3351
+ "SHELL",
3352
+ "LANG",
3353
+ "LC_ALL",
3354
+ "TERM",
3355
+ "TMPDIR",
3356
+ "TZ",
3357
+ "HTTP_PROXY",
3358
+ "HTTPS_PROXY",
3359
+ "NO_PROXY",
3360
+ "http_proxy",
3361
+ "https_proxy",
3362
+ "no_proxy",
3363
+ "SOLIX_HOME",
3364
+ "SOLIX_HOST",
3365
+ "SOLIX_PORT",
3366
+ "SOLIX_GATE_ENABLED",
3367
+ "SOLIX_GATE_POLICY",
3368
+ "SOLIX_GATE_TIMEOUT"
3369
+ ];
3370
+ const extra = (process.env.SOLIX_ENV_PASSTHROUGH ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3371
+ const keep = /* @__PURE__ */ new Set([...allow, ...extra]);
3372
+ const env = {};
3373
+ for (const k of keep) {
3374
+ if (process.env[k] !== void 0) env[k] = process.env[k];
3375
+ }
3376
+ for (const k of Object.keys(process.env)) {
3377
+ if (k.startsWith("ANTHROPIC_") || k.startsWith("CLAUDE_")) {
3378
+ env[k] = process.env[k];
3379
+ }
3380
+ }
3381
+ return env;
3382
+ }
3383
+ function sandboxWrap(file, args) {
3384
+ const cmd = (process.env.SOLIX_SANDBOX_CMD ?? "").trim();
3385
+ if (!cmd) return { file, args };
3386
+ const parts = cmd.split(/\s+/);
3387
+ const bin = parts[0];
3388
+ return { file: bin, args: [...parts.slice(1), file, ...args] };
3389
+ }
2812
3390
  var Launcher = class {
2813
3391
  constructor(db, broadcaster) {
2814
3392
  this.db = db;
@@ -2838,15 +3416,17 @@ var Launcher = class {
2838
3416
  return this.pinSynthetic(advisor.id, advisor.codename, cwd);
2839
3417
  }
2840
3418
  try {
2841
- const child = spawn(
2842
- "claude",
2843
- ["--agent", advisor.id, "--no-tty"],
2844
- {
2845
- cwd,
2846
- stdio: ["pipe", "pipe", "pipe"],
2847
- detached: false
2848
- }
2849
- );
3419
+ const spawnSpec = sandboxWrap("claude", [
3420
+ "--agent",
3421
+ advisor.id,
3422
+ "--no-tty"
3423
+ ]);
3424
+ const child = spawn(spawnSpec.file, spawnSpec.args, {
3425
+ cwd,
3426
+ stdio: ["pipe", "pipe", "pipe"],
3427
+ detached: false,
3428
+ env: buildSpawnEnv()
3429
+ });
2850
3430
  const pid = child.pid;
2851
3431
  if (!pid) {
2852
3432
  this.broadcaster.broadcast({
@@ -2885,7 +3465,7 @@ var Launcher = class {
2885
3465
  }
2886
3466
  pinSynthetic(advisorId, codename, cwd) {
2887
3467
  const project = ensureProject(this.db, cwd);
2888
- const sessionId = `advisor-${advisorId}-${nanoid5(6)}`;
3468
+ const sessionId = `advisor-${advisorId}-${nanoid7(6)}`;
2889
3469
  const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
2890
3470
  const session = upsertSession(this.db, {
2891
3471
  id: sessionId,
@@ -2961,6 +3541,14 @@ var Launcher = class {
2961
3541
  */
2962
3542
  launch(opts) {
2963
3543
  if (!opts.initialPrompt.trim()) return { ok: false };
3544
+ if (opts.useAgentView) {
3545
+ return this.dispatchAgentView({
3546
+ cwd: opts.cwd,
3547
+ model: opts.model,
3548
+ initialPrompt: opts.initialPrompt,
3549
+ agentName: opts.agentName
3550
+ });
3551
+ }
2964
3552
  let spawnCwd = opts.cwd;
2965
3553
  let worktreePath;
2966
3554
  if (opts.worktreeBranch?.trim()) {
@@ -2991,7 +3579,9 @@ var Launcher = class {
2991
3579
  cwd: spawnCwd,
2992
3580
  model: opts.model,
2993
3581
  initialPrompt: opts.initialPrompt,
2994
- worktreePath
3582
+ worktreePath,
3583
+ budgetUsd: opts.budgetUsd,
3584
+ goalId: opts.goalId
2995
3585
  });
2996
3586
  }
2997
3587
  if (!existsSync9(spawnCwd)) {
@@ -3005,14 +3595,74 @@ var Launcher = class {
3005
3595
  const args = ["--print"];
3006
3596
  if (opts.model) args.push("--model", String(opts.model));
3007
3597
  args.push(opts.initialPrompt);
3008
- const sessionId = `task-${nanoid5(8)}`;
3598
+ const sessionId = `task-${nanoid7(8)}`;
3009
3599
  return this.spawnPrint({
3010
3600
  sessionId,
3011
3601
  cwd: spawnCwd,
3012
3602
  args,
3013
3603
  isFollowUp: false,
3014
- worktreePath
3604
+ worktreePath,
3605
+ budgetUsd: opts.budgetUsd,
3606
+ goalId: opts.goalId
3607
+ });
3608
+ }
3609
+ /**
3610
+ * Sprint L: dispatch via Anthropic's Agent View daemon. Runs
3611
+ * `claude --bg "<prompt>"` so the session is hosted by the
3612
+ * supervisor and picked up by Solix's filesystem watcher within ~1s.
3613
+ * Returns the short id parsed from claude's output line:
3614
+ * backgrounded · 7c5dcf5d
3615
+ */
3616
+ dispatchAgentView(opts) {
3617
+ if (FAKE_CLAUDE) {
3618
+ this.broadcaster.broadcast({
3619
+ type: "toast",
3620
+ level: "info",
3621
+ message: "(SOLIX_FAKE_CLAUDE=1) Agent View dispatch skipped"
3622
+ });
3623
+ return { ok: true };
3624
+ }
3625
+ if (!existsSync9(opts.cwd)) {
3626
+ this.broadcaster.broadcast({
3627
+ type: "toast",
3628
+ level: "error",
3629
+ message: `Agent View dispatch failed: cwd does not exist (${opts.cwd})`
3630
+ });
3631
+ return { ok: false };
3632
+ }
3633
+ const args = [];
3634
+ if (opts.agentName) args.push("--agent", opts.agentName);
3635
+ if (opts.model) args.push("--model", String(opts.model));
3636
+ args.push("--bg", opts.initialPrompt);
3637
+ let child;
3638
+ try {
3639
+ const spawnSpec = sandboxWrap("claude", args);
3640
+ child = spawn(spawnSpec.file, spawnSpec.args, {
3641
+ cwd: opts.cwd,
3642
+ stdio: ["ignore", "pipe", "pipe"],
3643
+ detached: false,
3644
+ env: buildSpawnEnv()
3645
+ });
3646
+ } catch (err) {
3647
+ this.broadcaster.broadcast({
3648
+ type: "toast",
3649
+ level: "error",
3650
+ message: `claude --bg spawn failed: ${err.message}`
3651
+ });
3652
+ return { ok: false };
3653
+ }
3654
+ let stdout = "";
3655
+ child.stdout?.setEncoding("utf8").on("data", (c) => stdout += c);
3656
+ child.on("exit", () => {
3657
+ const match = stdout.match(/backgrounded[^a-z0-9]+([a-f0-9]{6,16})/i);
3658
+ const shortId = match?.[1];
3659
+ this.broadcaster.broadcast({
3660
+ type: "toast",
3661
+ level: "info",
3662
+ message: shortId ? `Dispatched to Agent View \xB7 ${shortId}` : "Dispatched to Agent View"
3663
+ });
3015
3664
  });
3665
+ return { ok: true };
3016
3666
  }
3017
3667
  sendPromptToInternal(sessionId, text) {
3018
3668
  if (!text.trim()) return false;
@@ -3035,6 +3685,15 @@ var Launcher = class {
3035
3685
  });
3036
3686
  return false;
3037
3687
  }
3688
+ const full = getSession(this.db, sessionId);
3689
+ if (full?.budgetUsd != null && full.costUsd >= full.budgetUsd) {
3690
+ this.broadcaster.broadcast({
3691
+ type: "toast",
3692
+ level: "warn",
3693
+ message: `Budget reached for ${full.name ?? sessionId.slice(0, 8)} ($${full.costUsd.toFixed(2)}/$${full.budgetUsd.toFixed(2)}). Raise the cap to continue.`
3694
+ });
3695
+ return false;
3696
+ }
3038
3697
  if (FAKE_CLAUDE) {
3039
3698
  this.broadcaster.broadcast({
3040
3699
  type: "chat_delta",
@@ -3066,13 +3725,29 @@ var Launcher = class {
3066
3725
  }
3067
3726
  return void 0;
3068
3727
  }
3728
+ /** Sprint M — budget cap recorded at launch for a cwd, if any. */
3729
+ budgetForInternalCwd(cwd) {
3730
+ for (const rec of this.internalTasks.values()) {
3731
+ if (rec.cwd === cwd && rec.budgetUsd != null) return rec.budgetUsd;
3732
+ }
3733
+ return void 0;
3734
+ }
3735
+ /** Sprint M — goal recorded at launch for a cwd, if any. */
3736
+ goalForInternalCwd(cwd) {
3737
+ for (const rec of this.internalTasks.values()) {
3738
+ if (rec.cwd === cwd && rec.goalId) return rec.goalId;
3739
+ }
3740
+ return void 0;
3741
+ }
3069
3742
  spawnPrint(opts) {
3070
3743
  let child;
3071
3744
  try {
3072
- child = spawn("claude", opts.args, {
3745
+ const spawnSpec = sandboxWrap("claude", opts.args);
3746
+ child = spawn(spawnSpec.file, spawnSpec.args, {
3073
3747
  cwd: opts.cwd,
3074
3748
  stdio: ["ignore", "pipe", "pipe"],
3075
- detached: false
3749
+ detached: false,
3750
+ env: buildSpawnEnv()
3076
3751
  });
3077
3752
  } catch (err) {
3078
3753
  this.broadcaster.broadcast({
@@ -3087,7 +3762,9 @@ var Launcher = class {
3087
3762
  if (!opts.isFollowUp) {
3088
3763
  this.internalTasks.set(opts.sessionId, {
3089
3764
  cwd: opts.cwd,
3090
- worktreePath: opts.worktreePath
3765
+ worktreePath: opts.worktreePath,
3766
+ budgetUsd: opts.budgetUsd,
3767
+ goalId: opts.goalId
3091
3768
  });
3092
3769
  }
3093
3770
  let stdout = "";
@@ -3133,7 +3810,7 @@ var Launcher = class {
3133
3810
  }
3134
3811
  launchSynthetic(opts) {
3135
3812
  const project = ensureProject(this.db, opts.cwd);
3136
- const sessionId = `task-${nanoid5(8)}`;
3813
+ const sessionId = `task-${nanoid7(8)}`;
3137
3814
  const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
3138
3815
  upsertSession(this.db, {
3139
3816
  id: sessionId,
@@ -3144,6 +3821,8 @@ var Launcher = class {
3144
3821
  model: opts.model ?? "sonnet",
3145
3822
  worktreePath: opts.worktreePath
3146
3823
  });
3824
+ if (opts.budgetUsd != null) setSessionBudget(this.db, sessionId, opts.budgetUsd);
3825
+ if (opts.goalId) setSessionGoal(this.db, sessionId, opts.goalId);
3147
3826
  const active = setSessionStatus(this.db, sessionId, "active");
3148
3827
  if (active)
3149
3828
  this.broadcaster.broadcast({ type: "session_upsert", session: active });
@@ -3179,12 +3858,12 @@ var Launcher = class {
3179
3858
  };
3180
3859
 
3181
3860
  // ../server/src/router.ts
3182
- import { nanoid as nanoid7 } from "nanoid";
3861
+ import { nanoid as nanoid9 } from "nanoid";
3183
3862
 
3184
3863
  // ../server/src/state/toolcalls.ts
3185
- import { nanoid as nanoid6 } from "nanoid";
3864
+ import { nanoid as nanoid8 } from "nanoid";
3186
3865
  function recordToolCall(db, input) {
3187
- const id = nanoid6();
3866
+ const id = nanoid8();
3188
3867
  const ts2 = now();
3189
3868
  const status = input.status ?? "running";
3190
3869
  db.prepare(
@@ -3286,6 +3965,8 @@ var EventRouter = class {
3286
3965
  const sessionId = this.extractSessionId(event);
3287
3966
  const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
3288
3967
  const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
3968
+ const launchBudget = this.launcher?.budgetForInternalCwd(event.cwd);
3969
+ const launchGoal = this.launcher?.goalForInternalCwd(event.cwd);
3289
3970
  const wrapper = claimWrapperForCwd(event.cwd);
3290
3971
  const session = upsertSession(this.db, {
3291
3972
  id: sessionId,
@@ -3301,7 +3982,14 @@ var EventRouter = class {
3301
3982
  wrapperSocketPath: wrapper?.socketPath
3302
3983
  });
3303
3984
  if (wrapper) bindWrapperToSession(wrapper.wrapperId, session.id);
3304
- this.broadcaster.broadcast({ type: "session_upsert", session });
3985
+ let enriched = session;
3986
+ if (launchBudget != null) {
3987
+ enriched = setSessionBudget(this.db, session.id, launchBudget) ?? enriched;
3988
+ }
3989
+ if (launchGoal) {
3990
+ enriched = setSessionGoal(this.db, session.id, launchGoal) ?? enriched;
3991
+ }
3992
+ this.broadcaster.broadcast({ type: "session_upsert", session: enriched });
3305
3993
  if (!session.parentSessionId) {
3306
3994
  this.transcripts?.startWatching(sessionId, event.cwd);
3307
3995
  }
@@ -3322,7 +4010,7 @@ var EventRouter = class {
3322
4010
  model: this.extractModel(event)
3323
4011
  });
3324
4012
  }
3325
- const mission = startMission(this.db, sessionId, prompt);
4013
+ const mission = startMission(this.db, sessionId, prompt, session.currentGoalId);
3326
4014
  const updated = setSessionMission(this.db, sessionId, mission.id);
3327
4015
  const active = updated ? setSessionStatus(this.db, sessionId, "active") : null;
3328
4016
  this.broadcaster.broadcast({ type: "mission_upsert", mission });
@@ -3366,7 +4054,7 @@ var EventRouter = class {
3366
4054
  const parentSessionId = this.extractSessionId(event);
3367
4055
  const parent = getSession(this.db, parentSessionId);
3368
4056
  if (!parent) return;
3369
- const subId = nanoid7();
4057
+ const subId = nanoid9();
3370
4058
  const sub = upsertSession(this.db, {
3371
4059
  id: subId,
3372
4060
  pid: event.pid,
@@ -3443,7 +4131,7 @@ var EventRouter = class {
3443
4131
  const p = event.payload;
3444
4132
  const message = typeof p.message === "string" ? p.message : "Permission requested";
3445
4133
  const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
3446
- const requestId = nanoid7();
4134
+ const requestId = nanoid9();
3447
4135
  this.permissions.set(requestId, {
3448
4136
  requestId,
3449
4137
  sessionId,
@@ -3471,6 +4159,90 @@ var EventRouter = class {
3471
4159
  message: `Permission requested: ${message}`
3472
4160
  });
3473
4161
  }
4162
+ /**
4163
+ * Synchronous human-in-the-loop gate for the blocking PreToolUse path.
4164
+ * Records the tool call (so the comet/timeline visuals still fire — the gate
4165
+ * hook no longer POSTs to /events), broadcasts a permission_request, and
4166
+ * returns a promise that resolves when a human answers via `permission_response`
4167
+ * (reused unchanged) or when the server-side timeout fires.
4168
+ */
4169
+ requestPermission(event) {
4170
+ const sessionId = this.extractSessionId(event);
4171
+ const p = event.payload;
4172
+ const { tool, args } = this.describeGatedTool(event.event, p);
4173
+ const session = getSession(this.db, sessionId);
4174
+ if (session) {
4175
+ const toolCall = recordToolCall(this.db, {
4176
+ sessionId,
4177
+ missionId: session.currentMissionId,
4178
+ tool,
4179
+ args
4180
+ });
4181
+ if (event.event === "pre_tool_file" && session.currentMissionId && typeof args.file_path === "string" && args.file_path) {
4182
+ addTouchedFile(this.db, session.currentMissionId, args.file_path);
4183
+ }
4184
+ this.broadcaster.broadcast({ type: "tool_call", toolCall });
4185
+ }
4186
+ const requestId = nanoid9();
4187
+ const timeoutMs = Number(process.env.SOLIX_GATE_TIMEOUT_MS ?? 3e5);
4188
+ return new Promise((resolve4) => {
4189
+ const timer = setTimeout(() => {
4190
+ const pending = this.permissions.get(requestId);
4191
+ if (!pending) return;
4192
+ this.permissions.delete(requestId);
4193
+ const s = setSessionStatus(this.db, sessionId, "active");
4194
+ if (s) this.broadcaster.broadcast({ type: "session_upsert", session: s });
4195
+ resolve4({ approved: false, timedOut: true });
4196
+ }, timeoutMs);
4197
+ this.permissions.set(requestId, {
4198
+ requestId,
4199
+ sessionId,
4200
+ tool,
4201
+ args,
4202
+ createdAt: Date.now(),
4203
+ resolve: (approved) => resolve4({ approved, timedOut: false }),
4204
+ timer
4205
+ });
4206
+ const updated = setSessionStatus(
4207
+ this.db,
4208
+ sessionId,
4209
+ "awaiting_permission"
4210
+ );
4211
+ if (updated) {
4212
+ this.broadcaster.broadcast({ type: "session_upsert", session: updated });
4213
+ }
4214
+ this.broadcaster.broadcast({
4215
+ type: "permission_request",
4216
+ sessionId,
4217
+ tool,
4218
+ args,
4219
+ requestId
4220
+ });
4221
+ this.broadcaster.broadcast({
4222
+ type: "toast",
4223
+ level: "warn",
4224
+ message: `Approval requested: ${tool}`
4225
+ });
4226
+ });
4227
+ }
4228
+ describeGatedTool(eventName, p) {
4229
+ const toolInput = p.tool_input ?? {};
4230
+ if (eventName === "pre_tool_bash") {
4231
+ const command = typeof p.command === "string" ? p.command : typeof toolInput.command === "string" ? toolInput.command : "";
4232
+ return { tool: "Bash", args: { command } };
4233
+ }
4234
+ if (eventName === "pre_tool_file") {
4235
+ const tool2 = typeof p.tool_name === "string" ? p.tool_name : "File";
4236
+ const filePath = typeof p.file_path === "string" ? p.file_path : typeof toolInput.file_path === "string" ? toolInput.file_path : "";
4237
+ return { tool: tool2, args: { file_path: filePath } };
4238
+ }
4239
+ if (eventName === "pre_tool_task") {
4240
+ const tool2 = typeof p.tool_name === "string" ? p.tool_name : "Task";
4241
+ return { tool: tool2, args: toolInput };
4242
+ }
4243
+ const tool = typeof p.tool_name === "string" ? p.tool_name : "tool";
4244
+ return { tool, args: toolInput };
4245
+ }
3474
4246
  invokeAdvisor(advisorId, targetSessionId, prompt) {
3475
4247
  const advisor = getAdvisor(this.db, advisorId);
3476
4248
  if (!advisor) return { ok: false };
@@ -3523,6 +4295,20 @@ var EventRouter = class {
3523
4295
  }
3524
4296
  return ok;
3525
4297
  }
4298
+ /** Sprint N — enable/disable an advisor and broadcast the change so open
4299
+ * browsers (and the CLI path) update live. Returns the updated advisor. */
4300
+ setAdvisorEnabled(advisorId, enabled) {
4301
+ const advisor = setAdvisorEnabled(this.db, advisorId, enabled);
4302
+ if (advisor) {
4303
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
4304
+ this.broadcaster.broadcast({
4305
+ type: "toast",
4306
+ level: "info",
4307
+ message: `${advisor.codename} ${enabled ? "added to crew" : "disabled"}`
4308
+ });
4309
+ }
4310
+ return advisor;
4311
+ }
3526
4312
  unpinAdvisor(advisorId) {
3527
4313
  if (this.launcher) {
3528
4314
  this.launcher.unpin(advisorId);
@@ -3544,6 +4330,8 @@ var EventRouter = class {
3544
4330
  const pending = this.permissions.get(requestId);
3545
4331
  if (!pending) return false;
3546
4332
  this.permissions.delete(requestId);
4333
+ if (pending.timer) clearTimeout(pending.timer);
4334
+ if (pending.resolve) pending.resolve(approved);
3547
4335
  const status = approved ? "active" : "idle";
3548
4336
  const session = setSessionStatus(this.db, pending.sessionId, status);
3549
4337
  if (session)
@@ -3579,7 +4367,29 @@ var EventRouter = class {
3579
4367
  model: opts.model,
3580
4368
  initialPrompt: opts.initialPrompt,
3581
4369
  worktreeBranch: opts.worktreeBranch,
3582
- worktreeBaseRef: opts.worktreeBaseRef
4370
+ worktreeBaseRef: opts.worktreeBaseRef,
4371
+ useAgentView: opts.useAgentView,
4372
+ agentName: opts.agentName,
4373
+ budgetUsd: opts.budgetUsd,
4374
+ goalId: opts.goalId
4375
+ });
4376
+ }
4377
+ /** Sprint M — raise (or clear) a session's budget cap. Clears any standing
4378
+ * budget breach by re-broadcasting the current cost against the new cap. */
4379
+ raiseBudget(sessionId, budgetUsd) {
4380
+ const session = setSessionBudget(this.db, sessionId, budgetUsd);
4381
+ if (!session) return;
4382
+ this.broadcaster.broadcast({ type: "session_upsert", session });
4383
+ this.broadcaster.broadcast({
4384
+ type: "cost_update",
4385
+ sessionId,
4386
+ costUsd: session.costUsd,
4387
+ budgetUsd: session.budgetUsd
4388
+ });
4389
+ this.broadcaster.broadcast({
4390
+ type: "toast",
4391
+ level: "info",
4392
+ message: `Budget raised to $${budgetUsd.toFixed(2)} for ${session.name ?? sessionId.slice(0, 8)}`
3583
4393
  });
3584
4394
  }
3585
4395
  sendPromptToSession(sessionId, text) {
@@ -3624,6 +4434,19 @@ var EventRouter = class {
3624
4434
  broadcastSessionUpsert(session) {
3625
4435
  this.broadcaster.broadcast({ type: "session_upsert", session });
3626
4436
  }
4437
+ // Sprint M — broadcast helpers for schedule/goal CRUD driven by HTTP/CLI.
4438
+ broadcastScheduleUpsert(schedule2) {
4439
+ this.broadcaster.broadcast({ type: "schedule_upsert", schedule: schedule2 });
4440
+ }
4441
+ broadcastScheduleRemove(scheduleId) {
4442
+ this.broadcaster.broadcast({ type: "schedule_remove", scheduleId });
4443
+ }
4444
+ broadcastGoalUpsert(goal2) {
4445
+ this.broadcaster.broadcast({ type: "goal_upsert", goal: goal2 });
4446
+ }
4447
+ broadcastGoalRemove(goalId) {
4448
+ this.broadcaster.broadcast({ type: "goal_remove", goalId });
4449
+ }
3627
4450
  broadcastGalaxyImported(manifest) {
3628
4451
  this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
3629
4452
  this.broadcaster.broadcast({
@@ -3679,7 +4502,9 @@ function attachWs(server, ctx) {
3679
4502
  sessions: listActiveSessions(ctx.db),
3680
4503
  missions: listMissions(ctx.db, { limit: 100 }),
3681
4504
  advisors: listAdvisors(ctx.db),
3682
- skills: listSkills(ctx.db)
4505
+ skills: listSkills(ctx.db),
4506
+ schedules: listSchedules(ctx.db),
4507
+ goals: listGoals(ctx.db)
3683
4508
  };
3684
4509
  ctx.broadcaster.send(ws, snapshot);
3685
4510
  for (const p of ctx.router.pendingPermissions()) {
@@ -3726,9 +4551,18 @@ function handleClientMessage(ctx, _ws, msg) {
3726
4551
  model: msg.model,
3727
4552
  initialPrompt: msg.initialPrompt,
3728
4553
  worktreeBranch: msg.worktreeBranch,
3729
- worktreeBaseRef: msg.worktreeBaseRef
4554
+ worktreeBaseRef: msg.worktreeBaseRef,
4555
+ useAgentView: msg.useAgentView,
4556
+ agentName: msg.agentName,
4557
+ budgetUsd: msg.budgetUsd,
4558
+ goalId: msg.goalId
3730
4559
  });
3731
4560
  break;
4561
+ case "raise_budget":
4562
+ ctx.router.raiseBudget(msg.sessionId, msg.budgetUsd);
4563
+ break;
4564
+ case "dismiss_budget_alert":
4565
+ break;
3732
4566
  case "invoke_advisor":
3733
4567
  ctx.router.invokeAdvisor(
3734
4568
  msg.advisorId,
@@ -3742,6 +4576,9 @@ function handleClientMessage(ctx, _ws, msg) {
3742
4576
  case "unpin_advisor":
3743
4577
  ctx.router.unpinAdvisor(msg.advisorId);
3744
4578
  break;
4579
+ case "set_advisor_enabled":
4580
+ ctx.router.setAdvisorEnabled(msg.advisorId, msg.enabled);
4581
+ break;
3745
4582
  default:
3746
4583
  break;
3747
4584
  }
@@ -3777,12 +4614,15 @@ var TranscriptWatcherManager = class {
3777
4614
  constructor(db, broadcaster) {
3778
4615
  this.db = db;
3779
4616
  this.broadcaster = broadcaster;
3780
- void this.db;
3781
4617
  }
3782
4618
  db;
3783
4619
  broadcaster;
3784
4620
  records = /* @__PURE__ */ new Map();
3785
4621
  deferredRetry = /* @__PURE__ */ new Map();
4622
+ // Sprint M: sessions we've already raised a budget alert for, so we don't
4623
+ // re-fire on every subsequent assistant message. Cleared when spend drops
4624
+ // back under the cap (e.g. after the cap is raised).
4625
+ budgetAlerted = /* @__PURE__ */ new Set();
3786
4626
  /**
3787
4627
  * Begin tailing this session's transcript. Idempotent. If the file doesn't
3788
4628
  * exist yet, retries every second for up to 10 s (Claude Code creates the
@@ -3914,6 +4754,39 @@ var TranscriptWatcherManager = class {
3914
4754
  sessionId,
3915
4755
  usagePct: pct
3916
4756
  });
4757
+ const inc = costForUsage(message.model, message.usage);
4758
+ const session = getSession(this.db, sessionId);
4759
+ if (session) {
4760
+ const updated = setSessionCost(this.db, sessionId, session.costUsd + inc);
4761
+ const costUsd = updated?.costUsd ?? session.costUsd + inc;
4762
+ const cap = updated?.budgetUsd ?? session.budgetUsd;
4763
+ this.broadcaster.broadcast({
4764
+ type: "cost_update",
4765
+ sessionId,
4766
+ costUsd,
4767
+ budgetUsd: cap
4768
+ });
4769
+ if (session.currentMissionId) {
4770
+ addMissionTokens(
4771
+ this.db,
4772
+ session.currentMissionId,
4773
+ totalTokens(message.usage)
4774
+ );
4775
+ }
4776
+ if (cap != null && costUsd >= cap) {
4777
+ if (!this.budgetAlerted.has(sessionId)) {
4778
+ this.budgetAlerted.add(sessionId);
4779
+ this.broadcaster.broadcast({
4780
+ type: "budget_alert",
4781
+ sessionId,
4782
+ costUsd,
4783
+ budgetUsd: cap
4784
+ });
4785
+ }
4786
+ } else {
4787
+ this.budgetAlerted.delete(sessionId);
4788
+ }
4789
+ }
3917
4790
  }
3918
4791
  const content = this.flattenAssistantContent(message.content);
3919
4792
  if (!content) return;
@@ -3991,6 +4864,196 @@ ${text.slice(0, 600)}`);
3991
4864
  }
3992
4865
  };
3993
4866
 
4867
+ // ../server/src/state/agentview.ts
4868
+ import { existsSync as existsSync11, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6, watch as watch2 } from "fs";
4869
+ import { homedir as homedir10 } from "os";
4870
+ import { join as join14 } from "path";
4871
+ var ROSTER_PATH = join14(homedir10(), ".claude", "daemon", "roster.json");
4872
+ var JOBS_DIR = join14(homedir10(), ".claude", "jobs");
4873
+ function mapStatus(state) {
4874
+ switch (state) {
4875
+ case "working":
4876
+ return "active";
4877
+ case "needs_input":
4878
+ return "awaiting_input";
4879
+ case "idle":
4880
+ return "idle";
4881
+ case "completed":
4882
+ return "terminated";
4883
+ case "failed":
4884
+ return "error";
4885
+ case "stopped":
4886
+ return "terminated";
4887
+ default:
4888
+ return "idle";
4889
+ }
4890
+ }
4891
+ function mapPrStatus(s) {
4892
+ if (!s) return void 0;
4893
+ if (s === "pending" || s === "success" || s === "failure" || s === "neutral")
4894
+ return s;
4895
+ return void 0;
4896
+ }
4897
+ function readRoster() {
4898
+ if (!existsSync11(ROSTER_PATH)) return [];
4899
+ try {
4900
+ const raw = readFileSync7(ROSTER_PATH, "utf8");
4901
+ const parsed = JSON.parse(raw);
4902
+ if (Array.isArray(parsed)) return parsed;
4903
+ if (parsed && Array.isArray(parsed.sessions)) return parsed.sessions;
4904
+ return [];
4905
+ } catch {
4906
+ return [];
4907
+ }
4908
+ }
4909
+ function readJobIds() {
4910
+ if (!existsSync11(JOBS_DIR)) return [];
4911
+ try {
4912
+ return readdirSync5(JOBS_DIR).filter((entry) => {
4913
+ try {
4914
+ return statSync6(join14(JOBS_DIR, entry)).isDirectory();
4915
+ } catch {
4916
+ return false;
4917
+ }
4918
+ });
4919
+ } catch {
4920
+ return [];
4921
+ }
4922
+ }
4923
+ function readJobState(jobId) {
4924
+ const p = join14(JOBS_DIR, jobId, "state.json");
4925
+ if (!existsSync11(p)) return null;
4926
+ try {
4927
+ return JSON.parse(readFileSync7(p, "utf8"));
4928
+ } catch {
4929
+ return null;
4930
+ }
4931
+ }
4932
+ function syncFromDisk({ db, broadcaster }) {
4933
+ const roster = readRoster();
4934
+ const jobIds = readJobIds();
4935
+ const liveIds = /* @__PURE__ */ new Set();
4936
+ for (const e of roster) if (e.id) liveIds.add(e.id);
4937
+ for (const id of jobIds) liveIds.add(id);
4938
+ for (const agentViewId of liveIds) {
4939
+ const state = readJobState(agentViewId);
4940
+ if (!state) continue;
4941
+ const cwd = state.cwd ?? "";
4942
+ if (!cwd) continue;
4943
+ const solixId = `av-${agentViewId}`;
4944
+ const existing = getSession(db, solixId);
4945
+ const status = mapStatus(state.state);
4946
+ const summary = state.summary ?? null;
4947
+ const prUrl = state.pr_url ?? null;
4948
+ const prCheckStatus = mapPrStatus(state.pr_check_status) ?? null;
4949
+ if (!existing) {
4950
+ const project = ensureProject(db, cwd);
4951
+ const session = upsertSession(db, {
4952
+ id: solixId,
4953
+ pid: 0,
4954
+ // we don't know the pid; supervisor owns it
4955
+ projectId: project.id,
4956
+ cwd,
4957
+ origin: "agentview",
4958
+ model: state.model ?? "default",
4959
+ kind: "user",
4960
+ worktreePath: state.worktree_path ?? void 0,
4961
+ agentViewId,
4962
+ agentViewSummary: summary ?? void 0,
4963
+ prUrl: prUrl ?? void 0,
4964
+ prCheckStatus: prCheckStatus ?? void 0
4965
+ });
4966
+ const updated2 = setAgentViewFields(db, solixId, { status });
4967
+ broadcaster.broadcast({
4968
+ type: "session_upsert",
4969
+ session: updated2 ?? session
4970
+ });
4971
+ continue;
4972
+ }
4973
+ const changed = existing.status !== status || (existing.agentViewSummary ?? null) !== summary || (existing.prUrl ?? null) !== prUrl || (existing.prCheckStatus ?? null) !== prCheckStatus;
4974
+ if (!changed) continue;
4975
+ const updated = setAgentViewFields(db, solixId, {
4976
+ status,
4977
+ agentViewSummary: summary,
4978
+ prUrl,
4979
+ prCheckStatus
4980
+ });
4981
+ if (updated) broadcaster.broadcast({ type: "session_upsert", session: updated });
4982
+ }
4983
+ const rows = db.prepare(
4984
+ `SELECT id, agent_view_id FROM sessions
4985
+ WHERE origin = 'agentview' AND status != 'terminated'`
4986
+ ).all();
4987
+ for (const r of rows) {
4988
+ if (r.agent_view_id && !liveIds.has(r.agent_view_id)) {
4989
+ const updated = setAgentViewFields(db, r.id, { status: "terminated" });
4990
+ if (updated)
4991
+ broadcaster.broadcast({ type: "session_upsert", session: updated });
4992
+ }
4993
+ }
4994
+ }
4995
+ function debounce(fn, ms) {
4996
+ let h = null;
4997
+ return () => {
4998
+ if (h) clearTimeout(h);
4999
+ h = setTimeout(() => {
5000
+ h = null;
5001
+ fn();
5002
+ }, ms);
5003
+ };
5004
+ }
5005
+ function startAgentViewBridge(opts) {
5006
+ const claudeRoot = join14(homedir10(), ".claude");
5007
+ if (!existsSync11(claudeRoot)) return () => {
5008
+ };
5009
+ const sync = () => {
5010
+ try {
5011
+ syncFromDisk(opts);
5012
+ } catch (err) {
5013
+ console.warn("[agentview] sync failed:", err.message);
5014
+ }
5015
+ };
5016
+ const debounced = debounce(sync, 50);
5017
+ sync();
5018
+ const watchers = [];
5019
+ const daemonDir = join14(homedir10(), ".claude", "daemon");
5020
+ if (existsSync11(daemonDir)) {
5021
+ try {
5022
+ watchers.push(watch2(daemonDir, { persistent: false }, debounced));
5023
+ } catch (err) {
5024
+ console.warn(
5025
+ "[agentview] could not watch daemon dir:",
5026
+ err.message
5027
+ );
5028
+ }
5029
+ }
5030
+ if (existsSync11(JOBS_DIR)) {
5031
+ try {
5032
+ watchers.push(watch2(JOBS_DIR, { recursive: true, persistent: false }, debounced));
5033
+ } catch (err) {
5034
+ console.warn("[agentview] recursive watch unsupported; falling back to poll");
5035
+ const poll = setInterval(sync, 3e3);
5036
+ return () => {
5037
+ clearInterval(poll);
5038
+ for (const w of watchers) {
5039
+ try {
5040
+ w.close();
5041
+ } catch {
5042
+ }
5043
+ }
5044
+ };
5045
+ }
5046
+ }
5047
+ return () => {
5048
+ for (const w of watchers) {
5049
+ try {
5050
+ w.close();
5051
+ } catch {
5052
+ }
5053
+ }
5054
+ };
5055
+ }
5056
+
3994
5057
  // ../server/src/create.ts
3995
5058
  async function createSolixServer(opts = {}) {
3996
5059
  const port = opts.port ?? 4242;
@@ -4009,7 +5072,17 @@ async function createSolixServer(opts = {}) {
4009
5072
  const launcher = new Launcher(db, broadcaster);
4010
5073
  const transcripts = new TranscriptWatcherManager(db, broadcaster);
4011
5074
  const router = new EventRouter(db, broadcaster, launcher, transcripts);
4012
- const app = createHttpApp({ db, router });
5075
+ const tokenPath = join15(
5076
+ process.env.SOLIX_HOME ?? join15(homedir11(), ".solix"),
5077
+ "token"
5078
+ );
5079
+ let token = null;
5080
+ try {
5081
+ token = readFileSync8(tokenPath, "utf8").trim() || null;
5082
+ } catch {
5083
+ token = null;
5084
+ }
5085
+ const app = createHttpApp({ db, router, token });
4013
5086
  const server = serve({
4014
5087
  fetch: app.fetch,
4015
5088
  port,
@@ -4020,10 +5093,33 @@ async function createSolixServer(opts = {}) {
4020
5093
  router,
4021
5094
  broadcaster
4022
5095
  });
5096
+ const stopAgentViewBridge = startAgentViewBridge({ db, broadcaster });
5097
+ const scheduleTimer = setInterval(() => {
5098
+ try {
5099
+ const due = listDueSchedules(db, now());
5100
+ for (const s of due) {
5101
+ if (!s.cwd) continue;
5102
+ launcher.launch({ cwd: s.cwd, initialPrompt: s.prompt });
5103
+ const updated = markScheduleRun(db, s.id);
5104
+ if (updated) {
5105
+ broadcaster.broadcast({ type: "schedule_upsert", schedule: updated });
5106
+ broadcaster.broadcast({
5107
+ type: "toast",
5108
+ level: "info",
5109
+ message: `Heartbeat fired: ${s.name ?? s.prompt.slice(0, 32)}`
5110
+ });
5111
+ }
5112
+ }
5113
+ } catch (err) {
5114
+ console.warn("[scheduler] tick failed:", err.message);
5115
+ }
5116
+ }, 3e4);
4023
5117
  return {
4024
5118
  port,
4025
5119
  hostname,
4026
5120
  close: () => new Promise((resolve4) => {
5121
+ clearInterval(scheduleTimer);
5122
+ stopAgentViewBridge();
4027
5123
  transcripts.shutdownAll();
4028
5124
  launcher.shutdownAll();
4029
5125
  server.close(() => resolve4());
@@ -4071,20 +5167,20 @@ async function start(opts = {}) {
4071
5167
  }
4072
5168
 
4073
5169
  // src/uninstall.ts
4074
- import { copyFileSync as copyFileSync2, existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
5170
+ import { copyFileSync as copyFileSync2, existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
4075
5171
  function uninstall() {
4076
5172
  uninstallShim();
4077
- if (existsSync11(CLAUDE_BACKUP)) {
5173
+ if (existsSync12(CLAUDE_BACKUP)) {
4078
5174
  copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
4079
5175
  console.log(`[solix] restored settings.json from backup`);
4080
5176
  return;
4081
5177
  }
4082
- if (!existsSync11(CLAUDE_SETTINGS)) {
5178
+ if (!existsSync12(CLAUDE_SETTINGS)) {
4083
5179
  console.log("[solix] nothing to uninstall (no settings.json found)");
4084
5180
  return;
4085
5181
  }
4086
5182
  const cur = JSON.parse(
4087
- readFileSync7(CLAUDE_SETTINGS, "utf8")
5183
+ readFileSync9(CLAUDE_SETTINGS, "utf8")
4088
5184
  );
4089
5185
  if (cur.hooks) {
4090
5186
  for (const [evt, entries] of Object.entries(cur.hooks)) {
@@ -4100,7 +5196,7 @@ function uninstall() {
4100
5196
 
4101
5197
  // src/index.ts
4102
5198
  var program = new Command();
4103
- program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.4.2");
5199
+ program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.8.0");
4104
5200
  program.command("start", { isDefault: true }).description("Start the Solix server and open the browser").option("-p, --port <port>", "port to listen on", (v) => parseInt(v, 10), 4242).option("--no-open", "do not open browser automatically").action(async (opts) => {
4105
5201
  await start({ port: opts.port, noOpen: !opts.open });
4106
5202
  });
@@ -4169,6 +5265,36 @@ galaxy.command("publish <slug>").description("Publish the current galaxy to the
4169
5265
  galaxy.command("install <slug>").description("Pull and install a galaxy from the configured registry").action(async (slug) => {
4170
5266
  await installFromRegistryCmd(slug);
4171
5267
  });
5268
+ var schedule = program.command("schedule").description('Manage recurring "heartbeat" tasks (Sprint M)');
5269
+ schedule.command("list", { isDefault: true }).description("List all scheduled tasks").action(async () => {
5270
+ await listSchedulesCmd();
5271
+ });
5272
+ schedule.command("add <prompt>").description("Schedule a recurring task").option("--cwd <dir>", "working directory (default: current dir)").option("--every <cadence>", "cadence: 30m, 2h, 1d", "1h").option("--name <name>", "short label for the galaxy node").action(
5273
+ async (prompt, opts) => {
5274
+ await addScheduleCmd(prompt, opts);
5275
+ }
5276
+ );
5277
+ schedule.command("enable <id>").description("Enable a schedule").action(async (id) => {
5278
+ await enableScheduleCmd(id);
5279
+ });
5280
+ schedule.command("disable <id>").description("Disable a schedule (keeps it, stops firing)").action(async (id) => {
5281
+ await disableScheduleCmd(id);
5282
+ });
5283
+ schedule.command("remove <id>").description("Delete a schedule").action(async (id) => {
5284
+ await removeScheduleCmd(id);
5285
+ });
5286
+ var goal = program.command("goal").description("Manage goals that missions roll up to (Sprint M)");
5287
+ goal.command("list", { isDefault: true }).description("List all goals").action(async () => {
5288
+ await listGoalsCmd();
5289
+ });
5290
+ goal.command("add <name>").description("Create a goal").option("--description <desc>", "optional description").option("--color <hex>", "optional hex color for the constellation").action(
5291
+ async (name, opts) => {
5292
+ await addGoalCmd(name, opts);
5293
+ }
5294
+ );
5295
+ goal.command("remove <id>").description("Delete a goal (detaches it from sessions/missions)").action(async (id) => {
5296
+ await removeGoalCmd(id);
5297
+ });
4172
5298
  program.parseAsync(process.argv).catch((err) => {
4173
5299
  console.error(err);
4174
5300
  process.exit(1);