svamp-cli 0.2.98 → 0.2.100

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 (41) hide show
  1. package/README.md +7 -5
  2. package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +49 -0
  3. package/bin/skills/loop/SKILL.md +99 -0
  4. package/bin/skills/loop/bin/channel-core.mjs +161 -0
  5. package/bin/skills/loop/bin/channel-server.mjs +151 -0
  6. package/bin/skills/loop/bin/inject-loop.mjs +41 -0
  7. package/bin/skills/loop/bin/loop-init.mjs +128 -0
  8. package/bin/skills/loop/bin/loop-status.mjs +38 -0
  9. package/bin/skills/loop/bin/precompact.mjs +27 -0
  10. package/bin/skills/loop/bin/routine-cli.mjs +121 -0
  11. package/bin/skills/loop/bin/routine-core.mjs +126 -0
  12. package/bin/skills/loop/bin/routine-runner.mjs +125 -0
  13. package/bin/skills/loop/bin/routine-store.mjs +49 -0
  14. package/bin/skills/loop/bin/state-fp.mjs +113 -0
  15. package/bin/skills/loop/bin/stop-gate.mjs +170 -0
  16. package/bin/skills/loop/routines.process.yaml +20 -0
  17. package/bin/skills/loop/test/test-channel-core.mjs +86 -0
  18. package/bin/skills/loop/test/test-loop-gate.mjs +246 -0
  19. package/bin/skills/loop/test/test-routine-core.mjs +54 -0
  20. package/bin/skills/loop/test/test-routine-engine.mjs +122 -0
  21. package/dist/{agentCommands-BULNvfKa.mjs → agentCommands-muy26BZI.mjs} +2 -2
  22. package/dist/{auth-BfDOBBPy.mjs → auth-RVq9wRhV.mjs} +1 -1
  23. package/dist/{caddy-BMbX-mFX.mjs → caddy-CuTbE3NY.mjs} +1 -14
  24. package/dist/cli.mjs +76 -77
  25. package/dist/{commands-h2Dzb5m1.mjs → commands-ChzeHFd3.mjs} +1 -1
  26. package/dist/{commands-C9DbNFz1.mjs → commands-Cu96nDGv.mjs} +2 -2
  27. package/dist/{commands-FhGCsATM.mjs → commands-EwE87XNi.mjs} +1 -1
  28. package/dist/{commands-qE4ZGLzB.mjs → commands-lSqc48Ib.mjs} +6 -6
  29. package/dist/{commands-DIhhodx8.mjs → commands-rSREfaQg.mjs} +34 -42
  30. package/dist/{fleet-Cmma7Iu-.mjs → fleet-qN96q6Qb.mjs} +1 -1
  31. package/dist/{frpc-BZ4l4-os.mjs → frpc-CIkmTNdJ.mjs} +2 -15
  32. package/dist/{headlessCli-xRpI9fdk.mjs → headlessCli-BVcAcLr1.mjs} +2 -2
  33. package/dist/index.mjs +1 -1
  34. package/dist/package-B7S5w1VE.mjs +63 -0
  35. package/dist/{run-DxzG-3JD.mjs → run-CdtYIBbd.mjs} +158 -709
  36. package/dist/{run-DTIEcH-W.mjs → run-zXRdkYtk.mjs} +1 -1
  37. package/dist/{serveCommands-CzllIFB_.mjs → serveCommands-BZd0reEj.mjs} +5 -5
  38. package/dist/{serveManager-C6_Vloil.mjs → serveManager-lmPtmRnR.mjs} +3 -3
  39. package/dist/{sideband-wPe3a3m1.mjs → sideband-JeID_jF-.mjs} +1 -1
  40. package/package.json +3 -3
  41. package/dist/package-DD227VZO.mjs +0 -63
@@ -3,7 +3,7 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
3
3
  import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
4
  import path__default, { join, dirname, basename, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
6
+ import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
@@ -2077,7 +2077,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2077
2077
  const tunnels = handlers.tunnels;
2078
2078
  if (!tunnels) throw new Error("Tunnel management not available");
2079
2079
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2080
- const { FrpcTunnel } = await import('./frpc-BZ4l4-os.mjs');
2080
+ const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
2081
2081
  const tunnel = new FrpcTunnel({
2082
2082
  name: params.name,
2083
2083
  ports: params.ports,
@@ -2338,7 +2338,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2338
2338
  }
2339
2339
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2340
2340
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2341
- const { toolsForRole } = await import('./sideband-wPe3a3m1.mjs');
2341
+ const { toolsForRole } = await import('./sideband-JeID_jF-.mjs');
2342
2342
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2343
2343
  return fmt(r2);
2344
2344
  }
@@ -3319,7 +3319,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3319
3319
  },
3320
3320
  /**
3321
3321
  * Patch the session config file (.svamp/{sessionId}/config.json).
3322
- * Used by the frontend to set title, session_link, ralph_loop, etc.
3322
+ * Used by the frontend to set title, session_link, loop activation, etc.
3323
3323
  * Null values remove keys from the config.
3324
3324
  */
3325
3325
  updateConfig: async (patch, context) => {
@@ -9095,6 +9095,20 @@ async function ensureAutoInstalledSkills(logger) {
9095
9095
  }
9096
9096
  },
9097
9097
  marketplaceVersion: async () => readBundledSkillVersion("artifact")
9098
+ },
9099
+ {
9100
+ // The self-verifying `loop` skill drives loop mode (Stop-hook gate +
9101
+ // LOOP.md injection). Bundled in the npm package (bin/skills/loop/) so the
9102
+ // daemon can run loop-init.mjs even offline / before marketplace publish.
9103
+ name: "loop",
9104
+ install: async () => {
9105
+ try {
9106
+ installBundledSkill("loop");
9107
+ } catch {
9108
+ await installSkillFromMarketplace("loop");
9109
+ }
9110
+ },
9111
+ marketplaceVersion: async () => readBundledSkillVersion("loop")
9098
9112
  }
9099
9113
  ];
