@hasna/loops 0.4.4 → 0.4.6
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 +39 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +368 -193
- package/dist/daemon/index.js +317 -163
- package/dist/index.js +339 -175
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/storage/index.js +56 -5
- package/dist/lib/storage/sqlite.js +56 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +56 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +321 -166
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +130 -11
- package/dist/sdk/index.js +320 -167
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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;");
|
|
@@ -1640,7 +1643,6 @@ class Store {
|
|
|
1640
1643
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1641
1644
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1642
1645
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1643
|
-
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1644
1646
|
|
|
1645
1647
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1646
1648
|
id TEXT PRIMARY KEY,
|
|
@@ -1949,9 +1951,12 @@ class Store {
|
|
|
1949
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1950
1952
|
if (rows.length === 0)
|
|
1951
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1952
|
-
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)
|
|
1953
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1954
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1955
1960
|
}
|
|
1956
1961
|
requireLoop(idOrName) {
|
|
1957
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2292,6 +2297,7 @@ class Store {
|
|
|
2292
2297
|
return this.transact(() => {
|
|
2293
2298
|
const loop = this.requireLoop(idOrName);
|
|
2294
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2295
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2296
2302
|
return res.changes > 0;
|
|
2297
2303
|
});
|
|
@@ -2776,7 +2782,10 @@ class Store {
|
|
|
2776
2782
|
return rowToGoal(row);
|
|
2777
2783
|
}
|
|
2778
2784
|
if (context.sourceType && context.sourceId) {
|
|
2779
|
-
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);
|
|
2780
2789
|
if (row)
|
|
2781
2790
|
return rowToGoal(row);
|
|
2782
2791
|
}
|
|
@@ -3196,6 +3205,41 @@ class Store {
|
|
|
3196
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3197
3206
|
return run;
|
|
3198
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
|
+
}
|
|
3199
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3200
3244
|
return this.transact(() => {
|
|
3201
3245
|
const now = nowIso();
|
|
@@ -3633,7 +3677,8 @@ class Store {
|
|
|
3633
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3634
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3635
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,
|
|
3636
|
-
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);
|
|
3637
3682
|
const run = this.getRun(id);
|
|
3638
3683
|
if (!run)
|
|
3639
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4161,12 +4206,18 @@ class Store {
|
|
|
4161
4206
|
}
|
|
4162
4207
|
close() {
|
|
4163
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
|
+
}
|
|
4164
4215
|
}
|
|
4165
4216
|
}
|
|
4166
4217
|
// package.json
|
|
4167
4218
|
var package_default = {
|
|
4168
4219
|
name: "@hasna/loops",
|
|
4169
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.6",
|
|
4170
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4171
4222
|
type: "module",
|
|
4172
4223
|
main: "dist/index.js",
|
|
@@ -4625,10 +4676,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4625
4676
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4626
4677
|
try {
|
|
4627
4678
|
const lease = store.getDaemonLease();
|
|
4628
|
-
|
|
4629
|
-
removePid(path);
|
|
4630
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4631
|
-
}
|
|
4679
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4632
4680
|
try {
|
|
4633
4681
|
process.kill(state.pid, "SIGTERM");
|
|
4634
4682
|
} catch {
|
|
@@ -4648,11 +4696,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4648
4696
|
} catch {}
|
|
4649
4697
|
await sleep(150);
|
|
4650
4698
|
removePid(path);
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
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
|
+
}
|
|
4656
4707
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4657
4708
|
} finally {
|
|
4658
4709
|
store.close();
|
|
@@ -4990,7 +5041,12 @@ function notifySpawn(pid, opts) {
|
|
|
4990
5041
|
if (!pid)
|
|
4991
5042
|
return;
|
|
4992
5043
|
opts.onSpawn?.(pid);
|
|
4993
|
-
|
|
5044
|
+
const startedMs = processStartTimeMs(pid);
|
|
5045
|
+
opts.onSpawnProcess?.({
|
|
5046
|
+
pid,
|
|
5047
|
+
pgid: pid,
|
|
5048
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
5049
|
+
});
|
|
4994
5050
|
}
|
|
4995
5051
|
function shellQuote(value) {
|
|
4996
5052
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5345,6 +5401,13 @@ function numberField(value, key) {
|
|
|
5345
5401
|
function codewithAgentStatus(readJson) {
|
|
5346
5402
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5347
5403
|
}
|
|
5404
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5405
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5406
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5407
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5408
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5409
|
+
return agentSeq ?? snapshotSeq;
|
|
5410
|
+
}
|
|
5348
5411
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5349
5412
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5350
5413
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5375,6 +5438,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5375
5438
|
events
|
|
5376
5439
|
}, null, 2);
|
|
5377
5440
|
}
|
|
5441
|
+
function codewithAgentProgress(readJson) {
|
|
5442
|
+
const agent = recordField(readJson, "agent");
|
|
5443
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5444
|
+
return {
|
|
5445
|
+
provider: "codewith",
|
|
5446
|
+
agentId: stringField(agent, "agentId"),
|
|
5447
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5448
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5449
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5450
|
+
threadId: stringField(agent, "threadId"),
|
|
5451
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5452
|
+
pid: numberField(agent, "pid"),
|
|
5453
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5378
5456
|
function sleep(ms) {
|
|
5379
5457
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5380
5458
|
}
|
|
@@ -5409,6 +5487,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5409
5487
|
if (!agentId) {
|
|
5410
5488
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5411
5489
|
}
|
|
5490
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5412
5491
|
const stopAgent = async () => {
|
|
5413
5492
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5414
5493
|
cwd: spec.cwd,
|
|
@@ -5451,10 +5530,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5451
5530
|
});
|
|
5452
5531
|
}
|
|
5453
5532
|
const status = codewithAgentStatus(lastReadJson);
|
|
5454
|
-
const fingerprint = JSON.stringify({
|
|
5533
|
+
const fingerprint = JSON.stringify({
|
|
5534
|
+
status,
|
|
5535
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5536
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5537
|
+
});
|
|
5455
5538
|
if (fingerprint !== lastFingerprint) {
|
|
5456
5539
|
lastFingerprint = fingerprint;
|
|
5457
5540
|
lastProgressAt = Date.now();
|
|
5541
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5458
5542
|
}
|
|
5459
5543
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5460
5544
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6380,7 +6464,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6380
6464
|
|
|
6381
6465
|
`);
|
|
6382
6466
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6383
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6467
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6384
6468
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6385
6469
|
return {
|
|
6386
6470
|
title,
|
|
@@ -6395,7 +6479,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6395
6479
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6396
6480
|
},
|
|
6397
6481
|
futureNativeUpsert: {
|
|
6398
|
-
command: "todos upsert",
|
|
6482
|
+
command: "todos task upsert",
|
|
6399
6483
|
fields: {
|
|
6400
6484
|
title,
|
|
6401
6485
|
description,
|
|
@@ -7090,173 +7174,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
7090
7174
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
7091
7175
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
7092
7176
|
}
|
|
7177
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
7178
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
7179
|
+
try {
|
|
7180
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
7181
|
+
} catch {}
|
|
7182
|
+
}
|
|
7093
7183
|
const ordered = workflowExecutionOrder(workflow);
|
|
7094
7184
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
7095
7185
|
let blockingError;
|
|
7096
7186
|
let terminalStatus = "succeeded";
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
const
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
store.
|
|
7123
|
-
|
|
7124
|
-
|
|
7187
|
+
try {
|
|
7188
|
+
for (const step of ordered) {
|
|
7189
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7190
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7191
|
+
blockingError = "workflow run was cancelled";
|
|
7192
|
+
break;
|
|
7193
|
+
}
|
|
7194
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7195
|
+
if (pendingTimeout) {
|
|
7196
|
+
terminalStatus = "timed_out";
|
|
7197
|
+
blockingError = pendingTimeout;
|
|
7198
|
+
break;
|
|
7199
|
+
}
|
|
7200
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7201
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
7202
|
+
continue;
|
|
7203
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
7204
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
7205
|
+
const dependencyStep = byId.get(dependencyId);
|
|
7206
|
+
if (dependencyRun?.status === "succeeded")
|
|
7207
|
+
return false;
|
|
7208
|
+
return !dependencyStep?.continueOnFailure;
|
|
7209
|
+
});
|
|
7210
|
+
if (blockedBy) {
|
|
7211
|
+
opts.beforePersist?.();
|
|
7212
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
7213
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
7214
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7215
|
+
});
|
|
7216
|
+
continue;
|
|
7217
|
+
}
|
|
7218
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
7219
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
7220
|
+
terminalStatus = "failed";
|
|
7125
7221
|
continue;
|
|
7126
7222
|
}
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
});
|
|
7147
|
-
let result;
|
|
7148
|
-
const controller = new AbortController;
|
|
7149
|
-
const externalAbort = () => controller.abort();
|
|
7150
|
-
if (opts.signal?.aborted)
|
|
7151
|
-
controller.abort();
|
|
7152
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7153
|
-
const cancelTimer = setInterval(() => {
|
|
7154
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7223
|
+
opts.beforePersist?.();
|
|
7224
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
7225
|
+
if (startedStep.status !== "running") {
|
|
7226
|
+
terminalStatus = "failed";
|
|
7227
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
7228
|
+
break;
|
|
7229
|
+
}
|
|
7230
|
+
const stepContext = goalExecutionContext({
|
|
7231
|
+
loop: opts.loop,
|
|
7232
|
+
loopRun: opts.loopRun,
|
|
7233
|
+
scheduledFor: opts.scheduledFor,
|
|
7234
|
+
workflow,
|
|
7235
|
+
workflowRunId: run.id,
|
|
7236
|
+
workflowStepId: step.id
|
|
7237
|
+
});
|
|
7238
|
+
let result;
|
|
7239
|
+
const controller = new AbortController;
|
|
7240
|
+
const externalAbort = () => controller.abort();
|
|
7241
|
+
if (opts.signal?.aborted)
|
|
7155
7242
|
controller.abort();
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
opts
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7243
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7244
|
+
const cancelTimer = setInterval(() => {
|
|
7245
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7246
|
+
controller.abort();
|
|
7247
|
+
}, opts.cancelPollMs ?? 500);
|
|
7248
|
+
cancelTimer.unref();
|
|
7249
|
+
try {
|
|
7250
|
+
if (step.goal) {
|
|
7251
|
+
result = await runGoal(store, step.goal, {
|
|
7252
|
+
...opts,
|
|
7253
|
+
model: opts.goalModel,
|
|
7254
|
+
target: targetWithStepAccount(step),
|
|
7255
|
+
signal: controller.signal,
|
|
7256
|
+
context: stepContext
|
|
7257
|
+
});
|
|
7258
|
+
} else {
|
|
7259
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
7260
|
+
...opts,
|
|
7261
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
7262
|
+
signal: controller.signal,
|
|
7263
|
+
onAgentProgress: (progress) => {
|
|
7264
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7265
|
+
opts.beforePersist?.();
|
|
7266
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7267
|
+
stdout,
|
|
7268
|
+
payload: progress
|
|
7269
|
+
}, {
|
|
7270
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7271
|
+
});
|
|
7272
|
+
opts.onAgentProgress?.(progress);
|
|
7273
|
+
},
|
|
7274
|
+
onSpawn: (pid) => {
|
|
7275
|
+
opts.beforePersist?.();
|
|
7276
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7277
|
+
opts.onSpawn?.(pid);
|
|
7278
|
+
}
|
|
7279
|
+
});
|
|
7280
|
+
}
|
|
7281
|
+
} catch (error) {
|
|
7282
|
+
const finishedAt2 = nowIso();
|
|
7283
|
+
result = {
|
|
7284
|
+
status: "failed",
|
|
7285
|
+
stdout: "",
|
|
7286
|
+
stderr: "",
|
|
7287
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7288
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7289
|
+
finishedAt: finishedAt2,
|
|
7290
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7291
|
+
};
|
|
7292
|
+
} finally {
|
|
7293
|
+
clearInterval(cancelTimer);
|
|
7294
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7295
|
+
}
|
|
7296
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7297
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7298
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7299
|
+
}
|
|
7300
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7301
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7302
|
+
blockingError = "workflow run was cancelled";
|
|
7303
|
+
break;
|
|
7304
|
+
}
|
|
7305
|
+
opts.beforePersist?.();
|
|
7306
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7307
|
+
if (blockedExit) {
|
|
7308
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7309
|
+
status: "skipped",
|
|
7310
|
+
finishedAt: result.finishedAt,
|
|
7311
|
+
durationMs: result.durationMs,
|
|
7312
|
+
stdout: result.stdout,
|
|
7313
|
+
stderr: result.stderr,
|
|
7314
|
+
exitCode: result.exitCode,
|
|
7315
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7316
|
+
}, {
|
|
7317
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7177
7318
|
});
|
|
7319
|
+
continue;
|
|
7178
7320
|
}
|
|
7179
|
-
} catch (error) {
|
|
7180
|
-
const finishedAt2 = nowIso();
|
|
7181
|
-
result = {
|
|
7182
|
-
status: "failed",
|
|
7183
|
-
stdout: "",
|
|
7184
|
-
stderr: "",
|
|
7185
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7186
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7187
|
-
finishedAt: finishedAt2,
|
|
7188
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7189
|
-
};
|
|
7190
|
-
} finally {
|
|
7191
|
-
clearInterval(cancelTimer);
|
|
7192
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7193
|
-
}
|
|
7194
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7195
|
-
if (timeoutMessage && result.status === "failed") {
|
|
7196
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7197
|
-
}
|
|
7198
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7199
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7200
|
-
blockingError = "workflow run was cancelled";
|
|
7201
|
-
break;
|
|
7202
|
-
}
|
|
7203
|
-
opts.beforePersist?.();
|
|
7204
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7205
|
-
if (blockedExit) {
|
|
7206
7321
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7207
|
-
status:
|
|
7322
|
+
status: result.status,
|
|
7208
7323
|
finishedAt: result.finishedAt,
|
|
7209
7324
|
durationMs: result.durationMs,
|
|
7210
7325
|
stdout: result.stdout,
|
|
7211
7326
|
stderr: result.stderr,
|
|
7212
7327
|
exitCode: result.exitCode,
|
|
7213
|
-
error:
|
|
7328
|
+
error: result.error
|
|
7214
7329
|
}, {
|
|
7215
7330
|
daemonLeaseId: opts.daemonLeaseId
|
|
7216
7331
|
});
|
|
7217
|
-
|
|
7332
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7333
|
+
terminalStatus = result.status;
|
|
7334
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7335
|
+
break;
|
|
7336
|
+
}
|
|
7218
7337
|
}
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7338
|
+
if (terminalStatus !== "succeeded") {
|
|
7339
|
+
for (const step of ordered) {
|
|
7340
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7341
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7342
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7343
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7344
|
+
});
|
|
7345
|
+
}
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
const finishedAt = nowIso();
|
|
7349
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7350
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7351
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7352
|
+
}
|
|
7353
|
+
opts.beforePersist?.();
|
|
7354
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7355
|
+
finishedAt,
|
|
7356
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7357
|
+
error: blockingError
|
|
7227
7358
|
}, {
|
|
7228
7359
|
daemonLeaseId: opts.daemonLeaseId
|
|
7229
7360
|
});
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
if (
|
|
7240
|
-
store.
|
|
7361
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7362
|
+
} catch (error) {
|
|
7363
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7364
|
+
throw error;
|
|
7365
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7366
|
+
throw error;
|
|
7367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7368
|
+
const finishedAt = nowIso();
|
|
7369
|
+
try {
|
|
7370
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7371
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7372
|
+
finishedAt,
|
|
7373
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7374
|
+
error: message
|
|
7375
|
+
}, {
|
|
7241
7376
|
daemonLeaseId: opts.daemonLeaseId
|
|
7242
7377
|
});
|
|
7243
7378
|
}
|
|
7244
|
-
}
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7249
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7379
|
+
} catch {}
|
|
7380
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7381
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7382
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
7250
7383
|
}
|
|
7251
|
-
opts.beforePersist?.();
|
|
7252
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7253
|
-
finishedAt,
|
|
7254
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7255
|
-
error: blockingError
|
|
7256
|
-
}, {
|
|
7257
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
7258
|
-
});
|
|
7259
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7260
7384
|
}
|
|
7261
7385
|
function preflightWorkflow(workflow, opts = {}) {
|
|
7262
7386
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7627,6 +7751,25 @@ function advanceOptions(deps) {
|
|
|
7627
7751
|
onRun: deps.onRun
|
|
7628
7752
|
};
|
|
7629
7753
|
}
|
|
7754
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
7755
|
+
"succeeded",
|
|
7756
|
+
"failed",
|
|
7757
|
+
"timed_out",
|
|
7758
|
+
"abandoned",
|
|
7759
|
+
"skipped"
|
|
7760
|
+
]);
|
|
7761
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
7762
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
7763
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
7764
|
+
return;
|
|
7765
|
+
try {
|
|
7766
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
7767
|
+
} catch (error) {
|
|
7768
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
7769
|
+
return;
|
|
7770
|
+
throw error;
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7630
7773
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7631
7774
|
const now = deps.now?.() ?? new Date;
|
|
7632
7775
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7653,8 +7796,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7653
7796
|
return;
|
|
7654
7797
|
throw error;
|
|
7655
7798
|
}
|
|
7656
|
-
if (!claim)
|
|
7799
|
+
if (!claim) {
|
|
7800
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7657
7801
|
return;
|
|
7802
|
+
}
|
|
7658
7803
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7659
7804
|
deps.onRun?.(claim.run);
|
|
7660
7805
|
const finalRun = await executeClaimedRun({
|
|
@@ -7700,8 +7845,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7700
7845
|
return;
|
|
7701
7846
|
throw error;
|
|
7702
7847
|
}
|
|
7703
|
-
if (!claim)
|
|
7848
|
+
if (!claim) {
|
|
7849
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7704
7850
|
return;
|
|
7851
|
+
}
|
|
7705
7852
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7706
7853
|
deps.onRun?.(claim.run);
|
|
7707
7854
|
return claim;
|
|
@@ -8212,7 +8359,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8212
8359
|
guarded: true,
|
|
8213
8360
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8214
8361
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8215
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.
|
|
8362
|
+
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "paused" })) }))
|
|
8216
8363
|
},
|
|
8217
8364
|
{
|
|
8218
8365
|
name: "loops_resume",
|
|
@@ -8222,7 +8369,15 @@ var TOOL_REGISTRATIONS = [
|
|
|
8222
8369
|
guarded: true,
|
|
8223
8370
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8224
8371
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8225
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
8372
|
+
handler: ({ idOrName }) => withStore((store) => {
|
|
8373
|
+
const loop = store.requireUniqueLoop(idOrName);
|
|
8374
|
+
let nextRunAt = loop.nextRunAt;
|
|
8375
|
+
if (!nextRunAt) {
|
|
8376
|
+
const now = new Date;
|
|
8377
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8378
|
+
}
|
|
8379
|
+
return { loop: publicLoop(store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
8380
|
+
})
|
|
8226
8381
|
},
|
|
8227
8382
|
{
|
|
8228
8383
|
name: "loops_stop",
|
|
@@ -8233,7 +8388,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8233
8388
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8234
8389
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8235
8390
|
handler: ({ idOrName }) => withStore((store) => ({
|
|
8236
|
-
loop: publicLoop(store.updateLoop(store.
|
|
8391
|
+
loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
|
|
8237
8392
|
}))
|
|
8238
8393
|
},
|
|
8239
8394
|
{
|
|
@@ -8246,7 +8401,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8246
8401
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8247
8402
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
8248
8403
|
const daemon = daemonStatus(store);
|
|
8249
|
-
const result = await runLoopNow({ store, idOrName, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8404
|
+
const result = await runLoopNow({ store, idOrName: store.requireUniqueLoop(idOrName).id, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8250
8405
|
return {
|
|
8251
8406
|
scheduledFor: result.scheduledFor,
|
|
8252
8407
|
loop: publicLoop(result.loop),
|
|
@@ -8949,13 +9104,19 @@ class LoopsClient {
|
|
|
8949
9104
|
return this.store.requireLoop(idOrName);
|
|
8950
9105
|
}
|
|
8951
9106
|
pause(idOrName) {
|
|
8952
|
-
return this.store.updateLoop(this.
|
|
9107
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
|
|
8953
9108
|
}
|
|
8954
9109
|
resume(idOrName) {
|
|
8955
|
-
|
|
9110
|
+
const loop = this.store.requireUniqueLoop(idOrName);
|
|
9111
|
+
let nextRunAt = loop.nextRunAt;
|
|
9112
|
+
if (!nextRunAt) {
|
|
9113
|
+
const now = new Date;
|
|
9114
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
9115
|
+
}
|
|
9116
|
+
return this.store.updateLoop(loop.id, { status: "active", nextRunAt });
|
|
8956
9117
|
}
|
|
8957
9118
|
stop(idOrName) {
|
|
8958
|
-
return this.store.updateLoop(this.
|
|
9119
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined });
|
|
8959
9120
|
}
|
|
8960
9121
|
archive(idOrName) {
|
|
8961
9122
|
return this.store.archiveLoop(idOrName);
|
|
@@ -8964,7 +9125,7 @@ class LoopsClient {
|
|
|
8964
9125
|
return this.store.unarchiveLoop(idOrName);
|
|
8965
9126
|
}
|
|
8966
9127
|
delete(idOrName) {
|
|
8967
|
-
return this.store.deleteLoop(idOrName);
|
|
9128
|
+
return this.store.deleteLoop(this.store.requireUniqueLoop(idOrName).id);
|
|
8968
9129
|
}
|
|
8969
9130
|
runs(idOrName, filters = {}) {
|
|
8970
9131
|
let loopId;
|
|
@@ -8996,7 +9157,7 @@ class LoopsClient {
|
|
|
8996
9157
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
8997
9158
|
}
|
|
8998
9159
|
async runNow(idOrName) {
|
|
8999
|
-
const result = await runLoopNow({ store: this.store, idOrName, runnerId: this.runnerId });
|
|
9160
|
+
const result = await runLoopNow({ store: this.store, idOrName: this.store.requireUniqueLoop(idOrName).id, runnerId: this.runnerId });
|
|
9000
9161
|
return result.run;
|
|
9001
9162
|
}
|
|
9002
9163
|
exportBundle(opts = {}) {
|
|
@@ -9914,6 +10075,9 @@ function roleFragment(role, flow) {
|
|
|
9914
10075
|
function goalHeaderFragment(goal, role, flow) {
|
|
9915
10076
|
return [`/goal ${goal}`, "", roleFragment(role, flow)];
|
|
9916
10077
|
}
|
|
10078
|
+
function boundedStepHeaderFragment(objective, role, flow) {
|
|
10079
|
+
return [`Objective: ${objective}`, "", roleFragment(role, flow)];
|
|
10080
|
+
}
|
|
9917
10081
|
function adversarialReviewFragment(inspect, focus) {
|
|
9918
10082
|
return `Use fresh context. Inspect ${inspect}. Act as an adversarial reviewer focused on ${focus}.`;
|
|
9919
10083
|
}
|
|
@@ -11046,7 +11210,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11046
11210
|
const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
11047
11211
|
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.`;
|
|
11048
11212
|
const triagePrompt = [
|
|
11049
|
-
...
|
|
11213
|
+
...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
11050
11214
|
shared,
|
|
11051
11215
|
"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.",
|
|
11052
11216
|
"Do not implement repo changes in this step.",
|
|
@@ -11058,7 +11222,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11058
11222
|
].join(`
|
|
11059
11223
|
`);
|
|
11060
11224
|
const plannerPrompt = [
|
|
11061
|
-
...
|
|
11225
|
+
...boundedStepHeaderFragment(`Plan todos task ${input.taskId} before implementation.`, "planner", "lifecycle"),
|
|
11062
11226
|
shared,
|
|
11063
11227
|
"Read the triage comment and current task details.",
|
|
11064
11228
|
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
@@ -11069,7 +11233,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11069
11233
|
].join(`
|
|
11070
11234
|
`);
|
|
11071
11235
|
const workerPrompt = [
|
|
11072
|
-
...
|
|
11236
|
+
...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
11073
11237
|
shared,
|
|
11074
11238
|
todosStartLine(todosProjectPath, input.taskId),
|
|
11075
11239
|
"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.",
|
|
@@ -11078,7 +11242,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11078
11242
|
].filter(Boolean).join(`
|
|
11079
11243
|
`);
|
|
11080
11244
|
const verifierPrompt = [
|
|
11081
|
-
...
|
|
11245
|
+
...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
11082
11246
|
shared,
|
|
11083
11247
|
todosVerificationLine(todosProjectPath, input.taskId),
|
|
11084
11248
|
todosDoneLine(todosProjectPath, input.taskId),
|