svamp-cli 0.2.229 → 0.2.231-canary.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.
Files changed (24) hide show
  1. package/dist/{agentCommands-CsL5nKMU.mjs → agentCommands-DQ8AWomX.mjs} +5 -5
  2. package/dist/{auth-Wlu9tdQi.mjs → auth-DbbTsm6R.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-Cb4O-FK1.mjs → commands--XkWfxcR.mjs} +1 -1
  5. package/dist/{commands-B40vsIFN.mjs → commands-4ka5ZQyg.mjs} +1 -1
  6. package/dist/{commands-Bwh4YPl9.mjs → commands-C4-LaKZT.mjs} +2 -2
  7. package/dist/{commands-Dvx0xhxl.mjs → commands-DfKg_hXt.mjs} +2 -2
  8. package/dist/{commands-Hxd_3e90.mjs → commands-DyESdWcB.mjs} +1 -1
  9. package/dist/{commands-7UVn6rwT.mjs → commands-DyJ7HMJl.mjs} +1 -1
  10. package/dist/{commands-qNegeWoa.mjs → commands-gxKBfH-9.mjs} +6 -6
  11. package/dist/{fleet-DkANyXBc.mjs → fleet-Dkm9EQW-.mjs} +1 -1
  12. package/dist/{frpc-DLHENjxp.mjs → frpc-D4I-1CO9.mjs} +1 -1
  13. package/dist/{headlessCli-DDJv4_nj.mjs → headlessCli-DufjFViU.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/{package-D8QFNzpX.mjs → package-DkfV_XZC.mjs} +1 -1
  16. package/dist/{rpc-DBDPUPj4.mjs → rpc-DQ2uhpLR.mjs} +1 -1
  17. package/dist/{rpc-D1_ae6MP.mjs → rpc-qgv0GNgV.mjs} +1 -1
  18. package/dist/{run-CXQ-NEsY.mjs → run-CiJNUuye.mjs} +293 -100
  19. package/dist/{run-DBB1tUY1.mjs → run-Dehv9lrA.mjs} +1 -1
  20. package/dist/{scheduler-DPwwX1fQ.mjs → scheduler-BvwDI3YR.mjs} +1 -1
  21. package/dist/{serveCommands-DuhXXuPg.mjs → serveCommands-CH2u1qO2.mjs} +5 -5
  22. package/dist/{serveManager-BKTiO1hi.mjs → serveManager-ChzwgiY_.mjs} +2 -2
  23. package/dist/{sideband-BFQr64jf.mjs → sideband-HXnqgHgt.mjs} +1 -1
  24. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import os from 'node:os';
4
- import { c as connectToHypha } from './run-CXQ-NEsY.mjs';
4
+ import { c as connectToHypha } from './run-CiJNUuye.mjs';
5
5
  import { PINNED_CLAUDE_CODE_VERSION } from './pinnedClaudeCode-VIupR1NK.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
@@ -4,7 +4,7 @@ import { mkdirSync, writeFileSync, unlinkSync, existsSync, chmodSync, readFileSy
4
4
  import { join } from 'path';
5
5
  import { homedir, platform, arch } from 'os';
6
6
  import { randomUUID, createHash } from 'crypto';
7
- import { h as getFrpsSubdomainHost, i as getFrpsServerPort, j as getFrpsServerAddr } from './run-CXQ-NEsY.mjs';
7
+ import { h as getFrpsSubdomainHost, i as getFrpsServerPort, j as getFrpsServerAddr } from './run-CiJNUuye.mjs';
8
8
  import 'fs/promises';
9
9
  import 'url';
10
10
  import 'node:crypto';
@@ -1,5 +1,5 @@
1
- import { P as resolveModel, a2 as describeMisconfiguration, a3 as buildMachineDeps } from './run-CXQ-NEsY.mjs';
2
- import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-BFQr64jf.mjs';
1
+ import { P as resolveModel, a2 as describeMisconfiguration, a3 as buildMachineDeps } from './run-CiJNUuye.mjs';
2
+ import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-HXnqgHgt.mjs';
3
3
  import { WebSocket } from 'ws';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import 'os';
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-CXQ-NEsY.mjs';
1
+ export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-CiJNUuye.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.229";
2
+ var version = "0.2.231-canary.0";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
@@ -1,4 +1,4 @@
1
- import { m as resolveProjectRoot } from './run-CXQ-NEsY.mjs';
1
+ import { m as resolveProjectRoot } from './run-CiJNUuye.mjs';
2
2
  import { g as getWorkflow, s as setWorkflowEnabled, r as removeWorkflow, a as saveWorkflow, b as rawWorkflow, l as listWorkflows } from './store-BTs0H_y0.mjs';
3
3
  import { g as getRun, l as listRuns, r as runWorkflow } from './runStore-CtptN7US.mjs';
4
4
  import 'os';
@@ -1,4 +1,4 @@
1
- import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as resumeIssue, p as pauseIssue, q as addComment, t as addIssue, v as listIssues, w as searchIssues, x as isVisibleTo } from './run-CXQ-NEsY.mjs';
1
+ import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as resumeIssue, p as pauseIssue, q as addComment, t as addIssue, v as listIssues, w as searchIssues, x as isVisibleTo } from './run-CiJNUuye.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -2916,7 +2916,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2916
2916
  const tunnels = handlers.tunnels;
