@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/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." },
|
|
@@ -5183,6 +5187,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
|
5183
5187
|
var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
|
|
5184
5188
|
var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
5185
5189
|
var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
|
|
5190
|
+
var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
|
|
5191
|
+
"danger-full-access",
|
|
5192
|
+
"dangerously-bypass",
|
|
5193
|
+
"dangerously-skip"
|
|
5194
|
+
];
|
|
5186
5195
|
function compactJson(value) {
|
|
5187
5196
|
return JSON.stringify(value);
|
|
5188
5197
|
}
|
|
@@ -5204,9 +5213,17 @@ function rolePoolValue(pool, seed, role) {
|
|
|
5204
5213
|
const workerIndex = stableIndex(seed, pool.length);
|
|
5205
5214
|
if (role === "worker" || pool.length === 1)
|
|
5206
5215
|
return pool[workerIndex];
|
|
5207
|
-
|
|
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];
|
|
5208
5221
|
}
|
|
5209
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;
|
|
5210
5227
|
if (role === "worker" && input.workerAuthProfile)
|
|
5211
5228
|
return input.workerAuthProfile;
|
|
5212
5229
|
if (role === "verifier" && input.verifierAuthProfile)
|
|
@@ -5214,6 +5231,10 @@ function authProfileForRole(input, role, seed) {
|
|
|
5214
5231
|
return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
|
|
5215
5232
|
}
|
|
5216
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;
|
|
5217
5238
|
if (role === "worker" && input.workerAccount)
|
|
5218
5239
|
return input.workerAccount;
|
|
5219
5240
|
if (role === "verifier" && input.verifierAccount)
|
|
@@ -5400,10 +5421,10 @@ function worktreePrompt(plan) {
|
|
|
5400
5421
|
function assertNativeAuthProfileSupport(input, provider) {
|
|
5401
5422
|
if (provider === "codewith")
|
|
5402
5423
|
return;
|
|
5403
|
-
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);
|
|
5404
5425
|
if (!hasNativeAuthProfiles)
|
|
5405
5426
|
return;
|
|
5406
|
-
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`);
|
|
5407
5428
|
}
|
|
5408
5429
|
function failClosedSandbox(input, provider, sandbox) {
|
|
5409
5430
|
if (!["codewith", "codex"].includes(provider))
|
|
@@ -5455,9 +5476,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5455
5476
|
function workflowStepsWithWorktree(plan, steps) {
|
|
5456
5477
|
if (!plan.prepareStep)
|
|
5457
5478
|
return steps;
|
|
5479
|
+
const firstStepId = steps[0]?.id;
|
|
5458
5480
|
return [
|
|
5459
5481
|
plan.prepareStep,
|
|
5460
|
-
...steps.map((step) => step.id ===
|
|
5482
|
+
...steps.map((step) => step.id === firstStepId ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
|
|
5461
5483
|
];
|
|
5462
5484
|
}
|
|
5463
5485
|
function assertRecord(value, label) {
|
|
@@ -5505,6 +5527,13 @@ function validateCustomTemplateId(id, label) {
|
|
|
5505
5527
|
throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
|
|
5506
5528
|
}
|
|
5507
5529
|
}
|
|
5530
|
+
function optionalTemplateBoolean(value, label) {
|
|
5531
|
+
if (value === undefined)
|
|
5532
|
+
return;
|
|
5533
|
+
if (typeof value !== "boolean")
|
|
5534
|
+
throw new Error(`${label} must be a boolean`);
|
|
5535
|
+
return value;
|
|
5536
|
+
}
|
|
5508
5537
|
function validateCustomTemplateVariables(value, label) {
|
|
5509
5538
|
if (value === undefined)
|
|
5510
5539
|
return [];
|
|
@@ -5527,31 +5556,56 @@ function validateCustomTemplateVariables(value, label) {
|
|
|
5527
5556
|
if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
|
|
5528
5557
|
throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
|
|
5529
5558
|
}
|
|
5530
|
-
if (defaultValue
|
|
5531
|
-
throw new Error(`${entryLabel}.default cannot
|
|
5559
|
+
if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
|
|
5560
|
+
throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
|
|
5532
5561
|
}
|
|
5533
5562
|
return {
|
|
5534
5563
|
name,
|
|
5535
5564
|
description,
|
|
5536
|
-
required: entry.required
|
|
5565
|
+
required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
|
|
5537
5566
|
default: defaultValue,
|
|
5538
5567
|
type
|
|
5539
5568
|
};
|
|
5540
5569
|
});
|
|
5541
5570
|
}
|
|
5542
|
-
function
|
|
5571
|
+
function hasDangerousArg(value) {
|
|
5572
|
+
return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
|
|
5573
|
+
}
|
|
5574
|
+
function assertNoDangerousCustomTemplateScalars(value, label) {
|
|
5575
|
+
if (typeof value === "string") {
|
|
5576
|
+
if (hasDangerousArg(value)) {
|
|
5577
|
+
throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
|
|
5578
|
+
}
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5543
5581
|
if (!value || typeof value !== "object")
|
|
5544
5582
|
return;
|
|
5545
5583
|
if (Array.isArray(value)) {
|
|
5546
|
-
value.forEach((entry, index) =>
|
|
5584
|
+
value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
|
|
5547
5585
|
return;
|
|
5548
5586
|
}
|
|
5549
5587
|
for (const [key, entry] of Object.entries(value)) {
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5588
|
+
assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
function assertNoImplicitDangerFullAccess(value, label) {
|
|
5592
|
+
if (!value || typeof value !== "object")
|
|
5593
|
+
return;
|
|
5594
|
+
if (Array.isArray(value)) {
|
|
5595
|
+
value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
|
|
5596
|
+
return;
|
|
5597
|
+
}
|
|
5598
|
+
const object = value;
|
|
5599
|
+
if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
|
|
5600
|
+
throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
|
|
5554
5601
|
}
|
|
5602
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
5603
|
+
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
function assertCustomTemplateSafety(value, label) {
|
|
5607
|
+
assertNoDangerousCustomTemplateScalars(value, label);
|
|
5608
|
+
assertNoImplicitDangerFullAccess(value, label);
|
|
5555
5609
|
}
|
|
5556
5610
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
5557
5611
|
assertRecord(value, sourcePath);
|
|
@@ -5564,7 +5618,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
|
5564
5618
|
if (value.workflow === undefined)
|
|
5565
5619
|
throw new Error(`${sourcePath}.workflow is required`);
|
|
5566
5620
|
assertRecord(value.workflow, `${sourcePath}.workflow`);
|
|
5567
|
-
|
|
5621
|
+
assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
|
|
5568
5622
|
return { id, name, description, kind, variables, workflow: value.workflow };
|
|
5569
5623
|
}
|
|
5570
5624
|
function customTemplateSummary(definition, sourcePath) {
|
|
@@ -5605,11 +5659,11 @@ function assertNoTemplateCollisions(entries) {
|
|
|
5605
5659
|
}
|
|
5606
5660
|
}
|
|
5607
5661
|
}
|
|
5608
|
-
function
|
|
5662
|
+
function loadCustomLoopTemplatesRaw() {
|
|
5609
5663
|
const dir = customLoopTemplatesDir();
|
|
5610
5664
|
if (!existsSync3(dir))
|
|
5611
5665
|
return [];
|
|
5612
|
-
|
|
5666
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
5613
5667
|
const file = join5(dir, entry.name);
|
|
5614
5668
|
if (entry.isSymbolicLink())
|
|
5615
5669
|
throw new Error(`refusing symlinked custom template file: ${file}`);
|
|
@@ -5617,6 +5671,9 @@ function loadCustomLoopTemplates() {
|
|
|
5617
5671
|
throw new Error(`custom template registry entry is not a regular file: ${file}`);
|
|
5618
5672
|
return readCustomTemplateFile(file);
|
|
5619
5673
|
});
|
|
5674
|
+
}
|
|
5675
|
+
function loadCustomLoopTemplates() {
|
|
5676
|
+
const entries = loadCustomLoopTemplatesRaw();
|
|
5620
5677
|
assertNoTemplateCollisions(entries);
|
|
5621
5678
|
return entries;
|
|
5622
5679
|
}
|
|
@@ -5711,21 +5768,26 @@ function renderCustomTemplateNode(value, values, templateId) {
|
|
|
5711
5768
|
function renderCustomLoopTemplate(entry, values) {
|
|
5712
5769
|
const renderedValues = customTemplateValues(entry.definition, values);
|
|
5713
5770
|
const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
|
|
5714
|
-
|
|
5715
|
-
|
|
5771
|
+
assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
|
|
5772
|
+
const workflow = workflowBodyFromJson(rendered);
|
|
5773
|
+
assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
|
|
5774
|
+
return workflow;
|
|
5716
5775
|
}
|
|
5717
5776
|
function validateCustomLoopTemplateFile(file) {
|
|
5718
|
-
const
|
|
5719
|
-
|
|
5777
|
+
const source = resolve(file);
|
|
5778
|
+
const entry = readCustomTemplateFile(source);
|
|
5779
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
|
|
5780
|
+
assertNoTemplateCollisions([...existing, entry]);
|
|
5720
5781
|
return structuredClone(entry.summary);
|
|
5721
5782
|
}
|
|
5722
5783
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
5723
5784
|
const source = resolve(file);
|
|
5724
5785
|
const entry = readCustomTemplateFile(source);
|
|
5725
|
-
assertNoTemplateCollisions([entry]);
|
|
5726
5786
|
const dir = ensureCustomLoopTemplatesDir();
|
|
5727
5787
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
5728
5788
|
const replaced = existsSync3(destination);
|
|
5789
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
|
|
5790
|
+
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
5729
5791
|
if (replaced) {
|
|
5730
5792
|
const stat = lstatSync(destination);
|
|
5731
5793
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
@@ -5852,6 +5914,222 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
5852
5914
|
])
|
|
5853
5915
|
};
|
|
5854
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
|
+
}
|
|
5855
6133
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
5856
6134
|
if (!input.eventId?.trim())
|
|
5857
6135
|
throw new Error("eventId is required");
|
|
@@ -6017,11 +6295,39 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6017
6295
|
const taskId = values.taskId ?? "";
|
|
6018
6296
|
if (!taskId.trim())
|
|
6019
6297
|
throw new Error("taskId is required");
|
|
6020
|
-
return
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
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
|
|
6025
6331
|
});
|
|
6026
6332
|
}
|
|
6027
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
|
@@ -309,9 +309,11 @@ loops templates create-workflow custom-report \
|
|
|
309
309
|
Use `--source builtin`, `--source custom`, or `--source all` on
|
|
310
310
|
`list`, `show`, `render`, and `create-workflow` when automation needs an
|
|
311
311
|
explicit source. Custom template ids and names cannot override built-ins.
|
|
312
|
-
Custom templates fail closed for `danger-full-access
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
Custom templates fail closed for `danger-full-access`, dangerous passthrough
|
|
313
|
+
arguments, and implicit Codewith/Codex full-access defaults. If a custom
|
|
314
|
+
Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
|
|
315
|
+
`sandbox` to `workspace-write` or `read-only`. Use built-in templates with
|
|
316
|
+
explicit break-glass handling for emergency workflows that need full access.
|
|
315
317
|
|
|
316
318
|
Repo-mutating task/event routes should set `worktreeMode=required` so the
|
|
317
319
|
workflow fails fast instead of falling back to the main checkout. When
|
|
@@ -343,6 +345,7 @@ into a deduped one-shot workflow loop when route capacity allows:
|
|
|
343
345
|
|
|
344
346
|
```bash
|
|
345
347
|
cat task-created-event.json | loops events handle todos-task \
|
|
348
|
+
--template task-lifecycle \
|
|
346
349
|
--provider codewith \
|
|
347
350
|
--auth-profile-pool account004,account005,account006 \
|
|
348
351
|
--permission-mode bypass \
|
|
@@ -360,6 +363,20 @@ approval-required, or `no-auto` tasks. This guard exists even when the upstream
|
|
|
360
363
|
`@hasna/events` webhook filter is misconfigured, so task existence alone is not
|
|
361
364
|
permission to execute agent work.
|
|
362
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
|
+
|
|
363
380
|
Use route throttles to avoid stampeding agents when a producer creates many
|
|
364
381
|
tasks at once:
|
|
365
382
|
|
|
@@ -424,6 +441,7 @@ locks, or non-pending states stay queued in todos and are not routed:
|
|
|
424
441
|
```bash
|
|
425
442
|
loops events drain todos-task \
|
|
426
443
|
--todos-project "$HOME/.hasna/loops" \
|
|
444
|
+
--template task-lifecycle \
|
|
427
445
|
--task-list repoops-pr-queue \
|
|
428
446
|
--tags auto:route \
|
|
429
447
|
--project-path-prefix /home/hasna/workspace/hasna/opensource \
|
|
@@ -459,6 +477,7 @@ For the Hasna OSS task-created route, keep the drain deterministic and narrow:
|
|
|
459
477
|
loops routes schedule todos-task oss-task-route-drain \
|
|
460
478
|
--every 5m \
|
|
461
479
|
--todos-project "$HOME/.hasna/loops" \
|
|
480
|
+
--template task-lifecycle \
|
|
462
481
|
--project-path-prefix /home/hasna/workspace/hasna/opensource \
|
|
463
482
|
--tags auto:route \
|
|
464
483
|
--provider codewith \
|
package/package.json
CHANGED