@hasna/loops 0.3.51 → 0.3.53
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/README.md +15 -0
- package/dist/cli/index.js +374 -27
- package/dist/daemon/index.js +61 -2
- package/dist/index.js +330 -10
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +60 -1
- package/dist/lib/templates.d.ts +13 -0
- package/dist/sdk/index.js +60 -1
- package/docs/USAGE.md +17 -0
- package/package.json +1 -1
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 {
|
|
@@ -1567,6 +1567,65 @@ class Store {
|
|
|
1567
1567
|
throw new Error(`workflow invocation not found after create: ${id}`);
|
|
1568
1568
|
return rowToWorkflowInvocation(row);
|
|
1569
1569
|
}
|
|
1570
|
+
refreshWorkflowInvocationForWorkItem(workItemId, input) {
|
|
1571
|
+
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
1572
|
+
if (!sourceDedupeKey)
|
|
1573
|
+
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1574
|
+
const now = nowIso();
|
|
1575
|
+
const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
|
|
1576
|
+
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1577
|
+
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1578
|
+
const result = this.db.query(`UPDATE workflow_invocations
|
|
1579
|
+
SET workflow_id=COALESCE($workflowId, workflow_id),
|
|
1580
|
+
template_id=COALESCE($templateId, template_id),
|
|
1581
|
+
source_id=COALESCE($sourceId, source_id),
|
|
1582
|
+
source_json=$sourceJson,
|
|
1583
|
+
subject_kind=$subjectKind,
|
|
1584
|
+
subject_id=COALESCE($subjectId, subject_id),
|
|
1585
|
+
subject_path=COALESCE($subjectPath, subject_path),
|
|
1586
|
+
subject_url=COALESCE($subjectUrl, subject_url),
|
|
1587
|
+
subject_json=$subjectJson,
|
|
1588
|
+
intent=$intent,
|
|
1589
|
+
scope_json=COALESCE($scopeJson, scope_json),
|
|
1590
|
+
output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
|
|
1591
|
+
updated_at=$updated
|
|
1592
|
+
WHERE source_kind=$sourceKind
|
|
1593
|
+
AND source_dedupe_key=$sourceDedupeKey
|
|
1594
|
+
AND EXISTS (
|
|
1595
|
+
SELECT 1
|
|
1596
|
+
FROM workflow_work_items
|
|
1597
|
+
WHERE id=$workItemId
|
|
1598
|
+
AND invocation_id=workflow_invocations.id
|
|
1599
|
+
AND status IN (${placeholders})
|
|
1600
|
+
)`).run({
|
|
1601
|
+
$workItemId: workItemId,
|
|
1602
|
+
$sourceKind: input.sourceRef.kind,
|
|
1603
|
+
$sourceDedupeKey: sourceDedupeKey,
|
|
1604
|
+
$workflowId: input.workflowId ?? null,
|
|
1605
|
+
$templateId: input.templateId ?? null,
|
|
1606
|
+
$sourceId: input.sourceRef.id ?? null,
|
|
1607
|
+
$sourceJson: JSON.stringify(input.sourceRef),
|
|
1608
|
+
$subjectKind: input.subjectRef.kind,
|
|
1609
|
+
$subjectId: input.subjectRef.id ?? null,
|
|
1610
|
+
$subjectPath: input.subjectRef.path ?? null,
|
|
1611
|
+
$subjectUrl: input.subjectRef.url ?? null,
|
|
1612
|
+
$subjectJson: JSON.stringify(input.subjectRef),
|
|
1613
|
+
$intent: input.intent,
|
|
1614
|
+
$scopeJson: input.scope ? JSON.stringify(input.scope) : null,
|
|
1615
|
+
$outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
|
|
1616
|
+
$updated: now,
|
|
1617
|
+
...statusBindings
|
|
1618
|
+
});
|
|
1619
|
+
if (result.changes !== 1)
|
|
1620
|
+
throw new Error(`workflow work item is not refreshable: ${workItemId}`);
|
|
1621
|
+
const updated = this.db.query(`SELECT workflow_invocations.*
|
|
1622
|
+
FROM workflow_invocations
|
|
1623
|
+
JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
|
|
1624
|
+
WHERE workflow_work_items.id = ?`).get(workItemId);
|
|
1625
|
+
if (!updated)
|
|
1626
|
+
throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
|
|
1627
|
+
return rowToWorkflowInvocation(updated);
|
|
1628
|
+
}
|
|
1570
1629
|
getWorkflowInvocation(id) {
|
|
1571
1630
|
const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
|
|
1572
1631
|
return row ? rowToWorkflowInvocation(row) : undefined;
|
|
@@ -5908,7 +5967,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5908
5967
|
// package.json
|
|
5909
5968
|
var package_default = {
|
|
5910
5969
|
name: "@hasna/loops",
|
|
5911
|
-
version: "0.3.
|
|
5970
|
+
version: "0.3.53",
|
|
5912
5971
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5913
5972
|
type: "module",
|
|
5914
5973
|
main: "dist/index.js",
|
|
@@ -6108,6 +6167,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6108
6167
|
{ name: "taskId", required: true, description: "Todos task id." },
|
|
6109
6168
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
6110
6169
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6170
|
+
{ name: "triageAuthProfile", description: "Provider-native auth profile for the triage step." },
|
|
6171
|
+
{ name: "plannerAuthProfile", description: "Provider-native auth profile for the planner step." },
|
|
6172
|
+
{ name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
|
|
6173
|
+
{ name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
|
|
6111
6174
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
6112
6175
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6113
6176
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
@@ -6230,9 +6293,17 @@ function rolePoolValue(pool, seed, role) {
|
|
|
6230
6293
|
const workerIndex = stableIndex(seed, pool.length);
|
|
6231
6294
|
if (role === "worker" || pool.length === 1)
|
|
6232
6295
|
return pool[workerIndex];
|
|
6233
|
-
|
|
6296
|
+
if (role === "verifier")
|
|
6297
|
+
return pool[(workerIndex + 1) % pool.length];
|
|
6298
|
+
if (role === "planner")
|
|
6299
|
+
return pool[(workerIndex + 2) % pool.length];
|
|
6300
|
+
return pool[(workerIndex + 3) % pool.length];
|
|
6234
6301
|
}
|
|
6235
6302
|
function authProfileForRole(input, role, seed) {
|
|
6303
|
+
if (role === "triage" && input.triageAuthProfile)
|
|
6304
|
+
return input.triageAuthProfile;
|
|
6305
|
+
if (role === "planner" && input.plannerAuthProfile)
|
|
6306
|
+
return input.plannerAuthProfile;
|
|
6236
6307
|
if (role === "worker" && input.workerAuthProfile)
|
|
6237
6308
|
return input.workerAuthProfile;
|
|
6238
6309
|
if (role === "verifier" && input.verifierAuthProfile)
|
|
@@ -6240,6 +6311,10 @@ function authProfileForRole(input, role, seed) {
|
|
|
6240
6311
|
return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
|
|
6241
6312
|
}
|
|
6242
6313
|
function accountForRole(input, role, seed) {
|
|
6314
|
+
if (role === "triage" && input.triageAccount)
|
|
6315
|
+
return input.triageAccount;
|
|
6316
|
+
if (role === "planner" && input.plannerAccount)
|
|
6317
|
+
return input.plannerAccount;
|
|
6243
6318
|
if (role === "worker" && input.workerAccount)
|
|
6244
6319
|
return input.workerAccount;
|
|
6245
6320
|
if (role === "verifier" && input.verifierAccount)
|
|
@@ -6426,10 +6501,10 @@ function worktreePrompt(plan) {
|
|
|
6426
6501
|
function assertNativeAuthProfileSupport(input, provider) {
|
|
6427
6502
|
if (provider === "codewith")
|
|
6428
6503
|
return;
|
|
6429
|
-
const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.workerAuthProfile || input.verifierAuthProfile);
|
|
6504
|
+
const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.triageAuthProfile || input.plannerAuthProfile || input.workerAuthProfile || input.verifierAuthProfile);
|
|
6430
6505
|
if (!hasNativeAuthProfiles)
|
|
6431
6506
|
return;
|
|
6432
|
-
throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
|
|
6507
|
+
throw new Error(`authProfile, authProfilePool, triageAuthProfile, plannerAuthProfile, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
|
|
6433
6508
|
}
|
|
6434
6509
|
function failClosedSandbox(input, provider, sandbox) {
|
|
6435
6510
|
if (!["codewith", "codex"].includes(provider))
|
|
@@ -6481,9 +6556,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6481
6556
|
function workflowStepsWithWorktree(plan, steps) {
|
|
6482
6557
|
if (!plan.prepareStep)
|
|
6483
6558
|
return steps;
|
|
6559
|
+
const firstStepId = steps[0]?.id;
|
|
6484
6560
|
return [
|
|
6485
6561
|
plan.prepareStep,
|
|
6486
|
-
...steps.map((step) => step.id ===
|
|
6562
|
+
...steps.map((step) => step.id === firstStepId ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
|
|
6487
6563
|
];
|
|
6488
6564
|
}
|
|
6489
6565
|
function assertRecord(value, label) {
|
|
@@ -6918,6 +6994,222 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
6918
6994
|
])
|
|
6919
6995
|
};
|
|
6920
6996
|
}
|
|
6997
|
+
function renderTaskLifecycleWorkflow(input) {
|
|
6998
|
+
if (!input.taskId?.trim())
|
|
6999
|
+
throw new Error("taskId is required");
|
|
7000
|
+
if (!input.projectPath?.trim())
|
|
7001
|
+
throw new Error("projectPath is required");
|
|
7002
|
+
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
7003
|
+
const plan = worktreePlan(input, input.taskId);
|
|
7004
|
+
const taskContext = {
|
|
7005
|
+
taskId: input.taskId,
|
|
7006
|
+
taskTitle: input.taskTitle,
|
|
7007
|
+
taskDescription: input.taskDescription,
|
|
7008
|
+
eventId: input.eventId,
|
|
7009
|
+
eventType: input.eventType,
|
|
7010
|
+
projectPath: input.projectPath,
|
|
7011
|
+
routeProjectPath: input.routeProjectPath,
|
|
7012
|
+
projectGroup: input.projectGroup,
|
|
7013
|
+
todosProjectPath,
|
|
7014
|
+
worktree: {
|
|
7015
|
+
mode: plan.mode,
|
|
7016
|
+
enabled: plan.enabled,
|
|
7017
|
+
cwd: plan.cwd,
|
|
7018
|
+
path: plan.path,
|
|
7019
|
+
branch: plan.branch,
|
|
7020
|
+
reason: plan.reason
|
|
7021
|
+
}
|
|
7022
|
+
};
|
|
7023
|
+
const shared = [
|
|
7024
|
+
worktreePrompt(plan),
|
|
7025
|
+
`Todos project path: ${todosProjectPath}`,
|
|
7026
|
+
"Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
|
|
7027
|
+
`- Inspect first: todos --project ${todosProjectPath} inspect ${input.taskId}`,
|
|
7028
|
+
`- Record evidence: todos --project ${todosProjectPath} comment ${input.taskId} "<concise evidence, decision, or blocker>"`,
|
|
7029
|
+
"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.",
|
|
7030
|
+
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
7031
|
+
"",
|
|
7032
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
7033
|
+
].join(`
|
|
7034
|
+
`);
|
|
7035
|
+
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
7036
|
+
const gateCommand = (stage) => [
|
|
7037
|
+
"set -euo pipefail",
|
|
7038
|
+
`task_json="$(todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(input.taskId)})"`,
|
|
7039
|
+
`TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
|
|
7040
|
+
"const raw = process.env.TASK_JSON || '{}';",
|
|
7041
|
+
"const payload = JSON.parse(raw);",
|
|
7042
|
+
"const task = payload.task && typeof payload.task === 'object' ? payload.task : payload;",
|
|
7043
|
+
"const stage = process.env.STAGE || 'lifecycle';",
|
|
7044
|
+
`const goMarker = ${JSON.stringify(gateMarker(stage, "go"))};`,
|
|
7045
|
+
`const blockedMarker = ${JSON.stringify(gateMarker(stage, "blocked"))};`,
|
|
7046
|
+
"const status = String(task.status || '').toLowerCase().replace(/_/g, '-');",
|
|
7047
|
+
"const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};",
|
|
7048
|
+
"const automation = metadata.automation && typeof metadata.automation === 'object' ? metadata.automation : {};",
|
|
7049
|
+
"const comments = Array.isArray(task.comments) ? task.comments : [];",
|
|
7050
|
+
"const blockedStatuses = new Set(['blocked', 'cancelled', 'canceled', 'failed', 'archived', 'deleted', 'done', 'completed']);",
|
|
7051
|
+
"const truthy = (value) => value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes';",
|
|
7052
|
+
"const falsey = (value) => value === false || value === 0 || value === '0' || String(value).toLowerCase() === 'false' || String(value).toLowerCase() === 'no';",
|
|
7053
|
+
"const commentText = (comment) => String(comment?.content ?? comment?.text ?? comment?.body ?? comment?.comment ?? '');",
|
|
7054
|
+
"const tagsFrom = (value) => Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];",
|
|
7055
|
+
"const records = [task, metadata, automation].filter((entry) => entry && typeof entry === 'object');",
|
|
7056
|
+
"const tags = new Set(records.flatMap((entry) => [entry.tags, entry.task_tags, entry.taskTags].flatMap(tagsFrom)).map((tag) => tag.trim().toLowerCase()).filter(Boolean));",
|
|
7057
|
+
"const markerState = (comment) => {",
|
|
7058
|
+
" const line = commentText(comment).trimStart().split(/\\r?\\n/, 1)[0]?.trimEnd() || '';",
|
|
7059
|
+
" if (line === goMarker) return 'go';",
|
|
7060
|
+
" if (line === blockedMarker) return 'blocked';",
|
|
7061
|
+
" if (line.startsWith(`openloops:${stage}=`)) return `invalid marker: ${line}`;",
|
|
7062
|
+
" return undefined;",
|
|
7063
|
+
"};",
|
|
7064
|
+
"const markerTime = (comment, index) => {",
|
|
7065
|
+
" const rawTime = comment?.created_at ?? comment?.createdAt ?? comment?.updated_at ?? comment?.updatedAt;",
|
|
7066
|
+
" const parsed = rawTime ? Date.parse(String(rawTime)) : Number.NaN;",
|
|
7067
|
+
" return Number.isFinite(parsed) ? parsed : index;",
|
|
7068
|
+
"};",
|
|
7069
|
+
"const markers = comments",
|
|
7070
|
+
" .map((comment, index) => ({ state: markerState(comment), order: markerTime(comment, index), index }))",
|
|
7071
|
+
" .filter((entry) => entry.state)",
|
|
7072
|
+
" .sort((a, b) => a.order - b.order || a.index - b.index);",
|
|
7073
|
+
"const latestMarker = markers.at(-1)?.state;",
|
|
7074
|
+
"const blockers = [];",
|
|
7075
|
+
"if (blockedStatuses.has(status)) blockers.push(`task status is ${status}`);",
|
|
7076
|
+
"for (const tag of ['no-auto', 'manual', 'manual-required', 'approval-required']) {",
|
|
7077
|
+
" if (tags.has(tag)) blockers.push(`task has disallowed tag ${tag}`);",
|
|
7078
|
+
"}",
|
|
7079
|
+
"for (const [key, source] of records.entries()) {",
|
|
7080
|
+
" if (truthy(source.no_auto) || truthy(source.noAuto)) blockers.push(`${key}.no_auto is true`);",
|
|
7081
|
+
" if (truthy(source.manual) || truthy(source.manual_required) || truthy(source.manualRequired) || String(source.mode || '').toLowerCase() === 'manual') blockers.push(`${key}.manual/mode requires manual handling`);",
|
|
7082
|
+
" if (truthy(source.requires_approval) || truthy(source.requiresApproval) || truthy(source.approval_required) || truthy(source.approvalRequired)) blockers.push(`${key}.requires_approval is true`);",
|
|
7083
|
+
" 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`);",
|
|
7084
|
+
"}",
|
|
7085
|
+
"if (latestMarker !== 'go') blockers.push(latestMarker ? `latest ${stage} marker is ${latestMarker}` : `missing exact ${goMarker} comment`);",
|
|
7086
|
+
"if (blockers.length) {",
|
|
7087
|
+
" console.error(`task lifecycle ${stage} gate blocked ${task.id || task.taskId || 'task'}: ${blockers.join('; ')}`);",
|
|
7088
|
+
" process.exit(12);",
|
|
7089
|
+
"}",
|
|
7090
|
+
"console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);",
|
|
7091
|
+
"BUN"
|
|
7092
|
+
].join(`
|
|
7093
|
+
`);
|
|
7094
|
+
const triagePrompt = [
|
|
7095
|
+
`/goal Triage todos task ${input.taskId} for safe automated execution.`,
|
|
7096
|
+
"",
|
|
7097
|
+
"You are the triage step for a full task-triggered OpenLoops lifecycle.",
|
|
7098
|
+
shared,
|
|
7099
|
+
"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.",
|
|
7100
|
+
"Do not implement repo changes in this step.",
|
|
7101
|
+
`If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
|
|
7102
|
+
"Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
|
|
7103
|
+
`If the task should not proceed automatically, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
|
|
7104
|
+
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
7105
|
+
"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."
|
|
7106
|
+
].join(`
|
|
7107
|
+
`);
|
|
7108
|
+
const plannerPrompt = [
|
|
7109
|
+
`/goal Plan todos task ${input.taskId} before implementation.`,
|
|
7110
|
+
"",
|
|
7111
|
+
"You are the planner step for a full task-triggered OpenLoops lifecycle.",
|
|
7112
|
+
shared,
|
|
7113
|
+
"Read the triage comment and current task details.",
|
|
7114
|
+
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
7115
|
+
"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.",
|
|
7116
|
+
`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`,
|
|
7117
|
+
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
7118
|
+
"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."
|
|
7119
|
+
].join(`
|
|
7120
|
+
`);
|
|
7121
|
+
const workerPrompt = [
|
|
7122
|
+
`/goal Complete todos task ${input.taskId} according to the planner evidence.`,
|
|
7123
|
+
"",
|
|
7124
|
+
"You are the worker step for a full task-triggered OpenLoops lifecycle.",
|
|
7125
|
+
shared,
|
|
7126
|
+
`- Claim/start if appropriate: todos --project ${todosProjectPath} start ${input.taskId}`,
|
|
7127
|
+
"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.",
|
|
7128
|
+
"Do not mark the task complete in the worker step; the verifier step owns completion after independent validation."
|
|
7129
|
+
].join(`
|
|
7130
|
+
`);
|
|
7131
|
+
const verifierPrompt = [
|
|
7132
|
+
`/goal Verify todos task ${input.taskId} after the full lifecycle worker step.`,
|
|
7133
|
+
"",
|
|
7134
|
+
"You are the verifier step for a full task-triggered OpenLoops lifecycle.",
|
|
7135
|
+
shared,
|
|
7136
|
+
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
7137
|
+
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
7138
|
+
"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.",
|
|
7139
|
+
"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.",
|
|
7140
|
+
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks."
|
|
7141
|
+
].join(`
|
|
7142
|
+
`);
|
|
7143
|
+
return {
|
|
7144
|
+
name: `task-lifecycle-${input.taskId.slice(0, 8)}-triage-plan-worker-verifier`,
|
|
7145
|
+
description: `Full task lifecycle workflow for ${taskLabel(input)}`,
|
|
7146
|
+
version: 1,
|
|
7147
|
+
steps: workflowStepsWithWorktree(plan, [
|
|
7148
|
+
{
|
|
7149
|
+
id: "triage",
|
|
7150
|
+
name: "Triage",
|
|
7151
|
+
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
7152
|
+
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
7153
|
+
timeoutMs: 20 * 60000
|
|
7154
|
+
},
|
|
7155
|
+
{
|
|
7156
|
+
id: "triage-gate",
|
|
7157
|
+
name: "Triage Gate",
|
|
7158
|
+
description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
|
|
7159
|
+
dependsOn: ["triage"],
|
|
7160
|
+
target: {
|
|
7161
|
+
type: "command",
|
|
7162
|
+
command: "bash",
|
|
7163
|
+
args: ["-lc", gateCommand("triage")],
|
|
7164
|
+
cwd: plan.cwd,
|
|
7165
|
+
timeoutMs: 2 * 60000
|
|
7166
|
+
},
|
|
7167
|
+
timeoutMs: 2 * 60000
|
|
7168
|
+
},
|
|
7169
|
+
{
|
|
7170
|
+
id: "planner",
|
|
7171
|
+
name: "Planner",
|
|
7172
|
+
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
7173
|
+
dependsOn: ["triage-gate"],
|
|
7174
|
+
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
7175
|
+
timeoutMs: 25 * 60000
|
|
7176
|
+
},
|
|
7177
|
+
{
|
|
7178
|
+
id: "planner-gate",
|
|
7179
|
+
name: "Planner Gate",
|
|
7180
|
+
description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
|
|
7181
|
+
dependsOn: ["planner"],
|
|
7182
|
+
target: {
|
|
7183
|
+
type: "command",
|
|
7184
|
+
command: "bash",
|
|
7185
|
+
args: ["-lc", gateCommand("planner")],
|
|
7186
|
+
cwd: plan.cwd,
|
|
7187
|
+
timeoutMs: 2 * 60000
|
|
7188
|
+
},
|
|
7189
|
+
timeoutMs: 2 * 60000
|
|
7190
|
+
},
|
|
7191
|
+
{
|
|
7192
|
+
id: "worker",
|
|
7193
|
+
name: "Worker",
|
|
7194
|
+
description: "Implement the todos task according to triage and planner evidence.",
|
|
7195
|
+
dependsOn: ["planner-gate"],
|
|
7196
|
+
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7197
|
+
timeoutMs: 45 * 60000
|
|
7198
|
+
},
|
|
7199
|
+
{
|
|
7200
|
+
id: "verifier",
|
|
7201
|
+
name: "Verifier",
|
|
7202
|
+
description: "Adversarially verify worker output and update todos.",
|
|
7203
|
+
dependsOn: ["worker"],
|
|
7204
|
+
target: {
|
|
7205
|
+
...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
7206
|
+
idleTimeoutMs: 10 * 60000
|
|
7207
|
+
},
|
|
7208
|
+
timeoutMs: 30 * 60000
|
|
7209
|
+
}
|
|
7210
|
+
])
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
6921
7213
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
6922
7214
|
if (!input.eventId?.trim())
|
|
6923
7215
|
throw new Error("eventId is required");
|
|
@@ -7083,11 +7375,39 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7083
7375
|
const taskId = values.taskId ?? "";
|
|
7084
7376
|
if (!taskId.trim())
|
|
7085
7377
|
throw new Error("taskId is required");
|
|
7086
|
-
return
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7378
|
+
return renderTaskLifecycleWorkflow({
|
|
7379
|
+
taskId,
|
|
7380
|
+
taskTitle: values.taskTitle,
|
|
7381
|
+
taskDescription: values.taskDescription,
|
|
7382
|
+
projectPath,
|
|
7383
|
+
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
7384
|
+
routeProjectPath: values.routeProjectPath,
|
|
7385
|
+
projectGroup: values.projectGroup,
|
|
7386
|
+
provider: values.provider,
|
|
7387
|
+
authProfile: values.authProfile,
|
|
7388
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
7389
|
+
triageAuthProfile: values.triageAuthProfile,
|
|
7390
|
+
plannerAuthProfile: values.plannerAuthProfile,
|
|
7391
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
7392
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
7393
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
7394
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
7395
|
+
triageAccount: values.triageAccount ? { profile: values.triageAccount, tool: values.accountTool } : undefined,
|
|
7396
|
+
plannerAccount: values.plannerAccount ? { profile: values.plannerAccount, tool: values.accountTool } : undefined,
|
|
7397
|
+
workerAccount: values.workerAccount ? { profile: values.workerAccount, tool: values.accountTool } : undefined,
|
|
7398
|
+
verifierAccount: values.verifierAccount ? { profile: values.verifierAccount, tool: values.accountTool } : undefined,
|
|
7399
|
+
model: values.model,
|
|
7400
|
+
variant: values.variant,
|
|
7401
|
+
agent: values.agent,
|
|
7402
|
+
addDirs: listVar(values.addDirs ?? values.addDir),
|
|
7403
|
+
permissionMode: values.permissionMode,
|
|
7404
|
+
sandbox: values.sandbox,
|
|
7405
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
7406
|
+
worktreeMode: values.worktreeMode ?? "required",
|
|
7407
|
+
worktreeRoot: values.worktreeRoot,
|
|
7408
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7409
|
+
eventId: values.eventId,
|
|
7410
|
+
eventType: values.eventType
|
|
7091
7411
|
});
|
|
7092
7412
|
}
|
|
7093
7413
|
if (id === PR_REVIEW_TEMPLATE_ID) {
|
|
@@ -7506,8 +7826,8 @@ function goalFromOpts(opts) {
|
|
|
7506
7826
|
}, "goal");
|
|
7507
7827
|
}
|
|
7508
7828
|
function accountFromOpts(opts) {
|
|
7509
|
-
if (!opts.account && opts.accountTool && !opts.accountPool && !opts.workerAccount && !opts.verifierAccount) {
|
|
7510
|
-
throw new Error("--account-tool requires --account, --account-pool, --worker-account, or --verifier-account");
|
|
7829
|
+
if (!opts.account && opts.accountTool && !opts.accountPool && !opts.triageAccount && !opts.plannerAccount && !opts.workerAccount && !opts.verifierAccount) {
|
|
7830
|
+
throw new Error("--account-tool requires --account, --account-pool, --triage-account, --planner-account, --worker-account, or --verifier-account");
|
|
7511
7831
|
}
|
|
7512
7832
|
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
7513
7833
|
}
|
|
@@ -8048,6 +8368,17 @@ function routeThrottleDryRunPreview(args) {
|
|
|
8048
8368
|
limits: args.limits
|
|
8049
8369
|
};
|
|
8050
8370
|
}
|
|
8371
|
+
var TODOS_TASK_ROUTE_TEMPLATE_IDS = new Set([
|
|
8372
|
+
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
8373
|
+
TASK_LIFECYCLE_TEMPLATE_ID
|
|
8374
|
+
]);
|
|
8375
|
+
function todosTaskRouteTemplateId(opts) {
|
|
8376
|
+
const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
|
|
8377
|
+
if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
|
|
8378
|
+
throw new Error(`--template must be ${[...TODOS_TASK_ROUTE_TEMPLATE_IDS].join(" or ")} for todos-task routes`);
|
|
8379
|
+
}
|
|
8380
|
+
return id;
|
|
8381
|
+
}
|
|
8051
8382
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8052
8383
|
const raw = opts.eventJson ?? (opts.eventFile ? readFileSync3(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
|
|
8053
8384
|
const event = JSON.parse(raw);
|
|
@@ -8131,7 +8462,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8131
8462
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
8132
8463
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8133
8464
|
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
8134
|
-
|
|
8465
|
+
const templateId = todosTaskRouteTemplateId(opts);
|
|
8466
|
+
const workflowInput = {
|
|
8135
8467
|
taskId,
|
|
8136
8468
|
taskTitle,
|
|
8137
8469
|
taskDescription,
|
|
@@ -8141,10 +8473,14 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8141
8473
|
provider,
|
|
8142
8474
|
authProfile,
|
|
8143
8475
|
authProfilePool: splitList(opts.authProfilePool),
|
|
8476
|
+
triageAuthProfile: opts.triageAuthProfile,
|
|
8477
|
+
plannerAuthProfile: opts.plannerAuthProfile,
|
|
8144
8478
|
workerAuthProfile: opts.workerAuthProfile,
|
|
8145
8479
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
8146
8480
|
account: accountFromOpts(opts),
|
|
8147
8481
|
accountPool: accountPoolFromOpts(opts),
|
|
8482
|
+
triageAccount: roleAccountFromOpts(opts, opts.triageAccount),
|
|
8483
|
+
plannerAccount: roleAccountFromOpts(opts, opts.plannerAccount),
|
|
8148
8484
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
8149
8485
|
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
8150
8486
|
model: opts.model,
|
|
@@ -8160,17 +8496,19 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8160
8496
|
eventId: event.id,
|
|
8161
8497
|
eventType: event.type,
|
|
8162
8498
|
todosProjectPath: opts.todosProject
|
|
8163
|
-
}
|
|
8499
|
+
};
|
|
8500
|
+
let workflowBody = templateId === TASK_LIFECYCLE_TEMPLATE_ID ? renderTaskLifecycleWorkflow(workflowInput) : renderTodosTaskWorkerVerifierWorkflow(workflowInput);
|
|
8164
8501
|
workflowBody.name = workflowName;
|
|
8165
|
-
workflowBody.description = `Task-triggered
|
|
8502
|
+
workflowBody.description = `Task-triggered ${templateId} workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
|
|
8166
8503
|
workflowBody = normalizeWorkflowForStorage(workflowBody, {
|
|
8167
8504
|
name: workflowName,
|
|
8168
8505
|
type: "todos-task-event-workflow",
|
|
8169
8506
|
event: event.id
|
|
8170
8507
|
});
|
|
8171
8508
|
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
8509
|
+
const hasExplicitRoleAccount = Boolean(opts.triageAuthProfile || opts.plannerAuthProfile || opts.workerAuthProfile || opts.verifierAuthProfile) || Boolean(opts.triageAccount || opts.plannerAccount || opts.workerAccount || opts.verifierAccount);
|
|
8172
8510
|
const invocationInput = {
|
|
8173
|
-
templateId
|
|
8511
|
+
templateId,
|
|
8174
8512
|
sourceRef: {
|
|
8175
8513
|
kind: "event",
|
|
8176
8514
|
id: event.id,
|
|
@@ -8190,7 +8528,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8190
8528
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
8191
8529
|
permissions: permissionMode,
|
|
8192
8530
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8193
|
-
accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : "single",
|
|
8531
|
+
accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
|
|
8194
8532
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
8195
8533
|
},
|
|
8196
8534
|
outputPolicy: {
|
|
@@ -8257,8 +8595,9 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8257
8595
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
8258
8596
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
8259
8597
|
});
|
|
8598
|
+
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
8260
8599
|
if (throttle && !throttle.allowed)
|
|
8261
|
-
return { kind: "throttled", invocation, workItem, throttle };
|
|
8600
|
+
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
8262
8601
|
const workflow = routeWorkflowForStorage(store, workflowBody);
|
|
8263
8602
|
const loop = store.createLoop({
|
|
8264
8603
|
...loopInput,
|
|
@@ -8266,13 +8605,13 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8266
8605
|
type: "workflow",
|
|
8267
8606
|
workflowId: workflow.id,
|
|
8268
8607
|
input: {
|
|
8269
|
-
workflowInvocationId:
|
|
8608
|
+
workflowInvocationId: refreshedInvocation.id,
|
|
8270
8609
|
workflowWorkItemId: workItem.id
|
|
8271
8610
|
}
|
|
8272
8611
|
}
|
|
8273
8612
|
});
|
|
8274
8613
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by todos-task route" });
|
|
8275
|
-
return { kind: "created", invocation, workItem: admitted, workflow, loop, throttle };
|
|
8614
|
+
return { kind: "created", invocation: refreshedInvocation, workItem: admitted, workflow, loop, throttle };
|
|
8276
8615
|
});
|
|
8277
8616
|
if (outcome.kind === "deduped") {
|
|
8278
8617
|
return {
|
|
@@ -8473,8 +8812,9 @@ function routeGenericEvent(event, opts) {
|
|
|
8473
8812
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
8474
8813
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
8475
8814
|
});
|
|
8815
|
+
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
8476
8816
|
if (throttle && !throttle.allowed)
|
|
8477
|
-
return { kind: "throttled", invocation, workItem, throttle };
|
|
8817
|
+
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
8478
8818
|
const workflow = routeWorkflowForStorage(store, workflowBody);
|
|
8479
8819
|
const loop = store.createLoop({
|
|
8480
8820
|
...loopInput,
|
|
@@ -8482,13 +8822,13 @@ function routeGenericEvent(event, opts) {
|
|
|
8482
8822
|
type: "workflow",
|
|
8483
8823
|
workflowId: workflow.id,
|
|
8484
8824
|
input: {
|
|
8485
|
-
workflowInvocationId:
|
|
8825
|
+
workflowInvocationId: refreshedInvocation.id,
|
|
8486
8826
|
workflowWorkItemId: workItem.id
|
|
8487
8827
|
}
|
|
8488
8828
|
}
|
|
8489
8829
|
});
|
|
8490
8830
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by generic-event route" });
|
|
8491
|
-
return { kind: "created", invocation, workItem: admitted, workflow, loop, throttle };
|
|
8831
|
+
return { kind: "created", invocation: refreshedInvocation, workItem: admitted, workflow, loop, throttle };
|
|
8492
8832
|
});
|
|
8493
8833
|
if (outcome.kind === "deduped") {
|
|
8494
8834
|
return {
|
|
@@ -8798,10 +9138,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
8798
9138
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
8799
9139
|
var goal = program.command("goal").description("inspect goal runs");
|
|
8800
9140
|
function addRouteEventOptions(command) {
|
|
8801
|
-
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");
|
|
9141
|
+
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");
|
|
8802
9142
|
}
|
|
8803
9143
|
function addTodosDrainOptions(command, opts = {}) {
|
|
8804
|
-
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");
|
|
9144
|
+
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");
|
|
8805
9145
|
if (opts.includeDryRun ?? true) {
|
|
8806
9146
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
8807
9147
|
}
|
|
@@ -8834,13 +9174,18 @@ function routeDrainArgs(opts) {
|
|
|
8834
9174
|
add("--max-dispatch", opts.maxDispatch);
|
|
8835
9175
|
add("--evidence-dir", opts.evidenceDir);
|
|
8836
9176
|
addBool("--compact", opts.compact);
|
|
9177
|
+
add("--template", opts.template);
|
|
8837
9178
|
add("--provider", opts.provider);
|
|
8838
9179
|
add("--auth-profile", opts.authProfile);
|
|
8839
9180
|
add("--auth-profile-pool", opts.authProfilePool);
|
|
9181
|
+
add("--triage-auth-profile", opts.triageAuthProfile);
|
|
9182
|
+
add("--planner-auth-profile", opts.plannerAuthProfile);
|
|
8840
9183
|
add("--worker-auth-profile", opts.workerAuthProfile);
|
|
8841
9184
|
add("--verifier-auth-profile", opts.verifierAuthProfile);
|
|
8842
9185
|
add("--account", opts.account);
|
|
8843
9186
|
add("--account-pool", opts.accountPool);
|
|
9187
|
+
add("--triage-account", opts.triageAccount);
|
|
9188
|
+
add("--planner-account", opts.plannerAccount);
|
|
8844
9189
|
add("--worker-account", opts.workerAccount);
|
|
8845
9190
|
add("--verifier-account", opts.verifierAccount);
|
|
8846
9191
|
add("--account-tool", opts.accountTool);
|
|
@@ -8906,6 +9251,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8906
9251
|
const report = {
|
|
8907
9252
|
drainedAt: new Date().toISOString(),
|
|
8908
9253
|
todosProject,
|
|
9254
|
+
templateId: todosTaskRouteTemplateId(opts),
|
|
8909
9255
|
todosProjectId: opts.todosProjectId,
|
|
8910
9256
|
taskList: opts.taskList,
|
|
8911
9257
|
taskListId: taskListFilter,
|
|
@@ -8932,6 +9278,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8932
9278
|
const output = opts.compact ? {
|
|
8933
9279
|
drainedAt: report.drainedAt,
|
|
8934
9280
|
todosProject: report.todosProject,
|
|
9281
|
+
templateId: report.templateId,
|
|
8935
9282
|
todosProjectId: report.todosProjectId,
|
|
8936
9283
|
taskList: report.taskList,
|
|
8937
9284
|
taskListId: report.taskListId,
|
|
@@ -9157,7 +9504,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9157
9504
|
}
|
|
9158
9505
|
});
|
|
9159
9506
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
9160
|
-
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) => {
|
|
9507
|
+
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) => {
|
|
9161
9508
|
const event = await readEventEnvelopeFromStdin();
|
|
9162
9509
|
const result = routeTodosTaskEvent(event, opts);
|
|
9163
9510
|
print(result.value, result.human);
|