2917
2917
  if (!tunnels) throw new Error("Tunnel management not available");
2918
2918
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2919
- const { FrpcTunnel } = await import('./frpc-DLHENjxp.mjs');
2919
+ const { FrpcTunnel } = await import('./frpc-D4I-1CO9.mjs');
2920
2920
  const tunnel = new FrpcTunnel({
2921
2921
  name: params.name,
2922
2922
  ports: params.ports,
@@ -3363,7 +3363,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3363
3363
  }
3364
3364
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3365
3365
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3366
- const { toolsForRole } = await import('./sideband-BFQr64jf.mjs');
3366
+ const { toolsForRole } = await import('./sideband-HXnqgHgt.mjs');
3367
3367
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3368
3368
  return fmt(r2);
3369
3369
  }
@@ -3462,7 +3462,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3462
3462
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3463
3463
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3464
3464
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3465
- const { queryCore } = await import('./commands-Cb4O-FK1.mjs');
3465
+ const { queryCore } = await import('./commands--XkWfxcR.mjs');
3466
3466
  const timeout = c.reply?.timeout_sec || 120;
3467
3467
  let result;
3468
3468
  try {
@@ -10719,6 +10719,7 @@ ${o.task.trim()}`);
10719
10719
  ${o.until.trim()}`);
10720
10720
  lines.push("");
10721
10721
  lines.push("Use your tools (Read, Bash, Grep, Glob) to inspect the ACTUAL repository state \u2014 do NOT trust the transcript. Check the working tree, git log/diff, build/test output, and whether changes were actually built/published/deployed where the request implied it.");
