@bybrawe/opencode-loop 0.5.31 → 0.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1231,91 +1231,10 @@ function createCommandRouter(options = {}) {
1231
1231
  };
1232
1232
  }
1233
1233
 
1234
- // src/source/runtime/goal-runtime.js
1234
+ // src/source/runtime/goal-report.js
1235
1235
  import { promises as fs3 } from "fs";
1236
1236
  import path3 from "path";
1237
1237
  var GOAL_REPORT_DIR = "goals";
1238
- var GOAL_PROMPT_PREFIX = "EXPERIMENTAL OPENCODE GOAL MODE ITERATION";
1239
- async function buildGoalPrompt(directory, job) {
1240
- const sections = [];
1241
- sections.push(`Working directory:
1242
- ${path3.resolve(directory)}
1243
- Keep every file operation inside this directory. Prefer workspace-relative paths such as "src/index.js"; never turn a relative path into a root path such as "/src/index.js".`);
1244
- const objective = String(job.action || "").trim();
1245
- if (objective)
1246
- sections.push(`Goal objective:
1247
- ${objective}`);
1248
- if (job.goalFile) {
1249
- const text = await readSmallTextFile(path3.resolve(directory, job.goalFile), 120000);
1250
- if (text.trim())
1251
- sections.push(`Goal file ${job.goalFile}:
1252
- ${text.trim()}`);
1253
- else
1254
- sections.push(`Goal file ${job.goalFile} was requested but could not be read. Continue from the inline goal objective.`);
1255
- }
1256
- if (job.promptFile) {
1257
- const text = await readSmallTextFile(path3.resolve(directory, job.promptFile), 120000);
1258
- if (text.trim())
1259
- sections.push(`Extra goal instructions from ${job.promptFile}:
1260
- ${text.trim()}`);
1261
- }
1262
- if (job.goalAcceptance?.length)
1263
- sections.push(`Acceptance criteria:
1264
- ` + job.goalAcceptance.map((item, index) => `${index + 1}. ${item}`).join(`
1265
- `));
1266
- if (job.goalChecks?.length)
1267
- sections.push(`Verification commands that define useful evidence:
1268
- ` + job.goalChecks.map((item, index) => `${index + 1}. ${item}`).join(`
1269
- `));
1270
- if (job.verifyCommand)
1271
- sections.push(`Post-turn verify command configured by the loop: ${job.verifyCommand}`);
1272
- if (job.lastGoalChecks?.length)
1273
- sections.push(`Latest goal check results:
1274
- ` + job.lastGoalChecks.map((item) => `- ${item.command}: exit ${item.code}`).join(`
1275
- `));
1276
- if (job.lastVerifyFailure)
1277
- sections.push(`Previous verify/check failure summary:
1278
- ` + String(job.lastVerifyFailure).slice(0, 1600));
1279
- if (job.goalCompletionRejectedReason)
1280
- sections.push(`Previous completion attempt was rejected:
1281
- ${job.goalCompletionRejectedReason}`);
1282
- if ((job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0)
1283
- sections.push(`No-progress guard:
1284
- ${job.noProgressCount || 0}/${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS} recent turn(s) without recorded meaningful progress.`);
1285
- if (job.goalProgress?.length)
1286
- sections.push(`Recent goal progress:
1287
- ` + job.goalProgress.slice(-5).map((item) => `- ${item.time}: ${item.summary}`).join(`
1288
- `));
1289
- for (const file of job.includeFiles || []) {
1290
- const text = await readSmallTextFile(path3.resolve(directory, file), 80000);
1291
- if (text.trim())
1292
- sections.push(`Context from ${file}:
1293
- ${text.trim().slice(0, 20000)}`);
1294
- }
1295
- return `${GOAL_PROMPT_PREFIX}.
1296
-
1297
- You are pursuing an experimental persistent goal for this OpenCode session. This is not a timer loop and not a one-shot prompt. Keep working toward the goal until it is completed, blocked, paused, cleared, or stopped by safety limits.
1298
-
1299
- Rules:
1300
- - Work on the next smallest useful step toward the goal.
1301
- - Prefer direct code changes, tests, typechecks, builds, and evidence over discussion.
1302
- - Do not claim the goal is complete unless the acceptance criteria are satisfied and verification evidence supports it.
1303
- - If verification commands are configured, do not call opencode_loop_goal_complete until the latest relevant checks have passed unless the user explicitly overrides the goal.
1304
- - Completion evidence must be concrete: mention commands, files, checks, results, or code inspection details.
1305
- - When the goal is complete, call the tool opencode_loop_goal_complete with a summary and evidence.
1306
- - If you are truly blocked and need user input, call the tool opencode_loop_goal_blocked with the reason and what is needed.
1307
- - If you made meaningful progress but the goal is not complete, call the tool opencode_loop_goal_progress with the summary and next step.
1308
- - If you cannot make meaningful progress for this turn, call opencode_loop_goal_blocked instead of repeating the same attempt.
1309
- - Do not call completion tools just to be polite; only call them when the state is real.
1310
- - Do not ask the user questions unless blocked; make reasonable assumptions and continue.
1311
- - Follow safety rules: no destructive commands, force pushes, production deploys, production database resets, or deleting user data.
1312
-
1313
- ${sections.join(`
1314
-
1315
- ---
1316
-
1317
- `)}`;
1318
- }
1319
1238
  function goalReportPath(directory, sessionID, job) {
1320
1239
  return path3.join(stateDir(directory), GOAL_REPORT_DIR, `${safeID(sessionID)}-${safeID(job.name || job.id)}.md`);
1321
1240
  }
@@ -1375,21 +1294,8 @@ async function writeGoalReport(directory, sessionID, job) {
1375
1294
  await ensureDir(path3.dirname(target));
1376
1295
  await fs3.writeFile(target, goalReportText(job), "utf8");
1377
1296
  }
1378
- function pickGoalJob(state, target = "") {
1379
- const goals = (state.jobs || []).filter(isGoalJob);
1380
- if (!goals.length)
1381
- return;
1382
- const text = String(target || "").trim();
1383
- if (!text || ["active", "current", "goal"].includes(text.toLowerCase()))
1384
- return goals.find((job) => job.goalStatus === "active" && job.enabled !== false) || goals[0];
1385
- return goals.find((job, index) => matchJob(job, text, index));
1386
- }
1387
- function parseGoalToolText(args, fields) {
1388
- const result = {};
1389
- for (const field of fields)
1390
- result[field] = String(args?.[field] || "").trim();
1391
- return result;
1392
- }
1297
+
1298
+ // src/source/runtime/goal-evidence.js
1393
1299
  function hasConcreteGoalEvidence(value) {
1394
1300
  const text = String(value || "").trim();
1395
1301
  if (text.length < 24)
@@ -1432,6 +1338,107 @@ function goalMadeMeaningfulProgress(beforeJob, afterJob) {
1432
1338
  return true;
1433
1339
  return false;
1434
1340
  }
1341
+
1342
+ // src/source/runtime/goal-prompt.js
1343
+ import path4 from "path";
1344
+ var GOAL_PROMPT_PREFIX = "EXPERIMENTAL OPENCODE GOAL MODE ITERATION";
1345
+ async function buildGoalPrompt(directory, job) {
1346
+ const sections = [];
1347
+ sections.push(`Working directory:
1348
+ ${path4.resolve(directory)}
1349
+ Keep every file operation inside this directory. Prefer workspace-relative paths such as "src/index.js"; never turn a relative path into a root path such as "/src/index.js".`);
1350
+ const objective = String(job.action || "").trim();
1351
+ if (objective)
1352
+ sections.push(`Goal objective:
1353
+ ${objective}`);
1354
+ if (job.goalFile) {
1355
+ const text = await readSmallTextFile(path4.resolve(directory, job.goalFile), 120000);
1356
+ if (text.trim())
1357
+ sections.push(`Goal file ${job.goalFile}:
1358
+ ${text.trim()}`);
1359
+ else
1360
+ sections.push(`Goal file ${job.goalFile} was requested but could not be read. Continue from the inline goal objective.`);
1361
+ }
1362
+ if (job.promptFile) {
1363
+ const text = await readSmallTextFile(path4.resolve(directory, job.promptFile), 120000);
1364
+ if (text.trim())
1365
+ sections.push(`Extra goal instructions from ${job.promptFile}:
1366
+ ${text.trim()}`);
1367
+ }
1368
+ if (job.goalAcceptance?.length)
1369
+ sections.push(`Acceptance criteria:
1370
+ ` + job.goalAcceptance.map((item, index) => `${index + 1}. ${item}`).join(`
1371
+ `));
1372
+ if (job.goalChecks?.length)
1373
+ sections.push(`Verification commands that define useful evidence:
1374
+ ` + job.goalChecks.map((item, index) => `${index + 1}. ${item}`).join(`
1375
+ `));
1376
+ if (job.verifyCommand)
1377
+ sections.push(`Post-turn verify command configured by the loop: ${job.verifyCommand}`);
1378
+ if (job.lastGoalChecks?.length)
1379
+ sections.push(`Latest goal check results:
1380
+ ` + job.lastGoalChecks.map((item) => `- ${item.command}: exit ${item.code}`).join(`
1381
+ `));
1382
+ if (job.lastVerifyFailure)
1383
+ sections.push(`Previous verify/check failure summary:
1384
+ ` + String(job.lastVerifyFailure).slice(0, 1600));
1385
+ if (job.goalCompletionRejectedReason)
1386
+ sections.push(`Previous completion attempt was rejected:
1387
+ ${job.goalCompletionRejectedReason}`);
1388
+ if ((job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0)
1389
+ sections.push(`No-progress guard:
1390
+ ${job.noProgressCount || 0}/${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS} recent turn(s) without recorded meaningful progress.`);
1391
+ if (job.goalProgress?.length)
1392
+ sections.push(`Recent goal progress:
1393
+ ` + job.goalProgress.slice(-5).map((item) => `- ${item.time}: ${item.summary}`).join(`
1394
+ `));
1395
+ for (const file of job.includeFiles || []) {
1396
+ const text = await readSmallTextFile(path4.resolve(directory, file), 80000);
1397
+ if (text.trim())
1398
+ sections.push(`Context from ${file}:
1399
+ ${text.trim().slice(0, 20000)}`);
1400
+ }
1401
+ return `${GOAL_PROMPT_PREFIX}.
1402
+
1403
+ You are pursuing an experimental persistent goal for this OpenCode session. This is not a timer loop and not a one-shot prompt. Keep working toward the goal until it is completed, blocked, paused, cleared, or stopped by safety limits.
1404
+
1405
+ Rules:
1406
+ - Work on the next smallest useful step toward the goal.
1407
+ - Prefer direct code changes, tests, typechecks, builds, and evidence over discussion.
1408
+ - Do not claim the goal is complete unless the acceptance criteria are satisfied and verification evidence supports it.
1409
+ - If verification commands are configured, do not call opencode_loop_goal_complete until the latest relevant checks have passed unless the user explicitly overrides the goal.
1410
+ - Completion evidence must be concrete: mention commands, files, checks, results, or code inspection details.
1411
+ - When the goal is complete, call the tool opencode_loop_goal_complete with a summary and evidence.
1412
+ - If you are truly blocked and need user input, call the tool opencode_loop_goal_blocked with the reason and what is needed.
1413
+ - If you made meaningful progress but the goal is not complete, call the tool opencode_loop_goal_progress with the summary and next step.
1414
+ - If you cannot make meaningful progress for this turn, call opencode_loop_goal_blocked instead of repeating the same attempt.
1415
+ - Do not call completion tools just to be polite; only call them when the state is real.
1416
+ - Do not ask the user questions unless blocked; make reasonable assumptions and continue.
1417
+ - Follow safety rules: no destructive commands, force pushes, production deploys, production database resets, or deleting user data.
1418
+
1419
+ ${sections.join(`
1420
+
1421
+ ---
1422
+
1423
+ `)}`;
1424
+ }
1425
+
1426
+ // src/source/runtime/goal-runtime.js
1427
+ function pickGoalJob(state, target = "") {
1428
+ const goals = (state.jobs || []).filter(isGoalJob);
1429
+ if (!goals.length)
1430
+ return;
1431
+ const text = String(target || "").trim();
1432
+ if (!text || ["active", "current", "goal"].includes(text.toLowerCase()))
1433
+ return goals.find((job) => job.goalStatus === "active" && job.enabled !== false) || goals[0];
1434
+ return goals.find((job, index) => matchJob(job, text, index));
1435
+ }
1436
+ function parseGoalToolText(args, fields) {
1437
+ const result = {};
1438
+ for (const field of fields)
1439
+ result[field] = String(args?.[field] || "").trim();
1440
+ return result;
1441
+ }
1435
1442
  async function rejectGoalCompletion(directory, sessionID, state, job, reason) {
1436
1443
  job.goalCompletionRejectedAt = now();
1437
1444
  job.goalCompletionRejectedReason = reason;
@@ -1617,7 +1624,7 @@ function createGoalCommandHandlers(options = {}) {
1617
1624
 
1618
1625
  // src/source/opencode/loop-commands.js
1619
1626
  import { promises as fs4 } from "fs";
1620
- import path4 from "path";
1627
+ import path5 from "path";
1621
1628
  var SERVICE2 = "opencode-loop";
1622
1629
  var DEFAULT_PROGRESS_MD = `# Progress
1623
1630
 
@@ -1715,7 +1722,7 @@ function createLoopCommandHandlers(options = {}) {
1715
1722
  async function logsLoop(directory, client, sessionID) {
1716
1723
  let text = "No loop log found.";
1717
1724
  try {
1718
- text = (await readFile(path4.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
1725
+ text = (await readFile(path5.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
1719
1726
  `) || text;
1720
1727
  } catch {}
1721
1728
  await say2(client, sessionID, `OpenCode loop logs:
@@ -1775,7 +1782,7 @@ function createLoopCommandHandlers(options = {}) {
1775
1782
  }
1776
1783
  async function initLoop(directory, client, sessionID, args) {
1777
1784
  const target = String(args || "").trim() || "progress.md";
1778
- const full = path4.resolve(directory, target);
1785
+ const full = path5.resolve(directory, target);
1779
1786
  if (await pathExists2(full)) {
1780
1787
  await toast2(client, `${target} already exists.`, "warning");
1781
1788
  return;
@@ -2288,7 +2295,7 @@ ${item.output}`).join(`
2288
2295
 
2289
2296
  // src/source/runtime/job-workspace.js
2290
2297
  import { promises as fs5 } from "fs";
2291
- import path5 from "path";
2298
+ import path6 from "path";
2292
2299
  var MAX_SCAN_FILES = 200;
2293
2300
  var MAX_SCAN_BYTES = 2000000;
2294
2301
  function requireFunction6(value, name) {
@@ -2323,7 +2330,7 @@ function createJobWorkspaceRuntime(options = {}) {
2323
2330
  return await buildGoalPrompt2(directory, job);
2324
2331
  const sections = [];
2325
2332
  if (job.promptFile) {
2326
- const text = await readSmallTextFile2(path5.resolve(directory, job.promptFile));
2333
+ const text = await readSmallTextFile2(path6.resolve(directory, job.promptFile));
2327
2334
  if (text.trim())
2328
2335
  sections.push(`Instructions from ${job.promptFile}:
2329
2336
  ${text.trim()}`);
@@ -2333,7 +2340,7 @@ ${text.trim()}`);
2333
2340
  if (job.action)
2334
2341
  sections.push(decoratePrompt(job));
2335
2342
  for (const file of job.includeFiles || []) {
2336
- const text = await readSmallTextFile2(path5.resolve(directory, file), 80000);
2343
+ const text = await readSmallTextFile2(path6.resolve(directory, file), 80000);
2337
2344
  if (text.trim())
2338
2345
  sections.push(`Context from ${file}:
2339
2346
  ${text.trim().slice(0, 20000)}`);
@@ -2365,7 +2372,7 @@ ${text.trim().slice(0, 20000)}`);
2365
2372
  const snapshot = {};
2366
2373
  for (const file of files || []) {
2367
2374
  try {
2368
- const stat = await fs5.stat(path5.resolve(directory, file));
2375
+ const stat = await fs5.stat(path6.resolve(directory, file));
2369
2376
  snapshot[file] = `${stat.mtimeMs}:${stat.size}`;
2370
2377
  } catch {
2371
2378
  snapshot[file] = "missing";
@@ -2396,9 +2403,9 @@ ${text.trim().slice(0, 20000)}`);
2396
2403
  async function untilReached(directory, job) {
2397
2404
  if (!job.until)
2398
2405
  return false;
2399
- const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path5.join(".opencode", "opencode-loop", "until.txt")];
2406
+ const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path6.join(".opencode", "opencode-loop", "until.txt")];
2400
2407
  for (const file of files)
2401
- if (await fileContains(path5.resolve(directory, file), job.until))
2408
+ if (await fileContains(path6.resolve(directory, file), job.until))
2402
2409
  return true;
2403
2410
  let scanned = 0;
2404
2411
  async function walk(current) {
@@ -2415,7 +2422,7 @@ ${text.trim().slice(0, 20000)}`);
2415
2422
  return false;
2416
2423
  if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name))
2417
2424
  continue;
2418
- const full = path5.join(current, entry.name);
2425
+ const full = path6.join(current, entry.name);
2419
2426
  if (entry.isDirectory()) {
2420
2427
  if (await walk(full))
2421
2428
  return true;
@@ -2439,13 +2446,13 @@ ${text.trim().slice(0, 20000)}`);
2439
2446
  if (!status.stdout.trim())
2440
2447
  return;
2441
2448
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2442
- const checkpointDir = path5.join(stateDir(directory), "checkpoints", safeID(sessionID));
2449
+ const checkpointDir = path6.join(stateDir(directory), "checkpoints", safeID(sessionID));
2443
2450
  await ensureDir(checkpointDir);
2444
2451
  const diff = await runProcess2("git", ["diff", "--binary"], directory, 120000);
2445
2452
  const staged = await runProcess2("git", ["diff", "--cached", "--binary"], directory, 120000);
2446
2453
  const prefix = `${timestamp}-${safeID(job.name || job.id)}`;
2447
- await fs5.writeFile(path5.join(checkpointDir, `${prefix}.status.txt`), status.stdout + status.stderr);
2448
- await fs5.writeFile(path5.join(checkpointDir, `${prefix}.patch`), `${diff.stdout}
2454
+ await fs5.writeFile(path6.join(checkpointDir, `${prefix}.status.txt`), status.stdout + status.stderr);
2455
+ await fs5.writeFile(path6.join(checkpointDir, `${prefix}.patch`), `${diff.stdout}
2449
2456
  ${staged.stdout}`);
2450
2457
  if (job.gitCheckpoint) {
2451
2458
  await runProcess2("git", ["add", "-A"], directory, 120000);
@@ -2463,9 +2470,6 @@ ${staged.stdout}`);
2463
2470
  };
2464
2471
  }
