@hasna/loops 0.3.51 → 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/README.md +15 -0
- package/dist/cli/index.js +307 -21
- package/dist/daemon/index.js +2 -2
- package/dist/index.js +271 -10
- package/dist/lib/store.js +1 -1
- package/dist/lib/templates.d.ts +13 -0
- package/dist/sdk/index.js +1 -1
- package/docs/USAGE.md +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,6 +319,7 @@ into a deduped one-shot workflow loop when route capacity allows:
|
|
|
319
319
|
|
|
320
320
|
```bash
|
|
321
321
|
cat task-created-event.json | loops events handle todos-task \
|
|
322
|
+
--template task-lifecycle \
|
|
322
323
|
--provider codewith \
|
|
323
324
|
--auth-profile-pool account004,account005,account006 \
|
|
324
325
|
--permission-mode bypass \
|
|
@@ -328,6 +329,19 @@ cat task-created-event.json | loops events handle todos-task \
|
|
|
328
329
|
--worktree-mode required
|
|
329
330
|
```
|
|
330
331
|
|
|
332
|
+
By default, `todos-task` routes use `todos-task-worker-verifier` for backwards
|
|
333
|
+
compatibility. Use `--template task-lifecycle` to run the full triage ->
|
|
334
|
+
planner -> worker -> verifier lifecycle. The route rejects unrelated templates
|
|
335
|
+
such as `pr-review` so a todos task cannot accidentally use the wrong contract.
|
|
336
|
+
The lifecycle template adds deterministic gates after triage and planning. If
|
|
337
|
+
either step marks the task blocked, omits its contextual
|
|
338
|
+
`openloops:triage=go task=<id> event=<event-id>` /
|
|
339
|
+
`openloops:planner=go task=<id> event=<event-id>` marker comment, or the task
|
|
340
|
+
is no-auto/manual/approval-required, the worker step is not started. Use
|
|
341
|
+
`--triage-auth-profile`, `--planner-auth-profile`, `--worker-auth-profile`, and
|
|
342
|
+
`--verifier-auth-profile` when exact Codewith profiles are needed, or use
|
|
343
|
+
`--auth-profile-pool` for deterministic role rotation.
|
|
344
|
+
|
|
331
345
|
For other Hasna apps that expose `@hasna/events` webhooks, use the generic
|
|
332
346
|
handler:
|
|
333
347
|
|
|
@@ -383,6 +397,7 @@ dispatch:
|
|
|
383
397
|
loops routes schedule todos-task oss-task-route-drain \
|
|
384
398
|
--every 5m \
|
|
385
399
|
--todos-project "$HOME/.hasna/loops" \
|
|
400
|
+
--template task-lifecycle \
|
|
386
401
|
--project-path-prefix /home/hasna/workspace/hasna/opensource \
|
|
387
402
|
--tags auto:route \
|
|
388
403
|
--provider codewith \
|
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.
|
|
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." },
|
|
@@ -6230,9 +6234,17 @@ function rolePoolValue(pool, seed, role) {
|
|
|
6230
6234
|
const workerIndex = stableIndex(seed, pool.length);
|
|
6231
6235
|
if (role === "worker" || pool.length === 1)
|
|
6232
6236
|
return pool[workerIndex];
|
|
6233
|
-
|
|
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];
|
|
6234
6242
|
}
|
|
6235
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;
|
|
6236
6248
|
if (role === "worker" && input.workerAuthProfile)
|
|
6237
6249
|
return input.workerAuthProfile;
|
|
6238
6250
|
if (role === "verifier" && input.verifierAuthProfile)
|
|
@@ -6240,6 +6252,10 @@ function authProfileForRole(input, role, seed) {
|
|
|
6240
6252
|
return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
|
|
6241
6253
|
}
|
|
6242
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;
|
|
6243
6259
|
if (role === "worker" && input.workerAccount)
|
|
6244
6260
|
return input.workerAccount;
|
|
6245
6261
|
if (role === "verifier" && input.verifierAccount)
|
|
@@ -6426,10 +6442,10 @@ function worktreePrompt(plan) {
|
|
|
6426
6442
|
function assertNativeAuthProfileSupport(input, provider) {
|
|
6427
6443
|
if (provider === "codewith")
|
|
6428
6444
|
return;
|
|
6429
|
-
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);
|
|
6430
6446
|
if (!hasNativeAuthProfiles)
|
|
6431
6447
|
return;
|
|
6432
|
-
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`);
|
|
6433
6449
|
}
|
|
6434
6450
|
function failClosedSandbox(input, provider, sandbox) {
|
|
6435
6451
|
if (!["codewith", "codex"].includes(provider))
|
|
@@ -6481,9 +6497,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6481
6497
|
function workflowStepsWithWorktree(plan, steps) {
|
|
6482
6498
|
if (!plan.prepareStep)
|
|
6483
6499
|
return steps;
|
|
6500
|
+
const firstStepId = steps[0]?.id;
|
|
6484
6501
|
return [
|
|
6485
6502
|
plan.prepareStep,
|
|
6486
|
-
...steps.map((step) => step.id ===
|
|
6503
|
+
...steps.map((step) => step.id === firstStepId ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
|
|
6487
6504
|
];
|
|
6488
6505
|
}
|
|
6489
6506
|
function assertRecord(value, label) {
|
|
@@ -6918,6 +6935,222 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
6918
6935
|
])
|
|
6919
6936
|
};
|
|
6920
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
|
+
}
|
|
6921
7154
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
6922
7155
|
if (!input.eventId?.trim())
|
|
6923
7156
|
throw new Error("eventId is required");
|
|
@@ -7083,11 +7316,39 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7083
7316
|
const taskId = values.taskId ?? "";
|
|
7084
7317
|
if (!taskId.trim())
|
|
7085
7318
|
throw new Error("taskId is required");
|
|
7086
|
-
return
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
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
|
|
7091
7352
|
});
|
|
7092
7353
|
}
|
|
7093
7354
|
if (id === PR_REVIEW_TEMPLATE_ID) {
|
|
@@ -7506,8 +7767,8 @@ function goalFromOpts(opts) {
|
|
|
7506
7767
|
}, "goal");
|
|
7507
7768
|
}
|
|
7508
7769
|
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");
|
|
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");
|
|
7511
7772
|
}
|
|
7512
7773
|
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
7513
7774
|
}
|
|
@@ -8048,6 +8309,17 @@ function routeThrottleDryRunPreview(args) {
|
|
|
8048
8309
|
limits: args.limits
|
|
8049
8310
|
};
|
|
8050
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
|
+
}
|
|
8051
8323
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8052
8324
|
const raw = opts.eventJson ?? (opts.eventFile ? readFileSync3(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
|
|
8053
8325
|
const event = JSON.parse(raw);
|
|
@@ -8131,7 +8403,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8131
8403
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
8132
8404
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8133
8405
|
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
8134
|
-
|
|
8406
|
+
const templateId = todosTaskRouteTemplateId(opts);
|
|
8407
|
+
const workflowInput = {
|
|
8135
8408
|
taskId,
|
|
8136
8409
|
taskTitle,
|
|
8137
8410
|
taskDescription,
|
|
@@ -8141,10 +8414,14 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8141
8414
|
provider,
|
|
8142
8415
|
authProfile,
|
|
8143
8416
|
authProfilePool: splitList(opts.authProfilePool),
|
|
8417
|
+
triageAuthProfile: opts.triageAuthProfile,
|
|
8418
|
+
plannerAuthProfile: opts.plannerAuthProfile,
|
|
8144
8419
|
workerAuthProfile: opts.workerAuthProfile,
|
|
8145
8420
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
8146
8421
|
account: accountFromOpts(opts),
|
|
8147
8422
|
accountPool: accountPoolFromOpts(opts),
|
|
8423
|
+
triageAccount: roleAccountFromOpts(opts, opts.triageAccount),
|
|
8424
|
+
plannerAccount: roleAccountFromOpts(opts, opts.plannerAccount),
|
|
8148
8425
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
8149
8426
|
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
8150
8427
|
model: opts.model,
|
|
@@ -8160,17 +8437,19 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8160
8437
|
eventId: event.id,
|
|
8161
8438
|
eventType: event.type,
|
|
8162
8439
|
todosProjectPath: opts.todosProject
|
|
8163
|
-
}
|
|
8440
|
+
};
|
|
8441
|
+
let workflowBody = templateId === TASK_LIFECYCLE_TEMPLATE_ID ? renderTaskLifecycleWorkflow(workflowInput) : renderTodosTaskWorkerVerifierWorkflow(workflowInput);
|
|
8164
8442
|
workflowBody.name = workflowName;
|
|
8165
|
-
workflowBody.description = `Task-triggered
|
|
8443
|
+
workflowBody.description = `Task-triggered ${templateId} workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
|
|
8166
8444
|
workflowBody = normalizeWorkflowForStorage(workflowBody, {
|
|
8167
8445
|
name: workflowName,
|
|
8168
8446
|
type: "todos-task-event-workflow",
|
|
8169
8447
|
event: event.id
|
|
8170
8448
|
});
|
|
8171
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);
|
|
8172
8451
|
const invocationInput = {
|
|
8173
|
-
templateId
|
|
8452
|
+
templateId,
|
|
8174
8453
|
sourceRef: {
|
|
8175
8454
|
kind: "event",
|
|
8176
8455
|
id: event.id,
|
|
@@ -8190,7 +8469,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8190
8469
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
8191
8470
|
permissions: permissionMode,
|
|
8192
8471
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8193
|
-
accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : "single",
|
|
8472
|
+
accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
|
|
8194
8473
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
8195
8474
|
},
|
|
8196
8475
|
outputPolicy: {
|
|
@@ -8798,10 +9077,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
8798
9077
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
8799
9078
|
var goal = program.command("goal").description("inspect goal runs");
|
|
8800
9079
|
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");
|
|
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");
|
|
8802
9081
|
}
|
|
8803
9082
|
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");
|
|
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");
|
|
8805
9084
|
if (opts.includeDryRun ?? true) {
|
|
8806
9085
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
8807
9086
|
}
|
|
@@ -8834,13 +9113,18 @@ function routeDrainArgs(opts) {
|
|
|
8834
9113
|
add("--max-dispatch", opts.maxDispatch);
|
|
8835
9114
|
add("--evidence-dir", opts.evidenceDir);
|
|
8836
9115
|
addBool("--compact", opts.compact);
|
|
9116
|
+
add("--template", opts.template);
|
|
8837
9117
|
add("--provider", opts.provider);
|
|
8838
9118
|
add("--auth-profile", opts.authProfile);
|
|
8839
9119
|
add("--auth-profile-pool", opts.authProfilePool);
|
|
9120
|
+
add("--triage-auth-profile", opts.triageAuthProfile);
|
|
9121
|
+
add("--planner-auth-profile", opts.plannerAuthProfile);
|
|
8840
9122
|
add("--worker-auth-profile", opts.workerAuthProfile);
|
|
8841
9123
|
add("--verifier-auth-profile", opts.verifierAuthProfile);
|
|
8842
9124
|
add("--account", opts.account);
|
|
8843
9125
|
add("--account-pool", opts.accountPool);
|
|
9126
|
+
add("--triage-account", opts.triageAccount);
|
|
9127
|
+
add("--planner-account", opts.plannerAccount);
|
|
8844
9128
|
add("--worker-account", opts.workerAccount);
|
|
8845
9129
|
add("--verifier-account", opts.verifierAccount);
|
|
8846
9130
|
add("--account-tool", opts.accountTool);
|
|
@@ -8906,6 +9190,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8906
9190
|
const report = {
|
|
8907
9191
|
drainedAt: new Date().toISOString(),
|
|
8908
9192
|
todosProject,
|
|
9193
|
+
templateId: todosTaskRouteTemplateId(opts),
|
|
8909
9194
|
todosProjectId: opts.todosProjectId,
|
|
8910
9195
|
taskList: opts.taskList,
|
|
8911
9196
|
taskListId: taskListFilter,
|
|
@@ -8932,6 +9217,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8932
9217
|
const output = opts.compact ? {
|
|
8933
9218
|
drainedAt: report.drainedAt,
|
|
8934
9219
|
todosProject: report.todosProject,
|
|
9220
|
+
templateId: report.templateId,
|
|
8935
9221
|
todosProjectId: report.todosProjectId,
|
|
8936
9222
|
taskList: report.taskList,
|
|
8937
9223
|
taskListId: report.taskListId,
|
|
@@ -9157,7 +9443,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9157
9443
|
}
|
|
9158
9444
|
});
|
|
9159
9445
|
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) => {
|
|
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) => {
|
|
9161
9447
|
const event = await readEventEnvelopeFromStdin();
|
|
9162
9448
|
const result = routeTodosTaskEvent(event, opts);
|
|
9163
9449
|
print(result.value, result.human);
|
package/dist/daemon/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 {
|
|
@@ -5240,7 +5240,7 @@ function enableStartup(result) {
|
|
|
5240
5240
|
// package.json
|
|
5241
5241
|
var package_default = {
|
|
5242
5242
|
name: "@hasna/loops",
|
|
5243
|
-
version: "0.3.
|
|
5243
|
+
version: "0.3.52",
|
|
5244
5244
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5245
5245
|
type: "module",
|
|
5246
5246
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -621,7 +621,7 @@ function writeWorkflowRunManifest(args) {
|
|
|
621
621
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
622
622
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
623
623
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
624
|
-
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
|
|
624
|
+
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
625
625
|
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
626
626
|
function rowToLoop(row) {
|
|
627
627
|
return {
|
|
@@ -5087,6 +5087,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5087
5087
|
{ name: "taskId", required: true, description: "Todos task id." },
|
|
5088
5088
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
5089
5089
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
5090
|
+
{ name: "triageAuthProfile", description: "Provider-native auth profile for the triage step." },
|
|
5091
|
+
{ name: "plannerAuthProfile", description: "Provider-native auth profile for the planner step." },
|
|
5092
|
+
{ name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
|
|
5093
|
+
{ name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
|
|
5090
5094
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
5091
5095
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
5092
5096
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
@@ -5209,9 +5213,17 @@ function rolePoolValue(pool, seed, role) {
|
|
|
5209
5213
|
const workerIndex = stableIndex(seed, pool.length);
|
|
5210
5214
|
if (role === "worker" || pool.length === 1)
|
|
5211
5215
|
return pool[workerIndex];
|
|
5212
|
-
|
|
5216
|
+
if (role === "verifier")
|
|
5217
|
+
return pool[(workerIndex + 1) % pool.length];
|
|
5218
|
+
if (role === "planner")
|
|
5219
|
+
return pool[(workerIndex + 2) % pool.length];
|
|
5220
|
+
return pool[(workerIndex + 3) % pool.length];
|
|
5213
5221
|
}
|
|
5214
5222
|
function authProfileForRole(input, role, seed) {
|
|
5223
|
+
if (role === "triage" && input.triageAuthProfile)
|
|
5224
|
+
return input.triageAuthProfile;
|
|
5225
|
+
if (role === "planner" && input.plannerAuthProfile)
|
|
5226
|
+
return input.plannerAuthProfile;
|
|
5215
5227
|
if (role === "worker" && input.workerAuthProfile)
|
|
5216
5228
|
return input.workerAuthProfile;
|
|
5217
5229
|
if (role === "verifier" && input.verifierAuthProfile)
|
|
@@ -5219,6 +5231,10 @@ function authProfileForRole(input, role, seed) {
|
|
|
5219
5231
|
return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
|
|
5220
5232
|
}
|
|
5221
5233
|
function accountForRole(input, role, seed) {
|
|
5234
|
+
if (role === "triage" && input.triageAccount)
|
|
5235
|
+
return input.triageAccount;
|
|
5236
|
+
if (role === "planner" && input.plannerAccount)
|
|
5237
|
+
return input.plannerAccount;
|
|
5222
5238
|
if (role === "worker" && input.workerAccount)
|
|
5223
5239
|
return input.workerAccount;
|
|
5224
5240
|
if (role === "verifier" && input.verifierAccount)
|
|
@@ -5405,10 +5421,10 @@ function worktreePrompt(plan) {
|
|
|
5405
5421
|
function assertNativeAuthProfileSupport(input, provider) {
|
|
5406
5422
|
if (provider === "codewith")
|
|
5407
5423
|
return;
|
|
5408
|
-
const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.workerAuthProfile || input.verifierAuthProfile);
|
|
5424
|
+
const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.triageAuthProfile || input.plannerAuthProfile || input.workerAuthProfile || input.verifierAuthProfile);
|
|
5409
5425
|
if (!hasNativeAuthProfiles)
|
|
5410
5426
|
return;
|
|
5411
|
-
throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
|
|
5427
|
+
throw new Error(`authProfile, authProfilePool, triageAuthProfile, plannerAuthProfile, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
|
|
5412
5428
|
}
|
|
5413
5429
|
function failClosedSandbox(input, provider, sandbox) {
|
|
5414
5430
|
if (!["codewith", "codex"].includes(provider))
|
|
@@ -5460,9 +5476,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5460
5476
|
function workflowStepsWithWorktree(plan, steps) {
|
|
5461
5477
|
if (!plan.prepareStep)
|
|
5462
5478
|
return steps;
|
|
5479
|
+
const firstStepId = steps[0]?.id;
|
|
5463
5480
|
return [
|
|
5464
5481
|
plan.prepareStep,
|
|
5465
|
-
...steps.map((step) => step.id ===
|
|
5482
|
+
...steps.map((step) => step.id === firstStepId ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
|
|
5466
5483
|
];
|
|
5467
5484
|
}
|
|
5468
5485
|
function assertRecord(value, label) {
|
|
@@ -5897,6 +5914,222 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
5897
5914
|
])
|
|
5898
5915
|
};
|
|
5899
5916
|
}
|
|
5917
|
+
function renderTaskLifecycleWorkflow(input) {
|
|
5918
|
+
if (!input.taskId?.trim())
|
|
5919
|
+
throw new Error("taskId is required");
|
|
5920
|
+
if (!input.projectPath?.trim())
|
|
5921
|
+
throw new Error("projectPath is required");
|
|
5922
|
+
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
5923
|
+
const plan = worktreePlan(input, input.taskId);
|
|
5924
|
+
const taskContext = {
|
|
5925
|
+
taskId: input.taskId,
|
|
5926
|
+
taskTitle: input.taskTitle,
|
|
5927
|
+
taskDescription: input.taskDescription,
|
|
5928
|
+
eventId: input.eventId,
|
|
5929
|
+
eventType: input.eventType,
|
|
5930
|
+
projectPath: input.projectPath,
|
|
5931
|
+
routeProjectPath: input.routeProjectPath,
|
|
5932
|
+
projectGroup: input.projectGroup,
|
|
5933
|
+
todosProjectPath,
|
|
5934
|
+
worktree: {
|
|
5935
|
+
mode: plan.mode,
|
|
5936
|
+
enabled: plan.enabled,
|
|
5937
|
+
cwd: plan.cwd,
|
|
5938
|
+
path: plan.path,
|
|
5939
|
+
branch: plan.branch,
|
|
5940
|
+
reason: plan.reason
|
|
5941
|
+
}
|
|
5942
|
+
};
|
|
5943
|
+
const shared = [
|
|
5944
|
+
worktreePrompt(plan),
|
|
5945
|
+
`Todos project path: ${todosProjectPath}`,
|
|
5946
|
+
"Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
|
|
5947
|
+
`- Inspect first: todos --project ${todosProjectPath} inspect ${input.taskId}`,
|
|
5948
|
+
`- Record evidence: todos --project ${todosProjectPath} comment ${input.taskId} "<concise evidence, decision, or blocker>"`,
|
|
5949
|
+
"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.",
|
|
5950
|
+
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
5951
|
+
"",
|
|
5952
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
5953
|
+
].join(`
|
|
5954
|
+
`);
|
|
5955
|
+
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
5956
|
+
const gateCommand = (stage) => [
|
|
5957
|
+
"set -euo pipefail",
|
|
5958
|
+
`task_json="$(todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(input.taskId)})"`,
|
|
5959
|
+
`TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
|
|
5960
|
+
"const raw = process.env.TASK_JSON || '{}';",
|
|
5961
|
+
"const payload = JSON.parse(raw);",
|
|
5962
|
+
"const task = payload.task && typeof payload.task === 'object' ? payload.task : payload;",
|
|
5963
|
+
"const stage = process.env.STAGE || 'lifecycle';",
|
|
5964
|
+
`const goMarker = ${JSON.stringify(gateMarker(stage, "go"))};`,
|
|
5965
|
+
`const blockedMarker = ${JSON.stringify(gateMarker(stage, "blocked"))};`,
|
|
5966
|
+
"const status = String(task.status || '').toLowerCase().replace(/_/g, '-');",
|
|
5967
|
+
"const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};",
|
|
5968
|
+
"const automation = metadata.automation && typeof metadata.automation === 'object' ? metadata.automation : {};",
|
|
5969
|
+
"const comments = Array.isArray(task.comments) ? task.comments : [];",
|
|
5970
|
+
"const blockedStatuses = new Set(['blocked', 'cancelled', 'canceled', 'failed', 'archived', 'deleted', 'done', 'completed']);",
|
|
5971
|
+
"const truthy = (value) => value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes';",
|
|
5972
|
+
"const falsey = (value) => value === false || value === 0 || value === '0' || String(value).toLowerCase() === 'false' || String(value).toLowerCase() === 'no';",
|
|
5973
|
+
"const commentText = (comment) => String(comment?.content ?? comment?.text ?? comment?.body ?? comment?.comment ?? '');",
|
|
5974
|
+
"const tagsFrom = (value) => Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];",
|
|
5975
|
+
"const records = [task, metadata, automation].filter((entry) => entry && typeof entry === 'object');",
|
|
5976
|
+
"const tags = new Set(records.flatMap((entry) => [entry.tags, entry.task_tags, entry.taskTags].flatMap(tagsFrom)).map((tag) => tag.trim().toLowerCase()).filter(Boolean));",
|
|
5977
|
+
"const markerState = (comment) => {",
|
|
5978
|
+
" const line = commentText(comment).trimStart().split(/\\r?\\n/, 1)[0]?.trimEnd() || '';",
|
|
5979
|
+
" if (line === goMarker) return 'go';",
|
|
5980
|
+
" if (line === blockedMarker) return 'blocked';",
|
|
5981
|
+
" if (line.startsWith(`openloops:${stage}=`)) return `invalid marker: ${line}`;",
|
|
5982
|
+
" return undefined;",
|
|
5983
|
+
"};",
|
|
5984
|
+
"const markerTime = (comment, index) => {",
|
|
5985
|
+
" const rawTime = comment?.created_at ?? comment?.createdAt ?? comment?.updated_at ?? comment?.updatedAt;",
|
|
5986
|
+
" const parsed = rawTime ? Date.parse(String(rawTime)) : Number.NaN;",
|
|
5987
|
+
" return Number.isFinite(parsed) ? parsed : index;",
|
|
5988
|
+
"};",
|
|
5989
|
+
"const markers = comments",
|
|
5990
|
+
" .map((comment, index) => ({ state: markerState(comment), order: markerTime(comment, index), index }))",
|
|
5991
|
+
" .filter((entry) => entry.state)",
|
|
5992
|
+
" .sort((a, b) => a.order - b.order || a.index - b.index);",
|
|
5993
|
+
"const latestMarker = markers.at(-1)?.state;",
|
|
5994
|
+
"const blockers = [];",
|
|
5995
|
+
"if (blockedStatuses.has(status)) blockers.push(`task status is ${status}`);",
|
|
5996
|
+
"for (const tag of ['no-auto', 'manual', 'manual-required', 'approval-required']) {",
|
|
5997
|
+
" if (tags.has(tag)) blockers.push(`task has disallowed tag ${tag}`);",
|
|
5998
|
+
"}",
|
|
5999
|
+
"for (const [key, source] of records.entries()) {",
|
|
6000
|
+
" if (truthy(source.no_auto) || truthy(source.noAuto)) blockers.push(`${key}.no_auto is true`);",
|
|
6001
|
+
" if (truthy(source.manual) || truthy(source.manual_required) || truthy(source.manualRequired) || String(source.mode || '').toLowerCase() === 'manual') blockers.push(`${key}.manual/mode requires manual handling`);",
|
|
6002
|
+
" if (truthy(source.requires_approval) || truthy(source.requiresApproval) || truthy(source.approval_required) || truthy(source.approvalRequired)) blockers.push(`${key}.requires_approval is true`);",
|
|
6003
|
+
" 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`);",
|
|
6004
|
+
"}",
|
|
6005
|
+
"if (latestMarker !== 'go') blockers.push(latestMarker ? `latest ${stage} marker is ${latestMarker}` : `missing exact ${goMarker} comment`);",
|
|
6006
|
+
"if (blockers.length) {",
|
|
6007
|
+
" console.error(`task lifecycle ${stage} gate blocked ${task.id || task.taskId || 'task'}: ${blockers.join('; ')}`);",
|
|
6008
|
+
" process.exit(12);",
|
|
6009
|
+
"}",
|
|
6010
|
+
"console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);",
|
|
6011
|
+
"BUN"
|
|
6012
|
+
].join(`
|
|
6013
|
+
`);
|
|
6014
|
+
const triagePrompt = [
|
|
6015
|
+
`/goal Triage todos task ${input.taskId} for safe automated execution.`,
|
|
6016
|
+
"",
|
|
6017
|
+
"You are the triage step for a full task-triggered OpenLoops lifecycle.",
|
|
6018
|
+
shared,
|
|
6019
|
+
"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.",
|
|
6020
|
+
"Do not implement repo changes in this step.",
|
|
6021
|
+
`If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
|
|
6022
|
+
"Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
|
|
6023
|
+
`If the task should not proceed automatically, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
|
|
6024
|
+
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
6025
|
+
"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."
|
|
6026
|
+
].join(`
|
|
6027
|
+
`);
|
|
6028
|
+
const plannerPrompt = [
|
|
6029
|
+
`/goal Plan todos task ${input.taskId} before implementation.`,
|
|
6030
|
+
"",
|
|
6031
|
+
"You are the planner step for a full task-triggered OpenLoops lifecycle.",
|
|
6032
|
+
shared,
|
|
6033
|
+
"Read the triage comment and current task details.",
|
|
6034
|
+
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
6035
|
+
"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.",
|
|
6036
|
+
`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`,
|
|
6037
|
+
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
6038
|
+
"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."
|
|
6039
|
+
].join(`
|
|
6040
|
+
`);
|
|
6041
|
+
const workerPrompt = [
|
|
6042
|
+
`/goal Complete todos task ${input.taskId} according to the planner evidence.`,
|
|
6043
|
+
"",
|
|
6044
|
+
"You are the worker step for a full task-triggered OpenLoops lifecycle.",
|
|
6045
|
+
shared,
|
|
6046
|
+
`- Claim/start if appropriate: todos --project ${todosProjectPath} start ${input.taskId}`,
|
|
6047
|
+
"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.",
|
|
6048
|
+
"Do not mark the task complete in the worker step; the verifier step owns completion after independent validation."
|
|
6049
|
+
].join(`
|
|
6050
|
+
`);
|
|
6051
|
+
const verifierPrompt = [
|
|
6052
|
+
`/goal Verify todos task ${input.taskId} after the full lifecycle worker step.`,
|
|
6053
|
+
"",
|
|
6054
|
+
"You are the verifier step for a full task-triggered OpenLoops lifecycle.",
|
|
6055
|
+
shared,
|
|
6056
|
+
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
6057
|
+
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
6058
|
+
"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.",
|
|
6059
|
+
"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.",
|
|
6060
|
+
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks."
|
|
6061
|
+
].join(`
|
|
6062
|
+
`);
|
|
6063
|
+
return {
|
|
6064
|
+
name: `task-lifecycle-${input.taskId.slice(0, 8)}-triage-plan-worker-verifier`,
|
|
6065
|
+
description: `Full task lifecycle workflow for ${taskLabel(input)}`,
|
|
6066
|
+
version: 1,
|
|
6067
|
+
steps: workflowStepsWithWorktree(plan, [
|
|
6068
|
+
{
|
|
6069
|
+
id: "triage",
|
|
6070
|
+
name: "Triage",
|
|
6071
|
+
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
6072
|
+
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
6073
|
+
timeoutMs: 20 * 60000
|
|
6074
|
+
},
|
|
6075
|
+
{
|
|
6076
|
+
id: "triage-gate",
|
|
6077
|
+
name: "Triage Gate",
|
|
6078
|
+
description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
|
|
6079
|
+
dependsOn: ["triage"],
|
|
6080
|
+
target: {
|
|
6081
|
+
type: "command",
|
|
6082
|
+
command: "bash",
|
|
6083
|
+
args: ["-lc", gateCommand("triage")],
|
|
6084
|
+
cwd: plan.cwd,
|
|
6085
|
+
timeoutMs: 2 * 60000
|
|
6086
|
+
},
|
|
6087
|
+
timeoutMs: 2 * 60000
|
|
6088
|
+
},
|
|
6089
|
+
{
|
|
6090
|
+
id: "planner",
|
|
6091
|
+
name: "Planner",
|
|
6092
|
+
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
6093
|
+
dependsOn: ["triage-gate"],
|
|
6094
|
+
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
6095
|
+
timeoutMs: 25 * 60000
|
|
6096
|
+
},
|
|
6097
|
+
{
|
|
6098
|
+
id: "planner-gate",
|
|
6099
|
+
name: "Planner Gate",
|
|
6100
|
+
description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
|
|
6101
|
+
dependsOn: ["planner"],
|
|
6102
|
+
target: {
|
|
6103
|
+
type: "command",
|
|
6104
|
+
command: "bash",
|
|
6105
|
+
args: ["-lc", gateCommand("planner")],
|
|
6106
|
+
cwd: plan.cwd,
|
|
6107
|
+
timeoutMs: 2 * 60000
|
|
6108
|
+
},
|
|
6109
|
+
timeoutMs: 2 * 60000
|
|
6110
|
+
},
|
|
6111
|
+
{
|
|
6112
|
+
id: "worker",
|
|
6113
|
+
name: "Worker",
|
|
6114
|
+
description: "Implement the todos task according to triage and planner evidence.",
|
|
6115
|
+
dependsOn: ["planner-gate"],
|
|
6116
|
+
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6117
|
+
timeoutMs: 45 * 60000
|
|
6118
|
+
},
|
|
6119
|
+
{
|
|
6120
|
+
id: "verifier",
|
|
6121
|
+
name: "Verifier",
|
|
6122
|
+
description: "Adversarially verify worker output and update todos.",
|
|
6123
|
+
dependsOn: ["worker"],
|
|
6124
|
+
target: {
|
|
6125
|
+
...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6126
|
+
idleTimeoutMs: 10 * 60000
|
|
6127
|
+
},
|
|
6128
|
+
timeoutMs: 30 * 60000
|
|
6129
|
+
}
|
|
6130
|
+
])
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
5900
6133
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
5901
6134
|
if (!input.eventId?.trim())
|
|
5902
6135
|
throw new Error("eventId is required");
|
|
@@ -6062,11 +6295,39 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6062
6295
|
const taskId = values.taskId ?? "";
|
|
6063
6296
|
if (!taskId.trim())
|
|
6064
6297
|
throw new Error("taskId is required");
|
|
6065
|
-
return
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6298
|
+
return renderTaskLifecycleWorkflow({
|
|
6299
|
+
taskId,
|
|
6300
|
+
taskTitle: values.taskTitle,
|
|
6301
|
+
taskDescription: values.taskDescription,
|
|
6302
|
+
projectPath,
|
|
6303
|
+
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
6304
|
+
routeProjectPath: values.routeProjectPath,
|
|
6305
|
+
projectGroup: values.projectGroup,
|
|
6306
|
+
provider: values.provider,
|
|
6307
|
+
authProfile: values.authProfile,
|
|
6308
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
6309
|
+
triageAuthProfile: values.triageAuthProfile,
|
|
6310
|
+
plannerAuthProfile: values.plannerAuthProfile,
|
|
6311
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
6312
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
6313
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
6314
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
6315
|
+
triageAccount: values.triageAccount ? { profile: values.triageAccount, tool: values.accountTool } : undefined,
|
|
6316
|
+
plannerAccount: values.plannerAccount ? { profile: values.plannerAccount, tool: values.accountTool } : undefined,
|
|
6317
|
+
workerAccount: values.workerAccount ? { profile: values.workerAccount, tool: values.accountTool } : undefined,
|
|
6318
|
+
verifierAccount: values.verifierAccount ? { profile: values.verifierAccount, tool: values.accountTool } : undefined,
|
|
6319
|
+
model: values.model,
|
|
6320
|
+
variant: values.variant,
|
|
6321
|
+
agent: values.agent,
|
|
6322
|
+
addDirs: listVar(values.addDirs ?? values.addDir),
|
|
6323
|
+
permissionMode: values.permissionMode,
|
|
6324
|
+
sandbox: values.sandbox,
|
|
6325
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
6326
|
+
worktreeMode: values.worktreeMode ?? "required",
|
|
6327
|
+
worktreeRoot: values.worktreeRoot,
|
|
6328
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6329
|
+
eventId: values.eventId,
|
|
6330
|
+
eventType: values.eventType
|
|
6070
6331
|
});
|
|
6071
6332
|
}
|
|
6072
6333
|
if (id === PR_REVIEW_TEMPLATE_ID) {
|
package/dist/lib/store.js
CHANGED
|
@@ -621,7 +621,7 @@ function writeWorkflowRunManifest(args) {
|
|
|
621
621
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
622
622
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
623
623
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
624
|
-
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
|
|
624
|
+
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
625
625
|
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
626
626
|
function rowToLoop(row) {
|
|
627
627
|
return {
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -20,10 +20,14 @@ export interface TodosTaskWorkflowTemplateInput {
|
|
|
20
20
|
provider?: AgentProvider;
|
|
21
21
|
authProfile?: string;
|
|
22
22
|
authProfilePool?: string[];
|
|
23
|
+
triageAuthProfile?: string;
|
|
24
|
+
plannerAuthProfile?: string;
|
|
23
25
|
workerAuthProfile?: string;
|
|
24
26
|
verifierAuthProfile?: string;
|
|
25
27
|
account?: AccountRef;
|
|
26
28
|
accountPool?: AccountRef[];
|
|
29
|
+
triageAccount?: AccountRef;
|
|
30
|
+
plannerAccount?: AccountRef;
|
|
27
31
|
workerAccount?: AccountRef;
|
|
28
32
|
verifierAccount?: AccountRef;
|
|
29
33
|
model?: string;
|
|
@@ -52,10 +56,14 @@ export interface EventWorkflowTemplateInput {
|
|
|
52
56
|
provider?: AgentProvider;
|
|
53
57
|
authProfile?: string;
|
|
54
58
|
authProfilePool?: string[];
|
|
59
|
+
triageAuthProfile?: string;
|
|
60
|
+
plannerAuthProfile?: string;
|
|
55
61
|
workerAuthProfile?: string;
|
|
56
62
|
verifierAuthProfile?: string;
|
|
57
63
|
account?: AccountRef;
|
|
58
64
|
accountPool?: AccountRef[];
|
|
65
|
+
triageAccount?: AccountRef;
|
|
66
|
+
plannerAccount?: AccountRef;
|
|
59
67
|
workerAccount?: AccountRef;
|
|
60
68
|
verifierAccount?: AccountRef;
|
|
61
69
|
model?: string;
|
|
@@ -79,10 +87,14 @@ export interface BoundedAgentWorkflowTemplateInput {
|
|
|
79
87
|
provider?: AgentProvider;
|
|
80
88
|
authProfile?: string;
|
|
81
89
|
authProfilePool?: string[];
|
|
90
|
+
triageAuthProfile?: string;
|
|
91
|
+
plannerAuthProfile?: string;
|
|
82
92
|
workerAuthProfile?: string;
|
|
83
93
|
verifierAuthProfile?: string;
|
|
84
94
|
account?: AccountRef;
|
|
85
95
|
accountPool?: AccountRef[];
|
|
96
|
+
triageAccount?: AccountRef;
|
|
97
|
+
plannerAccount?: AccountRef;
|
|
86
98
|
workerAccount?: AccountRef;
|
|
87
99
|
verifierAccount?: AccountRef;
|
|
88
100
|
model?: string;
|
|
@@ -120,6 +132,7 @@ export declare function validateLoopTemplateRegistry(opts?: ListLoopTemplatesOpt
|
|
|
120
132
|
export declare function listLoopTemplates(opts?: ListLoopTemplatesOptions): LoopTemplateSummary[];
|
|
121
133
|
export declare function getLoopTemplate(id: string, opts?: ListLoopTemplatesOptions): LoopTemplateSummary | undefined;
|
|
122
134
|
export declare function renderTodosTaskWorkerVerifierWorkflow(input: TodosTaskWorkflowTemplateInput): CreateWorkflowInput;
|
|
135
|
+
export declare function renderTaskLifecycleWorkflow(input: TodosTaskWorkflowTemplateInput): CreateWorkflowInput;
|
|
123
136
|
export declare function renderEventWorkerVerifierWorkflow(input: EventWorkflowTemplateInput): CreateWorkflowInput;
|
|
124
137
|
export declare function renderBoundedAgentWorkerVerifierWorkflow(input: BoundedAgentWorkflowTemplateInput): CreateWorkflowInput;
|
|
125
138
|
export declare function renderLoopTemplate(id: string, values: Record<string, string | undefined>, opts?: ListLoopTemplatesOptions): CreateWorkflowInput;
|
package/dist/sdk/index.js
CHANGED
|
@@ -621,7 +621,7 @@ function writeWorkflowRunManifest(args) {
|
|
|
621
621
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
622
622
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
623
623
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
624
|
-
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
|
|
624
|
+
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
625
625
|
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
626
626
|
function rowToLoop(row) {
|
|
627
627
|
return {
|
package/docs/USAGE.md
CHANGED
|
@@ -345,6 +345,7 @@ into a deduped one-shot workflow loop when route capacity allows:
|
|
|
345
345
|
|
|
346
346
|
```bash
|
|
347
347
|
cat task-created-event.json | loops events handle todos-task \
|
|
348
|
+
--template task-lifecycle \
|
|
348
349
|
--provider codewith \
|
|
349
350
|
--auth-profile-pool account004,account005,account006 \
|
|
350
351
|
--permission-mode bypass \
|
|
@@ -362,6 +363,20 @@ approval-required, or `no-auto` tasks. This guard exists even when the upstream
|
|
|
362
363
|
`@hasna/events` webhook filter is misconfigured, so task existence alone is not
|
|
363
364
|
permission to execute agent work.
|
|
364
365
|
|
|
366
|
+
By default, `todos-task` routes use `todos-task-worker-verifier` for backwards
|
|
367
|
+
compatibility. Use `--template task-lifecycle` when the task should run the full
|
|
368
|
+
triage -> planner -> worker -> verifier lifecycle. The route rejects unrelated
|
|
369
|
+
workflow templates such as `pr-review` so a todos task cannot accidentally use a
|
|
370
|
+
template with the wrong contract.
|
|
371
|
+
The lifecycle template inserts deterministic gate steps after triage and after
|
|
372
|
+
planning. If either agent marks the task blocked, omits its contextual
|
|
373
|
+
`openloops:triage=go task=<id> event=<event-id>` /
|
|
374
|
+
`openloops:planner=go task=<id> event=<event-id>` marker comment, or the task
|
|
375
|
+
is marked no-auto/manual/approval-required, the next agent step is not started.
|
|
376
|
+
Use `--triage-auth-profile`, `--planner-auth-profile`,
|
|
377
|
+
`--worker-auth-profile`, and `--verifier-auth-profile` for exact Codewith role
|
|
378
|
+
profiles, or use `--auth-profile-pool` for deterministic role rotation.
|
|
379
|
+
|
|
365
380
|
Use route throttles to avoid stampeding agents when a producer creates many
|
|
366
381
|
tasks at once:
|
|
367
382
|
|
|
@@ -426,6 +441,7 @@ locks, or non-pending states stay queued in todos and are not routed:
|
|
|
426
441
|
```bash
|
|
427
442
|
loops events drain todos-task \
|
|
428
443
|
--todos-project "$HOME/.hasna/loops" \
|
|
444
|
+
--template task-lifecycle \
|
|
429
445
|
--task-list repoops-pr-queue \
|
|
430
446
|
--tags auto:route \
|
|
431
447
|
--project-path-prefix /home/hasna/workspace/hasna/opensource \
|
|
@@ -461,6 +477,7 @@ For the Hasna OSS task-created route, keep the drain deterministic and narrow:
|
|
|
461
477
|
loops routes schedule todos-task oss-task-route-drain \
|
|
462
478
|
--every 5m \
|
|
463
479
|
--todos-project "$HOME/.hasna/loops" \
|
|
480
|
+
--template task-lifecycle \
|
|
464
481
|
--project-path-prefix /home/hasna/workspace/hasna/opensource \
|
|
465
482
|
--tags auto:route \
|
|
466
483
|
--provider codewith \
|
package/package.json
CHANGED