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