@hasna/loops 0.4.5 → 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 +27 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +368 -192
- package/dist/daemon/index.js +317 -162
- package/dist/index.js +339 -174
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/storage/index.js +56 -4
- package/dist/lib/storage/sqlite.js +56 -4
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +56 -4
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +321 -165
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +130 -11
- package/dist/sdk/index.js +320 -166
- 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;");
|
|
@@ -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.6",
|
|
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, `'\\''`)}'`;
|
|
@@ -5344,6 +5401,13 @@ function numberField(value, key) {
|
|
|
5344
5401
|
function codewithAgentStatus(readJson) {
|
|
5345
5402
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5346
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
|
+
}
|
|
5347
5411
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5348
5412
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5349
5413
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5374,6 +5438,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5374
5438
|
events
|
|
5375
5439
|
}, null, 2);
|
|
5376
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
|
+
}
|
|
5377
5456
|
function sleep(ms) {
|
|
5378
5457
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5379
5458
|
}
|
|
@@ -5408,6 +5487,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5408
5487
|
if (!agentId) {
|
|
5409
5488
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5410
5489
|
}
|
|
5490
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5411
5491
|
const stopAgent = async () => {
|
|
5412
5492
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5413
5493
|
cwd: spec.cwd,
|
|
@@ -5450,10 +5530,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5450
5530
|
});
|
|
5451
5531
|
}
|
|
5452
5532
|
const status = codewithAgentStatus(lastReadJson);
|
|
5453
|
-
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
|
+
});
|
|
5454
5538
|
if (fingerprint !== lastFingerprint) {
|
|
5455
5539
|
lastFingerprint = fingerprint;
|
|
5456
5540
|
lastProgressAt = Date.now();
|
|
5541
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5457
5542
|
}
|
|
5458
5543
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5459
5544
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6379,7 +6464,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6379
6464
|
|
|
6380
6465
|
`);
|
|
6381
6466
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6382
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6467
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6383
6468
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6384
6469
|
return {
|
|
6385
6470
|
title,
|
|
@@ -6394,7 +6479,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6394
6479
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6395
6480
|
},
|
|
6396
6481
|
futureNativeUpsert: {
|
|
6397
|
-
command: "todos upsert",
|
|
6482
|
+
command: "todos task upsert",
|
|
6398
6483
|
fields: {
|
|
6399
6484
|
title,
|
|
6400
6485
|
description,
|
|
@@ -7089,173 +7174,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
7089
7174
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
7090
7175
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
7091
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
|
+
}
|
|
7092
7183
|
const ordered = workflowExecutionOrder(workflow);
|
|
7093
7184
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
7094
7185
|
let blockingError;
|
|
7095
7186
|
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
|
-
|
|
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";
|
|
7124
7221
|
continue;
|
|
7125
7222
|
}
|
|
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")
|
|
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)
|
|
7154
7242
|
controller.abort();
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
opts
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
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
|
|
7176
7318
|
});
|
|
7319
|
+
continue;
|
|
7177
7320
|
}
|
|
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
7321
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7206
|
-
status:
|
|
7322
|
+
status: result.status,
|
|
7207
7323
|
finishedAt: result.finishedAt,
|
|
7208
7324
|
durationMs: result.durationMs,
|
|
7209
7325
|
stdout: result.stdout,
|
|
7210
7326
|
stderr: result.stderr,
|
|
7211
7327
|
exitCode: result.exitCode,
|
|
7212
|
-
error:
|
|
7328
|
+
error: result.error
|
|
7213
7329
|
}, {
|
|
7214
7330
|
daemonLeaseId: opts.daemonLeaseId
|
|
7215
7331
|
});
|
|
7216
|
-
|
|
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
|
+
}
|
|
7217
7337
|
}
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
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
|
|
7226
7358
|
}, {
|
|
7227
7359
|
daemonLeaseId: opts.daemonLeaseId
|
|
7228
7360
|
});
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
if (
|
|
7239
|
-
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
|
+
}, {
|
|
7240
7376
|
daemonLeaseId: opts.daemonLeaseId
|
|
7241
7377
|
});
|
|
7242
7378
|
}
|
|
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);
|
|
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);
|
|
7249
7383
|
}
|
|
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
7384
|
}
|
|
7260
7385
|
function preflightWorkflow(workflow, opts = {}) {
|
|
7261
7386
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7626,6 +7751,25 @@ function advanceOptions(deps) {
|
|
|
7626
7751
|
onRun: deps.onRun
|
|
7627
7752
|
};
|
|
7628
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
|
+
}
|
|
7629
7773
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7630
7774
|
const now = deps.now?.() ?? new Date;
|
|
7631
7775
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7652,8 +7796,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7652
7796
|
return;
|
|
7653
7797
|
throw error;
|
|
7654
7798
|
}
|
|
7655
|
-
if (!claim)
|
|
7799
|
+
if (!claim) {
|
|
7800
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7656
7801
|
return;
|
|
7802
|
+
}
|
|
7657
7803
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7658
7804
|
deps.onRun?.(claim.run);
|
|
7659
7805
|
const finalRun = await executeClaimedRun({
|
|
@@ -7699,8 +7845,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7699
7845
|
return;
|
|
7700
7846
|
throw error;
|
|
7701
7847
|
}
|
|
7702
|
-
if (!claim)
|
|
7848
|
+
if (!claim) {
|
|
7849
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7703
7850
|
return;
|
|
7851
|
+
}
|
|
7704
7852
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7705
7853
|
deps.onRun?.(claim.run);
|
|
7706
7854
|
return claim;
|
|
@@ -8211,7 +8359,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8211
8359
|
guarded: true,
|
|
8212
8360
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8213
8361
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8214
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.
|
|
8362
|
+
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "paused" })) }))
|
|
8215
8363
|
},
|
|
8216
8364
|
{
|
|
8217
8365
|
name: "loops_resume",
|
|
@@ -8221,7 +8369,15 @@ var TOOL_REGISTRATIONS = [
|
|
|
8221
8369
|
guarded: true,
|
|
8222
8370
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8223
8371
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8224
|
-
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
|
+
})
|
|
8225
8381
|
},
|
|
8226
8382
|
{
|
|
8227
8383
|
name: "loops_stop",
|
|
@@ -8232,7 +8388,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8232
8388
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8233
8389
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8234
8390
|
handler: ({ idOrName }) => withStore((store) => ({
|
|
8235
|
-
loop: publicLoop(store.updateLoop(store.
|
|
8391
|
+
loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
|
|
8236
8392
|
}))
|
|
8237
8393
|
},
|
|
8238
8394
|
{
|
|
@@ -8245,7 +8401,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8245
8401
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8246
8402
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
8247
8403
|
const daemon = daemonStatus(store);
|
|
8248
|
-
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" });
|
|
8249
8405
|
return {
|
|
8250
8406
|
scheduledFor: result.scheduledFor,
|
|
8251
8407
|
loop: publicLoop(result.loop),
|
|
@@ -8948,13 +9104,19 @@ class LoopsClient {
|
|
|
8948
9104
|
return this.store.requireLoop(idOrName);
|
|
8949
9105
|
}
|
|
8950
9106
|
pause(idOrName) {
|
|
8951
|
-
return this.store.updateLoop(this.
|
|
9107
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
|
|
8952
9108
|
}
|
|
8953
9109
|
resume(idOrName) {
|
|
8954
|
-
|
|
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 });
|
|
8955
9117
|
}
|
|
8956
9118
|
stop(idOrName) {
|
|
8957
|
-
return this.store.updateLoop(this.
|
|
9119
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined });
|
|
8958
9120
|
}
|
|
8959
9121
|
archive(idOrName) {
|
|
8960
9122
|
return this.store.archiveLoop(idOrName);
|
|
@@ -8963,7 +9125,7 @@ class LoopsClient {
|
|
|
8963
9125
|
return this.store.unarchiveLoop(idOrName);
|
|
8964
9126
|
}
|
|
8965
9127
|
delete(idOrName) {
|
|
8966
|
-
return this.store.deleteLoop(idOrName);
|
|
9128
|
+
return this.store.deleteLoop(this.store.requireUniqueLoop(idOrName).id);
|
|
8967
9129
|
}
|
|
8968
9130
|
runs(idOrName, filters = {}) {
|
|
8969
9131
|
let loopId;
|
|
@@ -8995,7 +9157,7 @@ class LoopsClient {
|
|
|
8995
9157
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
8996
9158
|
}
|
|
8997
9159
|
async runNow(idOrName) {
|
|
8998
|
-
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 });
|
|
8999
9161
|
return result.run;
|
|
9000
9162
|
}
|
|
9001
9163
|
exportBundle(opts = {}) {
|
|
@@ -9913,6 +10075,9 @@ function roleFragment(role, flow) {
|
|
|
9913
10075
|
function goalHeaderFragment(goal, role, flow) {
|
|
9914
10076
|
return [`/goal ${goal}`, "", roleFragment(role, flow)];
|
|
9915
10077
|
}
|
|
10078
|
+
function boundedStepHeaderFragment(objective, role, flow) {
|
|
10079
|
+
return [`Objective: ${objective}`, "", roleFragment(role, flow)];
|
|
10080
|
+
}
|
|
9916
10081
|
function adversarialReviewFragment(inspect, focus) {
|
|
9917
10082
|
return `Use fresh context. Inspect ${inspect}. Act as an adversarial reviewer focused on ${focus}.`;
|
|
9918
10083
|
}
|
|
@@ -11045,7 +11210,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11045
11210
|
const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
11046
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.`;
|
|
11047
11212
|
const triagePrompt = [
|
|
11048
|
-
...
|
|
11213
|
+
...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
11049
11214
|
shared,
|
|
11050
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.",
|
|
11051
11216
|
"Do not implement repo changes in this step.",
|
|
@@ -11057,7 +11222,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11057
11222
|
].join(`
|
|
11058
11223
|
`);
|
|
11059
11224
|
const plannerPrompt = [
|
|
11060
|
-
...
|
|
11225
|
+
...boundedStepHeaderFragment(`Plan todos task ${input.taskId} before implementation.`, "planner", "lifecycle"),
|
|
11061
11226
|
shared,
|
|
11062
11227
|
"Read the triage comment and current task details.",
|
|
11063
11228
|
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
@@ -11068,7 +11233,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11068
11233
|
].join(`
|
|
11069
11234
|
`);
|
|
11070
11235
|
const workerPrompt = [
|
|
11071
|
-
...
|
|
11236
|
+
...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
11072
11237
|
shared,
|
|
11073
11238
|
todosStartLine(todosProjectPath, input.taskId),
|
|
11074
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.",
|
|
@@ -11077,7 +11242,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11077
11242
|
].filter(Boolean).join(`
|
|
11078
11243
|
`);
|
|
11079
11244
|
const verifierPrompt = [
|
|
11080
|
-
...
|
|
11245
|
+
...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
11081
11246
|
shared,
|
|
11082
11247
|
todosVerificationLine(todosProjectPath, input.taskId),
|
|
11083
11248
|
todosDoneLine(todosProjectPath, input.taskId),
|