@hasna/loops 0.4.5 → 0.4.7
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 +40 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +613 -230
- package/dist/daemon/index.js +367 -170
- package/dist/index.js +413 -182
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/types.d.ts +6 -0
- package/dist/lib/storage/index.js +57 -5
- package/dist/lib/storage/sqlite.js +57 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +57 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/lib/templates.d.ts +10 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +371 -173
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +180 -19
- package/dist/sdk/index.js +370 -174
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1491
1491
|
class Store {
|
|
1492
1492
|
db;
|
|
1493
1493
|
rootDir;
|
|
1494
|
+
memoryRootDir;
|
|
1494
1495
|
constructor(path) {
|
|
1495
1496
|
const file = path ?? dbPath();
|
|
1496
1497
|
if (file !== ":memory:")
|
|
1497
1498
|
ensurePrivateStorePath(file);
|
|
1498
1499
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1500
|
+
if (file === ":memory:")
|
|
1501
|
+
this.memoryRootDir = this.rootDir;
|
|
1499
1502
|
this.db = new Database(file);
|
|
1500
1503
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1501
1504
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1948,9 +1951,12 @@ class Store {
|
|
|
1948
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1949
1952
|
if (rows.length === 0)
|
|
1950
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1951
|
-
if (rows.length
|
|
1954
|
+
if (rows.length === 1)
|
|
1955
|
+
return rowToLoop(rows[0]);
|
|
1956
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1957
|
+
if (active.length !== 1)
|
|
1952
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1953
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1954
1960
|
}
|
|
1955
1961
|
requireLoop(idOrName) {
|
|
1956
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2291,6 +2297,7 @@ class Store {
|
|
|
2291
2297
|
return this.transact(() => {
|
|
2292
2298
|
const loop = this.requireLoop(idOrName);
|
|
2293
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2294
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2295
2302
|
return res.changes > 0;
|
|
2296
2303
|
});
|
|
@@ -2775,7 +2782,10 @@ class Store {
|
|
|
2775
2782
|
return rowToGoal(row);
|
|
2776
2783
|
}
|
|
2777
2784
|
if (context.sourceType && context.sourceId) {
|
|
2778
|
-
const
|
|
2785
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2786
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2787
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2788
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2779
2789
|
if (row)
|
|
2780
2790
|
return rowToGoal(row);
|
|
2781
2791
|
}
|
|
@@ -3195,6 +3205,41 @@ class Store {
|
|
|
3195
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3196
3206
|
return run;
|
|
3197
3207
|
}
|
|
3208
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3209
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3210
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3211
|
+
try {
|
|
3212
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3213
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3214
|
+
stderr=COALESCE($stderr, stderr),
|
|
3215
|
+
updated_at=$updated
|
|
3216
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3217
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3218
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3219
|
+
))`).run({
|
|
3220
|
+
$workflowRunId: workflowRunId,
|
|
3221
|
+
$stepId: stepId,
|
|
3222
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3223
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3224
|
+
$updated: now,
|
|
3225
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
|
+
$now: now
|
|
3227
|
+
});
|
|
3228
|
+
if (res.changes === 1) {
|
|
3229
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3230
|
+
}
|
|
3231
|
+
this.db.exec("COMMIT");
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.db.exec("ROLLBACK");
|
|
3235
|
+
} catch {}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3239
|
+
if (!run)
|
|
3240
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3241
|
+
return run;
|
|
3242
|
+
}
|
|
3198
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3199
3244
|
return this.transact(() => {
|
|
3200
3245
|
const now = nowIso();
|
|
@@ -3632,7 +3677,8 @@ class Store {
|
|
|
3632
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3633
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3634
3679
|
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3635
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3680
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3681
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3636
3682
|
const run = this.getRun(id);
|
|
3637
3683
|
if (!run)
|
|
3638
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4160,12 +4206,18 @@ class Store {
|
|
|
4160
4206
|
}
|
|
4161
4207
|
close() {
|
|
4162
4208
|
this.db.close();
|
|
4209
|
+
if (this.memoryRootDir) {
|
|
4210
|
+
try {
|
|
4211
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4212
|
+
} catch {}
|
|
4213
|
+
this.memoryRootDir = undefined;
|
|
4214
|
+
}
|
|
4163
4215
|
}
|
|
4164
4216
|
}
|
|
4165
4217
|
// package.json
|
|
4166
4218
|
var package_default = {
|
|
4167
4219
|
name: "@hasna/loops",
|
|
4168
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.7",
|
|
4169
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4170
4222
|
type: "module",
|
|
4171
4223
|
main: "dist/index.js",
|
|
@@ -4624,10 +4676,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4624
4676
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4625
4677
|
try {
|
|
4626
4678
|
const lease = store.getDaemonLease();
|
|
4627
|
-
|
|
4628
|
-
removePid(path);
|
|
4629
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4630
|
-
}
|
|
4679
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4631
4680
|
try {
|
|
4632
4681
|
process.kill(state.pid, "SIGTERM");
|
|
4633
4682
|
} catch {
|
|
@@ -4647,11 +4696,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4647
4696
|
} catch {}
|
|
4648
4697
|
await sleep(150);
|
|
4649
4698
|
removePid(path);
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4699
|
+
let reapedPgids = [];
|
|
4700
|
+
if (leaseVerified && lease) {
|
|
4701
|
+
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
4702
|
+
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
4703
|
+
sleep,
|
|
4704
|
+
graceMs: opts.reapGraceMs
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4655
4707
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4656
4708
|
} finally {
|
|
4657
4709
|
store.close();
|
|
@@ -4989,7 +5041,12 @@ function notifySpawn(pid, opts) {
|
|
|
4989
5041
|
if (!pid)
|
|
4990
5042
|
return;
|
|
4991
5043
|
opts.onSpawn?.(pid);
|
|
4992
|
-
|
|
5044
|
+
const startedMs = processStartTimeMs(pid);
|
|
5045
|
+
opts.onSpawnProcess?.({
|
|
5046
|
+
pid,
|
|
5047
|
+
pgid: pid,
|
|
5048
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
5049
|
+
});
|
|
4993
5050
|
}
|
|
4994
5051
|
function shellQuote(value) {
|
|
4995
5052
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5314,18 +5371,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5314
5371
|
"agent",
|
|
5315
5372
|
command,
|
|
5316
5373
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5374
|
+
"--json",
|
|
5317
5375
|
agentId
|
|
5318
5376
|
];
|
|
5319
5377
|
}
|
|
5378
|
+
function extractFirstJsonObject(text) {
|
|
5379
|
+
let depth = 0;
|
|
5380
|
+
let start = -1;
|
|
5381
|
+
let inString = false;
|
|
5382
|
+
let escaped = false;
|
|
5383
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5384
|
+
const ch = text[i];
|
|
5385
|
+
if (inString) {
|
|
5386
|
+
if (escaped)
|
|
5387
|
+
escaped = false;
|
|
5388
|
+
else if (ch === "\\")
|
|
5389
|
+
escaped = true;
|
|
5390
|
+
else if (ch === '"')
|
|
5391
|
+
inString = false;
|
|
5392
|
+
continue;
|
|
5393
|
+
}
|
|
5394
|
+
if (ch === '"') {
|
|
5395
|
+
inString = true;
|
|
5396
|
+
} else if (ch === "{") {
|
|
5397
|
+
if (depth === 0)
|
|
5398
|
+
start = i;
|
|
5399
|
+
depth += 1;
|
|
5400
|
+
} else if (ch === "}") {
|
|
5401
|
+
if (depth > 0) {
|
|
5402
|
+
depth -= 1;
|
|
5403
|
+
if (depth === 0 && start !== -1)
|
|
5404
|
+
return text.slice(start, i + 1);
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5320
5410
|
function parseJsonOutput(stdout, label) {
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5411
|
+
const raw = stdout || "{}";
|
|
5412
|
+
const candidates = [raw.trim()];
|
|
5413
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5414
|
+
if (scanned && scanned !== raw.trim())
|
|
5415
|
+
candidates.push(scanned);
|
|
5416
|
+
let lastError;
|
|
5417
|
+
for (const candidate of candidates) {
|
|
5418
|
+
try {
|
|
5419
|
+
const value = JSON.parse(candidate);
|
|
5420
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5421
|
+
throw new Error("not an object");
|
|
5422
|
+
return value;
|
|
5423
|
+
} catch (error) {
|
|
5424
|
+
lastError = error;
|
|
5425
|
+
}
|
|
5328
5426
|
}
|
|
5427
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5329
5428
|
}
|
|
5330
5429
|
function recordField(value, key) {
|
|
5331
5430
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5344,6 +5443,13 @@ function numberField(value, key) {
|
|
|
5344
5443
|
function codewithAgentStatus(readJson) {
|
|
5345
5444
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5346
5445
|
}
|
|
5446
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5447
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5448
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5449
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5450
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5451
|
+
return agentSeq ?? snapshotSeq;
|
|
5452
|
+
}
|
|
5347
5453
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5348
5454
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5349
5455
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5374,6 +5480,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5374
5480
|
events
|
|
5375
5481
|
}, null, 2);
|
|
5376
5482
|
}
|
|
5483
|
+
function codewithAgentProgress(readJson) {
|
|
5484
|
+
const agent = recordField(readJson, "agent");
|
|
5485
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5486
|
+
return {
|
|
5487
|
+
provider: "codewith",
|
|
5488
|
+
agentId: stringField(agent, "agentId"),
|
|
5489
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5490
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5491
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5492
|
+
threadId: stringField(agent, "threadId"),
|
|
5493
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5494
|
+
pid: numberField(agent, "pid"),
|
|
5495
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5496
|
+
};
|
|
5497
|
+
}
|
|
5377
5498
|
function sleep(ms) {
|
|
5378
5499
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5379
5500
|
}
|
|
@@ -5408,6 +5529,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5408
5529
|
if (!agentId) {
|
|
5409
5530
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5410
5531
|
}
|
|
5532
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5411
5533
|
const stopAgent = async () => {
|
|
5412
5534
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5413
5535
|
cwd: spec.cwd,
|
|
@@ -5450,10 +5572,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5450
5572
|
});
|
|
5451
5573
|
}
|
|
5452
5574
|
const status = codewithAgentStatus(lastReadJson);
|
|
5453
|
-
const fingerprint = JSON.stringify({
|
|
5575
|
+
const fingerprint = JSON.stringify({
|
|
5576
|
+
status,
|
|
5577
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5578
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5579
|
+
});
|
|
5454
5580
|
if (fingerprint !== lastFingerprint) {
|
|
5455
5581
|
lastFingerprint = fingerprint;
|
|
5456
5582
|
lastProgressAt = Date.now();
|
|
5583
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5457
5584
|
}
|
|
5458
5585
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5459
5586
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6379,7 +6506,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6379
6506
|
|
|
6380
6507
|
`);
|
|
6381
6508
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6382
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6509
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6383
6510
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6384
6511
|
return {
|
|
6385
6512
|
title,
|
|
@@ -6394,7 +6521,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6394
6521
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6395
6522
|
},
|
|
6396
6523
|
futureNativeUpsert: {
|
|
6397
|
-
command: "todos upsert",
|
|
6524
|
+
command: "todos task upsert",
|
|
6398
6525
|
fields: {
|
|
6399
6526
|
title,
|
|
6400
6527
|
description,
|
|
@@ -7089,173 +7216,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
7089
7216
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
7090
7217
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
7091
7218
|
}
|
|
7219
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
7220
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
7221
|
+
try {
|
|
7222
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
7223
|
+
} catch {}
|
|
7224
|
+
}
|
|
7092
7225
|
const ordered = workflowExecutionOrder(workflow);
|
|
7093
7226
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
7094
7227
|
let blockingError;
|
|
7095
7228
|
let terminalStatus = "succeeded";
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
store.
|
|
7122
|
-
|
|
7123
|
-
|
|
7229
|
+
try {
|
|
7230
|
+
for (const step of ordered) {
|
|
7231
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7232
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7233
|
+
blockingError = "workflow run was cancelled";
|
|
7234
|
+
break;
|
|
7235
|
+
}
|
|
7236
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7237
|
+
if (pendingTimeout) {
|
|
7238
|
+
terminalStatus = "timed_out";
|
|
7239
|
+
blockingError = pendingTimeout;
|
|
7240
|
+
break;
|
|
7241
|
+
}
|
|
7242
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7243
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
7244
|
+
continue;
|
|
7245
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
7246
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
7247
|
+
const dependencyStep = byId.get(dependencyId);
|
|
7248
|
+
if (dependencyRun?.status === "succeeded")
|
|
7249
|
+
return false;
|
|
7250
|
+
return !dependencyStep?.continueOnFailure;
|
|
7251
|
+
});
|
|
7252
|
+
if (blockedBy) {
|
|
7253
|
+
opts.beforePersist?.();
|
|
7254
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
7255
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
7256
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7257
|
+
});
|
|
7258
|
+
continue;
|
|
7259
|
+
}
|
|
7260
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
7261
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
7262
|
+
terminalStatus = "failed";
|
|
7124
7263
|
continue;
|
|
7125
7264
|
}
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
});
|
|
7146
|
-
let result;
|
|
7147
|
-
const controller = new AbortController;
|
|
7148
|
-
const externalAbort = () => controller.abort();
|
|
7149
|
-
if (opts.signal?.aborted)
|
|
7150
|
-
controller.abort();
|
|
7151
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7152
|
-
const cancelTimer = setInterval(() => {
|
|
7153
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7265
|
+
opts.beforePersist?.();
|
|
7266
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
7267
|
+
if (startedStep.status !== "running") {
|
|
7268
|
+
terminalStatus = "failed";
|
|
7269
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
7270
|
+
break;
|
|
7271
|
+
}
|
|
7272
|
+
const stepContext = goalExecutionContext({
|
|
7273
|
+
loop: opts.loop,
|
|
7274
|
+
loopRun: opts.loopRun,
|
|
7275
|
+
scheduledFor: opts.scheduledFor,
|
|
7276
|
+
workflow,
|
|
7277
|
+
workflowRunId: run.id,
|
|
7278
|
+
workflowStepId: step.id
|
|
7279
|
+
});
|
|
7280
|
+
let result;
|
|
7281
|
+
const controller = new AbortController;
|
|
7282
|
+
const externalAbort = () => controller.abort();
|
|
7283
|
+
if (opts.signal?.aborted)
|
|
7154
7284
|
controller.abort();
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
opts
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7285
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7286
|
+
const cancelTimer = setInterval(() => {
|
|
7287
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7288
|
+
controller.abort();
|
|
7289
|
+
}, opts.cancelPollMs ?? 500);
|
|
7290
|
+
cancelTimer.unref();
|
|
7291
|
+
try {
|
|
7292
|
+
if (step.goal) {
|
|
7293
|
+
result = await runGoal(store, step.goal, {
|
|
7294
|
+
...opts,
|
|
7295
|
+
model: opts.goalModel,
|
|
7296
|
+
target: targetWithStepAccount(step),
|
|
7297
|
+
signal: controller.signal,
|
|
7298
|
+
context: stepContext
|
|
7299
|
+
});
|
|
7300
|
+
} else {
|
|
7301
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
7302
|
+
...opts,
|
|
7303
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
7304
|
+
signal: controller.signal,
|
|
7305
|
+
onAgentProgress: (progress) => {
|
|
7306
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7307
|
+
opts.beforePersist?.();
|
|
7308
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7309
|
+
stdout,
|
|
7310
|
+
payload: progress
|
|
7311
|
+
}, {
|
|
7312
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7313
|
+
});
|
|
7314
|
+
opts.onAgentProgress?.(progress);
|
|
7315
|
+
},
|
|
7316
|
+
onSpawn: (pid) => {
|
|
7317
|
+
opts.beforePersist?.();
|
|
7318
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7319
|
+
opts.onSpawn?.(pid);
|
|
7320
|
+
}
|
|
7321
|
+
});
|
|
7322
|
+
}
|
|
7323
|
+
} catch (error) {
|
|
7324
|
+
const finishedAt2 = nowIso();
|
|
7325
|
+
result = {
|
|
7326
|
+
status: "failed",
|
|
7327
|
+
stdout: "",
|
|
7328
|
+
stderr: "",
|
|
7329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7330
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7331
|
+
finishedAt: finishedAt2,
|
|
7332
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7333
|
+
};
|
|
7334
|
+
} finally {
|
|
7335
|
+
clearInterval(cancelTimer);
|
|
7336
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7337
|
+
}
|
|
7338
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7339
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7340
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7341
|
+
}
|
|
7342
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7343
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7344
|
+
blockingError = "workflow run was cancelled";
|
|
7345
|
+
break;
|
|
7346
|
+
}
|
|
7347
|
+
opts.beforePersist?.();
|
|
7348
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7349
|
+
if (blockedExit) {
|
|
7350
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7351
|
+
status: "skipped",
|
|
7352
|
+
finishedAt: result.finishedAt,
|
|
7353
|
+
durationMs: result.durationMs,
|
|
7354
|
+
stdout: result.stdout,
|
|
7355
|
+
stderr: result.stderr,
|
|
7356
|
+
exitCode: result.exitCode,
|
|
7357
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7358
|
+
}, {
|
|
7359
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7176
7360
|
});
|
|
7361
|
+
continue;
|
|
7177
7362
|
}
|
|
7178
|
-
} catch (error) {
|
|
7179
|
-
const finishedAt2 = nowIso();
|
|
7180
|
-
result = {
|
|
7181
|
-
status: "failed",
|
|
7182
|
-
stdout: "",
|
|
7183
|
-
stderr: "",
|
|
7184
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7185
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7186
|
-
finishedAt: finishedAt2,
|
|
7187
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7188
|
-
};
|
|
7189
|
-
} finally {
|
|
7190
|
-
clearInterval(cancelTimer);
|
|
7191
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7192
|
-
}
|
|
7193
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7194
|
-
if (timeoutMessage && result.status === "failed") {
|
|
7195
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7196
|
-
}
|
|
7197
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7198
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7199
|
-
blockingError = "workflow run was cancelled";
|
|
7200
|
-
break;
|
|
7201
|
-
}
|
|
7202
|
-
opts.beforePersist?.();
|
|
7203
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7204
|
-
if (blockedExit) {
|
|
7205
7363
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7206
|
-
status:
|
|
7364
|
+
status: result.status,
|
|
7207
7365
|
finishedAt: result.finishedAt,
|
|
7208
7366
|
durationMs: result.durationMs,
|
|
7209
7367
|
stdout: result.stdout,
|
|
7210
7368
|
stderr: result.stderr,
|
|
7211
7369
|
exitCode: result.exitCode,
|
|
7212
|
-
error:
|
|
7370
|
+
error: result.error
|
|
7213
7371
|
}, {
|
|
7214
7372
|
daemonLeaseId: opts.daemonLeaseId
|
|
7215
7373
|
});
|
|
7216
|
-
|
|
7374
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7375
|
+
terminalStatus = result.status;
|
|
7376
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7377
|
+
break;
|
|
7378
|
+
}
|
|
7217
7379
|
}
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7380
|
+
if (terminalStatus !== "succeeded") {
|
|
7381
|
+
for (const step of ordered) {
|
|
7382
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7383
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7384
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7385
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7390
|
+
const finishedAt = nowIso();
|
|
7391
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7392
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7393
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7394
|
+
}
|
|
7395
|
+
opts.beforePersist?.();
|
|
7396
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7397
|
+
finishedAt,
|
|
7398
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7399
|
+
error: blockingError
|
|
7226
7400
|
}, {
|
|
7227
7401
|
daemonLeaseId: opts.daemonLeaseId
|
|
7228
7402
|
});
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
if (
|
|
7239
|
-
store.
|
|
7403
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7404
|
+
} catch (error) {
|
|
7405
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7406
|
+
throw error;
|
|
7407
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7408
|
+
throw error;
|
|
7409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7410
|
+
const finishedAt = nowIso();
|
|
7411
|
+
try {
|
|
7412
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7413
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7414
|
+
finishedAt,
|
|
7415
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7416
|
+
error: message
|
|
7417
|
+
}, {
|
|
7240
7418
|
daemonLeaseId: opts.daemonLeaseId
|
|
7241
7419
|
});
|
|
7242
7420
|
}
|
|
7243
|
-
}
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7248
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7421
|
+
} catch {}
|
|
7422
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7423
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7424
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
7249
7425
|
}
|
|
7250
|
-
opts.beforePersist?.();
|
|
7251
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7252
|
-
finishedAt,
|
|
7253
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7254
|
-
error: blockingError
|
|
7255
|
-
}, {
|
|
7256
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
7257
|
-
});
|
|
7258
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7259
7426
|
}
|
|
7260
7427
|
function preflightWorkflow(workflow, opts = {}) {
|
|
7261
7428
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7626,6 +7793,25 @@ function advanceOptions(deps) {
|
|
|
7626
7793
|
onRun: deps.onRun
|
|
7627
7794
|
};
|
|
7628
7795
|
}
|
|
7796
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
7797
|
+
"succeeded",
|
|
7798
|
+
"failed",
|
|
7799
|
+
"timed_out",
|
|
7800
|
+
"abandoned",
|
|
7801
|
+
"skipped"
|
|
7802
|
+
]);
|
|
7803
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
7804
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
7805
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
7806
|
+
return;
|
|
7807
|
+
try {
|
|
7808
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
7809
|
+
} catch (error) {
|
|
7810
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
7811
|
+
return;
|
|
7812
|
+
throw error;
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7629
7815
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7630
7816
|
const now = deps.now?.() ?? new Date;
|
|
7631
7817
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7652,8 +7838,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7652
7838
|
return;
|
|
7653
7839
|
throw error;
|
|
7654
7840
|
}
|
|
7655
|
-
if (!claim)
|
|
7841
|
+
if (!claim) {
|
|
7842
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7656
7843
|
return;
|
|
7844
|
+
}
|
|
7657
7845
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7658
7846
|
deps.onRun?.(claim.run);
|
|
7659
7847
|
const finalRun = await executeClaimedRun({
|
|
@@ -7699,8 +7887,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7699
7887
|
return;
|
|
7700
7888
|
throw error;
|
|
7701
7889
|
}
|
|
7702
|
-
if (!claim)
|
|
7890
|
+
if (!claim) {
|
|
7891
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7703
7892
|
return;
|
|
7893
|
+
}
|
|
7704
7894
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7705
7895
|
deps.onRun?.(claim.run);
|
|
7706
7896
|
return claim;
|
|
@@ -8211,7 +8401,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8211
8401
|
guarded: true,
|
|
8212
8402
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8213
8403
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8214
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.
|
|
8404
|
+
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "paused" })) }))
|
|
8215
8405
|
},
|
|
8216
8406
|
{
|
|
8217
8407
|
name: "loops_resume",
|
|
@@ -8221,7 +8411,15 @@ var TOOL_REGISTRATIONS = [
|
|
|
8221
8411
|
guarded: true,
|
|
8222
8412
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8223
8413
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8224
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
8414
|
+
handler: ({ idOrName }) => withStore((store) => {
|
|
8415
|
+
const loop = store.requireUniqueLoop(idOrName);
|
|
8416
|
+
let nextRunAt = loop.nextRunAt;
|
|
8417
|
+
if (!nextRunAt) {
|
|
8418
|
+
const now = new Date;
|
|
8419
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8420
|
+
}
|
|
8421
|
+
return { loop: publicLoop(store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
8422
|
+
})
|
|
8225
8423
|
},
|
|
8226
8424
|
{
|
|
8227
8425
|
name: "loops_stop",
|
|
@@ -8232,7 +8430,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8232
8430
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8233
8431
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8234
8432
|
handler: ({ idOrName }) => withStore((store) => ({
|
|
8235
|
-
loop: publicLoop(store.updateLoop(store.
|
|
8433
|
+
loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
|
|
8236
8434
|
}))
|
|
8237
8435
|
},
|
|
8238
8436
|
{
|
|
@@ -8245,7 +8443,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8245
8443
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8246
8444
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
8247
8445
|
const daemon = daemonStatus(store);
|
|
8248
|
-
const result = await runLoopNow({ store, idOrName, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8446
|
+
const result = await runLoopNow({ store, idOrName: store.requireUniqueLoop(idOrName).id, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8249
8447
|
return {
|
|
8250
8448
|
scheduledFor: result.scheduledFor,
|
|
8251
8449
|
loop: publicLoop(result.loop),
|
|
@@ -8948,13 +9146,19 @@ class LoopsClient {
|
|
|
8948
9146
|
return this.store.requireLoop(idOrName);
|
|
8949
9147
|
}
|
|
8950
9148
|
pause(idOrName) {
|
|
8951
|
-
return this.store.updateLoop(this.
|
|
9149
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
|
|
8952
9150
|
}
|
|
8953
9151
|
resume(idOrName) {
|
|
8954
|
-
|
|
9152
|
+
const loop = this.store.requireUniqueLoop(idOrName);
|
|
9153
|
+
let nextRunAt = loop.nextRunAt;
|
|
9154
|
+
if (!nextRunAt) {
|
|
9155
|
+
const now = new Date;
|
|
9156
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
9157
|
+
}
|
|
9158
|
+
return this.store.updateLoop(loop.id, { status: "active", nextRunAt });
|
|
8955
9159
|
}
|
|
8956
9160
|
stop(idOrName) {
|
|
8957
|
-
return this.store.updateLoop(this.
|
|
9161
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined });
|
|
8958
9162
|
}
|
|
8959
9163
|
archive(idOrName) {
|
|
8960
9164
|
return this.store.archiveLoop(idOrName);
|
|
@@ -8963,7 +9167,7 @@ class LoopsClient {
|
|
|
8963
9167
|
return this.store.unarchiveLoop(idOrName);
|
|
8964
9168
|
}
|
|
8965
9169
|
delete(idOrName) {
|
|
8966
|
-
return this.store.deleteLoop(idOrName);
|
|
9170
|
+
return this.store.deleteLoop(this.store.requireUniqueLoop(idOrName).id);
|
|
8967
9171
|
}
|
|
8968
9172
|
runs(idOrName, filters = {}) {
|
|
8969
9173
|
let loopId;
|
|
@@ -8995,7 +9199,7 @@ class LoopsClient {
|
|
|
8995
9199
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
8996
9200
|
}
|
|
8997
9201
|
async runNow(idOrName) {
|
|
8998
|
-
const result = await runLoopNow({ store: this.store, idOrName, runnerId: this.runnerId });
|
|
9202
|
+
const result = await runLoopNow({ store: this.store, idOrName: this.store.requireUniqueLoop(idOrName).id, runnerId: this.runnerId });
|
|
8999
9203
|
return result.run;
|
|
9000
9204
|
}
|
|
9001
9205
|
exportBundle(opts = {}) {
|
|
@@ -9913,6 +10117,9 @@ function roleFragment(role, flow) {
|
|
|
9913
10117
|
function goalHeaderFragment(goal, role, flow) {
|
|
9914
10118
|
return [`/goal ${goal}`, "", roleFragment(role, flow)];
|
|
9915
10119
|
}
|
|
10120
|
+
function boundedStepHeaderFragment(objective, role, flow) {
|
|
10121
|
+
return [`Objective: ${objective}`, "", roleFragment(role, flow)];
|
|
10122
|
+
}
|
|
9916
10123
|
function adversarialReviewFragment(inspect, focus) {
|
|
9917
10124
|
return `Use fresh context. Inspect ${inspect}. Act as an adversarial reviewer focused on ${focus}.`;
|
|
9918
10125
|
}
|
|
@@ -10564,6 +10771,28 @@ function importCustomLoopTemplate(file, reservedKeys, opts = {}) {
|
|
|
10564
10771
|
function compactJson(value) {
|
|
10565
10772
|
return JSON.stringify(value);
|
|
10566
10773
|
}
|
|
10774
|
+
function prReviewRoutingContext(input) {
|
|
10775
|
+
return input.prReviewRouting?.required ? input.prReviewRouting : undefined;
|
|
10776
|
+
}
|
|
10777
|
+
function prReviewFollowUpFragment(input) {
|
|
10778
|
+
const routing = prReviewRoutingContext(input);
|
|
10779
|
+
const author = routing?.author?.trim();
|
|
10780
|
+
const reviewers = routing?.reviewers?.map((reviewer) => reviewer.trim()).filter(Boolean) ?? [];
|
|
10781
|
+
const evidenceLines = [
|
|
10782
|
+
author ? `- Source PR author evidence: GitHub author is ${author}` : undefined,
|
|
10783
|
+
reviewers.length ? `- Source PR reviewer evidence: GitHub reviewer pool: ${reviewers.join(", ")}` : undefined,
|
|
10784
|
+
routing?.selectedReviewer ? `- Selected non-author reviewer: ${routing.selectedReviewer}` : undefined
|
|
10785
|
+
].filter(Boolean);
|
|
10786
|
+
return [
|
|
10787
|
+
"PR-derived follow-up todos: If any lifecycle step creates a follow-up todo that references a GitHub PR, PR approval, PR review, or PR merge work, the todo description must include parser-compatible routing evidence so downstream drains can select a non-author reviewer.",
|
|
10788
|
+
...evidenceLines,
|
|
10789
|
+
"Copy these exact evidence lines from the source task when present, or derive them from the referenced PR before creating the follow-up todo:",
|
|
10790
|
+
"GitHub author is <login>",
|
|
10791
|
+
"GitHub reviewer pool: <login>, <login>",
|
|
10792
|
+
"When the source PR author or reviewer pool cannot be determined, do not create an auto-routable PR-derived follow-up todo; comment the source task with the blocker instead."
|
|
10793
|
+
].join(`
|
|
10794
|
+
`);
|
|
10795
|
+
}
|
|
10567
10796
|
function taskLabel(input) {
|
|
10568
10797
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
10569
10798
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
@@ -11019,6 +11248,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11019
11248
|
routeProjectPath: input.routeProjectPath,
|
|
11020
11249
|
projectGroup: input.projectGroup,
|
|
11021
11250
|
todosProjectPath,
|
|
11251
|
+
prReviewRouting: prReviewRoutingContext(input),
|
|
11022
11252
|
worktree: worktreeContextFragment(plan)
|
|
11023
11253
|
};
|
|
11024
11254
|
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
@@ -11036,6 +11266,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11036
11266
|
]),
|
|
11037
11267
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
11038
11268
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
11269
|
+
prReviewFollowUpFragment(input),
|
|
11039
11270
|
"",
|
|
11040
11271
|
`Task context JSON: ${compactJson(taskContext)}`,
|
|
11041
11272
|
prHandoffGuidance
|
|
@@ -11045,7 +11276,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11045
11276
|
const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
11046
11277
|
const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
|
|
11047
11278
|
const triagePrompt = [
|
|
11048
|
-
...
|
|
11279
|
+
...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
11049
11280
|
shared,
|
|
11050
11281
|
"Decide whether the task is eligible for loop execution. Check status, dependencies, duplicate tasks, no-auto/manual/approval metadata, project path, acceptance criteria, and whether the requested work should be split before implementation.",
|
|
11051
11282
|
"Do not implement repo changes in this step.",
|
|
@@ -11057,7 +11288,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11057
11288
|
].join(`
|
|
11058
11289
|
`);
|
|
11059
11290
|
const plannerPrompt = [
|
|
11060
|
-
...
|
|
11291
|
+
...boundedStepHeaderFragment(`Plan todos task ${input.taskId} before implementation.`, "planner", "lifecycle"),
|
|
11061
11292
|
shared,
|
|
11062
11293
|
"Read the triage comment and current task details.",
|
|
11063
11294
|
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
@@ -11068,7 +11299,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11068
11299
|
].join(`
|
|
11069
11300
|
`);
|
|
11070
11301
|
const workerPrompt = [
|
|
11071
|
-
...
|
|
11302
|
+
...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
11072
11303
|
shared,
|
|
11073
11304
|
todosStartLine(todosProjectPath, input.taskId),
|
|
11074
11305
|
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits, evidence, blockers, and residual risks.",
|
|
@@ -11077,7 +11308,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11077
11308
|
].filter(Boolean).join(`
|
|
11078
11309
|
`);
|
|
11079
11310
|
const verifierPrompt = [
|
|
11080
|
-
...
|
|
11311
|
+
...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
11081
11312
|
shared,
|
|
11082
11313
|
todosVerificationLine(todosProjectPath, input.taskId),
|
|
11083
11314
|
todosDoneLine(todosProjectPath, input.taskId),
|