@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/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 > 1)
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(rows[0]);
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 row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
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 WHERE id=$id`).run(params);
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.5",
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
- if (!lease || lease.pid !== state.pid || new Date(lease.expiresAt).getTime() <= Date.now()) {
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
- const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
4651
- const reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
4652
- sleep,
4653
- graceMs: opts.reapGraceMs
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
- opts.onSpawnProcess?.({ pid, pgid: pid, processStartedAt: nowIso() });
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
- try {
5322
- const value = JSON.parse(stdout || "{}");
5323
- if (!value || typeof value !== "object" || Array.isArray(value))
5324
- throw new Error("not an object");
5325
- return value;
5326
- } catch (error) {
5327
- throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
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({ status, snapshot: recordField(lastReadJson, "statusSnapshot") ?? null });
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
- for (const step of ordered) {
7097
- if (store.isWorkflowRunTerminal(run.id)) {
7098
- terminalStatus = store.requireWorkflowRun(run.id).status;
7099
- blockingError = "workflow run was cancelled";
7100
- break;
7101
- }
7102
- const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
7103
- if (pendingTimeout) {
7104
- terminalStatus = "timed_out";
7105
- blockingError = pendingTimeout;
7106
- break;
7107
- }
7108
- const existing = store.getWorkflowStepRun(run.id, step.id);
7109
- if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
7110
- continue;
7111
- const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
7112
- const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
7113
- const dependencyStep = byId.get(dependencyId);
7114
- if (dependencyRun?.status === "succeeded")
7115
- return false;
7116
- return !dependencyStep?.continueOnFailure;
7117
- });
7118
- if (blockedBy) {
7119
- opts.beforePersist?.();
7120
- if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
7121
- store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
7122
- daemonLeaseId: opts.daemonLeaseId
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
- store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
7127
- blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
7128
- terminalStatus = "failed";
7129
- continue;
7130
- }
7131
- opts.beforePersist?.();
7132
- const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
7133
- if (startedStep.status !== "running") {
7134
- terminalStatus = "failed";
7135
- blockingError = `step ${step.id} could not start because workflow is no longer running`;
7136
- break;
7137
- }
7138
- const stepContext = goalExecutionContext({
7139
- loop: opts.loop,
7140
- loopRun: opts.loopRun,
7141
- scheduledFor: opts.scheduledFor,
7142
- workflow,
7143
- workflowRunId: run.id,
7144
- workflowStepId: step.id
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
- }, opts.cancelPollMs ?? 500);
7156
- cancelTimer.unref();
7157
- try {
7158
- if (step.goal) {
7159
- result = await runGoal(store, step.goal, {
7160
- ...opts,
7161
- model: opts.goalModel,
7162
- target: targetWithStepAccount(step),
7163
- signal: controller.signal,
7164
- context: stepContext
7165
- });
7166
- } else {
7167
- result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
7168
- ...opts,
7169
- machine: opts.machine ?? opts.loop?.machine,
7170
- signal: controller.signal,
7171
- onSpawn: (pid) => {
7172
- opts.beforePersist?.();
7173
- store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
7174
- opts.onSpawn?.(pid);
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: "skipped",
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: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
7370
+ error: result.error
7213
7371
  }, {
7214
7372
  daemonLeaseId: opts.daemonLeaseId
7215
7373
  });
7216
- continue;
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
- store.finalizeWorkflowStepRun(run.id, step.id, {
7219
- status: result.status,
7220
- finishedAt: result.finishedAt,
7221
- durationMs: result.durationMs,
7222
- stdout: result.stdout,
7223
- stderr: result.stderr,
7224
- exitCode: result.exitCode,
7225
- error: result.error
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
- if (result.status !== "succeeded" && !step.continueOnFailure) {
7230
- terminalStatus = result.status;
7231
- blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
7232
- break;
7233
- }
7234
- }
7235
- if (terminalStatus !== "succeeded") {
7236
- for (const step of ordered) {
7237
- const existing = store.getWorkflowStepRun(run.id, step.id);
7238
- if (existing?.status === "pending" || existing?.status === "running") {
7239
- store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
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
- const finishedAt = nowIso();
7246
- if (store.isWorkflowRunTerminal(run.id)) {
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.requireLoop(idOrName).id, { status: "paused" })) }))
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) => ({ loop: publicLoop(store.updateLoop(store.requireLoop(idOrName).id, { status: "active" })) }))
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.requireLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
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.get(idOrName).id, { status: "paused" });
9149
+ return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
8952
9150
  }
8953
9151
  resume(idOrName) {
8954
- return this.store.updateLoop(this.get(idOrName).id, { status: "active" });
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.get(idOrName).id, { status: "stopped", nextRunAt: undefined });
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
- ...goalHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
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
- ...goalHeaderFragment(`Plan todos task ${input.taskId} before implementation.`, "planner", "lifecycle"),
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
- ...goalHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
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
- ...goalHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
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),