10722
+ lines.push('IMPORTANT \u2014 the loop oracle only counts OPEN (ready/in_progress) issues, so an issue removed from the queue without genuine resolution is invisible to it. Run `svamp issue list --all` (or read `.svamp/issues/`) and confirm NO issue tied to this goal was left PAUSED without a real pending user question, or otherwise sidelined, merely to make the queue look empty. Reject "done" if any goal-relevant work is neither genuinely complete nor legitimately blocked on the user.');
10722
10723
  if (o.oracle) {
10723
10724
  lines.push("");
10724
10725
  lines.push(`The loop oracle is \`${o.oracle.trim()}\` and it currently PASSES${o.oracleOutput ? ` (output: ${o.oracleOutput.trim().slice(0, 400)})` : ""}. A green oracle here only means "no OPEN issues" \u2014 it does NOT mean each CLOSED issue actually works.`);
@@ -10749,10 +10750,58 @@ function buildGoalCondition(o) {
10749
10750
  if (!o.until && !o.oracle && o.task) parts.push("Done when the task is fully complete and you have shown the evidence that proves it.");
10750
10751
  parts.push(`Or stop after ${o.maxIterations && o.maxIterations > 0 ? o.maxIterations : 20} turns.`);
10751
10752
  if (o.inboxDrain !== false) {
10752
- parts.push("(\u{1F4EC} Inbox: at the start of each turn, run `svamp session inbox list` and handle any unread peer message \u2014 reply/triage/merge \u2014 before continuing the task above. This is how a long loop drains queued non-urgent mail; it is NOT a completion criterion.)");
10753
+ parts.push("(\u{1F4EC} Inbox: at each turn, BRIEFLY run `svamp session inbox list` and handle anything relevant (reply/triage) before continuing \u2014 but do NOT derail the task to over-service ambient chatter. This drains queued non-urgent peer mail at loop pace; urgent/VIP already interrupt separately. It is NOT a completion criterion.)");
10753
10754
  }
10754
10755
  return parts.join(" ");
10755
10756
  }
10757
+ const HOUR_MS = 36e5;
10758
+ function sumUsageTokens(usage) {
10759
+ if (!usage || typeof usage !== "object") return 0;
10760
+ const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
10761
+ return n(usage.input_tokens) + n(usage.output_tokens) + n(usage.cache_creation_input_tokens);
10762
+ }
10763
+ function accumulateLedger(prev, sample) {
10764
+ const base = prev && typeof prev === "object" ? { turns: prev.turns || 0, tokens: prev.tokens || 0, window: Array.isArray(prev.window) ? prev.window : [] } : { turns: 0, tokens: 0, window: [] };
10765
+ const addTurns = sample.turns > 0 ? sample.turns : 0;
10766
+ const addTokens = sample.tokens > 0 ? sample.tokens : 0;
10767
+ const window = [...base.window, [sample.ts, addTokens]].filter(([ts]) => sample.ts - ts < HOUR_MS);
10768
+ return { turns: base.turns + addTurns, tokens: base.tokens + addTokens, window };
10769
+ }
10770
+ function tokensInLastHour(ledger, now) {
10771
+ if (!ledger || !Array.isArray(ledger.window)) return 0;
10772
+ return ledger.window.filter(([ts]) => now - ts < HOUR_MS).reduce((s, [, t]) => s + (t > 0 ? t : 0), 0);
10773
+ }
10774
+ function checkLoopBudget(ledger, limits, now, startedAt) {
10775
+ const L = limits || {};
10776
+ const led = ledger || { turns: 0, tokens: 0, window: [] };
10777
+ const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10778
+ if (pos(L.max_iterations) && led.turns >= L.max_iterations) {
10779
+ return { exceeded: true, reason: `iteration budget reached (${led.turns}/${L.max_iterations} turns)` };
10780
+ }
10781
+ if (pos(L.max_runtime_sec) && pos(startedAt)) {
10782
+ const elapsed = (now - startedAt) / 1e3;
10783
+ if (elapsed >= L.max_runtime_sec) {
10784
+ return { exceeded: true, reason: `runtime budget reached (${Math.round(elapsed)}s/${L.max_runtime_sec}s)` };
10785
+ }
10786
+ }
10787
+ if (pos(L.max_tokens) && led.tokens >= L.max_tokens) {
10788
+ return { exceeded: true, reason: `token budget reached (${led.tokens}/${L.max_tokens} tokens)` };
10789
+ }
10790
+ if (pos(L.max_tokens_per_hour)) {
10791
+ const rate = tokensInLastHour(led, now);
10792
+ if (rate >= L.max_tokens_per_hour) {
10793
+ return { exceeded: true, reason: `token-rate budget reached (${rate}/${L.max_tokens_per_hour} tokens in the last hour)` };
10794
+ }
10795
+ }
10796
+ return { exceeded: false };
10797
+ }
10798
+ const DEFAULT_LOOP_EVALUATOR_MODEL = "claude-haiku-4-5-20251001";
10799
+ function resolveEvaluatorModel(envValue) {
10800
+ if (envValue === void 0) return DEFAULT_LOOP_EVALUATOR_MODEL;
10801
+ const v = envValue.trim();
10802
+ if (!v || v.toLowerCase() === "inherit" || v.toLowerCase() === "default") return void 0;
10803
+ return v;
10804
+ }
10756
10805
  function parseEvaluatorVerdict(text) {
10757
10806
  if (!text) return null;
10758
10807
  const candidates = [];
@@ -11636,6 +11685,7 @@ function writeSvampConfig(configPath, config) {
11636
11685
  renameSync$1(tmpPath, configPath);
11637
11686
  return content;
11638
11687
  }
11688
+ const DEFAULT_LOOP_MAX_ITERATIONS = 1e3;
11639
11689
  function getLoopDir(directory, sessionId) {
11640
11690
  if (sessionId) {
11641
11691
  const scoped = join$1(directory, ".svamp", sessionId, "loop");
@@ -11739,6 +11789,90 @@ function stripOldLoopHooks(directory) {
11739
11789
  return false;
11740
11790
  }
11741
11791
  }
11792
+ function spawnHeadlessEvaluator(prompt, opts) {
11793
+ return new Promise((resolve2, reject) => {
11794
+ const evalArgs = [
11795
+ "--print",
11796
+ prompt,
11797
+ // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects real repo
11798
+ // state but must not mutate the working tree it's judging.
11799
+ "--allowedTools",
11800
+ "Read",
11801
+ "Bash",
11802
+ "Grep",
11803
+ "Glob",
11804
+ "--disallowedTools",
11805
+ "Write",
11806
+ "Edit",
11807
+ "MultiEdit",
11808
+ "NotebookEdit",
11809
+ "--permission-mode",
11810
+ "bypassPermissions",
11811
+ "--output-format",
11812
+ "stream-json",
11813
+ "--verbose",
11814
+ ...opts.model ? ["--model", opts.model] : []
11815
+ ];
11816
+ const evalEnv = { ...process.env };
11817
+ applyClaudeProxyEnv(evalEnv);
11818
+ delete evalEnv.CLAUDECODE;
11819
+ let proc;
11820
+ try {
11821
+ proc = spawn$1("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
11822
+ } catch (e) {
11823
+ reject(e);
11824
+ return;
11825
+ }
11826
+ let out = "", buf = "", stderr = "", settled = false;
11827
+ const timer = setTimeout(() => {
11828
+ if (settled) return;
11829
+ settled = true;
11830
+ try {
11831
+ proc.kill("SIGTERM");
11832
+ } catch {
11833
+ }
11834
+ reject(new Error("evaluator timeout"));
11835
+ }, opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e5);
11836
+ proc.stderr?.on("data", (c) => {
11837
+ stderr += c.toString();
11838
+ });
11839
+ proc.stdout.on("data", (chunk) => {
11840
+ buf += chunk.toString();
11841
+ let nl;
11842
+ while ((nl = buf.indexOf("\n")) !== -1) {
11843
+ const line = buf.slice(0, nl);
11844
+ buf = buf.slice(nl + 1);
11845
+ if (!line.trim()) continue;
11846
+ try {
11847
+ const m = JSON.parse(line);
11848
+ if (m.type === "assistant" && m.message?.content) {
11849
+ for (const b of m.message.content) if (b.type === "text" && b.text) out += b.text;
11850
+ } else if (m.type === "result") {
11851
+ if (settled) continue;
11852
+ settled = true;
11853
+ clearTimeout(timer);
11854
+ if (m.is_error) reject(new Error(String(m.result || "evaluator error")));
11855
+ else resolve2(out || String(m.result || ""));
11856
+ }
11857
+ } catch {
11858
+ }
11859
+ }
11860
+ });
11861
+ proc.on("exit", (code) => {
11862
+ if (settled) return;
11863
+ settled = true;
11864
+ clearTimeout(timer);
11865
+ if (code === 0) resolve2(out);
11866
+ else reject(new Error(stderr.trim() || `evaluator exited with code ${code}`));
11867
+ });
11868
+ proc.on("error", (e) => {
11869
+ if (settled) return;
11870
+ settled = true;
11871
+ clearTimeout(timer);
11872
+ reject(e);
11873
+ });
11874
+ });
11875
+ }
11742
11876
  function deactivateLoop(directory, sessionId) {
11743
11877
  try {
11744
11878
  const p = join$1(getLoopDir(directory, sessionId), "loop-state.json");
@@ -11939,6 +12073,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11939
12073
  } catch {
11940
12074
  }
11941
12075
  const wasStopped = existing.active === false || existing.phase === "done" || existing.phase === "gave_up" || existing.phase === "cancelled";
12076
+ const bumpedBudget = parseLoopBudget(cfg.budget);
11942
12077
  if (wasStopped) {
11943
12078
  try {
11944
12079
  const sp = join$1(getLoopDir(directory, sessionId), "loop-state.json");
@@ -11947,6 +12082,8 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11947
12082
  active: true,
11948
12083
  phase: "continue",
11949
12084
  iteration: 0,
12085
+ ...newMax != null ? { max_iterations: newMax } : {},
12086
+ ...bumpedBudget ? { budget: { ...existing.budget || {}, ...bumpedBudget } } : {},
11950
12087
  completed_at: void 0,
11951
12088
  gave_up_reason: void 0,
11952
12089
  stall_hinted: false,
@@ -11958,18 +12095,33 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11958
12095
  setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID$1(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop", createdAt: Date.now() }] }));
11959
12096
  onLoopActivated?.();
11960
12097
  }
12098
+ if (!wasStopped && (newMax != null || bumpedBudget)) {
12099
+ try {
12100
+ const cur = readLoopState(directory, sessionId);
12101
+ if (cur) writeGoalLoopState(directory, sessionId, {
12102
+ ...cur,
12103
+ ...newMax != null ? { max_iterations: newMax } : {},
12104
+ ...bumpedBudget ? { budget: { ...cur.budget || {}, ...bumpedBudget } } : {}
12105
+ });
12106
+ } catch {
12107
+ }
12108
+ }
11961
12109
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop limit updated${newMax != null ? ` \u2192 max ${newMax} iterations` : ""}${wasStopped ? " \u2014 resuming" : ""}.` }, "event");
11962
12110
  logger.log(`[svampConfig] Loop limit modified (max=${newMax}, resumed=${wasStopped}, restarted=${wasStopped})`);
11963
12111
  }
11964
12112
  } else if (cfg && typeof cfg === "object" && (task || until)) {
11965
12113
  const oracle = typeof cfg.oracle === "string" && cfg.oracle.trim() ? cfg.oracle.trim() : void 0;
11966
- const maxIterations = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : 20;
11967
- const condition = buildGoalCondition({ task, until, oracle, maxIterations });
12114
+ const explicitMax = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : void 0;
12115
+ const hasExplicitMax = typeof explicitMax === "number" && explicitMax > 0;
12116
+ const hardMax = hasExplicitMax ? explicitMax : DEFAULT_LOOP_MAX_ITERATIONS;
12117
+ const softTurnCap = hasExplicitMax ? explicitMax : 20;
12118
+ const condition = buildGoalCondition({ task, until, oracle, maxIterations: softTurnCap });
12119
+ const budget = parseLoopBudget(cfg.budget);
11968
12120
  try {
11969
12121
  stripOldLoopHooks(directory);
11970
12122
  } catch {
11971
12123
  }
11972
- writeGoalLoopState(directory, sessionId, { active: true, phase: "running", iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: maxIterations, started_at: Date.now(), holds: 0 });
12124
+ writeGoalLoopState(directory, sessionId, { active: true, phase: "running", iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: hardMax, budget, started_at: Date.now(), holds: 0, ledger: { turns: 0, tokens: 0, window: [] } });
11973
12125
  const existingQueue = getMetadata().messageQueue || [];
11974
12126
  setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
11975
12127
  onLoopActivated?.();
@@ -12418,7 +12570,7 @@ async function startDaemon(options) {
12418
12570
  saveExposedTunnels(list);
12419
12571
  }
12420
12572
  async function createExposedTunnel(spec) {
12421
- const { FrpcTunnel } = await import('./frpc-DLHENjxp.mjs');
12573
+ const { FrpcTunnel } = await import('./frpc-D4I-1CO9.mjs');
12422
12574
  const tunnel = new FrpcTunnel({
12423
12575
  name: spec.name,
12424
12576
  ports: spec.ports,
@@ -12438,7 +12590,7 @@ async function startDaemon(options) {
12438
12590
  return tunnel;
12439
12591
  }
12440
12592
  const tunnelRecreateState = /* @__PURE__ */ new Map();
12441
- const { ServeManager } = await import('./serveManager-BKTiO1hi.mjs');
12593
+ const { ServeManager } = await import('./serveManager-ChzwgiY_.mjs');
12442
12594
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
12443
12595
  ensureAutoInstalledSkills(logger).catch(() => {
12444
12596
  });
@@ -13069,87 +13221,7 @@ ${parts.join("\n")}`);
13069
13221
  };
13070
13222
  sessionService.updateMetadata(sessionMetadata);
13071
13223
  };
13072
- const runHeadlessEvaluator = (prompt) => new Promise((resolve2, reject) => {
13073
- const evalArgs = [
13074
- "--print",
13075
- prompt,
13076
- // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects
13077
- // real repo state but must not mutate the working tree it's judging.
13078
- "--allowedTools",
13079
- "Read",
13080
- "Bash",
13081
- "Grep",
13082
- "Glob",
13083
- "--disallowedTools",
13084
- "Write",
13085
- "Edit",
13086
- "MultiEdit",
13087
- "NotebookEdit",
13088
- "--permission-mode",
13089
- "bypassPermissions",
13090
- "--output-format",
13091
- "stream-json",
13092
- "--verbose"
13093
- ];
13094
- const evalEnv = { ...process.env };
13095
- applyClaudeProxyEnv(evalEnv);
13096
- delete evalEnv.CLAUDECODE;
13097
- let proc;
13098
- try {
13099
- proc = spawn$1("claude", evalArgs, { cwd: directory, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
13100
- } catch (e) {
13101
- reject(e);
13102
- return;
13103
- }
13104
- let out = "", buf = "", stderr = "", settled = false;
13105
- const timer = setTimeout(() => {
13106
- if (settled) return;
13107
- settled = true;
13108
- try {
13109
- proc.kill("SIGTERM");
13110
- } catch {
13111
- }
13112
- reject(new Error("evaluator timeout"));
13113
- }, 3e5);
13114
- proc.stderr?.on("data", (c) => {
13115
- stderr += c.toString();
13116
- });
13117
- proc.stdout.on("data", (chunk) => {
13118
- buf += chunk.toString();
13119
- let nl;
13120
- while ((nl = buf.indexOf("\n")) !== -1) {
13121
- const line = buf.slice(0, nl);
13122
- buf = buf.slice(nl + 1);
13123
- if (!line.trim()) continue;
13124
- try {
13125
- const m = JSON.parse(line);
13126
- if (m.type === "assistant" && m.message?.content) {
13127
- for (const b of m.message.content) if (b.type === "text" && b.text) out += b.text;
13128
- } else if (m.type === "result") {
13129
- if (settled) continue;
13130
- settled = true;
13131
- clearTimeout(timer);
13132
- if (m.is_error) reject(new Error(String(m.result || "evaluator error")));
13133
- else resolve2(out || String(m.result || ""));
13134
- }
13135
- } catch {
13136
- }
13137
- }
13138
- });
13139
- proc.on("exit", (code) => {
13140
- if (settled) return;
13141
- settled = true;
13142
- clearTimeout(timer);
13143
- if (code === 0) resolve2(out);
13144
- else reject(new Error(stderr.trim() || `evaluator exited with code ${code}`));
13145
- });
13146
- proc.on("error", (e) => {
13147
- if (settled) return;
13148
- settled = true;
13149
- clearTimeout(timer);
13150
- reject(e);
13151
- });
13152
- });
13224
+ const runHeadlessEvaluator = (prompt) => spawnHeadlessEvaluator(prompt, { cwd: directory, model: resolveEvaluatorModel(process.env.SVAMP_LOOP_EVALUATOR_MODEL) });
13153
13225
  let goalVerifyInFlight = false;
13154
13226
  const verifyGoalCompletion = async (ls) => {
13155
13227
  if (goalVerifyInFlight) return;
@@ -13180,6 +13252,8 @@ ${parts.join("\n")}`);
13180
13252
  log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
13181
13253
  }
13182
13254
  );
13255
+ const ledger = ls.ledger;
13256
+ const budget = ls.budget;
13183
13257
  if (result.action === "rekick") {
13184
13258
  const cond = ls.task || "";
13185
13259
  writeGoalLoopState(directory, sessionId, {
@@ -13191,18 +13265,20 @@ ${parts.join("\n")}`);
13191
13265
  until: ls.until,
13192
13266
  oracle: oracleCmd,
13193
13267
  max_iterations: ls.max_iterations,
13268
+ budget,
13194
13269
  started_at: startedAt,
13195
- holds: holds + 1
13270
+ holds: holds + 1,
13271
+ ledger
13196
13272
  });
13197
13273
  const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
13198
13274
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
13199
13275
  enqueueLoopMessage(`/goal ${cond} \u2014 INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
13200
13276
  if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
13201
13277
  } else if (result.action === "gave_up") {
13202
- writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds });
13278
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
13203
13279
  checkSvampConfig?.();
13204
13280
  } else {
13205
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds });
13281
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds, ledger });
13206
13282
  checkSvampConfig?.();
13207
13283
  }
13208
13284
  } catch (e) {
@@ -13663,9 +13739,30 @@ ${parts.join("\n")}`);
13663
13739
  try {
13664
13740
  const ls = readLoopState(directory, sessionId);
13665
13741
  if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
13666
- if (process.env.SVAMP_LOOP_VERIFY === "0") {
13667
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now() });
13742
+ const now = Date.now();
13743
+ const ledger = accumulateLedger(ls.ledger, {
13744
+ turns: Number(msg.num_turns) || 0,
13745
+ tokens: sumUsageTokens(msg.usage),
13746
+ ts: now
13747
+ });
13748
+ ls.ledger = ledger;
13749
+ const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
13750
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
13751
+ if (budgetCheck.exceeded) {
13752
+ writeGoalLoopState(directory, sessionId, {
13753
+ ...ls,
13754
+ active: false,
13755
+ phase: "gave_up",
13756
+ completed_at: now,
13757
+ gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`,
13758
+ ledger
13759
+ });
13760
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason}. Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
13761
+ checkSvampConfig?.();
13762
+ } else if (process.env.SVAMP_LOOP_VERIFY === "0") {
13763
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
13668
13764
  } else {
13765
+ writeGoalLoopState(directory, sessionId, { ...ls, ledger });
13669
13766
  void verifyGoalCompletion(ls);
13670
13767
  }
13671
13768
  }
@@ -14502,11 +14599,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14502
14599
  });
14503
14600
  },
14504
14601
  onIssue: async (params) => {
14505
- const { issueRpc } = await import('./rpc-D1_ae6MP.mjs');
14602
+ const { issueRpc } = await import('./rpc-qgv0GNgV.mjs');
14506
14603
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
14507
14604
  },
14508
14605
  onWorkflow: async (params) => {
14509
- const { workflowRpc } = await import('./rpc-DBDPUPj4.mjs');
14606
+ const { workflowRpc } = await import('./rpc-DQ2uhpLR.mjs');
14510
14607
  return workflowRpc(params?.cwd || directory, params || {});
14511
14608
  },
14512
14609
  onRipgrep: async (args, cwd) => {
@@ -15039,11 +15136,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15039
15136
  });
15040
15137
  },
15041
15138
  onIssue: async (params) => {
15042
- const { issueRpc } = await import('./rpc-D1_ae6MP.mjs');
15139
+ const { issueRpc } = await import('./rpc-qgv0GNgV.mjs');
15043
15140
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
15044
15141
  },
15045
15142
  onWorkflow: async (params) => {
15046
- const { workflowRpc } = await import('./rpc-DBDPUPj4.mjs');
15143
+ const { workflowRpc } = await import('./rpc-DQ2uhpLR.mjs');
15047
15144
  return workflowRpc(params?.cwd || directory, params || {});
15048
15145
  },
15049
15146
  onRipgrep: async (args, cwd) => {
@@ -15211,6 +15308,100 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15211
15308
  }
15212
15309
  let acpStopped = false;
15213
15310
  let acpBackendReady = false;
15311
+ let acpVerifyInFlight = false;
15312
+ const dispatchAcpPrompt = (text, displayText) => {
15313
+ if (acpStopped) return;
15314
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
15315
+ sessionService.updateMetadata(sessionMetadata);
15316
+ sessionService.sendKeepAlive(true);
15317
+ sessionService.pushMessage(displayText, "user");
15318
+ agentBackend.sendPrompt(sessionId, text).catch((err) => {
15319
+ logger.error(`[Session ${sessionId}] ACP loop re-kick send failed: ${err.message}`);
15320
+ if (!acpStopped) {
15321
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
15322
+ sessionService.updateMetadata(sessionMetadata);
15323
+ sessionService.sendSessionEnd();
15324
+ }
15325
+ });
15326
+ };
15327
+ const verifyAcpLoopCompletion = async () => {
15328
+ if (acpVerifyInFlight || acpStopped) return;
15329
+ const ls = readLoopState(directory, sessionId);
15330
+ if (!ls || ls.engine !== "goal" || ls.active === false || ls.phase === "done" || ls.phase === "gave_up" || ls.phase === "cancelled") return;
15331
+ if (!isLoopActiveForSession(directory, sessionId)) return;
15332
+ acpVerifyInFlight = true;
15333
+ const now = Date.now();
15334
+ const projectRoot = resolveProjectRoot(directory);
15335
+ const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
15336
+ const oracleCmd = typeof ls.oracle === "string" && ls.oracle.trim() ? ls.oracle.trim() : void 0;
15337
+ const ledger = accumulateLedger(ls.ledger, { turns: 1, tokens: 0, ts: now });
15338
+ ls.ledger = ledger;
15339
+ const holds = typeof ls.holds === "number" ? ls.holds : 0;
15340
+ const runOracle = async () => {
15341
+ if (oracleCmd) {
15342
+ return await new Promise((resolve2) => {
15343
+ exec$1(oracleCmd, { cwd: directory, timeout: 12e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
15344
+ if (err) resolve2({ ok: false, output: (String(stdout || "") + String(stderr || "")).slice(-500) });
15345
+ else resolve2({ ok: true, output: "" });
15346
+ });
15347
+ });
15348
+ }
15349
+ const pending = backlogOraclePending(projectRoot, sessionId);
15350
+ return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
15351
+ };
15352
+ try {
15353
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
15354
+ if (budgetCheck.exceeded) {
15355
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: now, gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`, ledger });
15356
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason}. Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
15357
+ checkSvampConfig?.();
15358
+ return;
15359
+ }
15360
+ const oracle = await runOracle();
15361
+ if (!oracle.ok) {
15362
+ writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", ledger });
15363
+ dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. Not done yet \u2014 the loop oracle still reports pending work:
15364
+ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
15365
+ return;
15366
+ }
15367
+ if (process.env.SVAMP_LOOP_VERIFY === "0") {
15368
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
15369
+ checkSvampConfig?.();
15370
+ return;
15371
+ }
15372
+ sessionService.pushMessage({ type: "message", message: "\u{1F50D} Verifying loop completion (independent review)\u2026" }, "event");
15373
+ const result = await runLoopVerification(
15374
+ { task: ls.goal_task || ls.task, until: ls.until, oracle: oracleCmd, startedAt, holds, maxHolds: 3 },
15375
+ {
15376
+ projectRoot,
15377
+ runOracle: async () => ({ ok: true, output: oracle.output }),
15378
+ runEvaluator: (prompt) => spawnHeadlessEvaluator(prompt, { cwd: directory, model: resolveEvaluatorModel(process.env.SVAMP_LOOP_EVALUATOR_MODEL) }),
15379
+ log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
15380
+ }
15381
+ );
15382
+ if (result.action === "rekick") {
15383
+ writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds: holds + 1, started_at: startedAt, ledger });
15384
+ const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
15385
+ sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
15386
+ dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
15387
+ } else if (result.action === "gave_up") {
15388
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
15389
+ checkSvampConfig?.();
15390
+ } else {
15391
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger });
15392
+ checkSvampConfig?.();
15393
+ }
15394
+ } catch (e) {
15395
+ logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing open (done): ${e?.message || e}`);
15396
+ try {
15397
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), ledger });
15398
+ checkSvampConfig?.();
15399
+ } catch {
15400
+ }
15401
+ } finally {
15402
+ acpVerifyInFlight = false;
15403
+ }
15404
+ };
15214
15405
  const onTurnEnd = (lastAssistantText) => {
15215
15406
  if (acpStopped) return;
15216
15407
  insideOnTurnEnd = true;
@@ -15246,6 +15437,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15246
15437
  sessionService.sendSessionEnd();
15247
15438
  }
15248
15439
  });
15440
+ } else if (isLoopActiveForSession(directory, sessionId)) {
15441
+ void verifyAcpLoopCompletion();
15249
15442
  }
15250
15443
  } finally {
15251
15444
  insideOnTurnEnd = false;
@@ -15817,7 +16010,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15817
16010
  const PING_TIMEOUT_MS = 15e3;
15818
16011
  const POST_RECONNECT_GRACE_MS = 2e4;
15819
16012
  const RECONNECT_JITTER_MS = 2500;
15820
- const { WorkflowScheduler } = await import('./scheduler-DPwwX1fQ.mjs');
16013
+ const { WorkflowScheduler } = await import('./scheduler-BvwDI3YR.mjs');
15821
16014
  const workflowScheduler = new WorkflowScheduler({
15822
16015
  projectRoots: () => {
15823
16016
  const dirs = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { a4 as applyClaudeProxyEnv, a5 as composeSessionId, a6 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a7 as generateHookSettings } from './run-CXQ-NEsY.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { a4 as applyClaudeProxyEnv, a5 as composeSessionId, a6 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a7 as generateHookSettings } from './run-CiJNUuye.mjs';
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
@@ -1,4 +1,4 @@
1
- import { m as resolveProjectRoot, y as cronMatches } from './run-CXQ-NEsY.mjs';
1
+ import { m as resolveProjectRoot, y as cronMatches } from './run-CiJNUuye.mjs';
2
2
  import { l as listWorkflows, i as isWorkflowEnabled, w as workflowCrons } from './store-BTs0H_y0.mjs';
3
3
  import { r as runWorkflow } from './runStore-CtptN7US.mjs';
4
4
  import 'os';