@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/CHANGELOG.md +14 -0
- package/README.md +28 -8
- package/agents/opencode-loop-local.md +2 -1
- package/package.json +3 -3
- package/scripts/action-dispatch-test.mjs +117 -0
- package/scripts/goal-companion-test.mjs +37 -1
- package/scripts/install-with-goals.mjs +26 -4
- package/scripts/run-admission-test.mjs +133 -0
- package/scripts/run-finalization-test.mjs +119 -0
- package/src/index.js +479 -369
- package/src/source/runtime/action-dispatch.js +108 -0
- package/src/source/runtime/executor.js +55 -228
- package/src/source/runtime/goal-evidence.js +39 -0
- package/src/source/runtime/goal-policy.js +1 -1
- package/src/source/runtime/goal-prompt.js +53 -0
- package/src/source/runtime/goal-report.js +52 -0
- package/src/source/runtime/goal-runtime.js +9 -141
- package/src/source/runtime/run-admission.js +132 -0
- package/src/source/runtime/run-finalization.js +100 -0
package/src/index.js
CHANGED
|
@@ -1231,91 +1231,10 @@ function createCommandRouter(options = {}) {
|
|
|
1231
1231
|
};
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
|
-
// src/source/runtime/goal-
|
|
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
|
-
|
|
1379
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
2448
|
-
await fs5.writeFile(
|
|
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/
|
|
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(`
|
|
2761
|
+
throw new TypeError(`createActionDispatcher requires ${label}`);
|
|
2760
2762
|
return value;
|
|
2761
2763
|
}
|
|
2762
|
-
function
|
|
2763
|
-
const
|
|
2764
|
-
const
|
|
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
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
if (
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3081
|
-
if (
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3302
|
-
const 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
|
-
|
|
4142
|
-
|
|
4251
|
+
OpenCodeLoopPlugin2 as OpenCodeLoopPlugin,
|
|
4252
|
+
v1_default as default
|
|
4143
4253
|
};
|