2465
2472
 
2466
- // src/source/runtime/executor.js
2467
- import path6 from "path";
2468
-
2469
2473
  // src/source/runtime/session-status.js
2470
2474
  var DEFAULT_STALE_ACTIVE_RECOVERY_MS = 45000;
2471
2475
  var DEFAULT_SESSION_STATUS_CACHE_MS = 1500;
@@ -2751,86 +2755,205 @@ function createCompactionRuntime(options = {}) {
2751
2755
  };
2752
2756
  }
2753
2757
 
2754
- // src/source/runtime/executor.js
2755
- var DEFAULT_ACTIVE_GUARD_MS = 45000;
2756
- var DEFAULT_BUSY_RETRY_MS2 = 5000;
2758
+ // src/source/runtime/action-dispatch.js
2757
2759
  function requireFunction7(value, label) {
2758
2760
  if (typeof value !== "function")
2759
- throw new TypeError(`createLoopExecutor requires ${label}`);
2761
+ throw new TypeError(`createActionDispatcher requires ${label}`);
2760
2762
  return value;
2761
2763
  }
2762
- function createLoopExecutor(options = {}) {
2763
- const workspace = options.workspace || {};
2764
- const goalPolicy = options.goalPolicy || {};
2765
- const scheduler = options.scheduler || {};
2766
- const buildPrompt = requireFunction7(workspace.buildPrompt, "workspace.buildPrompt");
2767
- const ensureBranch = requireFunction7(workspace.ensureBranch, "workspace.ensureBranch");
2768
- const watchChanged = requireFunction7(workspace.watchChanged, "workspace.watchChanged");
2769
- const untilReached = requireFunction7(workspace.untilReached, "workspace.untilReached");
2770
- const createCheckpoint = requireFunction7(workspace.createCheckpoint, "workspace.createCheckpoint");
2771
- const runGoalChecks = requireFunction7(goalPolicy.runGoalChecks, "goalPolicy.runGoalChecks");
2772
- const applyGoalNoProgressGuard = requireFunction7(goalPolicy.applyGoalNoProgressGuard, "goalPolicy.applyGoalNoProgressGuard");
2773
- const rememberSession = requireFunction7(scheduler.rememberSession, "scheduler.rememberSession");
2774
- const scheduleDueWork = requireFunction7(scheduler.scheduleDueWork, "scheduler.scheduleDueWork");
2775
- const now2 = typeof options.now === "function" ? options.now : now;
2776
- const readState2 = typeof options.readState === "function" ? options.readState : readState;
2777
- const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
2778
- const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
2764
+ function createActionDispatcher(options = {}) {
2765
+ const buildPrompt = requireFunction7(options.buildPrompt, "buildPrompt");
2766
+ const startCompaction = requireFunction7(options.compactionRuntime?.start, "compactionRuntime.start");
2779
2767
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
2780
- const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
2781
- const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
2782
- const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : sdkErrorMessage;
2783
2768
  const sdkCall2 = typeof options.sdkCall === "function" ? options.sdkCall : sdkCall;
2784
2769
  const normalizedModelRef2 = typeof options.normalizedModelRef === "function" ? options.normalizedModelRef : normalizedModelRef;
2785
2770
  const fireSdk2 = typeof options.fireSdk === "function" ? options.fireSdk : fireSdk;
2786
2771
  const compactTuiCommandName2 = typeof options.compactTuiCommandName === "function" ? options.compactTuiCommandName : compactTuiCommandName;
2787
- const log2 = typeof options.log === "function" ? options.log : log;
2788
2772
  const toast2 = typeof options.toast === "function" ? options.toast : toast;
2789
2773
  const guardLoopOwnedUserMessage2 = typeof options.guardLoopOwnedUserMessage === "function" ? options.guardLoopOwnedUserMessage : guardLoopOwnedUserMessage;
2790
- const writeGoalReport2 = typeof options.writeGoalReport === "function" ? options.writeGoalReport : writeGoalReport;
2791
2774
  const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
2792
- const activeGuardMs = Number.isFinite(Number(options.activeGuardMs)) && Number(options.activeGuardMs) > 0 ? Number(options.activeGuardMs) : DEFAULT_ACTIVE_GUARD_MS;
2793
- const busyRetryMs = Number.isFinite(Number(options.busyRetryMs)) && Number(options.busyRetryMs) > 0 ? Number(options.busyRetryMs) : DEFAULT_BUSY_RETRY_MS2;
2794
- const activeRuns = new Map;
2795
- const runLocks = new Map;
2796
- const statusRuntime = createSessionStatusRuntime({
2797
- activeRuns,
2798
- appendLoopLog: appendLoopLog2,
2799
- now: now2,
2800
- activeRunCompletionFromMessages: options.activeRunCompletionFromMessages,
2801
- staleActiveRecoveryMs: options.staleActiveRecoveryMs,
2802
- sessionStatusCacheMs: options.sessionStatusCacheMs
2803
- });
2804
- const {
2805
- updateSessionStatusFromEvent,
2806
- staleActiveRun,
2807
- canFinalizeActiveRun,
2808
- sessionIsIdle,
2809
- markSessionStatus,
2810
- clearSessionStatus
2811
- } = statusRuntime;
2812
- const compactionRuntime = createCompactionRuntime({
2813
- activeRuns,
2814
- finalizeActiveRun,
2815
- appendLoopLog: appendLoopLog2,
2816
- compactSession: options.compactSession,
2817
- log: log2,
2818
- errorMessage,
2819
- now: now2
2820
- });
2821
- function dueJobs(state, force = false) {
2822
- const current = now2();
2823
- const due = (state.jobs || []).filter((job) => {
2824
- if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
2825
- return false;
2826
- if (!job.enabled || job.paused)
2827
- return false;
2828
- if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
2829
- return false;
2830
- if (job.maxRuntimeMs > 0 && current - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs)
2831
- return true;
2832
- if (Number(job.runNowRequestedAt || 0) > 0)
2833
- return true;
2775
+ async function fireAction(directory, client, sessionID, job) {
2776
+ const action = String(job.action || "").trim();
2777
+ const kind = actionKind(action, job);
2778
+ const agent = job.agent || "build";
2779
+ const model = normalizedModelRef2(job.model);
2780
+ if (kind === "compact") {
2781
+ const ok = await startCompaction(directory, client, sessionID, job.id, model, false);
2782
+ return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
2783
+ }
2784
+ if (kind === "command") {
2785
+ const normalized = action.startsWith("/") ? action.slice(1) : action;
2786
+ const [command, argumentsText] = splitFirst(normalized);
2787
+ if (!command) {
2788
+ await toast2(client, "Loop command action is empty. Example: /loop-command 200m /compact", "warning");
2789
+ return { startsAssistantTurn: false, pause: true, reason: "empty_command" };
2790
+ }
2791
+ const tuiCommand = compactTuiCommandName2(command);
2792
+ if (tuiCommand) {
2793
+ guardLoopOwnedUserMessage2(sessionID);
2794
+ const ok = await startCompaction(directory, client, sessionID, job.id, model, false);
2795
+ return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
2796
+ }
2797
+ guardLoopOwnedUserMessage2(sessionID);
2798
+ const commandBody = { command, arguments: argumentsText, agent };
2799
+ if (model)
2800
+ commandBody.model = `${model.providerID}/${model.modelID}`;
2801
+ await sdkCall2(client.session.command.bind(client.session), { path: { id: sessionID }, body: commandBody }, { path: { sessionID }, body: commandBody }, { sessionID, ...commandBody });
2802
+ return { startsAssistantTurn: true };
2803
+ }
2804
+ if (kind === "shell") {
2805
+ const command = action.replace(/^[!$]\s*/, "").trim();
2806
+ if (job.safe && dangerousShell2(command)) {
2807
+ await toast2(client, `Blocked dangerous shell command in safe mode: ${command}`, "error");
2808
+ await appendLoopLog2(directory, "blocked", { sessionID, job: job.name || job.id, command });
2809
+ return { startsAssistantTurn: false, pause: true, reason: "safe_shell_blocked" };
2810
+ }
2811
+ guardLoopOwnedUserMessage2(sessionID);
2812
+ const shellBody = { command, agent };
2813
+ if (model)
2814
+ shellBody.model = model;
2815
+ const dispatch2 = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
2816
+ return { startsAssistantTurn: true, dispatch: dispatch2 };
2817
+ }
2818
+ const prompt = await buildPrompt(directory, job);
2819
+ const prefix = kind === "goal" ? "EXPERIMENTAL GOAL MODE CONTINUATION. Continue pursuing the active goal. Do not explain the /loop-goal command. Use the goal tools only when progress/completion/block state is real." : "AUTONOMOUS OPENCODE LOOP ITERATION. Continue the configured task now. Do not explain the /loop command. Do not search for documentation about this plugin. Do not create scheduler files. Do not ask questions. Make reasonable assumptions and work directly.";
2820
+ const promptText = `${prefix}
2821
+
2822
+ ${prompt}`;
2823
+ guardLoopOwnedUserMessage2(sessionID);
2824
+ const promptBody = { agent, parts: [{ type: "text", text: promptText }] };
2825
+ if (model)
2826
+ promptBody.model = model;
2827
+ const dispatch = fireSdk2(client, "session.prompt", client.session.prompt.bind(client.session), { path: { id: sessionID }, body: promptBody }, { path: { sessionID }, body: promptBody }, { sessionID, ...promptBody });
2828
+ return { startsAssistantTurn: true, dispatch };
2829
+ }
2830
+ return { fireAction };
2831
+ }
2832
+
2833
+ // src/source/runtime/run-finalization.js
2834
+ function requireFunction8(value, label) {
2835
+ if (typeof value !== "function")
2836
+ throw new TypeError(`createRunFinalizationRuntime requires ${label}`);
2837
+ return value;
2838
+ }
2839
+ function createRunFinalizationRuntime(options = {}) {
2840
+ const runGoalChecks = requireFunction8(options.runGoalChecks, "runGoalChecks");
2841
+ const applyGoalNoProgressGuard = requireFunction8(options.applyGoalNoProgressGuard, "applyGoalNoProgressGuard");
2842
+ const createCheckpoint = requireFunction8(options.createCheckpoint, "createCheckpoint");
2843
+ const scheduleDueWork = requireFunction8(options.scheduleDueWork, "scheduleDueWork");
2844
+ const now2 = typeof options.now === "function" ? options.now : now;
2845
+ const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
2846
+ const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
2847
+ const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
2848
+ const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
2849
+ const toast2 = typeof options.toast === "function" ? options.toast : toast;
2850
+ const writeGoalReport2 = typeof options.writeGoalReport === "function" ? options.writeGoalReport : writeGoalReport;
2851
+ const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
2852
+ async function finalizeJob(directory, client, sessionID, state, job, previousJob) {
2853
+ if (job.verifyCommand) {
2854
+ const verify = await runShellCommand2(job.verifyCommand, directory, job.timeoutMs || 300000);
2855
+ job.lastVerifyAt = now2();
2856
+ job.lastVerifyCode = verify.code;
2857
+ if (verify.code === 0) {
2858
+ job.failureCount = 0;
2859
+ job.lastVerifyFailure = "";
2860
+ await toast2(client, "Loop verify passed: " + job.verifyCommand, "success");
2861
+ } else {
2862
+ job.failureCount = (job.failureCount || 0) + 1;
2863
+ job.lastVerifyFailure = (job.verifyCommand + `
2864
+ exit=` + verify.code + `
2865
+ ` + verify.stdout + `
2866
+ ` + verify.stderr).slice(0, 4000);
2867
+ await toast2(client, "Loop verify failed: " + job.verifyCommand, "warning");
2868
+ if (job.pauseOnVerifyFail || job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
2869
+ job.paused = true;
2870
+ await notifyJob2(directory, job, "verify_failed");
2871
+ }
2872
+ }
2873
+ await appendLoopLog2(directory, "verify", {
2874
+ sessionID,
2875
+ job: job.name || job.id,
2876
+ command: job.verifyCommand,
2877
+ code: verify.code,
2878
+ failures: job.failureCount || 0
2879
+ });
2880
+ }
2881
+ if (job.postrunCommand) {
2882
+ if (job.safe && dangerousShell2(job.postrunCommand)) {
2883
+ await appendLoopLog2(directory, "postrun-blocked", {
2884
+ sessionID,
2885
+ job: job.name || job.id,
2886
+ command: job.postrunCommand
2887
+ });
2888
+ } else {
2889
+ const postrun = await runShellCommand2(job.postrunCommand, directory, job.timeoutMs || 300000);
2890
+ job.lastPostrunCode = postrun.code;
2891
+ job.lastPostrunAt = now2();
2892
+ if (postrun.code !== 0) {
2893
+ job.failureCount = (job.failureCount || 0) + 1;
2894
+ job.lastPostrunFailure = (job.postrunCommand + `
2895
+ exit=` + postrun.code + `
2896
+ ` + postrun.stdout + `
2897
+ ` + postrun.stderr).slice(0, 4000);
2898
+ if (job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
2899
+ job.paused = true;
2900
+ await notifyJob2(directory, job, "postrun_failed");
2901
+ }
2902
+ }
2903
+ await appendLoopLog2(directory, "postrun", {
2904
+ sessionID,
2905
+ job: job.name || job.id,
2906
+ command: job.postrunCommand,
2907
+ code: postrun.code
2908
+ });
2909
+ }
2910
+ }
2911
+ if (isGoalJob(job)) {
2912
+ job = await runGoalChecks(directory, sessionID, job, client);
2913
+ job = await applyGoalNoProgressGuard(directory, client, sessionID, job, previousJob);
2914
+ }
2915
+ state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate).filter((candidate) => candidate.enabled !== false || isGoalJob(candidate));
2916
+ await writeState2(directory, sessionID, state);
2917
+ if (isGoalJob(job))
2918
+ await writeGoalReport2(directory, sessionID, job);
2919
+ await createCheckpoint(directory, sessionID, job, client);
2920
+ await scheduleDueWork(directory, client, sessionID);
2921
+ return job;
2922
+ }
2923
+ return { finalizeJob };
2924
+ }
2925
+
2926
+ // src/source/runtime/run-admission.js
2927
+ import path7 from "path";
2928
+ function requireFunction9(value, label) {
2929
+ if (typeof value !== "function")
2930
+ throw new TypeError(`createRunAdmissionRuntime requires ${label}`);
2931
+ return value;
2932
+ }
2933
+ function createRunAdmissionRuntime(options = {}) {
2934
+ const untilReached = requireFunction9(options.untilReached, "untilReached");
2935
+ const scheduleDueWork = requireFunction9(options.scheduleDueWork, "scheduleDueWork");
2936
+ const now2 = typeof options.now === "function" ? options.now : now;
2937
+ const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
2938
+ const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
2939
+ const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
2940
+ const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
2941
+ const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
2942
+ const toast2 = typeof options.toast === "function" ? options.toast : toast;
2943
+ const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
2944
+ function dueJobs(state, force = false) {
2945
+ const current = now2();
2946
+ const due = (state.jobs || []).filter((job) => {
2947
+ if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
2948
+ return false;
2949
+ if (!job.enabled || job.paused)
2950
+ return false;
2951
+ if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
2952
+ return false;
2953
+ if (job.maxRuntimeMs > 0 && current - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs)
2954
+ return true;
2955
+ if (Number(job.runNowRequestedAt || 0) > 0)
2956
+ return true;
2834
2957
  if (force)
2835
2958
  return true;
2836
2959
  if (job.watchPaths?.length)
@@ -2839,6 +2962,170 @@ function createLoopExecutor(options = {}) {
2839
2962
  });
2840
2963
  return due.sort((a, b) => Number(Number(b.runNowRequestedAt || 0) > 0) - Number(Number(a.runNowRequestedAt || 0) > 0));
2841
2964
  }
2965
+ async function reschedule(directory, client, sessionID) {
2966
+ await scheduleDueWork(directory, client, sessionID);
2967
+ }
2968
+ async function stopAndRemove(directory, client, sessionID, state, job, reason, message, logEvent) {
2969
+ state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
2970
+ await writeState2(directory, sessionID, state);
2971
+ await notifyJob2(directory, job, reason);
2972
+ await toast2(client, message, "success");
2973
+ if (logEvent)
2974
+ await appendLoopLog2(directory, logEvent, { sessionID, job: job.name || job.id });
2975
+ await reschedule(directory, client, sessionID);
2976
+ return { admitted: false, reason };
2977
+ }
2978
+ async function admitJob(directory, client, sessionID, state, job) {
2979
+ const runNowRequested = Number(job.runNowRequestedAt || 0) > 0;
2980
+ if (job.maxRuntimeMs > 0 && now2() - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs) {
2981
+ return await stopAndRemove(directory, client, sessionID, state, job, "max_runtime_reached", `Loop stopped by --max-runtime: ${job.name || job.id}`, "max-runtime");
2982
+ }
2983
+ if (job.stopFile && await pathExists2(path7.resolve(directory, job.stopFile))) {
2984
+ return await stopAndRemove(directory, client, sessionID, state, job, "stop_file", "Loop stopped by --stop-file: " + job.stopFile);
2985
+ }
2986
+ if (await untilReached(directory, job)) {
2987
+ return await stopAndRemove(directory, client, sessionID, state, job, "until_reached", `Loop stopped by --until: ${job.until}`);
2988
+ }
2989
+ if (job.preflightCommand) {
2990
+ if (job.safe && dangerousShell2(job.preflightCommand)) {
2991
+ if (runNowRequested)
2992
+ delete job.runNowRequestedAt;
2993
+ job.paused = true;
2994
+ await writeState2(directory, sessionID, state);
2995
+ await notifyJob2(directory, job, "preflight_blocked");
2996
+ await toast2(client, "Preflight blocked in safe mode and loop paused: " + job.preflightCommand, "error");
2997
+ await reschedule(directory, client, sessionID);
2998
+ return { admitted: false, reason: "preflight_blocked" };
2999
+ }
3000
+ const preflight = await runShellCommand2(job.preflightCommand, directory, job.timeoutMs || 300000);
3001
+ await appendLoopLog2(directory, "preflight", {
3002
+ sessionID,
3003
+ job: job.name || job.id,
3004
+ command: job.preflightCommand,
3005
+ code: preflight.code
3006
+ });
3007
+ if (preflight.code !== 0) {
3008
+ if (runNowRequested)
3009
+ delete job.runNowRequestedAt;
3010
+ job.paused = true;
3011
+ job.failureCount = (job.failureCount || 0) + 1;
3012
+ job.lastPreflightFailure = (job.preflightCommand + `
3013
+ exit=` + preflight.code + `
3014
+ ` + preflight.stdout + `
3015
+ ` + preflight.stderr).slice(0, 4000);
3016
+ state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
3017
+ await writeState2(directory, sessionID, state);
3018
+ await notifyJob2(directory, job, "preflight_failed");
3019
+ await toast2(client, "Preflight failed and loop paused: " + job.preflightCommand, "warning");
3020
+ await reschedule(directory, client, sessionID);
3021
+ return { admitted: false, reason: "preflight_failed" };
3022
+ }
3023
+ }
3024
+ return { admitted: true, job, runNowRequested };
3025
+ }
3026
+ return { dueJobs, admitJob };
3027
+ }
3028
+
3029
+ // src/source/runtime/executor.js
3030
+ var DEFAULT_ACTIVE_GUARD_MS = 45000;
3031
+ var DEFAULT_BUSY_RETRY_MS2 = 5000;
3032
+ function requireFunction10(value, label) {
3033
+ if (typeof value !== "function")
3034
+ throw new TypeError(`createLoopExecutor requires ${label}`);
3035
+ return value;
3036
+ }
3037
+ function createLoopExecutor(options = {}) {
3038
+ const workspace = options.workspace || {};
3039
+ const goalPolicy = options.goalPolicy || {};
3040
+ const scheduler = options.scheduler || {};
3041
+ const buildPrompt = requireFunction10(workspace.buildPrompt, "workspace.buildPrompt");
3042
+ const ensureBranch = requireFunction10(workspace.ensureBranch, "workspace.ensureBranch");
3043
+ const watchChanged = requireFunction10(workspace.watchChanged, "workspace.watchChanged");
3044
+ const untilReached = requireFunction10(workspace.untilReached, "workspace.untilReached");
3045
+ const createCheckpoint = requireFunction10(workspace.createCheckpoint, "workspace.createCheckpoint");
3046
+ const runGoalChecks = requireFunction10(goalPolicy.runGoalChecks, "goalPolicy.runGoalChecks");
3047
+ const applyGoalNoProgressGuard = requireFunction10(goalPolicy.applyGoalNoProgressGuard, "goalPolicy.applyGoalNoProgressGuard");
3048
+ const rememberSession = requireFunction10(scheduler.rememberSession, "scheduler.rememberSession");
3049
+ const scheduleDueWork = requireFunction10(scheduler.scheduleDueWork, "scheduler.scheduleDueWork");
3050
+ const now2 = typeof options.now === "function" ? options.now : now;
3051
+ const readState2 = typeof options.readState === "function" ? options.readState : readState;
3052
+ const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
3053
+ const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
3054
+ const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
3055
+ const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
3056
+ const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : sdkErrorMessage;
3057
+ const fireSdk2 = typeof options.fireSdk === "function" ? options.fireSdk : fireSdk;
3058
+ const log2 = typeof options.log === "function" ? options.log : log;
3059
+ const toast2 = typeof options.toast === "function" ? options.toast : toast;
3060
+ const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
3061
+ const activeGuardMs = Number.isFinite(Number(options.activeGuardMs)) && Number(options.activeGuardMs) > 0 ? Number(options.activeGuardMs) : DEFAULT_ACTIVE_GUARD_MS;
3062
+ const busyRetryMs = Number.isFinite(Number(options.busyRetryMs)) && Number(options.busyRetryMs) > 0 ? Number(options.busyRetryMs) : DEFAULT_BUSY_RETRY_MS2;
3063
+ const activeRuns = new Map;
3064
+ const runLocks = new Map;
3065
+ const statusRuntime = createSessionStatusRuntime({
3066
+ activeRuns,
3067
+ appendLoopLog: appendLoopLog2,
3068
+ now: now2,
3069
+ activeRunCompletionFromMessages: options.activeRunCompletionFromMessages,
3070
+ staleActiveRecoveryMs: options.staleActiveRecoveryMs,
3071
+ sessionStatusCacheMs: options.sessionStatusCacheMs
3072
+ });
3073
+ const {
3074
+ updateSessionStatusFromEvent,
3075
+ staleActiveRun,
3076
+ canFinalizeActiveRun,
3077
+ sessionIsIdle,
3078
+ markSessionStatus,
3079
+ clearSessionStatus
3080
+ } = statusRuntime;
3081
+ const compactionRuntime = createCompactionRuntime({
3082
+ activeRuns,
3083
+ finalizeActiveRun,
3084
+ appendLoopLog: appendLoopLog2,
3085
+ compactSession: options.compactSession,
3086
+ log: log2,
3087
+ errorMessage,
3088
+ now: now2
3089
+ });
3090
+ const actionDispatcher = createActionDispatcher({
3091
+ buildPrompt,
3092
+ compactionRuntime,
3093
+ appendLoopLog: appendLoopLog2,
3094
+ sdkCall: options.sdkCall,
3095
+ normalizedModelRef: options.normalizedModelRef,
3096
+ fireSdk: options.fireSdk,
3097
+ compactTuiCommandName: options.compactTuiCommandName,
3098
+ toast: toast2,
3099
+ guardLoopOwnedUserMessage: options.guardLoopOwnedUserMessage,
3100
+ dangerousShell: dangerousShell2
3101
+ });
3102
+ const finalizationRuntime = createRunFinalizationRuntime({
3103
+ runGoalChecks,
3104
+ applyGoalNoProgressGuard,
3105
+ createCheckpoint,
3106
+ scheduleDueWork,
3107
+ now: now2,
3108
+ writeState: writeState2,
3109
+ appendLoopLog: appendLoopLog2,
3110
+ runShellCommand: runShellCommand2,
3111
+ notifyJob: notifyJob2,
3112
+ toast: toast2,
3113
+ writeGoalReport: options.writeGoalReport,
3114
+ dangerousShell: dangerousShell2
3115
+ });
3116
+ const admissionRuntime = createRunAdmissionRuntime({
3117
+ untilReached,
3118
+ scheduleDueWork,
3119
+ now: now2,
3120
+ pathExists: options.pathExists,
3121
+ writeState: writeState2,
3122
+ appendLoopLog: appendLoopLog2,
3123
+ runShellCommand: runShellCommand2,
3124
+ notifyJob: notifyJob2,
3125
+ toast: toast2,
3126
+ dangerousShell: dangerousShell2
3127
+ });
3128
+ const dueJobs = admissionRuntime.dueJobs;
2842
3129
  function clearActiveRun(sessionID) {
2843
3130
  const active = activeRuns.get(sessionID);
2844
3131
  if (active?.timer)
@@ -2911,131 +3198,10 @@ function createLoopExecutor(options = {}) {
2911
3198
  startedAt: active.startedAt
2912
3199
  });
2913
3200
  }
2914
- if (job.verifyCommand) {
2915
- const verify = await runShellCommand2(job.verifyCommand, directory, job.timeoutMs || 300000);
2916
- job.lastVerifyAt = now2();
2917
- job.lastVerifyCode = verify.code;
2918
- if (verify.code === 0) {
2919
- job.failureCount = 0;
2920
- job.lastVerifyFailure = "";
2921
- await toast2(client, "Loop verify passed: " + job.verifyCommand, "success");
2922
- } else {
2923
- job.failureCount = (job.failureCount || 0) + 1;
2924
- job.lastVerifyFailure = (job.verifyCommand + `
2925
- exit=` + verify.code + `
2926
- ` + verify.stdout + `
2927
- ` + verify.stderr).slice(0, 4000);
2928
- await toast2(client, "Loop verify failed: " + job.verifyCommand, "warning");
2929
- if (job.pauseOnVerifyFail || job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
2930
- job.paused = true;
2931
- await notifyJob2(directory, job, "verify_failed");
2932
- }
2933
- }
2934
- await appendLoopLog2(directory, "verify", {
2935
- sessionID,
2936
- job: job.name || job.id,
2937
- command: job.verifyCommand,
2938
- code: verify.code,
2939
- failures: job.failureCount || 0
2940
- });
2941
- }
2942
- if (job.postrunCommand) {
2943
- if (job.safe && dangerousShell2(job.postrunCommand)) {
2944
- await appendLoopLog2(directory, "postrun-blocked", {
2945
- sessionID,
2946
- job: job.name || job.id,
2947
- command: job.postrunCommand
2948
- });
2949
- } else {
2950
- const postrun = await runShellCommand2(job.postrunCommand, directory, job.timeoutMs || 300000);
2951
- job.lastPostrunCode = postrun.code;
2952
- job.lastPostrunAt = now2();
2953
- if (postrun.code !== 0) {
2954
- job.failureCount = (job.failureCount || 0) + 1;
2955
- job.lastPostrunFailure = (job.postrunCommand + `
2956
- exit=` + postrun.code + `
2957
- ` + postrun.stdout + `
2958
- ` + postrun.stderr).slice(0, 4000);
2959
- if (job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
2960
- job.paused = true;
2961
- await notifyJob2(directory, job, "postrun_failed");
2962
- }
2963
- }
2964
- await appendLoopLog2(directory, "postrun", {
2965
- sessionID,
2966
- job: job.name || job.id,
2967
- command: job.postrunCommand,
2968
- code: postrun.code
2969
- });
2970
- }
2971
- }
2972
- if (isGoalJob(job)) {
2973
- job = await runGoalChecks(directory, sessionID, job, client);
2974
- job = await applyGoalNoProgressGuard(directory, client, sessionID, job, active.job);
2975
- }
2976
- state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate).filter((candidate) => candidate.enabled !== false || isGoalJob(candidate));
2977
- await writeState2(directory, sessionID, state);
2978
- if (isGoalJob(job))
2979
- await writeGoalReport2(directory, sessionID, job);
2980
- await createCheckpoint(directory, sessionID, job, client);
2981
- await scheduleDueWork(directory, client, sessionID);
3201
+ await finalizationRuntime.finalizeJob(directory, client, sessionID, state, job, active.job);
2982
3202
  return true;
2983
3203
  }
2984
- async function fireAction(directory, client, sessionID, job) {
2985
- const action = String(job.action || "").trim();
2986
- const kind = actionKind(action, job);
2987
- const agent = job.agent || "build";
2988
- const model = normalizedModelRef2(job.model);
2989
- if (kind === "compact") {
2990
- const ok = await compactionRuntime.start(directory, client, sessionID, job.id, model, false);
2991
- return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
2992
- }
2993
- if (kind === "command") {
2994
- const normalized = action.startsWith("/") ? action.slice(1) : action;
2995
- const [command, argumentsText] = splitFirst(normalized);
2996
- if (!command) {
2997
- await toast2(client, "Loop command action is empty. Example: /loop-command 200m /compact", "warning");
2998
- return { startsAssistantTurn: false, pause: true, reason: "empty_command" };
2999
- }
3000
- const tuiCommand = compactTuiCommandName2(command);
3001
- if (tuiCommand) {
3002
- guardLoopOwnedUserMessage2(sessionID);
3003
- const ok = await compactionRuntime.start(directory, client, sessionID, job.id, model, false);
3004
- return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
3005
- }
3006
- guardLoopOwnedUserMessage2(sessionID);
3007
- const commandBody = { command, arguments: argumentsText, agent };
3008
- if (model)
3009
- commandBody.model = `${model.providerID}/${model.modelID}`;
3010
- await sdkCall2(client.session.command.bind(client.session), { path: { id: sessionID }, body: commandBody }, { path: { sessionID }, body: commandBody }, { sessionID, ...commandBody });
3011
- return { startsAssistantTurn: true };
3012
- }
3013
- if (kind === "shell") {
3014
- const command = action.replace(/^[!$]\s*/, "").trim();
3015
- if (job.safe && dangerousShell2(command)) {
3016
- await toast2(client, `Blocked dangerous shell command in safe mode: ${command}`, "error");
3017
- await appendLoopLog2(directory, "blocked", { sessionID, job: job.name || job.id, command });
3018
- return { startsAssistantTurn: false, pause: true, reason: "safe_shell_blocked" };
3019
- }
3020
- guardLoopOwnedUserMessage2(sessionID);
3021
- const shellBody = { command, agent };
3022
- if (model)
3023
- shellBody.model = model;
3024
- const dispatch2 = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
3025
- return { startsAssistantTurn: true, dispatch: dispatch2 };
3026
- }
3027
- const prompt = await buildPrompt(directory, job);
3028
- const prefix = kind === "goal" ? "EXPERIMENTAL GOAL MODE CONTINUATION. Continue pursuing the active goal. Do not explain the /loop-goal command. Use the goal tools only when progress/completion/block state is real." : "AUTONOMOUS OPENCODE LOOP ITERATION. Continue the configured task now. Do not explain the /loop command. Do not search for documentation about this plugin. Do not create scheduler files. Do not ask questions. Make reasonable assumptions and work directly.";
3029
- const promptText = `${prefix}
3030
-
3031
- ${prompt}`;
3032
- guardLoopOwnedUserMessage2(sessionID);
3033
- const promptBody = { agent, parts: [{ type: "text", text: promptText }] };
3034
- if (model)
3035
- promptBody.model = model;
3036
- const dispatch = fireSdk2(client, "session.prompt", client.session.prompt.bind(client.session), { path: { id: sessionID }, body: promptBody }, { path: { sessionID }, body: promptBody }, { sessionID, ...promptBody });
3037
- return { startsAssistantTurn: true, dispatch };
3038
- }
3204
+ const fireAction = actionDispatcher.fireAction;
3039
3205
  async function maybeRunDueJobs(directory, client, sessionID, runOptions = {}) {
3040
3206
  rememberSession(directory, client, sessionID);
3041
3207
  const reschedule = async (minDelayMs = 0) => {
@@ -3077,67 +3243,11 @@ ${prompt}`;
3077
3243
  return;
3078
3244
  }
3079
3245
  job = due[0];
3080
- const runNowRequested = Number(job.runNowRequestedAt || 0) > 0;
3081
- if (job.maxRuntimeMs > 0 && now2() - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs) {
3082
- state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
3083
- await writeState2(directory, sessionID, state);
3084
- await notifyJob2(directory, job, "max_runtime_reached");
3085
- await toast2(client, `Loop stopped by --max-runtime: ${job.name || job.id}`, "success");
3086
- await appendLoopLog2(directory, "max-runtime", { sessionID, job: job.name || job.id });
3087
- await reschedule();
3246
+ const admission = await admissionRuntime.admitJob(directory, client, sessionID, state, job);
3247
+ if (!admission.admitted)
3088
3248
  return;
3089
- }
3090
- if (job.stopFile && await pathExists2(path6.resolve(directory, job.stopFile))) {
3091
- state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
3092
- await writeState2(directory, sessionID, state);
3093
- await notifyJob2(directory, job, "stop_file");
3094
- await toast2(client, "Loop stopped by --stop-file: " + job.stopFile, "success");
3095
- await reschedule();
3096
- return;
3097
- }
3098
- if (await untilReached(directory, job)) {
3099
- state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
3100
- await writeState2(directory, sessionID, state);
3101
- await notifyJob2(directory, job, "until_reached");
3102
- await toast2(client, `Loop stopped by --until: ${job.until}`, "success");
3103
- await reschedule();
3104
- return;
3105
- }
3106
- if (job.preflightCommand) {
3107
- if (job.safe && dangerousShell2(job.preflightCommand)) {
3108
- if (runNowRequested)
3109
- delete job.runNowRequestedAt;
3110
- job.paused = true;
3111
- await writeState2(directory, sessionID, state);
3112
- await notifyJob2(directory, job, "preflight_blocked");
3113
- await toast2(client, "Preflight blocked in safe mode and loop paused: " + job.preflightCommand, "error");
3114
- await reschedule();
3115
- return;
3116
- }
3117
- const preflight = await runShellCommand2(job.preflightCommand, directory, job.timeoutMs || 300000);
3118
- await appendLoopLog2(directory, "preflight", {
3119
- sessionID,
3120
- job: job.name || job.id,
3121
- command: job.preflightCommand,
3122
- code: preflight.code
3123
- });
3124
- if (preflight.code !== 0) {
3125
- if (runNowRequested)
3126
- delete job.runNowRequestedAt;
3127
- job.paused = true;
3128
- job.failureCount = (job.failureCount || 0) + 1;
3129
- job.lastPreflightFailure = (job.preflightCommand + `
3130
- exit=` + preflight.code + `
3131
- ` + preflight.stdout + `
3132
- ` + preflight.stderr).slice(0, 4000);
3133
- state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
3134
- await writeState2(directory, sessionID, state);
3135
- await notifyJob2(directory, job, "preflight_failed");
3136
- await toast2(client, "Preflight failed and loop paused: " + job.preflightCommand, "warning");
3137
- await reschedule();
3138
- return;
3139
- }
3140
- }
3249
+ job = admission.job;
3250
+ const runNowRequested = admission.runNowRequested;
3141
3251
  job = await ensureBranch(directory, job, client, sessionID);
3142
3252
  const compactResult = await compactionRuntime.maybeCompact(directory, client, sessionID, job);
3143
3253
  job = compactResult.job;
@@ -3254,7 +3364,7 @@ exit=` + preflight.code + `
3254
3364
  // src/source/runtime/goal-steering.js
3255
3365
  var DEFAULT_STEERING_SUPPRESSION_MS = 5 * 60000;
3256
3366
  var DEFAULT_SEEN_USER_MESSAGE_MS = 10 * 60000;
3257
- function requireFunction8(value, label) {
3367
+ function requireFunction11(value, label) {
3258
3368
  if (typeof value !== "function")
3259
3369
  throw new TypeError(`createGoalSteeringRuntime requires ${label}`);
3260
3370
  return value;
@@ -3298,8 +3408,8 @@ function activeGoalJobs(state) {
3298
3408
  });
3299
3409
  }
3300
3410
  function createGoalSteeringRuntime(options = {}) {
3301
- const getActiveRun = requireFunction8(options.getActiveRun, "getActiveRun");
3302
- const clearActiveRun = requireFunction8(options.clearActiveRun, "clearActiveRun");
3411
+ const getActiveRun = requireFunction11(options.getActiveRun, "getActiveRun");
3412
+ const clearActiveRun = requireFunction11(options.clearActiveRun, "clearActiveRun");
3303
3413
  const isLoopOwnedUserMessage = typeof options.isLoopOwnedUserMessage === "function" ? options.isLoopOwnedUserMessage : () => false;
3304
3414
  const readState2 = typeof options.readState === "function" ? options.readState : readState;
3305
3415
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
@@ -4138,6 +4248,6 @@ var OpenCodeLoopPlugin2 = async (input = {}) => {
4138
4248
  };
4139
4249
  var v1_default = OpenCodeLoopPlugin2;
4140
4250
  export {
4141
- v1_default as default,
4142
- OpenCodeLoopPlugin2 as OpenCodeLoopPlugin
4251
+ OpenCodeLoopPlugin2 as OpenCodeLoopPlugin,
4252
+ v1_default as default
4143
4253
  };