9100
9114
  for (const task of tasks) {
@@ -9224,72 +9238,58 @@ function writeSvampConfig(configPath, config) {
9224
9238
  renameSync(tmpPath, configPath);
9225
9239
  return content;
9226
9240
  }
9227
- function getRalphStateFilePath(directory, sessionId) {
9228
- return join(getSessionDir(directory, sessionId), "ralph-loop.md");
9241
+ function getLoopDir(directory) {
9242
+ return join(directory, ".claude", "loop");
9229
9243
  }
9230
- function readRalphState(filePath) {
9244
+ function readLoopState(directory) {
9231
9245
  try {
9232
- if (!existsSync$1(filePath)) return null;
9233
- const content = readFileSync$1(filePath, "utf-8");
9234
- const parts = content.split(/^---$/m);
9235
- if (parts.length < 3) return null;
9236
- const frontmatter = parts[1];
9237
- const task = parts.slice(2).join("---").trim();
9238
- if (!task) return null;
9239
- const fields = {};
9240
- for (const line of frontmatter.split("\n")) {
9241
- const match = line.match(/^(\w+):\s*(.*)$/);
9242
- if (match) fields[match[1]] = match[2].replace(/^["']|["']$/g, "");
9243
- }
9244
- return {
9245
- iteration: parseInt(fields.iteration || "1", 10) || 1,
9246
- max_iterations: parseInt(fields.max_iterations || "0", 10) || 0,
9247
- completion_promise: fields.completion_promise === "none" ? null : fields.completion_promise || "DONE",
9248
- cooldown_seconds: parseInt(fields.cooldown_seconds || "1", 10) || 1,
9249
- started_at: fields.started_at || (/* @__PURE__ */ new Date()).toISOString(),
9250
- last_iteration_at: fields.last_iteration_at || void 0,
9251
- context_mode: fields.context_mode === "fresh" || fields.context_mode === "continue" ? fields.context_mode : void 0,
9252
- original_resume_id: fields.original_resume_id || void 0,
9253
- task
9254
- };
9246
+ const p = join(getLoopDir(directory), "loop-state.json");
9247
+ if (!existsSync$1(p)) return null;
9248
+ return JSON.parse(readFileSync$1(p, "utf-8"));
9255
9249
  } catch {
9256
9250
  return null;
9257
9251
  }
9258
9252
  }
9259
- function writeRalphState(filePath, state) {
9260
- mkdirSync(dirname(filePath), { recursive: true });
9261
- const promiseYaml = state.completion_promise === null ? "none" : state.completion_promise.includes(":") || state.completion_promise.includes('"') ? `"${state.completion_promise.replace(/"/g, '\\"')}"` : `"${state.completion_promise}"`;
9262
- const lastIterLine = state.last_iteration_at ? `
9263
- last_iteration_at: "${state.last_iteration_at}"` : "";
9264
- const contextModeLine = state.context_mode ? `
9265
- context_mode: "${state.context_mode}"` : "";
9266
- const originalResumeLine = state.original_resume_id ? `
9267
- original_resume_id: "${state.original_resume_id}"` : "";
9268
- const content = `---
9269
- iteration: ${state.iteration}
9270
- max_iterations: ${state.max_iterations}
9271
- completion_promise: ${promiseYaml}
9272
- cooldown_seconds: ${state.cooldown_seconds}
9273
- started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResumeLine}
9274
- ---
9275
-
9276
- ${state.task}
9277
- `;
9278
- const tmpPath = `${filePath}.tmp`;
9279
- writeFileSync(tmpPath, content);
9280
- renameSync(tmpPath, filePath);
9253
+ function isLoopActive(directory) {
9254
+ const s = readLoopState(directory);
9255
+ return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
9281
9256
  }
9282
- function removeRalphState(filePath) {
9257
+ function resolveLoopInit() {
9258
+ const candidates = [
9259
+ join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9260
+ ...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9261
+ ];
9262
+ for (const c of candidates) if (existsSync$1(c)) return c;
9263
+ return null;
9264
+ }
9265
+ function initLoop(directory, cfg) {
9266
+ const initScript = resolveLoopInit();
9267
+ if (!initScript) return false;
9268
+ const args = [initScript, directory, "--task", cfg.task];
9269
+ if (cfg.criteria) args.push("--criteria", cfg.criteria);
9270
+ if (cfg.oracle) args.push("--oracle", cfg.oracle);
9271
+ if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
9272
+ args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
9273
+ if (cfg.model) args.push("--model", cfg.model);
9274
+ const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
9275
+ return res.status === 0;
9276
+ }
9277
+ function deactivateLoop(directory) {
9283
9278
  try {
9284
- unlinkSync$1(filePath);
9279
+ const p = join(getLoopDir(directory), "loop-state.json");
9280
+ if (!existsSync$1(p)) return;
9281
+ const s = JSON.parse(readFileSync$1(p, "utf-8"));
9282
+ s.active = false;
9283
+ s.phase = "cancelled";
9284
+ const tmp = p + ".tmp";
9285
+ writeFileSync(tmp, JSON.stringify(s, null, 2));
9286
+ renameSync(tmp, p);
9285
9287
  } catch {
9286
9288
  }
9287
9289
  }
9288
- function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onRalphLoopActivated) {
9290
+ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onLoopActivated) {
9289
9291
  const configPath = getSvampConfigPath(directory, sessionId);
9290
- const ralphStatePath = getRalphStateFilePath(directory, sessionId);
9291
9292
  let lastConfigContent = "";
9292
- let lastRalphContent = "";
9293
9293
  if (existsSync$1(configPath)) {
9294
9294
  try {
9295
9295
  lastConfigContent = readFileSync$1(configPath, "utf-8");
@@ -9300,13 +9300,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9300
9300
  } catch {
9301
9301
  }
9302
9302
  }
9303
- if (existsSync$1(ralphStatePath)) {
9304
- try {
9305
- lastRalphContent = readFileSync$1(ralphStatePath, "utf-8");
9306
- } catch {
9307
- }
9308
- }
9309
- let needsInitialRalphProcess = !!lastRalphContent;
9310
9303
  function processConfig(config, meta) {
9311
9304
  if (typeof config.title === "string" && config.title.trim()) {
9312
9305
  const newTitle = config.title.trim();
@@ -9351,56 +9344,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9351
9344
  }
9352
9345
  }
9353
9346
  }
9354
- function processRalphState() {
9355
- const meta = getMetadata();
9356
- const prevRalph = meta.ralphLoop;
9357
- const state = readRalphState(ralphStatePath);
9358
- if (state) {
9359
- const ralphLoop = {
9360
- active: true,
9361
- task: state.task,
9362
- completionPromise: state.completion_promise ?? "none",
9363
- maxIterations: state.max_iterations,
9364
- currentIteration: state.iteration,
9365
- startedAt: state.started_at,
9366
- cooldownSeconds: state.cooldown_seconds,
9367
- contextMode: state.context_mode || "fresh",
9368
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
9369
- };
9370
- if (!prevRalph?.active) {
9371
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
9372
- const prompt = buildRalphPrompt(state.task, state);
9373
- const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
9374
- const existingQueue = getMetadata().messageQueue || [];
9375
- setMetadata((m) => ({
9376
- ...m,
9377
- ralphLoop,
9378
- messageQueue: [...existingQueue, {
9379
- id: randomUUID$1(),
9380
- text: prompt,
9381
- displayText: state.task,
9382
- createdAt: Date.now(),
9383
- ralphSystemPrompt: ralphSysPrompt
9384
- }]
9385
- }));
9386
- sessionService.pushMessage(
9387
- { type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
9388
- "event"
9389
- );
9390
- logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
9391
- onRalphLoopActivated?.();
9392
- } else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
9393
- setMetadata((m) => ({ ...m, ralphLoop }));
9394
- }
9395
- } else if (prevRalph?.active) {
9396
- setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
9397
- sessionService.pushMessage(
9398
- { type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
9399
- "event"
9400
- );
9401
- logger.log(`[svampConfig] Ralph loop state file removed \u2014 cancelled at iteration ${prevRalph.currentIteration}`);
9402
- }
9403
- }
9404
9347
  const configChecker = () => {
9405
9348
  try {
9406
9349
  if (existsSync$1(configPath)) {
@@ -9414,50 +9357,54 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9414
9357
  } catch {
9415
9358
  }
9416
9359
  };
9417
- const ralphChecker = () => {
9418
- try {
9419
- if (existsSync$1(ralphStatePath)) {
9420
- const content = readFileSync$1(ralphStatePath, "utf-8");
9421
- if (content !== lastRalphContent) {
9422
- lastRalphContent = content;
9423
- processRalphState();
9424
- }
9425
- } else if (lastRalphContent) {
9426
- lastRalphContent = "";
9427
- processRalphState();
9428
- }
9429
- } catch {
9430
- }
9431
- };
9432
9360
  const checker = () => {
9433
9361
  configChecker();
9434
- ralphChecker();
9435
9362
  };
9436
9363
  const writeConfig = (patch) => {
9437
- if ("ralph_loop" in patch) {
9438
- const rl = patch.ralph_loop;
9439
- if (rl && typeof rl === "object" && typeof rl.task === "string") {
9440
- const contextMode = rl.context_mode === "fresh" || rl.context_mode === "continue" ? rl.context_mode : void 0;
9441
- writeRalphState(ralphStatePath, {
9442
- iteration: typeof rl.current_iteration === "number" ? Math.max(0, rl.current_iteration - 1) : 0,
9443
- max_iterations: typeof rl.max_iterations === "number" ? rl.max_iterations : 0,
9444
- completion_promise: rl.completion_promise === "none" || rl.completion_promise === null ? null : rl.completion_promise || "DONE",
9445
- cooldown_seconds: (() => {
9446
- const cd = typeof rl.cooldown_seconds === "number" ? rl.cooldown_seconds : 1;
9447
- const isInfinite = !rl.completion_promise || rl.completion_promise === "none";
9448
- return isInfinite ? Math.max(1, cd) : cd;
9449
- })(),
9450
- started_at: rl.started_at || (/* @__PURE__ */ new Date()).toISOString(),
9451
- context_mode: contextMode,
9452
- task: rl.task.trim()
9364
+ if ("loop" in patch) {
9365
+ const lp = patch.loop;
9366
+ if (lp && typeof lp === "object" && typeof lp.task === "string" && lp.task.trim()) {
9367
+ const oracle = typeof lp.oracle === "string" && lp.oracle.trim() ? lp.oracle.trim() : void 0;
9368
+ const maxIterations = typeof lp.max_iterations === "number" ? lp.max_iterations : 20;
9369
+ const evaluator = lp.evaluator !== false;
9370
+ const ok = initLoop(directory, {
9371
+ task: lp.task.trim(),
9372
+ criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
9373
+ oracle,
9374
+ maxIterations,
9375
+ evaluator
9453
9376
  });
9454
- ralphChecker();
9377
+ if (ok) {
9378
+ const existingQueue = getMetadata().messageQueue || [];
9379
+ const kickoff = "Begin the loop. Read LOOP.md and work on the task until the exit conditions are met. Do not stop early \u2014 an independent Stop gate will re-check before the loop can end.";
9380
+ setMetadata((m) => ({
9381
+ ...m,
9382
+ messageQueue: [...existingQueue, {
9383
+ id: randomUUID$1(),
9384
+ text: kickoff,
9385
+ displayText: `\u{1F501} Loop started: ${lp.task.trim().slice(0, 100)}`,
9386
+ createdAt: Date.now()
9387
+ }]
9388
+ }));
9389
+ sessionService.pushMessage(
9390
+ { type: "message", message: `\u{1F501} Loop started \u2014 iterating until done (oracle: ${oracle || "none"}, evaluator ${evaluator ? "on" : "off"}, max ${maxIterations}).` },
9391
+ "event"
9392
+ );
9393
+ logger.log(`[svampConfig] Loop started: "${lp.task.trim().slice(0, 50)}..."`);
9394
+ onLoopActivated?.();
9395
+ } else {
9396
+ sessionService.pushMessage(
9397
+ { type: "message", message: "Failed to start loop \u2014 the loop skill could not be located. Reinstall with: svamp skills install loop --force", level: "error" },
9398
+ "event"
9399
+ );
9400
+ logger.log(`[svampConfig] Loop init failed \u2014 loop-init.mjs not found`);
9401
+ }
9455
9402
  } else {
9456
- removeRalphState(ralphStatePath);
9457
- lastRalphContent = "";
9458
- processRalphState();
9403
+ deactivateLoop(directory);
9404
+ sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
9405
+ logger.log(`[svampConfig] Loop cancelled`);
9459
9406
  }
9460
- const { ralph_loop: _, ...restPatch } = patch;
9407
+ const { loop: _, ...restPatch } = patch;
9461
9408
  patch = restPatch;
9462
9409
  }
9463
9410
  if (Object.keys(patch).length > 0) {
@@ -9476,32 +9423,11 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9476
9423
  mkdirSync(configDir, { recursive: true });
9477
9424
  watcher = watch(configDir, (eventType, filename) => {
9478
9425
  if (filename === "config.json") configChecker();
9479
- if (filename === "ralph-loop.md") ralphChecker();
9480
9426
  });
9481
9427
  watcher.on("error", () => {
9482
9428
  });
9483
9429
  } catch {
9484
9430
  }
9485
- if (needsInitialRalphProcess) {
9486
- const state = readRalphState(ralphStatePath);
9487
- if (state) {
9488
- setMetadata((m) => ({
9489
- ...m,
9490
- ralphLoop: {
9491
- active: true,
9492
- task: state.task,
9493
- completionPromise: state.completion_promise ?? "none",
9494
- maxIterations: state.max_iterations,
9495
- currentIteration: state.iteration,
9496
- startedAt: state.started_at,
9497
- cooldownSeconds: state.cooldown_seconds,
9498
- contextMode: state.context_mode || "fresh",
9499
- lastIterationStartedAt: state.last_iteration_at || (/* @__PURE__ */ new Date()).toISOString()
9500
- }
9501
- }));
9502
- logger.log(`[svampConfig] Ralph loop state restored (iteration ${state.iteration}): "${state.task.slice(0, 50)}..."`);
9503
- }
9504
- }
9505
9431
  return {
9506
9432
  check: checker,
9507
9433
  cleanup: () => {
@@ -9510,133 +9436,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9510
9436
  writeConfig
9511
9437
  };
9512
9438
  }
9513
- function buildIterationStatus(iteration, maxIterations, completionPromise) {
9514
- const iterStr = maxIterations > 0 ? `${iteration}/${maxIterations}` : `${iteration}`;
9515
- if (completionPromise) {
9516
- return `Ralph iteration ${iterStr} | To stop: output <promise>${completionPromise}</promise>`;
9517
- }
9518
- return `Ralph iteration ${iterStr} | Manual stop only`;
9519
- }
9520
- function buildRalphSystemPrompt(state, progressFilePath) {
9521
- const isFresh = state.context_mode === "fresh" || !state.context_mode;
9522
- if (isFresh && progressFilePath) {
9523
- return [
9524
- "# Ralph Loop \u2014 Fresh Context Mode",
9525
- "",
9526
- "You are an autonomous coding agent in an automated loop.",
9527
- "Each iteration spawns a fresh process \u2014 you have NO memory of previous iterations.",
9528
- "",
9529
- "## Your Workflow",
9530
- "",
9531
- `1. Read the progress file at \`${progressFilePath}\` (check Patterns section first)`,
9532
- "2. Check workspace files and git history (`git log --oneline -10`) to understand current state",
9533
- "3. Pick the next incomplete task and implement it",
9534
- "4. Run quality checks (tests, typecheck, build \u2014 whatever the project requires)",
9535
- "5. Commit your changes with a clear commit message",
9536
- `6. Update \`${progressFilePath}\` with your progress (ALWAYS append, never replace)`,
9537
- "",
9538
- "## Progress Report Format",
9539
- "",
9540
- `APPEND to \`${progressFilePath}\`:`,
9541
- "```",
9542
- "## [Date/Time] \u2014 What was done",
9543
- "- What was implemented",
9544
- "- Files changed",
9545
- "- **Learnings for future iterations:**",
9546
- " - Patterns discovered",
9547
- " - Gotchas encountered",
9548
- " - Useful context",
9549
- "---",
9550
- "```",
9551
- "",
9552
- "## Consolidate Patterns",
9553
- "",
9554
- `If you discover a reusable pattern, add it to the \`## Patterns\` section at the TOP of \`${progressFilePath}\`.`,
9555
- "Only add patterns that are general and reusable, not task-specific details.",
9556
- "",
9557
- "## Quality Requirements",
9558
- "",
9559
- "- Do NOT commit broken code",
9560
- "- Keep changes focused and minimal",
9561
- "- Follow existing code patterns",
9562
- ...state.completion_promise ? [
9563
- "",
9564
- "## Stop Condition",
9565
- "",
9566
- `To signal completion, output: <promise>${state.completion_promise}</promise>`,
9567
- "",
9568
- "ONLY output this when the task is FULLY and PERMANENTLY complete.",
9569
- "Do NOT output it if there is ANY remaining work.",
9570
- "When in doubt, do NOT output the promise \u2014 the loop will give you another turn.",
9571
- "",
9572
- "CRITICAL: Do NOT output a false promise to exit the loop.",
9573
- "The loop is designed to continue until genuine completion. Trust the process."
9574
- ] : [
9575
- "",
9576
- "## Continuous Mode",
9577
- "",
9578
- "This is a continuous/infinite loop with no completion signal.",
9579
- "Just do meaningful work each iteration. The loop will continue until manually cancelled.",
9580
- "Focus on making progress, documenting what you did, and setting up the next iteration."
9581
- ]
9582
- ].join("\n");
9583
- }
9584
- return [
9585
- "# Ralph Loop \u2014 Continue Mode",
9586
- "",
9587
- "You are in an automated loop. After this turn ends, the system will automatically",
9588
- "start a new turn with the SAME PROMPT \u2014 you do NOT need to finish everything now.",
9589
- "Your previous work persists in conversation history and files.",
9590
- "Just do meaningful work this turn and let the loop continue.",
9591
- "",
9592
- ...state.completion_promise ? [
9593
- "## Stop Condition",
9594
- "",
9595
- `To signal completion, output: <promise>${state.completion_promise}</promise>`,
9596
- "",
9597
- "CRITICAL \u2014 Do NOT output the promise if:",
9598
- '- The task says "continuously", "forever", "keep running", or "until I stop you"',
9599
- "- There is ANY remaining work, follow-up, or next step you could do",
9600
- "- You just finished one pass/cycle of a recurring task (the loop handles repetition)",
9601
- "- You are uncertain whether the task is truly done",
9602
- "",
9603
- "CRITICAL: Do NOT output a false promise to exit the loop, even if you think you're",
9604
- "stuck or should exit for other reasons. The loop is designed to continue until genuine",
9605
- "completion. Trust the process."
9606
- ] : [
9607
- "## Continuous Mode",
9608
- "",
9609
- "This is a continuous/infinite loop with no completion signal.",
9610
- "Just do meaningful work this turn and let the loop continue.",
9611
- "The loop will run until manually cancelled."
9612
- ]
9613
- ].join("\n");
9614
- }
9615
- function buildRalphPrompt(task, state) {
9616
- const isFresh = state.context_mode === "fresh" || !state.context_mode;
9617
- if (isFresh) {
9618
- return task;
9619
- }
9620
- const iterStr = state.max_iterations > 0 ? `${state.iteration}/${state.max_iterations}` : `${state.iteration}`;
9621
- const reminderLines = [
9622
- "<system-reminder>",
9623
- `Ralph Loop \u2014 Iteration ${iterStr} (Continue Mode)`,
9624
- "Your conversation history persists. Continue from where you left off."
9625
- ];
9626
- if (state.completion_promise) {
9627
- reminderLines.push(`To signal completion, output EXACTLY: <promise>${state.completion_promise}</promise>`);
9628
- reminderLines.push("Only output the promise when the task is FULLY and PERMANENTLY complete.");
9629
- reminderLines.push("Do NOT output a false promise to exit the loop.");
9630
- } else {
9631
- reminderLines.push("This is a continuous loop \u2014 no completion signal needed. Just do meaningful work.");
9632
- }
9633
- reminderLines.push("</system-reminder>");
9634
- const reminder = reminderLines.join("\n");
9635
- return task + "\n\n" + reminder;
9636
- }
9637
- function getRalphProgressFilePath(directory, sessionId) {
9638
- return join(getSessionDir(directory, sessionId), "ralph-progress.md");
9639
- }
9640
9439
  function loadSessionIndex() {
9641
9440
  if (!existsSync$1(SESSION_INDEX_FILE)) return {};
9642
9441
  try {
@@ -9682,16 +9481,6 @@ function deletePersistedSession(sessionId) {
9682
9481
  if (existsSync$1(configFile)) unlinkSync$1(configFile);
9683
9482
  } catch {
9684
9483
  }
9685
- const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
9686
- try {
9687
- if (existsSync$1(ralphStateFile)) unlinkSync$1(ralphStateFile);
9688
- } catch {
9689
- }
9690
- const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
9691
- try {
9692
- if (existsSync$1(ralphProgressFile)) unlinkSync$1(ralphProgressFile);
9693
- } catch {
9694
- }
9695
9484
  const sessionDir = getSessionDir(entry.directory, sessionId);
9696
9485
  try {
9697
9486
  rmdirSync(sessionDir);
@@ -10045,7 +9834,7 @@ async function startDaemon(options) {
10045
9834
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10046
9835
  saveExposedTunnels(list);
10047
9836
  }
10048
- const { ServeManager } = await import('./serveManager-C6_Vloil.mjs');
9837
+ const { ServeManager } = await import('./serveManager-lmPtmRnR.mjs');
10049
9838
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10050
9839
  ensureAutoInstalledSkills(logger).catch(() => {
10051
9840
  });
@@ -10157,7 +9946,7 @@ async function startDaemon(options) {
10157
9946
  }
10158
9947
  }, shouldAutoAllow2 = function(toolName, toolInput) {
10159
9948
  if (toolName === "AskUserQuestion") {
10160
- return sessionMetadata.ralphLoop?.active === true;
9949
+ return isLoopActive(directory);
10161
9950
  }
10162
9951
  if (toolName === "Bash") {
10163
9952
  const inputObj = toolInput;
@@ -10170,7 +9959,7 @@ async function startDaemon(options) {
10170
9959
  } else if (allowedTools.has(toolName)) {
10171
9960
  return true;
10172
9961
  }
10173
- if (sessionMetadata.ralphLoop?.active) return true;
9962
+ if (isLoopActive(directory)) return true;
10174
9963
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
10175
9964
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
10176
9965
  return false;
@@ -10261,10 +10050,7 @@ async function startDaemon(options) {
10261
10050
  const sessionCreatedAt = persisted?.createdAt || Date.now();
10262
10051
  let lastSpawnMeta = persisted?.spawnMeta || {};
10263
10052
  let sessionWasProcessing = !!options2.wasProcessing;
10264
- let lastAssistantText = "";
10265
10053
  let lastMainModel;
10266
- let consecutiveRalphErrors = 0;
10267
- const MAX_RALPH_ERRORS = 3;
10268
10054
  let spawnHasReceivedInit = false;
10269
10055
  let startupFailureRetryPending = false;
10270
10056
  let startupRetryMessage;
@@ -10287,23 +10073,23 @@ async function startDaemon(options) {
10287
10073
  stuckWatchdogTimer = setInterval(() => {
10288
10074
  if (!claudeProcess || claudeProcess.exitCode !== null) return;
10289
10075
  if (!sessionWasProcessing) return;
10290
- const ralphState = readRalphState(getRalphStateFilePath(directory, sessionId));
10291
- if (!ralphState) return;
10076
+ if (!isLoopActive(directory)) return;
10292
10077
  if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
10293
10078
  lastOutputTime = Date.now();
10294
10079
  return;
10295
10080
  }
10296
10081
  const elapsed = Date.now() - lastOutputTime;
10297
10082
  if (elapsed > STUCK_PROCESS_TIMEOUT_MS) {
10298
- logger.log(`[Session ${sessionId}] Ralph Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to unblock next iteration`);
10083
+ logger.log(`[Session ${sessionId}] Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to resume the loop`);
10299
10084
  sessionService.pushMessage(
10300
- { type: "message", message: `Ralph Loop iteration appears stuck (no output for ${Math.round(elapsed / 6e4)} minutes, no active tools). Restarting to continue loop...`, level: "warning" },
10085
+ { type: "message", message: `Loop appears stuck (no output for ${Math.round(elapsed / 6e4)} minutes, no active tools). Restarting to continue...`, level: "warning" },
10301
10086
  "event"
10302
10087
  );
10303
10088
  claudeProcess.kill("SIGTERM");
10304
10089
  setTimeout(() => {
10305
- if (!trackedSession.stopped) {
10306
- logger.log(`[Session ${sessionId}] Stuck watchdog: nudging Ralph loop to resume`);
10090
+ if (!trackedSession.stopped && isLoopActive(directory)) {
10091
+ logger.log(`[Session ${sessionId}] Stuck watchdog: nudging loop to resume`);
10092
+ enqueueLoopContinue();
10307
10093
  processMessageQueueRef?.();
10308
10094
  }
10309
10095
  }, 3e3);
@@ -10316,6 +10102,20 @@ async function startDaemon(options) {
10316
10102
  stuckWatchdogTimer = null;
10317
10103
  }
10318
10104
  };
10105
+ const enqueueLoopContinue = () => {
10106
+ const existingQueue = sessionMetadata.messageQueue || [];
10107
+ const text = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
10108
+ sessionMetadata = {
10109
+ ...sessionMetadata,
10110
+ messageQueue: [...existingQueue, {
10111
+ id: randomUUID$1(),
10112
+ text,
10113
+ displayText: "\u{1F501} Resuming loop",
10114
+ createdAt: Date.now()
10115
+ }]
10116
+ };
10117
+ sessionService.updateMetadata(sessionMetadata);
10118
+ };
10319
10119
  const signalProcessing = (processing) => {
10320
10120
  sessionService.sendKeepAlive(processing);
10321
10121
  const newState = processing ? "running" : "idle";
@@ -10395,7 +10195,7 @@ async function startDaemon(options) {
10395
10195
  if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
10396
10196
  rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
10397
10197
  }
10398
- if (sessionMetadata.ralphLoop?.active) {
10198
+ if (isLoopActive(directory)) {
10399
10199
  rawPermissionMode = "bypassPermissions";
10400
10200
  }
10401
10201
  const permissionMode = toClaudePermissionMode(rawPermissionMode);
@@ -10622,10 +10422,6 @@ async function startDaemon(options) {
10622
10422
  logger.log(`[Session ${sessionId}] Background task launched: ${label} (count=${backgroundTaskCount})`);
10623
10423
  }
10624
10424
  }
10625
- const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
10626
- if (textBlocks.length > 0) {
10627
- lastAssistantText += textBlocks.join("\n");
10628
- }
10629
10425
  }
10630
10426
  if (msg.type === "result") {
10631
10427
  if (msg.is_error) {
@@ -10666,8 +10462,8 @@ async function startDaemon(options) {
10666
10462
  }
10667
10463
  }
10668
10464
  if (msg.type === "result") {
10669
- const ralphActive = !!readRalphState(getRalphStateFilePath(directory, sessionId));
10670
- if (!turnInitiatedByUser && !ralphActive) {
10465
+ const loopActive = isLoopActive(directory);
10466
+ if (!turnInitiatedByUser && !loopActive) {
10671
10467
  logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
10672
10468
  const hasBackgroundTasks = backgroundTaskCount > 0;
10673
10469
  if (hasBackgroundTasks) {
@@ -10692,8 +10488,8 @@ async function startDaemon(options) {
10692
10488
  turnInitiatedByUser = true;
10693
10489
  continue;
10694
10490
  }
10695
- if (!turnInitiatedByUser && ralphActive) {
10696
- logger.log(`[Session ${sessionId}] SDK-initiated result during active Ralph loop \u2014 processing anyway to avoid stalling`);
10491
+ if (!turnInitiatedByUser && loopActive) {
10492
+ logger.log(`[Session ${sessionId}] SDK-initiated result during active loop \u2014 processing anyway to avoid stalling`);
10697
10493
  turnInitiatedByUser = true;
10698
10494
  }
10699
10495
  if (msg.session_id) {
@@ -10743,191 +10539,8 @@ async function startDaemon(options) {
10743
10539
  sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
10744
10540
  }
10745
10541
  const queueLen = sessionMetadata.messageQueue?.length ?? 0;
10746
- if (msg.is_error) {
10747
- const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
10748
- if (rlStateForError) {
10749
- consecutiveRalphErrors++;
10750
- logger.log(`[Session ${sessionId}] Ralph loop: error result (consecutive=${consecutiveRalphErrors}/${MAX_RALPH_ERRORS})`);
10751
- if (consecutiveRalphErrors >= MAX_RALPH_ERRORS) {
10752
- logger.log(`[Session ${sessionId}] Ralph loop: ${MAX_RALPH_ERRORS} consecutive errors \u2014 stopping loop`);
10753
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10754
- if (lastSpawnMeta.appendSystemPrompt) {
10755
- const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
10756
- lastSpawnMeta = rest;
10757
- }
10758
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10759
- sessionService.updateMetadata(sessionMetadata);
10760
- sessionService.pushMessage(
10761
- { type: "message", message: `Ralph loop stopped \u2014 ${consecutiveRalphErrors} consecutive errors. Last error: ${msg.result || "unknown"}`, level: "error" },
10762
- "event"
10763
- );
10764
- consecutiveRalphErrors = 0;
10765
- signalProcessing(false);
10766
- sessionService.sendSessionEnd();
10767
- break;
10768
- }
10769
- }
10770
- } else {
10771
- consecutiveRalphErrors = 0;
10772
- }
10773
10542
  if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
10774
10543
  setTimeout(() => processMessageQueueRef?.(), 200);
10775
- } else if (claudeResumeId) {
10776
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
10777
- if (rlState) {
10778
- let promiseFulfilled = false;
10779
- if (rlState.completion_promise) {
10780
- const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
10781
- promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
10782
- }
10783
- const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
10784
- if (promiseFulfilled || maxReached) {
10785
- const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
10786
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10787
- if (lastSpawnMeta.appendSystemPrompt) {
10788
- const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
10789
- lastSpawnMeta = rest;
10790
- }
10791
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10792
- sessionService.updateMetadata(sessionMetadata);
10793
- const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
10794
- logger.log(`[Session ${sessionId}] ${reason}`);
10795
- sessionService.pushMessage({ type: "message", message: reason }, "event");
10796
- if (isFreshMode && rlState.original_resume_id) {
10797
- claudeResumeId = rlState.original_resume_id;
10798
- (async () => {
10799
- try {
10800
- if (claudeProcess && claudeProcess.exitCode === null) {
10801
- isKillingClaude = true;
10802
- await killAndWaitForExit2(claudeProcess);
10803
- isKillingClaude = false;
10804
- }
10805
- if (trackedSession.stopped) return;
10806
- if (isRestartingClaude || isSwitchingMode) return;
10807
- const progressPath = getRalphProgressFilePath(directory, sessionId);
10808
- let resumeMessage;
10809
- try {
10810
- if (existsSync$1(progressPath)) {
10811
- const progressContent = readFileSync$1(progressPath, "utf-8").trim();
10812
- if (progressContent) {
10813
- resumeMessage = `<system-reminder>
10814
- The Ralph Loop has completed (${reason}).
10815
- Below is the progress log from all iterations:
10816
-
10817
- ${progressContent}
10818
- </system-reminder>
10819
-
10820
- The automated loop has finished. Review the progress above and let me know if you need anything else.`;
10821
- unlinkSync$1(progressPath);
10822
- logger.log(`[Session ${sessionId}] Injected progress file content and deleted ${progressPath}`);
10823
- }
10824
- }
10825
- } catch (progressErr) {
10826
- logger.log(`[Session ${sessionId}] Could not read/delete progress file: ${progressErr.message}`);
10827
- }
10828
- spawnClaude(resumeMessage);
10829
- logger.log(`[Session ${sessionId}] Resumed original session ${rlState.original_resume_id}`);
10830
- } catch (err) {
10831
- logger.log(`[Session ${sessionId}] Error resuming original session: ${err.message}`);
10832
- isKillingClaude = false;
10833
- }
10834
- })();
10835
- } else {
10836
- sessionService.sendSessionEnd();
10837
- }
10838
- } else {
10839
- const nextIteration = rlState.iteration + 1;
10840
- const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
10841
- const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
10842
- const updatedRlState = { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp };
10843
- if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
10844
- updatedRlState.original_resume_id = claudeResumeId;
10845
- }
10846
- try {
10847
- writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
10848
- } catch (writeErr) {
10849
- logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
10850
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
10851
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10852
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10853
- sessionService.updateMetadata(sessionMetadata);
10854
- break;
10855
- }
10856
- const ralphLoop = {
10857
- active: true,
10858
- task: rlState.task,
10859
- completionPromise: rlState.completion_promise ?? "none",
10860
- maxIterations: rlState.max_iterations,
10861
- currentIteration: nextIteration,
10862
- startedAt: rlState.started_at,
10863
- cooldownSeconds: rlState.cooldown_seconds,
10864
- contextMode: rlState.context_mode || "fresh",
10865
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
10866
- };
10867
- sessionMetadata = { ...sessionMetadata, ralphLoop };
10868
- sessionService.updateMetadata(sessionMetadata);
10869
- logger.log(`[Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning (${isFreshMode ? "fresh" : "continue"})`);
10870
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
10871
- const prompt = buildRalphPrompt(rlState.task, updatedRlState);
10872
- const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
10873
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
10874
- if (isFreshMode) {
10875
- isKillingClaude = true;
10876
- setTimeout(async () => {
10877
- try {
10878
- if (claudeProcess && claudeProcess.exitCode === null) {
10879
- await killAndWaitForExit2(claudeProcess);
10880
- }
10881
- isKillingClaude = false;
10882
- if (trackedSession.stopped) return;
10883
- if (isRestartingClaude || isSwitchingMode) return;
10884
- claudeResumeId = void 0;
10885
- userMessagePending = true;
10886
- turnInitiatedByUser = true;
10887
- sessionWasProcessing = true;
10888
- signalProcessing(true);
10889
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
10890
- sessionService.pushMessage(rlState.task, "user");
10891
- spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
10892
- } catch (err) {
10893
- logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
10894
- isKillingClaude = false;
10895
- sessionWasProcessing = false;
10896
- signalProcessing(false);
10897
- }
10898
- }, cooldownMs);
10899
- } else {
10900
- setTimeout(() => {
10901
- if (trackedSession.stopped) return;
10902
- if (isRestartingClaude || isSwitchingMode) return;
10903
- try {
10904
- userMessagePending = true;
10905
- turnInitiatedByUser = true;
10906
- sessionWasProcessing = true;
10907
- signalProcessing(true);
10908
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
10909
- sessionService.pushMessage(rlState.task, "user");
10910
- if (claudeProcess && claudeProcess.exitCode === null) {
10911
- const stdinMsg = JSON.stringify({
10912
- type: "user",
10913
- message: { role: "user", content: prompt }
10914
- });
10915
- claudeProcess.stdin?.write(stdinMsg + "\n");
10916
- } else {
10917
- spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
10918
- }
10919
- } catch (err) {
10920
- logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
10921
- sessionWasProcessing = false;
10922
- signalProcessing(false);
10923
- }
10924
- }, cooldownMs);
10925
- }
10926
- }
10927
- } else {
10928
- signalProcessing(false);
10929
- sessionService.sendSessionEnd();
10930
- }
10931
10544
  } else {
10932
10545
  signalProcessing(false);
10933
10546
  sessionService.sendSessionEnd();
@@ -10935,7 +10548,6 @@ The automated loop has finished. Review the progress above and let me know if yo
10935
10548
  }
10936
10549
  sessionService.pushMessage(msg, "agent");
10937
10550
  } else if (msg.type === "system" && msg.subtype === "init") {
10938
- lastAssistantText = "";
10939
10551
  consecutiveOverloadRetries = 0;
10940
10552
  overloadBailedThisTurn = false;
10941
10553
  if (!userMessagePending) {
@@ -11241,10 +10853,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11241
10853
  signalProcessing(false);
11242
10854
  return;
11243
10855
  }
11244
- if (msgMeta?.ralphFreshContext && claudeResumeId) {
11245
- logger.log(`[Session ${sessionId}] Ralph fresh context: clearing resumeId for fresh spawn`);
11246
- claudeResumeId = void 0;
11247
- }
11248
10856
  if (msgMeta?.btw && claudeResumeId) {
11249
10857
  logger.log(`[Session ${sessionId}] /btw side-channel: "${text.substring(0, 80)}..."`);
11250
10858
  sessionService.pushMessage(
@@ -11539,7 +11147,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11539
11147
  const newCustomTitle = newMeta.customTitle;
11540
11148
  const oldLink = sessionMetadata.sessionLink;
11541
11149
  const newLink = newMeta.sessionLink;
11542
- const prevRalphLoop = sessionMetadata.ralphLoop;
11543
11150
  sessionMetadata = {
11544
11151
  ...newMeta,
11545
11152
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
@@ -11549,10 +11156,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11549
11156
  ...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
11550
11157
  ...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
11551
11158
  // Preserve parentSessionId — set at spawn time, frontend may not track it
11552
- ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
11553
- // Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
11554
- // which would wipe the active loop state and cause the bar to disappear mid-run.
11555
- ...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
11159
+ ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
11556
11160
  };
11557
11161
  const cfgPatch = {};
11558
11162
  if (newTitle !== oldTitle) cfgPatch.title = newTitle ?? null;
@@ -11567,9 +11171,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11567
11171
  logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
11568
11172
  }
11569
11173
  }
11570
- if (prevRalphLoop && !newMeta.ralphLoop) {
11571
- sessionService.updateMetadata(sessionMetadata);
11572
- }
11573
11174
  const queue = newMeta.messageQueue;
11574
11175
  if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
11575
11176
  setTimeout(() => {
@@ -11709,49 +11310,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11709
11310
  sessionService.pushMessage(next.displayText || next.text, "user");
11710
11311
  userMessagePending = true;
11711
11312
  turnInitiatedByUser = true;
11712
- const queueMeta = next.ralphSystemPrompt ? { appendSystemPrompt: next.ralphSystemPrompt } : void 0;
11713
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
11714
- const isRalphFresh = rlState && (rlState.context_mode === "fresh" || !rlState.context_mode);
11715
- if (isRalphFresh && claudeProcess && claudeProcess.exitCode === null) {
11716
- logger.log(`[Session ${sessionId}] Ralph fresh context: killing process for fresh spawn`);
11717
- (async () => {
11718
- try {
11719
- if (!rlState.original_resume_id && claudeResumeId) {
11720
- writeRalphState(getRalphStateFilePath(directory, sessionId), {
11721
- ...rlState,
11722
- original_resume_id: claudeResumeId
11723
- });
11724
- }
11725
- isKillingClaude = true;
11726
- await killAndWaitForExit2(claudeProcess);
11727
- isKillingClaude = false;
11728
- if (trackedSession?.stopped) return;
11729
- if (isRestartingClaude || isSwitchingMode) return;
11730
- claudeResumeId = void 0;
11731
- spawnClaude(next.text, queueMeta);
11732
- } catch (err) {
11733
- logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
11734
- isKillingClaude = false;
11735
- sessionWasProcessing = false;
11736
- signalProcessing(false);
11737
- }
11738
- })();
11739
- } else {
11740
- try {
11741
- if (!claudeProcess || claudeProcess.exitCode !== null) {
11742
- spawnClaude(next.text, queueMeta);
11743
- } else {
11744
- const stdinMsg = JSON.stringify({
11745
- type: "user",
11746
- message: { role: "user", content: next.text }
11747
- });
11748
- claudeProcess.stdin?.write(stdinMsg + "\n");
11749
- }
11750
- } catch (err) {
11751
- logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
11752
- sessionWasProcessing = false;
11753
- signalProcessing(false);
11313
+ try {
11314
+ if (!claudeProcess || claudeProcess.exitCode !== null) {
11315
+ spawnClaude(next.text);
11316
+ } else {
11317
+ const stdinMsg = JSON.stringify({
11318
+ type: "user",
11319
+ message: { role: "user", content: next.text }
11320
+ });
11321
+ claudeProcess.stdin?.write(stdinMsg + "\n");
11754
11322
  }
11323
+ } catch (err) {
11324
+ logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
11325
+ sessionWasProcessing = false;
11326
+ signalProcessing(false);
11755
11327
  }
11756
11328
  }
11757
11329
  };
@@ -11825,7 +11397,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11825
11397
  } else if (allowedTools.has(toolName)) {
11826
11398
  return true;
11827
11399
  }
11828
- if (sessionMetadata.ralphLoop?.active) return true;
11400
+ if (isLoopActive(directory)) return true;
11829
11401
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
11830
11402
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
11831
11403
  return false;
@@ -12035,7 +11607,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12035
11607
  const newCustomTitleAcp = newMeta.customTitle;
12036
11608
  const oldLinkAcp = sessionMetadata.sessionLink;
12037
11609
  const newLinkAcp = newMeta.sessionLink;
12038
- const prevRalphLoop = sessionMetadata.ralphLoop;
12039
11610
  sessionMetadata = {
12040
11611
  ...newMeta,
12041
11612
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
@@ -12043,10 +11614,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12043
11614
  ...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
12044
11615
  ...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
12045
11616
  // Preserve parentSessionId — set at spawn time, frontend may not track it
12046
- ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
12047
- // Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
12048
- // which would wipe the active loop state and cause the bar to disappear mid-run.
12049
- ...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
11617
+ ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
12050
11618
  };
12051
11619
  const cfgPatchAcp = {};
12052
11620
  if (newTitleAcp !== oldTitleAcp) cfgPatchAcp.title = newTitleAcp ?? null;
@@ -12061,9 +11629,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12061
11629
  logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
12062
11630
  }
12063
11631
  }
12064
- if (prevRalphLoop && !newMeta.ralphLoop) {
12065
- sessionService.updateMetadata(sessionMetadata);
12066
- }
12067
11632
  if (acpStopped) return;
12068
11633
  const queue = newMeta.messageQueue;
12069
11634
  if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
@@ -12197,7 +11762,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12197
11762
  const remaining = queue.slice(1);
12198
11763
  sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
12199
11764
  sessionService.updateMetadata(sessionMetadata);
12200
- logger.log(`[Session ${sessionId}] Processing queued message (ACP ralph activation): "${next.text.slice(0, 50)}..."`);
11765
+ logger.log(`[Session ${sessionId}] Processing queued message (ACP loop activation): "${next.text.slice(0, 50)}..."`);
12201
11766
  sessionService.sendKeepAlive(true);
12202
11767
  agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
12203
11768
  logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
@@ -12279,99 +11844,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12279
11844
  }
12280
11845
  } catch {
12281
11846
  }
12282
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
12283
- if (rlState) {
12284
- let promiseFulfilled = false;
12285
- if (rlState.completion_promise) {
12286
- const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
12287
- promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
12288
- }
12289
- const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
12290
- if (promiseFulfilled || maxReached) {
12291
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12292
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12293
- sessionService.updateMetadata(sessionMetadata);
12294
- const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
12295
- logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
12296
- sessionService.pushMessage({ type: "message", message: reason }, "event");
12297
- } else {
12298
- const pendingQueue = sessionMetadata.messageQueue;
12299
- if (pendingQueue && pendingQueue.length > 0) {
12300
- const next = pendingQueue[0];
12301
- const remaining = pendingQueue.slice(1);
12302
- sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
12303
- sessionService.updateMetadata(sessionMetadata);
12304
- sessionService.sendKeepAlive(true);
12305
- sessionService.pushMessage(next.displayText || next.text, "user");
12306
- logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
12307
- agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
12308
- logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
12309
- if (!acpStopped) {
12310
- sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
12311
- sessionService.updateMetadata(sessionMetadata);
12312
- sessionService.sendSessionEnd();
12313
- }
12314
- });
12315
- return;
12316
- }
12317
- const nextIteration = rlState.iteration + 1;
12318
- const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
12319
- try {
12320
- writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
12321
- } catch (writeErr) {
12322
- logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
12323
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
12324
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12325
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12326
- sessionService.updateMetadata(sessionMetadata);
12327
- return;
12328
- }
12329
- const ralphLoop = {
12330
- active: true,
12331
- task: rlState.task,
12332
- completionPromise: rlState.completion_promise ?? "none",
12333
- maxIterations: rlState.max_iterations,
12334
- currentIteration: nextIteration,
12335
- startedAt: rlState.started_at,
12336
- cooldownSeconds: rlState.cooldown_seconds,
12337
- contextMode: rlState.context_mode || "fresh",
12338
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
12339
- };
12340
- sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
12341
- sessionService.updateMetadata(sessionMetadata);
12342
- logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
12343
- const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
12344
- const prompt = buildRalphPrompt(rlState.task, updatedState);
12345
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
12346
- setTimeout(() => {
12347
- if (acpStopped) return;
12348
- const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
12349
- if (!liveRlState) {
12350
- sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
12351
- sessionService.updateMetadata(sessionMetadata);
12352
- sessionService.sendKeepAlive(false);
12353
- sessionService.sendSessionEnd();
12354
- return;
12355
- }
12356
- sessionService.sendKeepAlive(true);
12357
- sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
12358
- sessionService.updateMetadata(sessionMetadata);
12359
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
12360
- sessionService.pushMessage(rlState.task, "user");
12361
- agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
12362
- logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
12363
- if (!acpStopped) {
12364
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12365
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12366
- sessionService.updateMetadata(sessionMetadata);
12367
- sessionService.sendSessionEnd();
12368
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
12369
- }
12370
- });
12371
- }, cooldownMs);
12372
- return;
12373
- }
12374
- }
12375
11847
  const queue = sessionMetadata.messageQueue;
12376
11848
  if (queue && queue.length > 0) {
12377
11849
  const next = queue[0];
@@ -12673,7 +12145,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12673
12145
  const specs = loadExposedTunnels();
12674
12146
  if (specs.length === 0) return;
12675
12147
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12676
- const { FrpcTunnel } = await import('./frpc-BZ4l4-os.mjs');
12148
+ const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
12677
12149
  for (const spec of specs) {
12678
12150
  if (tunnels.has(spec.name)) continue;
12679
12151
  try {
@@ -12754,7 +12226,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12754
12226
  logger.log(`[staged-homes] sweep failed: ${err.message}`);
12755
12227
  }
12756
12228
  const sessionsToAutoContinue = [];
12757
- const sessionsToRalphResume = [];
12229
+ const sessionsToLoopResume = [];
12758
12230
  if (persistedSessions.length > 0) {
12759
12231
  logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
12760
12232
  for (const persisted of persistedSessions) {
@@ -12799,11 +12271,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12799
12271
  if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
12800
12272
  sessionsToAutoContinue.push(persisted.sessionId);
12801
12273
  }
12802
- if (!isOrphaned && !persisted.wasProcessing) {
12803
- const rlState = readRalphState(getRalphStateFilePath(persisted.directory, persisted.sessionId));
12804
- if (rlState) {
12805
- sessionsToRalphResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12806
- }
12274
+ if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
12275
+ sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12807
12276
  }
12808
12277
  } else {
12809
12278
  logger.log(`Failed to restore session ${persisted.sessionId}: ${result.type}`);
@@ -12837,54 +12306,34 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12837
12306
  } else if (sessionsToAutoContinue.length > 0) {
12838
12307
  logger.log(`Skipping auto-continue for ${sessionsToAutoContinue.length} interrupted session(s) (--no-auto-continue)`);
12839
12308
  }
12840
- if (sessionsToRalphResume.length > 0 && !options?.noAutoContinue) {
12841
- logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
12842
- for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
12309
+ if (sessionsToLoopResume.length > 0 && !options?.noAutoContinue) {
12310
+ logger.log(`Resuming loop for ${sessionsToLoopResume.length} session(s)...`);
12311
+ for (const { sessionId, directory: sessDir } of sessionsToLoopResume) {
12843
12312
  try {
12844
12313
  const tracked = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId);
12845
12314
  const rpc = tracked?.sessionRPCHandlers;
12846
12315
  if (!rpc) {
12847
- logger.log(`Session ${sessionId} RPC handlers not found for Ralph resume`);
12316
+ logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
12848
12317
  continue;
12849
12318
  }
12850
- const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
12851
- if (!rlState) continue;
12852
- const initDelayMs = 2e3;
12853
- let resumeDelayMs = initDelayMs;
12854
- if (rlState.last_iteration_at) {
12855
- const lastIterTime = new Date(rlState.last_iteration_at).getTime();
12856
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
12857
- const elapsedMs = Date.now() - lastIterTime;
12858
- const remainingCooldownMs = cooldownMs - elapsedMs;
12859
- resumeDelayMs = Math.max(initDelayMs, remainingCooldownMs + initDelayMs);
12860
- logger.log(`Ralph loop for session ${sessionId}: cooldown ${rlState.cooldown_seconds}s, elapsed ${Math.round(elapsedMs / 1e3)}s, resuming in ${Math.round(resumeDelayMs / 1e3)}s`);
12861
- }
12862
12319
  setTimeout(async () => {
12863
12320
  try {
12864
- const currentState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
12865
- if (!currentState) return;
12866
- const isFreshMode = currentState.context_mode === "fresh" || !currentState.context_mode;
12867
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
12868
- const prompt = buildRalphPrompt(currentState.task, currentState);
12869
- const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
12321
+ if (!isLoopActive(sessDir)) return;
12322
+ const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
12870
12323
  await rpc.sendMessage(
12871
12324
  JSON.stringify({
12872
12325
  role: "user",
12873
12326
  content: { type: "text", text: prompt },
12874
- meta: {
12875
- sentFrom: "svamp-daemon-ralph-resume",
12876
- appendSystemPrompt: ralphSysPrompt,
12877
- ...isFreshMode ? { ralphFreshContext: true } : {}
12878
- }
12327
+ meta: { sentFrom: "svamp-daemon-loop-resume" }
12879
12328
  })
12880
12329
  );
12881
- logger.log(`Resumed Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
12330
+ logger.log(`Resumed loop for session ${sessionId}`);
12882
12331
  } catch (err) {
12883
- logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
12332
+ logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
12884
12333
  }
12885
- }, resumeDelayMs);
12334
+ }, 2e3);
12886
12335
  } catch (err) {
12887
- logger.log(`Failed to find session service for Ralph resume ${sessionId}: ${err.message}`);
12336
+ logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
12888
12337
  }
12889
12338
  }
12890
12339
  }