@hasna/loops 0.3.50 → 0.3.52

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/cli/index.js CHANGED
@@ -623,7 +623,7 @@ function writeWorkflowRunManifest(args) {
623
623
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
624
624
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
625
625
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
626
- var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
626
+ var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
627
627
  var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
628
628
  function rowToLoop(row) {
629
629
  return {
@@ -5908,7 +5908,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5908
5908
  // package.json
5909
5909
  var package_default = {
5910
5910
  name: "@hasna/loops",
5911
- version: "0.3.50",
5911
+ version: "0.3.52",
5912
5912
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5913
5913
  type: "module",
5914
5914
  main: "dist/index.js",
@@ -6108,6 +6108,10 @@ var TEMPLATE_SUMMARIES = [
6108
6108
  { name: "taskId", required: true, description: "Todos task id." },
6109
6109
  { name: "projectPath", required: true, description: "Repository or project working directory." },
6110
6110
  { name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
6111
+ { name: "triageAuthProfile", description: "Provider-native auth profile for the triage step." },
6112
+ { name: "plannerAuthProfile", description: "Provider-native auth profile for the planner step." },
6113
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
6114
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
6111
6115
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
6112
6116
  { name: "provider", default: "codewith", description: "Agent provider." },
6113
6117
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
@@ -6204,6 +6208,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6204
6208
  var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
6205
6209
  var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
6206
6210
  var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
6211
+ var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
6212
+ "danger-full-access",
6213
+ "dangerously-bypass",
6214
+ "dangerously-skip"
6215
+ ];
6207
6216
  function compactJson(value) {
6208
6217
  return JSON.stringify(value);
6209
6218
  }
@@ -6225,9 +6234,17 @@ function rolePoolValue(pool, seed, role) {
6225
6234
  const workerIndex = stableIndex(seed, pool.length);
6226
6235
  if (role === "worker" || pool.length === 1)
6227
6236
  return pool[workerIndex];
6228
- return pool[(workerIndex + 1) % pool.length];
6237
+ if (role === "verifier")
6238
+ return pool[(workerIndex + 1) % pool.length];
6239
+ if (role === "planner")
6240
+ return pool[(workerIndex + 2) % pool.length];
6241
+ return pool[(workerIndex + 3) % pool.length];
6229
6242
  }
6230
6243
  function authProfileForRole(input, role, seed) {
6244
+ if (role === "triage" && input.triageAuthProfile)
6245
+ return input.triageAuthProfile;
6246
+ if (role === "planner" && input.plannerAuthProfile)
6247
+ return input.plannerAuthProfile;
6231
6248
  if (role === "worker" && input.workerAuthProfile)
6232
6249
  return input.workerAuthProfile;
6233
6250
  if (role === "verifier" && input.verifierAuthProfile)
@@ -6235,6 +6252,10 @@ function authProfileForRole(input, role, seed) {
6235
6252
  return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
6236
6253
  }
6237
6254
  function accountForRole(input, role, seed) {
6255
+ if (role === "triage" && input.triageAccount)
6256
+ return input.triageAccount;
6257
+ if (role === "planner" && input.plannerAccount)
6258
+ return input.plannerAccount;
6238
6259
  if (role === "worker" && input.workerAccount)
6239
6260
  return input.workerAccount;
6240
6261
  if (role === "verifier" && input.verifierAccount)
@@ -6421,10 +6442,10 @@ function worktreePrompt(plan) {
6421
6442
  function assertNativeAuthProfileSupport(input, provider) {
6422
6443
  if (provider === "codewith")
6423
6444
  return;
6424
- const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.workerAuthProfile || input.verifierAuthProfile);
6445
+ const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.triageAuthProfile || input.plannerAuthProfile || input.workerAuthProfile || input.verifierAuthProfile);
6425
6446
  if (!hasNativeAuthProfiles)
6426
6447
  return;
6427
- throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
6448
+ throw new Error(`authProfile, authProfilePool, triageAuthProfile, plannerAuthProfile, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
6428
6449
  }
6429
6450
  function failClosedSandbox(input, provider, sandbox) {
6430
6451
  if (!["codewith", "codex"].includes(provider))
@@ -6476,9 +6497,10 @@ function agentTarget(input, prompt, role, seed, plan) {
6476
6497
  function workflowStepsWithWorktree(plan, steps) {
6477
6498
  if (!plan.prepareStep)
6478
6499
  return steps;
6500
+ const firstStepId = steps[0]?.id;
6479
6501
  return [
6480
6502
  plan.prepareStep,
6481
- ...steps.map((step) => step.id === "worker" ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
6503
+ ...steps.map((step) => step.id === firstStepId ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
6482
6504
  ];
6483
6505
  }
6484
6506
  function assertRecord(value, label) {
@@ -6526,6 +6548,13 @@ function validateCustomTemplateId(id, label) {
6526
6548
  throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
6527
6549
  }
6528
6550
  }
6551
+ function optionalTemplateBoolean(value, label) {
6552
+ if (value === undefined)
6553
+ return;
6554
+ if (typeof value !== "boolean")
6555
+ throw new Error(`${label} must be a boolean`);
6556
+ return value;
6557
+ }
6529
6558
  function validateCustomTemplateVariables(value, label) {
6530
6559
  if (value === undefined)
6531
6560
  return [];
@@ -6548,32 +6577,57 @@ function validateCustomTemplateVariables(value, label) {
6548
6577
  if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
6549
6578
  throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
6550
6579
  }
6551
- if (defaultValue === "danger-full-access") {
6552
- throw new Error(`${entryLabel}.default cannot be danger-full-access in a custom template`);
6580
+ if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
6581
+ throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
6553
6582
  }
6554
6583
  return {
6555
6584
  name,
6556
6585
  description,
6557
- required: entry.required === undefined ? undefined : Boolean(entry.required),
6586
+ required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
6558
6587
  default: defaultValue,
6559
6588
  type
6560
6589
  };
6561
6590
  });
6562
6591
  }
6563
- function assertNoDangerFullAccess(value, label) {
6592
+ function hasDangerousArg(value) {
6593
+ return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
6594
+ }
6595
+ function assertNoDangerousCustomTemplateScalars(value, label) {
6596
+ if (typeof value === "string") {
6597
+ if (hasDangerousArg(value)) {
6598
+ throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
6599
+ }
6600
+ return;
6601
+ }
6564
6602
  if (!value || typeof value !== "object")
6565
6603
  return;
6566
6604
  if (Array.isArray(value)) {
6567
- value.forEach((entry, index) => assertNoDangerFullAccess(entry, `${label}[${index}]`));
6605
+ value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
6568
6606
  return;
6569
6607
  }
6570
6608
  for (const [key, entry] of Object.entries(value)) {
6571
- if (key === "sandbox" && entry === "danger-full-access") {
6572
- throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
6573
- }
6574
- assertNoDangerFullAccess(entry, `${label}.${key}`);
6609
+ assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
6575
6610
  }
6576
6611
  }
6612
+ function assertNoImplicitDangerFullAccess(value, label) {
6613
+ if (!value || typeof value !== "object")
6614
+ return;
6615
+ if (Array.isArray(value)) {
6616
+ value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
6617
+ return;
6618
+ }
6619
+ const object = value;
6620
+ if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
6621
+ throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
6622
+ }
6623
+ for (const [key, entry] of Object.entries(object)) {
6624
+ assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
6625
+ }
6626
+ }
6627
+ function assertCustomTemplateSafety(value, label) {
6628
+ assertNoDangerousCustomTemplateScalars(value, label);
6629
+ assertNoImplicitDangerFullAccess(value, label);
6630
+ }
6577
6631
  function customTemplateDefinitionFromJson(value, sourcePath) {
6578
6632
  assertRecord(value, sourcePath);
6579
6633
  const id = assertTemplateString(value.id, `${sourcePath}.id`);
@@ -6585,7 +6639,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
6585
6639
  if (value.workflow === undefined)
6586
6640
  throw new Error(`${sourcePath}.workflow is required`);
6587
6641
  assertRecord(value.workflow, `${sourcePath}.workflow`);
6588
- assertNoDangerFullAccess(value.workflow, `${sourcePath}.workflow`);
6642
+ assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
6589
6643
  return { id, name, description, kind, variables, workflow: value.workflow };
6590
6644
  }
6591
6645
  function customTemplateSummary(definition, sourcePath) {
@@ -6626,11 +6680,11 @@ function assertNoTemplateCollisions(entries) {
6626
6680
  }
6627
6681
  }
6628
6682
  }
6629
- function loadCustomLoopTemplates() {
6683
+ function loadCustomLoopTemplatesRaw() {
6630
6684
  const dir = customLoopTemplatesDir();
6631
6685
  if (!existsSync4(dir))
6632
6686
  return [];
6633
- const entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6687
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6634
6688
  const file = join5(dir, entry.name);
6635
6689
  if (entry.isSymbolicLink())
6636
6690
  throw new Error(`refusing symlinked custom template file: ${file}`);
@@ -6638,6 +6692,9 @@ function loadCustomLoopTemplates() {
6638
6692
  throw new Error(`custom template registry entry is not a regular file: ${file}`);
6639
6693
  return readCustomTemplateFile(file);
6640
6694
  });
6695
+ }
6696
+ function loadCustomLoopTemplates() {
6697
+ const entries = loadCustomLoopTemplatesRaw();
6641
6698
  assertNoTemplateCollisions(entries);
6642
6699
  return entries;
6643
6700
  }
@@ -6732,21 +6789,26 @@ function renderCustomTemplateNode(value, values, templateId) {
6732
6789
  function renderCustomLoopTemplate(entry, values) {
6733
6790
  const renderedValues = customTemplateValues(entry.definition, values);
6734
6791
  const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
6735
- assertNoDangerFullAccess(rendered, `custom template ${entry.definition.id}.workflow`);
6736
- return workflowBodyFromJson(rendered);
6792
+ assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
6793
+ const workflow = workflowBodyFromJson(rendered);
6794
+ assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
6795
+ return workflow;
6737
6796
  }
6738
6797
  function validateCustomLoopTemplateFile(file) {
6739
- const entry = readCustomTemplateFile(resolve(file));
6740
- assertNoTemplateCollisions([entry]);
6798
+ const source = resolve(file);
6799
+ const entry = readCustomTemplateFile(source);
6800
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
6801
+ assertNoTemplateCollisions([...existing, entry]);
6741
6802
  return structuredClone(entry.summary);
6742
6803
  }
6743
6804
  function importCustomLoopTemplate(file, opts = {}) {
6744
6805
  const source = resolve(file);
6745
6806
  const entry = readCustomTemplateFile(source);
6746
- assertNoTemplateCollisions([entry]);
6747
6807
  const dir = ensureCustomLoopTemplatesDir();
6748
6808
  const destination = join5(dir, `${entry.definition.id}.json`);
6749
6809
  const replaced = existsSync4(destination);
6810
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
6811
+ assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
6750
6812
  if (replaced) {
6751
6813
  const stat = lstatSync(destination);
6752
6814
  if (!stat.isFile() || stat.isSymbolicLink())
@@ -6873,6 +6935,222 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6873
6935
  ])
6874
6936
  };
6875
6937
  }
6938
+ function renderTaskLifecycleWorkflow(input) {
6939
+ if (!input.taskId?.trim())
6940
+ throw new Error("taskId is required");
6941
+ if (!input.projectPath?.trim())
6942
+ throw new Error("projectPath is required");
6943
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
6944
+ const plan = worktreePlan(input, input.taskId);
6945
+ const taskContext = {
6946
+ taskId: input.taskId,
6947
+ taskTitle: input.taskTitle,
6948
+ taskDescription: input.taskDescription,
6949
+ eventId: input.eventId,
6950
+ eventType: input.eventType,
6951
+ projectPath: input.projectPath,
6952
+ routeProjectPath: input.routeProjectPath,
6953
+ projectGroup: input.projectGroup,
6954
+ todosProjectPath,
6955
+ worktree: {
6956
+ mode: plan.mode,
6957
+ enabled: plan.enabled,
6958
+ cwd: plan.cwd,
6959
+ path: plan.path,
6960
+ branch: plan.branch,
6961
+ reason: plan.reason
6962
+ }
6963
+ };
6964
+ const shared = [
6965
+ worktreePrompt(plan),
6966
+ `Todos project path: ${todosProjectPath}`,
6967
+ "Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
6968
+ `- Inspect first: todos --project ${todosProjectPath} inspect ${input.taskId}`,
6969
+ `- Record evidence: todos --project ${todosProjectPath} comment ${input.taskId} "<concise evidence, decision, or blocker>"`,
6970
+ "Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
6971
+ "Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
6972
+ "",
6973
+ `Task context JSON: ${compactJson(taskContext)}`
6974
+ ].join(`
6975
+ `);
6976
+ const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
6977
+ const gateCommand = (stage) => [
6978
+ "set -euo pipefail",
6979
+ `task_json="$(todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(input.taskId)})"`,
6980
+ `TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
6981
+ "const raw = process.env.TASK_JSON || '{}';",
6982
+ "const payload = JSON.parse(raw);",
6983
+ "const task = payload.task && typeof payload.task === 'object' ? payload.task : payload;",
6984
+ "const stage = process.env.STAGE || 'lifecycle';",
6985
+ `const goMarker = ${JSON.stringify(gateMarker(stage, "go"))};`,
6986
+ `const blockedMarker = ${JSON.stringify(gateMarker(stage, "blocked"))};`,
6987
+ "const status = String(task.status || '').toLowerCase().replace(/_/g, '-');",
6988
+ "const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};",
6989
+ "const automation = metadata.automation && typeof metadata.automation === 'object' ? metadata.automation : {};",
6990
+ "const comments = Array.isArray(task.comments) ? task.comments : [];",
6991
+ "const blockedStatuses = new Set(['blocked', 'cancelled', 'canceled', 'failed', 'archived', 'deleted', 'done', 'completed']);",
6992
+ "const truthy = (value) => value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes';",
6993
+ "const falsey = (value) => value === false || value === 0 || value === '0' || String(value).toLowerCase() === 'false' || String(value).toLowerCase() === 'no';",
6994
+ "const commentText = (comment) => String(comment?.content ?? comment?.text ?? comment?.body ?? comment?.comment ?? '');",
6995
+ "const tagsFrom = (value) => Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];",
6996
+ "const records = [task, metadata, automation].filter((entry) => entry && typeof entry === 'object');",
6997
+ "const tags = new Set(records.flatMap((entry) => [entry.tags, entry.task_tags, entry.taskTags].flatMap(tagsFrom)).map((tag) => tag.trim().toLowerCase()).filter(Boolean));",
6998
+ "const markerState = (comment) => {",
6999
+ " const line = commentText(comment).trimStart().split(/\\r?\\n/, 1)[0]?.trimEnd() || '';",
7000
+ " if (line === goMarker) return 'go';",
7001
+ " if (line === blockedMarker) return 'blocked';",
7002
+ " if (line.startsWith(`openloops:${stage}=`)) return `invalid marker: ${line}`;",
7003
+ " return undefined;",
7004
+ "};",
7005
+ "const markerTime = (comment, index) => {",
7006
+ " const rawTime = comment?.created_at ?? comment?.createdAt ?? comment?.updated_at ?? comment?.updatedAt;",
7007
+ " const parsed = rawTime ? Date.parse(String(rawTime)) : Number.NaN;",
7008
+ " return Number.isFinite(parsed) ? parsed : index;",
7009
+ "};",
7010
+ "const markers = comments",
7011
+ " .map((comment, index) => ({ state: markerState(comment), order: markerTime(comment, index), index }))",
7012
+ " .filter((entry) => entry.state)",
7013
+ " .sort((a, b) => a.order - b.order || a.index - b.index);",
7014
+ "const latestMarker = markers.at(-1)?.state;",
7015
+ "const blockers = [];",
7016
+ "if (blockedStatuses.has(status)) blockers.push(`task status is ${status}`);",
7017
+ "for (const tag of ['no-auto', 'manual', 'manual-required', 'approval-required']) {",
7018
+ " if (tags.has(tag)) blockers.push(`task has disallowed tag ${tag}`);",
7019
+ "}",
7020
+ "for (const [key, source] of records.entries()) {",
7021
+ " if (truthy(source.no_auto) || truthy(source.noAuto)) blockers.push(`${key}.no_auto is true`);",
7022
+ " if (truthy(source.manual) || truthy(source.manual_required) || truthy(source.manualRequired) || String(source.mode || '').toLowerCase() === 'manual') blockers.push(`${key}.manual/mode requires manual handling`);",
7023
+ " if (truthy(source.requires_approval) || truthy(source.requiresApproval) || truthy(source.approval_required) || truthy(source.approvalRequired)) blockers.push(`${key}.requires_approval is true`);",
7024
+ " if (falsey(source.auto) || falsey(source.enabled) || falsey(source.allowed) || falsey(source.automation_allowed) || falsey(source.automationAllowed) || falsey(source.loop_allowed) || falsey(source.loopAllowed)) blockers.push(`${key} disallows loop automation`);",
7025
+ "}",
7026
+ "if (latestMarker !== 'go') blockers.push(latestMarker ? `latest ${stage} marker is ${latestMarker}` : `missing exact ${goMarker} comment`);",
7027
+ "if (blockers.length) {",
7028
+ " console.error(`task lifecycle ${stage} gate blocked ${task.id || task.taskId || 'task'}: ${blockers.join('; ')}`);",
7029
+ " process.exit(12);",
7030
+ "}",
7031
+ "console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);",
7032
+ "BUN"
7033
+ ].join(`
7034
+ `);
7035
+ const triagePrompt = [
7036
+ `/goal Triage todos task ${input.taskId} for safe automated execution.`,
7037
+ "",
7038
+ "You are the triage step for a full task-triggered OpenLoops lifecycle.",
7039
+ shared,
7040
+ "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.",
7041
+ "Do not implement repo changes in this step.",
7042
+ `If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
7043
+ "Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
7044
+ `If the task should not proceed automatically, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
7045
+ `Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
7046
+ "The deterministic triage gate will stop later steps unless the latest triage marker is the exact go marker and the task has no blocked/no-auto/manual/approval-required state."
7047
+ ].join(`
7048
+ `);
7049
+ const plannerPrompt = [
7050
+ `/goal Plan todos task ${input.taskId} before implementation.`,
7051
+ "",
7052
+ "You are the planner step for a full task-triggered OpenLoops lifecycle.",
7053
+ shared,
7054
+ "Read the triage comment and current task details.",
7055
+ `If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
7056
+ "In that same comment, include a concise implementation plan: files/areas to inspect, validation commands, risk checks, expected commit/PR behavior, and any cross-repo tasks that should be created separately.",
7057
+ `Do not implement repo changes in this step. If the task is too broad or unsafe for automation, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
7058
+ `Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
7059
+ "Create smaller deduped tasks and record evidence. The deterministic planner gate will stop the worker unless the latest planner marker is the exact go marker and the task has no blocked/no-auto/manual/approval-required state."
7060
+ ].join(`
7061
+ `);
7062
+ const workerPrompt = [
7063
+ `/goal Complete todos task ${input.taskId} according to the planner evidence.`,
7064
+ "",
7065
+ "You are the worker step for a full task-triggered OpenLoops lifecycle.",
7066
+ shared,
7067
+ `- Claim/start if appropriate: todos --project ${todosProjectPath} start ${input.taskId}`,
7068
+ "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.",
7069
+ "Do not mark the task complete in the worker step; the verifier step owns completion after independent validation."
7070
+ ].join(`
7071
+ `);
7072
+ const verifierPrompt = [
7073
+ `/goal Verify todos task ${input.taskId} after the full lifecycle worker step.`,
7074
+ "",
7075
+ "You are the verifier step for a full task-triggered OpenLoops lifecycle.",
7076
+ shared,
7077
+ `- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
7078
+ `- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
7079
+ "Use fresh context. Inspect triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria. Act as an adversarial reviewer focused on correctness, regressions, missing tests, security, and incomplete requirements.",
7080
+ "If the work is valid, record verification evidence in todos and mark/leave the task completed according to the todos CLI. If not valid, add precise follow-up tasks or comments and leave the original task open or blocked with clear evidence.",
7081
+ "Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks."
7082
+ ].join(`
7083
+ `);
7084
+ return {
7085
+ name: `task-lifecycle-${input.taskId.slice(0, 8)}-triage-plan-worker-verifier`,
7086
+ description: `Full task lifecycle workflow for ${taskLabel(input)}`,
7087
+ version: 1,
7088
+ steps: workflowStepsWithWorktree(plan, [
7089
+ {
7090
+ id: "triage",
7091
+ name: "Triage",
7092
+ description: "Check task eligibility, duplicates, dependencies, and automation gates.",
7093
+ target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
7094
+ timeoutMs: 20 * 60000
7095
+ },
7096
+ {
7097
+ id: "triage-gate",
7098
+ name: "Triage Gate",
7099
+ description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
7100
+ dependsOn: ["triage"],
7101
+ target: {
7102
+ type: "command",
7103
+ command: "bash",
7104
+ args: ["-lc", gateCommand("triage")],
7105
+ cwd: plan.cwd,
7106
+ timeoutMs: 2 * 60000
7107
+ },
7108
+ timeoutMs: 2 * 60000
7109
+ },
7110
+ {
7111
+ id: "planner",
7112
+ name: "Planner",
7113
+ description: "Create a concise implementation plan and split unsafe scope before work starts.",
7114
+ dependsOn: ["triage-gate"],
7115
+ target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
7116
+ timeoutMs: 25 * 60000
7117
+ },
7118
+ {
7119
+ id: "planner-gate",
7120
+ name: "Planner Gate",
7121
+ description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
7122
+ dependsOn: ["planner"],
7123
+ target: {
7124
+ type: "command",
7125
+ command: "bash",
7126
+ args: ["-lc", gateCommand("planner")],
7127
+ cwd: plan.cwd,
7128
+ timeoutMs: 2 * 60000
7129
+ },
7130
+ timeoutMs: 2 * 60000
7131
+ },
7132
+ {
7133
+ id: "worker",
7134
+ name: "Worker",
7135
+ description: "Implement the todos task according to triage and planner evidence.",
7136
+ dependsOn: ["planner-gate"],
7137
+ target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
7138
+ timeoutMs: 45 * 60000
7139
+ },
7140
+ {
7141
+ id: "verifier",
7142
+ name: "Verifier",
7143
+ description: "Adversarially verify worker output and update todos.",
7144
+ dependsOn: ["worker"],
7145
+ target: {
7146
+ ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7147
+ idleTimeoutMs: 10 * 60000
7148
+ },
7149
+ timeoutMs: 30 * 60000
7150
+ }
7151
+ ])
7152
+ };
7153
+ }
6876
7154
  function renderEventWorkerVerifierWorkflow(input) {
6877
7155
  if (!input.eventId?.trim())
6878
7156
  throw new Error("eventId is required");
@@ -7038,11 +7316,39 @@ function renderLifecycleBoundedTemplate(id, values) {
7038
7316
  const taskId = values.taskId ?? "";
7039
7317
  if (!taskId.trim())
7040
7318
  throw new Error("taskId is required");
7041
- return renderBoundedAgentWorkerVerifierWorkflow({
7042
- ...common,
7043
- name: values.name ?? `task-lifecycle-${slugSegment(taskId)}-worker-verifier`,
7044
- objective: values.objective ?? `Run the full task lifecycle for todos task ${taskId}.`,
7045
- prompt: values.prompt ?? "Triage and dedupe the task, verify it is eligible for loop execution, create or update a concise plan artifact/comment, execute only the allowed scope, validate, record evidence, and let the verifier decide final task state. Add follow-up tasks instead of broadening scope."
7319
+ return renderTaskLifecycleWorkflow({
7320
+ taskId,
7321
+ taskTitle: values.taskTitle,
7322
+ taskDescription: values.taskDescription,
7323
+ projectPath,
7324
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
7325
+ routeProjectPath: values.routeProjectPath,
7326
+ projectGroup: values.projectGroup,
7327
+ provider: values.provider,
7328
+ authProfile: values.authProfile,
7329
+ authProfilePool: listVar(values.authProfilePool),
7330
+ triageAuthProfile: values.triageAuthProfile,
7331
+ plannerAuthProfile: values.plannerAuthProfile,
7332
+ workerAuthProfile: values.workerAuthProfile,
7333
+ verifierAuthProfile: values.verifierAuthProfile,
7334
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
7335
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
7336
+ triageAccount: values.triageAccount ? { profile: values.triageAccount, tool: values.accountTool } : undefined,
7337
+ plannerAccount: values.plannerAccount ? { profile: values.plannerAccount, tool: values.accountTool } : undefined,
7338
+ workerAccount: values.workerAccount ? { profile: values.workerAccount, tool: values.accountTool } : undefined,
7339
+ verifierAccount: values.verifierAccount ? { profile: values.verifierAccount, tool: values.accountTool } : undefined,
7340
+ model: values.model,
7341
+ variant: values.variant,
7342
+ agent: values.agent,
7343
+ addDirs: listVar(values.addDirs ?? values.addDir),
7344
+ permissionMode: values.permissionMode,
7345
+ sandbox: values.sandbox,
7346
+ manualBreakGlass: booleanVar(values.manualBreakGlass),
7347
+ worktreeMode: values.worktreeMode ?? "required",
7348
+ worktreeRoot: values.worktreeRoot,
7349
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
7350
+ eventId: values.eventId,
7351
+ eventType: values.eventType
7046
7352
  });
7047
7353
  }
7048
7354
  if (id === PR_REVIEW_TEMPLATE_ID) {
@@ -7461,8 +7767,8 @@ function goalFromOpts(opts) {
7461
7767
  }, "goal");
7462
7768
  }
7463
7769
  function accountFromOpts(opts) {
7464
- if (!opts.account && opts.accountTool && !opts.accountPool && !opts.workerAccount && !opts.verifierAccount) {
7465
- throw new Error("--account-tool requires --account, --account-pool, --worker-account, or --verifier-account");
7770
+ if (!opts.account && opts.accountTool && !opts.accountPool && !opts.triageAccount && !opts.plannerAccount && !opts.workerAccount && !opts.verifierAccount) {
7771
+ throw new Error("--account-tool requires --account, --account-pool, --triage-account, --planner-account, --worker-account, or --verifier-account");
7466
7772
  }
7467
7773
  return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
7468
7774
  }
@@ -8003,6 +8309,17 @@ function routeThrottleDryRunPreview(args) {
8003
8309
  limits: args.limits
8004
8310
  };
8005
8311
  }
8312
+ var TODOS_TASK_ROUTE_TEMPLATE_IDS = new Set([
8313
+ TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
8314
+ TASK_LIFECYCLE_TEMPLATE_ID
8315
+ ]);
8316
+ function todosTaskRouteTemplateId(opts) {
8317
+ const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
8318
+ if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
8319
+ throw new Error(`--template must be ${[...TODOS_TASK_ROUTE_TEMPLATE_IDS].join(" or ")} for todos-task routes`);
8320
+ }
8321
+ return id;
8322
+ }
8006
8323
  async function readEventEnvelopeInput(opts = {}) {
8007
8324
  const raw = opts.eventJson ?? (opts.eventFile ? readFileSync3(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
8008
8325
  const event = JSON.parse(raw);
@@ -8086,7 +8403,8 @@ function routeTodosTaskEvent(event, opts) {
8086
8403
  const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
8087
8404
  const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
8088
8405
  const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
8089
- let workflowBody = renderTodosTaskWorkerVerifierWorkflow({
8406
+ const templateId = todosTaskRouteTemplateId(opts);
8407
+ const workflowInput = {
8090
8408
  taskId,
8091
8409
  taskTitle,
8092
8410
  taskDescription,
@@ -8096,10 +8414,14 @@ function routeTodosTaskEvent(event, opts) {
8096
8414
  provider,
8097
8415
  authProfile,
8098
8416
  authProfilePool: splitList(opts.authProfilePool),
8417
+ triageAuthProfile: opts.triageAuthProfile,
8418
+ plannerAuthProfile: opts.plannerAuthProfile,
8099
8419
  workerAuthProfile: opts.workerAuthProfile,
8100
8420
  verifierAuthProfile: opts.verifierAuthProfile,
8101
8421
  account: accountFromOpts(opts),
8102
8422
  accountPool: accountPoolFromOpts(opts),
8423
+ triageAccount: roleAccountFromOpts(opts, opts.triageAccount),
8424
+ plannerAccount: roleAccountFromOpts(opts, opts.plannerAccount),
8103
8425
  workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
8104
8426
  verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
8105
8427
  model: opts.model,
@@ -8115,17 +8437,19 @@ function routeTodosTaskEvent(event, opts) {
8115
8437
  eventId: event.id,
8116
8438
  eventType: event.type,
8117
8439
  todosProjectPath: opts.todosProject
8118
- });
8440
+ };
8441
+ let workflowBody = templateId === TASK_LIFECYCLE_TEMPLATE_ID ? renderTaskLifecycleWorkflow(workflowInput) : renderTodosTaskWorkerVerifierWorkflow(workflowInput);
8119
8442
  workflowBody.name = workflowName;
8120
- workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
8443
+ workflowBody.description = `Task-triggered ${templateId} workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
8121
8444
  workflowBody = normalizeWorkflowForStorage(workflowBody, {
8122
8445
  name: workflowName,
8123
8446
  type: "todos-task-event-workflow",
8124
8447
  event: event.id
8125
8448
  });
8126
8449
  const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
8450
+ const hasExplicitRoleAccount = Boolean(opts.triageAuthProfile || opts.plannerAuthProfile || opts.workerAuthProfile || opts.verifierAuthProfile) || Boolean(opts.triageAccount || opts.plannerAccount || opts.workerAccount || opts.verifierAccount);
8127
8451
  const invocationInput = {
8128
- templateId: "todos-task-worker-verifier",
8452
+ templateId,
8129
8453
  sourceRef: {
8130
8454
  kind: "event",
8131
8455
  id: event.id,
@@ -8145,7 +8469,7 @@ function routeTodosTaskEvent(event, opts) {
8145
8469
  worktreePolicy: opts.worktreeMode ?? "auto",
8146
8470
  permissions: permissionMode,
8147
8471
  manualBreakGlass: Boolean(opts.manualBreakGlass),
8148
- accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : "single",
8472
+ accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
8149
8473
  concurrencyGroup: projectGroup ?? routeProjectPath
8150
8474
  },
8151
8475
  outputPolicy: {
@@ -8753,10 +9077,10 @@ var events = program.command("events").description("handle Hasna event envelopes
8753
9077
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
8754
9078
  var goal = program.command("goal").description("inspect goal runs");
8755
9079
  function addRouteEventOptions(command) {
8756
- return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
9080
+ return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
8757
9081
  }
8758
9082
  function addTodosDrainOptions(command, opts = {}) {
8759
- let configured = command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
9083
+ let configured = command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
8760
9084
  if (opts.includeDryRun ?? true) {
8761
9085
  configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
8762
9086
  }
@@ -8789,13 +9113,18 @@ function routeDrainArgs(opts) {
8789
9113
  add("--max-dispatch", opts.maxDispatch);
8790
9114
  add("--evidence-dir", opts.evidenceDir);
8791
9115
  addBool("--compact", opts.compact);
9116
+ add("--template", opts.template);
8792
9117
  add("--provider", opts.provider);
8793
9118
  add("--auth-profile", opts.authProfile);
8794
9119
  add("--auth-profile-pool", opts.authProfilePool);
9120
+ add("--triage-auth-profile", opts.triageAuthProfile);
9121
+ add("--planner-auth-profile", opts.plannerAuthProfile);
8795
9122
  add("--worker-auth-profile", opts.workerAuthProfile);
8796
9123
  add("--verifier-auth-profile", opts.verifierAuthProfile);
8797
9124
  add("--account", opts.account);
8798
9125
  add("--account-pool", opts.accountPool);
9126
+ add("--triage-account", opts.triageAccount);
9127
+ add("--planner-account", opts.plannerAccount);
8799
9128
  add("--worker-account", opts.workerAccount);
8800
9129
  add("--verifier-account", opts.verifierAccount);
8801
9130
  add("--account-tool", opts.accountTool);
@@ -8861,6 +9190,7 @@ function drainTodosTaskRoutes(opts) {
8861
9190
  const report = {
8862
9191
  drainedAt: new Date().toISOString(),
8863
9192
  todosProject,
9193
+ templateId: todosTaskRouteTemplateId(opts),
8864
9194
  todosProjectId: opts.todosProjectId,
8865
9195
  taskList: opts.taskList,
8866
9196
  taskListId: taskListFilter,
@@ -8887,6 +9217,7 @@ function drainTodosTaskRoutes(opts) {
8887
9217
  const output = opts.compact ? {
8888
9218
  drainedAt: report.drainedAt,
8889
9219
  todosProject: report.todosProject,
9220
+ templateId: report.templateId,
8890
9221
  todosProjectId: report.todosProjectId,
8891
9222
  taskList: report.taskList,
8892
9223
  taskListId: report.taskListId,
@@ -9112,7 +9443,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
9112
9443
  }
9113
9444
  });
9114
9445
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
9115
- eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9446
+ eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9116
9447
  const event = await readEventEnvelopeFromStdin();
9117
9448
  const result = routeTodosTaskEvent(event, opts);
9118
9449
  print(result.value, result.human);