@hasna/loops 0.3.56 → 0.3.57
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 +188 -14
- package/dist/cli/index.js +1325 -175
- package/dist/daemon/index.js +143 -19
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4031 -2876
- package/dist/lib/health.d.ts +2 -2
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +46 -8
- package/dist/lib/templates.d.ts +4 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +5821 -0
- package/dist/sdk/index.js +134 -16
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +228 -0
- package/docs/USAGE.md +123 -15
- package/package.json +9 -3
package/dist/cli/index.js
CHANGED
|
@@ -461,6 +461,9 @@ function validateTarget(value, label, opts) {
|
|
|
461
461
|
}
|
|
462
462
|
if (value.model !== undefined)
|
|
463
463
|
assertString(value.model, `${label}.model`);
|
|
464
|
+
if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
|
|
465
|
+
throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
|
|
466
|
+
}
|
|
464
467
|
if (value.variant !== undefined)
|
|
465
468
|
assertString(value.variant, `${label}.variant`);
|
|
466
469
|
if (value.agent !== undefined)
|
|
@@ -1763,7 +1766,7 @@ class Store {
|
|
|
1763
1766
|
if (!sourceDedupeKey)
|
|
1764
1767
|
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1765
1768
|
const now = nowIso();
|
|
1766
|
-
const claimableStatuses = ["queued", "deferred"
|
|
1769
|
+
const claimableStatuses = ["queued", "deferred"];
|
|
1767
1770
|
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1768
1771
|
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1769
1772
|
const result = this.db.query(`UPDATE workflow_invocations
|
|
@@ -1844,28 +1847,35 @@ class Store {
|
|
|
1844
1847
|
project_group=excluded.project_group,
|
|
1845
1848
|
priority=excluded.priority,
|
|
1846
1849
|
status=CASE
|
|
1847
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1850
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
|
|
1848
1851
|
THEN workflow_work_items.status
|
|
1849
1852
|
ELSE excluded.status
|
|
1850
1853
|
END,
|
|
1851
1854
|
workflow_id=CASE
|
|
1852
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1855
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
|
|
1853
1856
|
ELSE NULL
|
|
1854
1857
|
END,
|
|
1855
1858
|
loop_id=CASE
|
|
1856
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1859
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
|
|
1857
1860
|
ELSE NULL
|
|
1858
1861
|
END,
|
|
1859
1862
|
workflow_run_id=CASE
|
|
1860
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1863
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
|
|
1861
1864
|
ELSE NULL
|
|
1862
1865
|
END,
|
|
1863
1866
|
lease_expires_at=CASE
|
|
1864
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1867
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
|
|
1865
1868
|
ELSE NULL
|
|
1866
1869
|
END,
|
|
1867
1870
|
next_attempt_at=excluded.next_attempt_at,
|
|
1868
|
-
last_reason=
|
|
1871
|
+
last_reason=CASE
|
|
1872
|
+
WHEN workflow_work_items.attempts > 0
|
|
1873
|
+
AND workflow_work_items.status IN ('queued', 'deferred')
|
|
1874
|
+
AND workflow_work_items.last_reason IS NOT NULL
|
|
1875
|
+
AND excluded.last_reason IS NOT NULL
|
|
1876
|
+
THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
|
|
1877
|
+
ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
|
|
1878
|
+
END,
|
|
1869
1879
|
updated_at=excluded.updated_at`).run({
|
|
1870
1880
|
$id: id,
|
|
1871
1881
|
$routeKey: input.routeKey,
|
|
@@ -1918,11 +1928,39 @@ class Store {
|
|
|
1918
1928
|
const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
|
|
1919
1929
|
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1920
1930
|
}
|
|
1931
|
+
requeueWorkflowWorkItem(id, patch = {}) {
|
|
1932
|
+
const current = this.getWorkflowWorkItem(id);
|
|
1933
|
+
if (!current)
|
|
1934
|
+
throw new Error(`workflow work item not found: ${id}`);
|
|
1935
|
+
const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
|
|
1936
|
+
if (!requeueableStatuses.includes(current.status)) {
|
|
1937
|
+
throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
|
|
1938
|
+
}
|
|
1939
|
+
const now = nowIso();
|
|
1940
|
+
const reason = patch.reason?.trim() || `requeued from ${current.status}`;
|
|
1941
|
+
const placeholders = requeueableStatuses.map(() => "?").join(",");
|
|
1942
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1943
|
+
SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
|
|
1944
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
|
|
1945
|
+
WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
|
|
1946
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1947
|
+
if (!item)
|
|
1948
|
+
throw new Error(`workflow work item not found after requeue: ${id}`);
|
|
1949
|
+
if (res.changes !== 1)
|
|
1950
|
+
throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
|
|
1951
|
+
return item;
|
|
1952
|
+
}
|
|
1921
1953
|
admitWorkflowWorkItem(id, patch) {
|
|
1922
1954
|
const now = nowIso();
|
|
1923
1955
|
const res = this.db.query(`UPDATE workflow_work_items
|
|
1924
1956
|
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1925
|
-
next_attempt_at=NULL,
|
|
1957
|
+
next_attempt_at=NULL,
|
|
1958
|
+
lease_expires_at=NULL,
|
|
1959
|
+
last_reason=CASE
|
|
1960
|
+
WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
|
|
1961
|
+
ELSE COALESCE($reason, last_reason)
|
|
1962
|
+
END,
|
|
1963
|
+
updated_at=$updated
|
|
1926
1964
|
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1927
1965
|
$id: id,
|
|
1928
1966
|
$workflowId: patch.workflowId,
|
|
@@ -3105,7 +3143,7 @@ class Store {
|
|
|
3105
3143
|
|
|
3106
3144
|
// src/cli/index.ts
|
|
3107
3145
|
import { createHash as createHash3, randomUUID } from "crypto";
|
|
3108
|
-
import { closeSync, existsSync as
|
|
3146
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync5, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3109
3147
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
3110
3148
|
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
3111
3149
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -3580,6 +3618,9 @@ function assertSupportedAgentOptions(target) {
|
|
|
3580
3618
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3581
3619
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3582
3620
|
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3621
|
+
if (target.provider === "opencode" && (target.model === undefined || target.model.trim() === "")) {
|
|
3622
|
+
throw new Error("opencode.model is required; pass a provider/model id such as openrouter/google/gemini-2.5-flash");
|
|
3623
|
+
}
|
|
3583
3624
|
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3584
3625
|
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3585
3626
|
}
|
|
@@ -4352,6 +4393,78 @@ function sameBlockerKey(values) {
|
|
|
4352
4393
|
return values.map((value) => value.trim()).filter(Boolean).join(`
|
|
4353
4394
|
`) || "goal completion remains unproven";
|
|
4354
4395
|
}
|
|
4396
|
+
function planStatusForGoal(goal) {
|
|
4397
|
+
if (goal.status === "usageLimited")
|
|
4398
|
+
return "blocked";
|
|
4399
|
+
return goal.status;
|
|
4400
|
+
}
|
|
4401
|
+
function syncReadyFlags(store, goal, nodes, opts) {
|
|
4402
|
+
const withReady = updateReadyFlags(nodes, planStatusForGoal(goal));
|
|
4403
|
+
for (const node of withReady) {
|
|
4404
|
+
const current = nodes.find((entry) => entry.key === node.key);
|
|
4405
|
+
if (current && current.ready !== node.ready) {
|
|
4406
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { ready: node.ready }, { daemonLeaseId: opts.daemonLeaseId });
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
return withReady;
|
|
4410
|
+
}
|
|
4411
|
+
function summarizeLabels(values, limit = 5) {
|
|
4412
|
+
if (values.length <= limit)
|
|
4413
|
+
return values.join(", ");
|
|
4414
|
+
return `${values.slice(0, limit).join(", ")} and ${values.length - limit} more`;
|
|
4415
|
+
}
|
|
4416
|
+
function noReadyDiagnostic(goal, nodes) {
|
|
4417
|
+
const planStatus = planStatusForGoal(goal);
|
|
4418
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
4419
|
+
const pendingNodes = nodes.filter((node) => node.status === "pending");
|
|
4420
|
+
const incompleteNodes = nodes.filter((node) => node.status !== "complete");
|
|
4421
|
+
const pendingDetails = pendingNodes.map((node) => ({
|
|
4422
|
+
key: node.key,
|
|
4423
|
+
status: node.status,
|
|
4424
|
+
ready: node.ready,
|
|
4425
|
+
tokenBudget: node.tokenBudget,
|
|
4426
|
+
tokensUsed: node.tokensUsed,
|
|
4427
|
+
budgetExhausted: nodeBudgetExhausted(node),
|
|
4428
|
+
unmetDependencies: node.dependsOn.map((dependency) => ({ key: dependency, status: byKey.get(dependency)?.status ?? "missing" })).filter((dependency) => dependency.status !== "complete")
|
|
4429
|
+
}));
|
|
4430
|
+
const incompleteDetails = incompleteNodes.map((node) => ({
|
|
4431
|
+
key: node.key,
|
|
4432
|
+
status: node.status,
|
|
4433
|
+
ready: node.ready
|
|
4434
|
+
}));
|
|
4435
|
+
let owner = "goal-plan";
|
|
4436
|
+
let cause = "goal plan has no ready nodes";
|
|
4437
|
+
if (planStatus !== "active") {
|
|
4438
|
+
owner = "goal";
|
|
4439
|
+
cause = `goal status is ${goal.status}`;
|
|
4440
|
+
} else if (pendingNodes.length === 0) {
|
|
4441
|
+
owner = "goal-plan";
|
|
4442
|
+
cause = `goal plan has no pending runnable nodes; incomplete nodes: ${summarizeLabels(incompleteDetails.map((node) => `${node.key}:${node.status}`))}`;
|
|
4443
|
+
} else if (pendingDetails.every((node) => node.budgetExhausted)) {
|
|
4444
|
+
owner = "goal-plan-node";
|
|
4445
|
+
cause = `all pending nodes are budget-exhausted: ${summarizeLabels(pendingDetails.map((node) => node.key))}`;
|
|
4446
|
+
} else {
|
|
4447
|
+
const missingDependencies = pendingDetails.flatMap((node) => node.unmetDependencies.filter((dependency) => dependency.status === "missing").map((dependency) => `${node.key}->${dependency.key}`));
|
|
4448
|
+
if (missingDependencies.length > 0) {
|
|
4449
|
+
owner = "goal-plan";
|
|
4450
|
+
cause = `pending nodes reference missing dependencies: ${summarizeLabels(missingDependencies)}`;
|
|
4451
|
+
} else if (pendingDetails.every((node) => node.unmetDependencies.length > 0 || node.budgetExhausted)) {
|
|
4452
|
+
owner = "goal-plan-node";
|
|
4453
|
+
const waiting = pendingDetails.filter((node) => node.unmetDependencies.length > 0).map((node) => `${node.key} waits on ${node.unmetDependencies.map((dependency) => `${dependency.key}:${dependency.status}`).join(",")}`);
|
|
4454
|
+
const exhausted = pendingDetails.filter((node) => node.budgetExhausted).map((node) => `${node.key} budget exhausted`);
|
|
4455
|
+
cause = `pending nodes are blocked by prerequisites: ${summarizeLabels([...waiting, ...exhausted])}`;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
const blocker = `${cause}; owner=${owner}`;
|
|
4459
|
+
return {
|
|
4460
|
+
owner,
|
|
4461
|
+
blocker,
|
|
4462
|
+
planStatus,
|
|
4463
|
+
rollup: rollupSummary(nodes),
|
|
4464
|
+
pendingNodes: pendingDetails,
|
|
4465
|
+
incompleteNodes: incompleteDetails
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4355
4468
|
function metadataFor(goal, node, context) {
|
|
4356
4469
|
return {
|
|
4357
4470
|
loopId: context?.loopId,
|
|
@@ -4412,13 +4525,14 @@ async function planGoal(store, goal, spec, model, opts) {
|
|
|
4412
4525
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4413
4526
|
return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
|
|
4414
4527
|
}
|
|
4415
|
-
function stdoutFor(goal, nodes, evidence, validation) {
|
|
4528
|
+
function stdoutFor(goal, nodes, evidence, validation, diagnostics) {
|
|
4416
4529
|
return JSON.stringify({
|
|
4417
4530
|
goal,
|
|
4418
4531
|
rollup: rollupSummary(nodes),
|
|
4419
4532
|
nodes,
|
|
4420
4533
|
evidence,
|
|
4421
|
-
validation
|
|
4534
|
+
validation,
|
|
4535
|
+
diagnostics
|
|
4422
4536
|
}, null, 2);
|
|
4423
4537
|
}
|
|
4424
4538
|
async function runGoal(store, input, opts = {}) {
|
|
@@ -4451,6 +4565,7 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4451
4565
|
let validation;
|
|
4452
4566
|
let lastBlocker = "";
|
|
4453
4567
|
let repeatedBlockerCount = 0;
|
|
4568
|
+
let lastDiagnostic;
|
|
4454
4569
|
if (budgetExhausted(goal)) {
|
|
4455
4570
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4456
4571
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
|
|
@@ -4461,13 +4576,13 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4461
4576
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
|
|
4462
4577
|
}
|
|
4463
4578
|
goal = store.requireGoal(goal.goalId);
|
|
4464
|
-
nodes = store.listGoalPlanNodes(goal.goalId);
|
|
4579
|
+
nodes = syncReadyFlags(store, goal, store.listGoalPlanNodes(goal.goalId), opts);
|
|
4465
4580
|
if (budgetExhausted(goal)) {
|
|
4466
4581
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4467
4582
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
|
|
4468
4583
|
}
|
|
4469
4584
|
const readyKeys = readyNodeKeys({
|
|
4470
|
-
status: goal
|
|
4585
|
+
status: planStatusForGoal(goal),
|
|
4471
4586
|
nodes
|
|
4472
4587
|
});
|
|
4473
4588
|
if (readyKeys.length > 0) {
|
|
@@ -4566,7 +4681,9 @@ ${result.stderr}`);
|
|
|
4566
4681
|
}
|
|
4567
4682
|
continue;
|
|
4568
4683
|
}
|
|
4569
|
-
const
|
|
4684
|
+
const diagnostic = noReadyDiagnostic(goal, nodes);
|
|
4685
|
+
lastDiagnostic = diagnostic;
|
|
4686
|
+
const blocker = diagnostic.blocker;
|
|
4570
4687
|
if (blocker === lastBlocker)
|
|
4571
4688
|
repeatedBlockerCount += 1;
|
|
4572
4689
|
else {
|
|
@@ -4578,15 +4695,16 @@ ${result.stderr}`);
|
|
|
4578
4695
|
turn,
|
|
4579
4696
|
phase: "status",
|
|
4580
4697
|
status: repeatedBlockerCount >= 3 ? "blocked" : "active",
|
|
4581
|
-
evidence:
|
|
4698
|
+
evidence: diagnostic
|
|
4582
4699
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4583
4700
|
if (repeatedBlockerCount >= 3) {
|
|
4584
4701
|
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
4585
|
-
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
|
|
4702
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation, diagnostic), blocker, startedAt);
|
|
4586
4703
|
}
|
|
4587
4704
|
}
|
|
4588
4705
|
goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4589
|
-
|
|
4706
|
+
const exhaustedError = lastDiagnostic?.blocker ?? (lastBlocker ? `${lastBlocker}; max turns exhausted` : "goal max turns exhausted");
|
|
4707
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation, lastDiagnostic), exhaustedError, startedAt);
|
|
4590
4708
|
}
|
|
4591
4709
|
|
|
4592
4710
|
// src/lib/workflow-runner.ts
|
|
@@ -5708,6 +5826,7 @@ function runDoctor(store) {
|
|
|
5708
5826
|
|
|
5709
5827
|
// src/lib/health.ts
|
|
5710
5828
|
import { createHash as createHash2 } from "crypto";
|
|
5829
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
5711
5830
|
var EVIDENCE_CHARS = 2000;
|
|
5712
5831
|
var FINGERPRINT_EVIDENCE_CHARS = 120;
|
|
5713
5832
|
var CLASSIFICATIONS = [
|
|
@@ -5718,6 +5837,7 @@ var CLASSIFICATIONS = [
|
|
|
5718
5837
|
"schema_response_format",
|
|
5719
5838
|
"node_init",
|
|
5720
5839
|
"preflight",
|
|
5840
|
+
"route_functional",
|
|
5721
5841
|
"timeout",
|
|
5722
5842
|
"sigsegv",
|
|
5723
5843
|
"skipped_previous_active",
|
|
@@ -5738,6 +5858,25 @@ function searchableText(run) {
|
|
|
5738
5858
|
return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
|
|
5739
5859
|
`).toLowerCase();
|
|
5740
5860
|
}
|
|
5861
|
+
function isRecord(value) {
|
|
5862
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
5863
|
+
}
|
|
5864
|
+
function stringValue(value) {
|
|
5865
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
5866
|
+
}
|
|
5867
|
+
function objectField(value, key) {
|
|
5868
|
+
if (!value)
|
|
5869
|
+
return;
|
|
5870
|
+
const field = value[key];
|
|
5871
|
+
return isRecord(field) ? field : undefined;
|
|
5872
|
+
}
|
|
5873
|
+
function tagsFromValue(value) {
|
|
5874
|
+
if (Array.isArray(value))
|
|
5875
|
+
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
5876
|
+
if (typeof value === "string")
|
|
5877
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5878
|
+
return [];
|
|
5879
|
+
}
|
|
5741
5880
|
function stableFingerprint(parts) {
|
|
5742
5881
|
return createHash2("sha256").update(parts.join(`
|
|
5743
5882
|
`)).digest("hex").slice(0, 16);
|
|
@@ -5751,6 +5890,13 @@ function stableFailureFingerprint(run, classification) {
|
|
|
5751
5890
|
(run.error ?? run.stderr ?? run.stdout ?? "").replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
5752
5891
|
]);
|
|
5753
5892
|
}
|
|
5893
|
+
function stableRouteFunctionalFingerprint(loop, reason) {
|
|
5894
|
+
return stableFingerprint([
|
|
5895
|
+
loop.id,
|
|
5896
|
+
"route_functional",
|
|
5897
|
+
reason.replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
5898
|
+
]);
|
|
5899
|
+
}
|
|
5754
5900
|
function healthRun(run) {
|
|
5755
5901
|
return {
|
|
5756
5902
|
...run,
|
|
@@ -5795,6 +5941,148 @@ function classifyRunFailure(run) {
|
|
|
5795
5941
|
}
|
|
5796
5942
|
};
|
|
5797
5943
|
}
|
|
5944
|
+
var ROUTE_FUNCTIONAL_DISALLOWED_TAGS = new Set([
|
|
5945
|
+
"no-auto",
|
|
5946
|
+
"manual",
|
|
5947
|
+
"manual-required",
|
|
5948
|
+
"approval-required",
|
|
5949
|
+
"blocked",
|
|
5950
|
+
"completed",
|
|
5951
|
+
"done",
|
|
5952
|
+
"cancelled",
|
|
5953
|
+
"canceled",
|
|
5954
|
+
"failed",
|
|
5955
|
+
"archived"
|
|
5956
|
+
]);
|
|
5957
|
+
var ROUTE_FUNCTIONAL_DISALLOWED_STATUSES = new Set([
|
|
5958
|
+
"blocked",
|
|
5959
|
+
"completed",
|
|
5960
|
+
"done",
|
|
5961
|
+
"cancelled",
|
|
5962
|
+
"canceled",
|
|
5963
|
+
"failed",
|
|
5964
|
+
"archived"
|
|
5965
|
+
]);
|
|
5966
|
+
function parseJsonObject(raw) {
|
|
5967
|
+
if (!raw?.trim())
|
|
5968
|
+
return;
|
|
5969
|
+
try {
|
|
5970
|
+
const parsed = JSON.parse(raw);
|
|
5971
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
5972
|
+
} catch {
|
|
5973
|
+
return;
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
function routeEvidenceReport(run) {
|
|
5977
|
+
const stdoutReport = parseJsonObject(run.stdout);
|
|
5978
|
+
const evidencePath = stringValue(stdoutReport?.evidencePath);
|
|
5979
|
+
if (evidencePath && existsSync4(evidencePath)) {
|
|
5980
|
+
try {
|
|
5981
|
+
return parseJsonObject(readFileSync3(evidencePath, "utf8")) ?? stdoutReport;
|
|
5982
|
+
} catch {
|
|
5983
|
+
return stdoutReport;
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
return stdoutReport;
|
|
5987
|
+
}
|
|
5988
|
+
function commandName(command) {
|
|
5989
|
+
return command.split(/[\\/]/).at(-1) ?? command;
|
|
5990
|
+
}
|
|
5991
|
+
function argsContainSequence(args, sequence) {
|
|
5992
|
+
for (let index = 0;index <= args.length - sequence.length; index += 1) {
|
|
5993
|
+
if (sequence.every((part, offset) => args[index + offset] === part))
|
|
5994
|
+
return true;
|
|
5995
|
+
}
|
|
5996
|
+
return false;
|
|
5997
|
+
}
|
|
5998
|
+
function isRouteDrainLoop(loop) {
|
|
5999
|
+
if (loop.target.type !== "command")
|
|
6000
|
+
return false;
|
|
6001
|
+
if (commandName(loop.target.command) !== "loops")
|
|
6002
|
+
return false;
|
|
6003
|
+
const args = loop.target.args ?? [];
|
|
6004
|
+
return argsContainSequence(args, ["events", "drain", "todos-task"]) || argsContainSequence(args, ["routes", "drain", "todos-task"]) || argsContainSequence(args, ["route", "drain", "todos-task"]);
|
|
6005
|
+
}
|
|
6006
|
+
function routeResultTaskState(result) {
|
|
6007
|
+
const event = objectField(result, "event");
|
|
6008
|
+
const data = objectField(event, "data");
|
|
6009
|
+
const task = objectField(data, "task");
|
|
6010
|
+
const payload = objectField(data, "payload");
|
|
6011
|
+
const payloadTask = objectField(payload, "task");
|
|
6012
|
+
const metadata = objectField(data, "metadata");
|
|
6013
|
+
const records = [data, task, payload, payloadTask, metadata].filter(isRecord);
|
|
6014
|
+
const tags = new Set;
|
|
6015
|
+
for (const record of records) {
|
|
6016
|
+
for (const tag of tagsFromValue(record.tags ?? record.task_tags ?? record.taskTags)) {
|
|
6017
|
+
tags.add(tag.toLowerCase());
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
const status = records.map((record) => stringValue(record.status ?? record.task_status ?? record.taskStatus)?.toLowerCase()).find(Boolean);
|
|
6021
|
+
return {
|
|
6022
|
+
taskId: stringValue(event?.subject) ?? stringValue(data?.id) ?? stringValue(task?.id) ?? stringValue(payloadTask?.id),
|
|
6023
|
+
tags: [...tags],
|
|
6024
|
+
status
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
function detectRouteFunctionalFailure(store, loop, run) {
|
|
6028
|
+
if (run.status !== "succeeded")
|
|
6029
|
+
return;
|
|
6030
|
+
if (!isRouteDrainLoop(loop))
|
|
6031
|
+
return;
|
|
6032
|
+
const report = routeEvidenceReport(run);
|
|
6033
|
+
const rawResults = Array.isArray(report?.results) ? report.results.filter(isRecord) : [];
|
|
6034
|
+
for (const result of rawResults) {
|
|
6035
|
+
const kind = stringValue(result.kind);
|
|
6036
|
+
const task = routeResultTaskState(result);
|
|
6037
|
+
const disallowedTag = task.tags.find((tag) => ROUTE_FUNCTIONAL_DISALLOWED_TAGS.has(tag));
|
|
6038
|
+
if (kind && kind !== "skipped" && disallowedTag) {
|
|
6039
|
+
const reason2 = `route drain ${kind} task ${task.taskId ?? "unknown"} with disallowed tag ${disallowedTag}`;
|
|
6040
|
+
return {
|
|
6041
|
+
classification: "route_functional",
|
|
6042
|
+
fingerprint: stableRouteFunctionalFingerprint(loop, reason2),
|
|
6043
|
+
evidence: { error: reason2, stdout: redactedEvidence(run.stdout), exitCode: run.exitCode }
|
|
6044
|
+
};
|
|
6045
|
+
}
|
|
6046
|
+
if (kind && kind !== "skipped" && task.status && ROUTE_FUNCTIONAL_DISALLOWED_STATUSES.has(task.status)) {
|
|
6047
|
+
const reason2 = `route drain ${kind} task ${task.taskId ?? "unknown"} with non-routable status ${task.status}`;
|
|
6048
|
+
return {
|
|
6049
|
+
classification: "route_functional",
|
|
6050
|
+
fingerprint: stableRouteFunctionalFingerprint(loop, reason2),
|
|
6051
|
+
evidence: { error: reason2, stdout: redactedEvidence(run.stdout), exitCode: run.exitCode }
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
const reason = stringValue(result.reason);
|
|
6055
|
+
if (kind === "skipped" && reason === "task metadata requires manual or approval-gated handling") {
|
|
6056
|
+
const message = `route drain skipped task ${task.taskId ?? "unknown"} with ambiguous manual-gate reason`;
|
|
6057
|
+
return {
|
|
6058
|
+
classification: "route_functional",
|
|
6059
|
+
fingerprint: stableRouteFunctionalFingerprint(loop, message),
|
|
6060
|
+
evidence: { error: message, stdout: redactedEvidence(run.stdout), exitCode: run.exitCode }
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
const sourceTaskUpdate = objectField(result, "sourceTaskUpdate");
|
|
6064
|
+
if (kind === "skipped" && sourceTaskUpdate && sourceTaskUpdate.ok === false) {
|
|
6065
|
+
const updateError = stringValue(sourceTaskUpdate.error);
|
|
6066
|
+
const message = `route drain skipped task ${task.taskId ?? "unknown"} but failed to update source task${updateError ? `: ${updateError}` : ""}`;
|
|
6067
|
+
return {
|
|
6068
|
+
classification: "route_functional",
|
|
6069
|
+
fingerprint: stableRouteFunctionalFingerprint(loop, message),
|
|
6070
|
+
evidence: { error: message, stdout: redactedEvidence(run.stdout), exitCode: run.exitCode }
|
|
6071
|
+
};
|
|
6072
|
+
}
|
|
6073
|
+
const childLoopId = stringValue(objectField(result, "loop")?.id) ?? stringValue(result.loopId);
|
|
6074
|
+
const childRun = childLoopId ? store.listRuns({ loopId: childLoopId, limit: 1 })[0] : undefined;
|
|
6075
|
+
if (childRun && !["succeeded", "running"].includes(childRun.status)) {
|
|
6076
|
+
const message = `route drain ${kind ?? "handled"} task ${task.taskId ?? "unknown"} but child loop ${childLoopId} latest run is ${childRun.status}`;
|
|
6077
|
+
return {
|
|
6078
|
+
classification: "route_functional",
|
|
6079
|
+
fingerprint: stableRouteFunctionalFingerprint(loop, message),
|
|
6080
|
+
evidence: { error: message, stdout: redactedEvidence(run.stdout), stderr: redactedEvidence(childRun.stderr), exitCode: childRun.exitCode }
|
|
6081
|
+
};
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
return;
|
|
6085
|
+
}
|
|
5798
6086
|
function targetRoute(loop) {
|
|
5799
6087
|
if (loop.target.type === "agent") {
|
|
5800
6088
|
return {
|
|
@@ -5882,6 +6170,18 @@ function expectationForLoop(store, loop) {
|
|
|
5882
6170
|
route
|
|
5883
6171
|
};
|
|
5884
6172
|
}
|
|
6173
|
+
const routeFailure = detectRouteFunctionalFailure(store, loop, latestRun);
|
|
6174
|
+
if (routeFailure) {
|
|
6175
|
+
return {
|
|
6176
|
+
loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
|
|
6177
|
+
ok: false,
|
|
6178
|
+
check: { id: "route-functional-health", status: "fail", message: routeFailure.evidence.error ?? "route functional blocker detected" },
|
|
6179
|
+
latestRun: healthRun(latestRun),
|
|
6180
|
+
failure: routeFailure,
|
|
6181
|
+
route,
|
|
6182
|
+
recommendedTask: recommendedTask(loop, latestRun, routeFailure, route)
|
|
6183
|
+
};
|
|
6184
|
+
}
|
|
5885
6185
|
if (latestRun.status === "succeeded") {
|
|
5886
6186
|
return {
|
|
5887
6187
|
loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
|
|
@@ -5940,6 +6240,8 @@ var PROVIDER_TOKENS = new Set([
|
|
|
5940
6240
|
"agent"
|
|
5941
6241
|
]);
|
|
5942
6242
|
var REPO_GENERIC_TOKENS = new Set(["repo", "repoops"]);
|
|
6243
|
+
var CADENCE_SUFFIX_TOKENS = new Set(["hourly", "daily", "weekly", "monthly"]);
|
|
6244
|
+
var CADENCE_SUFFIX_PATTERN = /^(?:every-?)?\d+(?:s|m|h|d|w)$/;
|
|
5943
6245
|
function slugify(value) {
|
|
5944
6246
|
return value.normalize("NFKD").replace(/[^\w\s.-]/g, "-").replace(/[_\s.:/]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
5945
6247
|
}
|
|
@@ -5992,6 +6294,16 @@ function taskSlug(loop, scope) {
|
|
|
5992
6294
|
if (deduped[deduped.length - 1] !== token)
|
|
5993
6295
|
deduped.push(token);
|
|
5994
6296
|
}
|
|
6297
|
+
while (deduped.length) {
|
|
6298
|
+
const last = deduped[deduped.length - 1];
|
|
6299
|
+
if (CADENCE_SUFFIX_TOKENS.has(last) || CADENCE_SUFFIX_PATTERN.test(last)) {
|
|
6300
|
+
deduped.pop();
|
|
6301
|
+
if (deduped[deduped.length - 1] === "every")
|
|
6302
|
+
deduped.pop();
|
|
6303
|
+
continue;
|
|
6304
|
+
}
|
|
6305
|
+
break;
|
|
6306
|
+
}
|
|
5995
6307
|
return deduped.join("-") || "loop";
|
|
5996
6308
|
}
|
|
5997
6309
|
function canonicalName(loop) {
|
|
@@ -6165,14 +6477,15 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
6165
6477
|
// package.json
|
|
6166
6478
|
var package_default = {
|
|
6167
6479
|
name: "@hasna/loops",
|
|
6168
|
-
version: "0.3.
|
|
6480
|
+
version: "0.3.57",
|
|
6169
6481
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
6170
6482
|
type: "module",
|
|
6171
6483
|
main: "dist/index.js",
|
|
6172
6484
|
types: "dist/index.d.ts",
|
|
6173
6485
|
bin: {
|
|
6174
6486
|
loops: "dist/cli/index.js",
|
|
6175
|
-
"loops-daemon": "dist/daemon/index.js"
|
|
6487
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
6488
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
6176
6489
|
},
|
|
6177
6490
|
exports: {
|
|
6178
6491
|
".": {
|
|
@@ -6183,6 +6496,10 @@ var package_default = {
|
|
|
6183
6496
|
types: "./dist/sdk/index.d.ts",
|
|
6184
6497
|
import: "./dist/sdk/index.js"
|
|
6185
6498
|
},
|
|
6499
|
+
"./mcp": {
|
|
6500
|
+
types: "./dist/mcp/index.d.ts",
|
|
6501
|
+
import: "./dist/mcp/index.js"
|
|
6502
|
+
},
|
|
6186
6503
|
"./storage": {
|
|
6187
6504
|
types: "./dist/lib/store.d.ts",
|
|
6188
6505
|
import: "./dist/lib/store.js"
|
|
@@ -6195,7 +6512,7 @@ var package_default = {
|
|
|
6195
6512
|
"LICENSE"
|
|
6196
6513
|
],
|
|
6197
6514
|
scripts: {
|
|
6198
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
6515
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
6199
6516
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
6200
6517
|
typecheck: "tsc --noEmit",
|
|
6201
6518
|
test: "bun test",
|
|
@@ -6231,6 +6548,7 @@ var package_default = {
|
|
|
6231
6548
|
dependencies: {
|
|
6232
6549
|
"@hasna/events": "^0.1.9",
|
|
6233
6550
|
"@hasna/machines": "0.0.49",
|
|
6551
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
6234
6552
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
6235
6553
|
ai: "6.0.204",
|
|
6236
6554
|
commander: "^13.1.0",
|
|
@@ -6255,7 +6573,7 @@ function packageVersion() {
|
|
|
6255
6573
|
|
|
6256
6574
|
// src/lib/templates.ts
|
|
6257
6575
|
import { execFileSync } from "child_process";
|
|
6258
|
-
import { existsSync as
|
|
6576
|
+
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
6259
6577
|
import { homedir as homedir3 } from "os";
|
|
6260
6578
|
import { basename as basename3, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
|
|
6261
6579
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
@@ -6296,7 +6614,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6296
6614
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6297
6615
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6298
6616
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
|
|
6299
|
-
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6617
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." },
|
|
6618
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6300
6619
|
]
|
|
6301
6620
|
},
|
|
6302
6621
|
{
|
|
@@ -6327,7 +6646,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6327
6646
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6328
6647
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6329
6648
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
|
|
6330
|
-
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6649
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." },
|
|
6650
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6331
6651
|
]
|
|
6332
6652
|
},
|
|
6333
6653
|
{
|
|
@@ -6355,7 +6675,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6355
6675
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6356
6676
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6357
6677
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
6358
|
-
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6678
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." },
|
|
6679
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6359
6680
|
]
|
|
6360
6681
|
},
|
|
6361
6682
|
{
|
|
@@ -6374,8 +6695,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6374
6695
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
6375
6696
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6376
6697
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6698
|
+
{ name: "prHandoff", default: "false", description: "Add a bounded network-enabled PR handoff task step after the worker." },
|
|
6377
6699
|
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6378
|
-
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6700
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." },
|
|
6701
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6379
6702
|
]
|
|
6380
6703
|
},
|
|
6381
6704
|
{
|
|
@@ -6390,7 +6713,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6390
6713
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6391
6714
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6392
6715
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6393
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
6716
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6717
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6394
6718
|
]
|
|
6395
6719
|
},
|
|
6396
6720
|
{
|
|
@@ -6404,7 +6728,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6404
6728
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6405
6729
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6406
6730
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6407
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
6731
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6732
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6408
6733
|
]
|
|
6409
6734
|
},
|
|
6410
6735
|
{
|
|
@@ -6418,7 +6743,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6418
6743
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6419
6744
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6420
6745
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6421
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
6746
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6747
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6422
6748
|
]
|
|
6423
6749
|
},
|
|
6424
6750
|
{
|
|
@@ -6432,7 +6758,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6432
6758
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6433
6759
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6434
6760
|
{ name: "sandbox", default: "read-only", description: "Provider sandbox mode." },
|
|
6435
|
-
{ name: "worktreeMode", default: "main", description: "Report-only workflows normally inspect the main checkout read-only." }
|
|
6761
|
+
{ name: "worktreeMode", default: "main", description: "Report-only workflows normally inspect the main checkout read-only." },
|
|
6762
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6436
6763
|
]
|
|
6437
6764
|
},
|
|
6438
6765
|
{
|
|
@@ -6447,7 +6774,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6447
6774
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
6448
6775
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6449
6776
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6450
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
6777
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6778
|
+
{ name: "verifierIdleTimeoutMs", default: "900000", description: "Verifier idle watchdog in milliseconds; use none/off to disable when an external heartbeat exists." }
|
|
6451
6779
|
]
|
|
6452
6780
|
},
|
|
6453
6781
|
{
|
|
@@ -6481,9 +6809,25 @@ function taskLabel(input) {
|
|
|
6481
6809
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
6482
6810
|
}
|
|
6483
6811
|
var UNLIMITED_AGENT_TIMEOUT_MS = null;
|
|
6812
|
+
var DEFAULT_VERIFIER_IDLE_TIMEOUT_MS = 15 * 60000;
|
|
6484
6813
|
function agentTimeoutMs(input) {
|
|
6485
6814
|
return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
|
|
6486
6815
|
}
|
|
6816
|
+
function verifierIdleTimeoutMs(input) {
|
|
6817
|
+
if (input.verifierIdleTimeoutMs === undefined)
|
|
6818
|
+
return DEFAULT_VERIFIER_IDLE_TIMEOUT_MS;
|
|
6819
|
+
return input.verifierIdleTimeoutMs > 0 ? input.verifierIdleTimeoutMs : undefined;
|
|
6820
|
+
}
|
|
6821
|
+
function verifierRuntimeGuidance(input) {
|
|
6822
|
+
const idleTimeout = verifierIdleTimeoutMs(input);
|
|
6823
|
+
return [
|
|
6824
|
+
"Verifier runtime contract:",
|
|
6825
|
+
idleTimeout ? `- OpenLoops will mark this verifier timed_out after ${idleTimeout}ms without stdout/stderr. Emit a concise heartbeat/progress line before long checks.` : "- The verifier idle watchdog is disabled for this workflow; still emit concise progress before long checks.",
|
|
6826
|
+
"- Keep final evidence compact: summarize changed files, validation commands/results, findings, and the task decision instead of pasting bulky logs.",
|
|
6827
|
+
"- If validation cannot finish, record a clear blocked/failed task comment with the last completed check and the next concrete action."
|
|
6828
|
+
].join(`
|
|
6829
|
+
`);
|
|
6830
|
+
}
|
|
6487
6831
|
function parseTemplateTimeoutMs(raw) {
|
|
6488
6832
|
if (raw === undefined || raw.trim() === "")
|
|
6489
6833
|
return;
|
|
@@ -6496,6 +6840,18 @@ function parseTemplateTimeoutMs(raw) {
|
|
|
6496
6840
|
}
|
|
6497
6841
|
return value;
|
|
6498
6842
|
}
|
|
6843
|
+
function parseTemplateIdleTimeoutMs(raw) {
|
|
6844
|
+
if (raw === undefined || raw.trim() === "")
|
|
6845
|
+
return;
|
|
6846
|
+
const normalized = raw.trim().toLowerCase();
|
|
6847
|
+
if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
|
|
6848
|
+
return 0;
|
|
6849
|
+
const value = Number(raw);
|
|
6850
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
6851
|
+
throw new Error("verifierIdleTimeoutMs must be a positive integer number of milliseconds, or none/off");
|
|
6852
|
+
}
|
|
6853
|
+
return value;
|
|
6854
|
+
}
|
|
6499
6855
|
function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
|
|
6500
6856
|
if (raw === undefined || raw.trim() === "")
|
|
6501
6857
|
return fallbackMs;
|
|
@@ -6556,6 +6912,179 @@ function stableHex(seed) {
|
|
|
6556
6912
|
function shellQuote2(value) {
|
|
6557
6913
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6558
6914
|
}
|
|
6915
|
+
function prHandoffArtifactPath(plan, taskId) {
|
|
6916
|
+
return join5(plan.cwd, ".openloops", "pr-handoff", `${slugSegment(taskId, "task")}.json`);
|
|
6917
|
+
}
|
|
6918
|
+
function prHandoffCommand(input, plan, todosProjectPath) {
|
|
6919
|
+
const artifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
6920
|
+
return [
|
|
6921
|
+
"set -euo pipefail",
|
|
6922
|
+
`export OPENLOOPS_PR_HANDOFF_ARTIFACT=${shellQuote2(artifactPath)}`,
|
|
6923
|
+
`export OPENLOOPS_PR_HANDOFF_TASK_ID=${shellQuote2(input.taskId)}`,
|
|
6924
|
+
`export OPENLOOPS_PR_HANDOFF_TODOS_PROJECT=${shellQuote2(todosProjectPath)}`,
|
|
6925
|
+
`export OPENLOOPS_PR_HANDOFF_WORKTREE=${shellQuote2(plan.cwd)}`,
|
|
6926
|
+
`export OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT=${shellQuote2(plan.path ?? plan.cwd)}`,
|
|
6927
|
+
`export OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH=${shellQuote2(plan.branch ?? "")}`,
|
|
6928
|
+
'if [ ! -s "$OPENLOOPS_PR_HANDOFF_ARTIFACT" ]; then',
|
|
6929
|
+
` printf 'no PR handoff artifact at %s\\n' "$OPENLOOPS_PR_HANDOFF_ARTIFACT"`,
|
|
6930
|
+
" exit 0",
|
|
6931
|
+
"fi",
|
|
6932
|
+
"bun - <<'BUN'",
|
|
6933
|
+
"const { readFileSync, realpathSync } = await import('node:fs');",
|
|
6934
|
+
"const { spawnSync } = await import('node:child_process');",
|
|
6935
|
+
"const artifactPath = process.env.OPENLOOPS_PR_HANDOFF_ARTIFACT || '';",
|
|
6936
|
+
"const taskId = process.env.OPENLOOPS_PR_HANDOFF_TASK_ID || '';",
|
|
6937
|
+
"const todosProject = process.env.OPENLOOPS_PR_HANDOFF_TODOS_PROJECT || '';",
|
|
6938
|
+
"const fallbackWorktree = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE || process.cwd();",
|
|
6939
|
+
"const expectedRoot = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT || fallbackWorktree;",
|
|
6940
|
+
"const expectedBranch = process.env.OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH || '';",
|
|
6941
|
+
"const todosBin = process.env.OPENLOOPS_PR_HANDOFF_TODOS_BIN || 'todos';",
|
|
6942
|
+
"const gitBin = process.env.OPENLOOPS_PR_HANDOFF_GIT_BIN || 'git';",
|
|
6943
|
+
"const ghBin = process.env.OPENLOOPS_PR_HANDOFF_GH_BIN || 'gh';",
|
|
6944
|
+
"const raw = readFileSync(artifactPath, 'utf8');",
|
|
6945
|
+
"const artifact = JSON.parse(raw);",
|
|
6946
|
+
"const stringField = (...keys) => {",
|
|
6947
|
+
" for (const key of keys) {",
|
|
6948
|
+
" const value = artifact[key];",
|
|
6949
|
+
" if (typeof value === 'string' && value.trim()) return value.trim();",
|
|
6950
|
+
" }",
|
|
6951
|
+
" return undefined;",
|
|
6952
|
+
"};",
|
|
6953
|
+
"const run = (command, args, options = {}) => spawnSync(command, args, { encoding: 'utf8', ...options });",
|
|
6954
|
+
"const todosArgs = (...args) => todosProject ? ['--project', todosProject, ...args] : args;",
|
|
6955
|
+
"const todos = (...args) => run(todosBin, todosArgs(...args));",
|
|
6956
|
+
"const comment = (text) => {",
|
|
6957
|
+
" const result = todos('comment', taskId, text);",
|
|
6958
|
+
" if (result.status !== 0) console.error(`failed to comment original task: ${result.stderr || result.stdout || result.status}`);",
|
|
6959
|
+
"};",
|
|
6960
|
+
"const repoPath = stringField('worktreePath', 'localRepoPath', 'repoPath', 'cwd') || fallbackWorktree;",
|
|
6961
|
+
"const artifactTaskId = stringField('taskId', 'sourceTaskId', 'originalTaskId');",
|
|
6962
|
+
"const branch = stringField('branch', 'headBranch');",
|
|
6963
|
+
"const base = stringField('base', 'baseBranch') || 'main';",
|
|
6964
|
+
"const remote = stringField('remote') || 'origin';",
|
|
6965
|
+
"let commit = stringField('commit', 'commitSha', 'sha');",
|
|
6966
|
+
"const repo = stringField('githubRepo', 'repoSlug', 'repository');",
|
|
6967
|
+
"const prUrl = stringField('prUrl', 'pullRequestUrl');",
|
|
6968
|
+
"const title = stringField('title', 'prTitle') || `PR handoff for ${taskId}`;",
|
|
6969
|
+
"const body = stringField('body', 'prBody') || [",
|
|
6970
|
+
" `OpenLoops PR handoff for task ${taskId}.`,",
|
|
6971
|
+
" `Commit: ${commit || 'unknown'}`,",
|
|
6972
|
+
" `Branch: ${branch || 'unknown'}`,",
|
|
6973
|
+
" artifact.validation ? `Validation: ${artifact.validation}` : undefined,",
|
|
6974
|
+
" artifact.error ? `Worker network error: ${artifact.error}` : undefined,",
|
|
6975
|
+
"].filter(Boolean).join('\\n\\n');",
|
|
6976
|
+
"const fingerprint = stringField('fingerprint') || `openloops:pr-handoff:${taskId}:${branch || 'missing-branch'}:${commit || 'missing-commit'}`;",
|
|
6977
|
+
"const repoTagSource = (repo || stringField('repo', 'remoteUrl') || repoPath).split(/[/:]/).filter(Boolean).at(-1) || 'unknown';",
|
|
6978
|
+
"const repoTag = `repo:${repoTagSource.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown'}`;",
|
|
6979
|
+
"const metadata = {",
|
|
6980
|
+
" route_enabled: true,",
|
|
6981
|
+
" source: 'openloops.pr-handoff',",
|
|
6982
|
+
" original_task_id: taskId,",
|
|
6983
|
+
" repo: repo || stringField('repo', 'remoteUrl') || '',",
|
|
6984
|
+
" branch: branch || '',",
|
|
6985
|
+
" base,",
|
|
6986
|
+
" commit: commit || '',",
|
|
6987
|
+
" artifact_path: artifactPath,",
|
|
6988
|
+
" fingerprint,",
|
|
6989
|
+
" automation: { allowed: true, mode: 'auto' },",
|
|
6990
|
+
" no_tmux_dispatch: true,",
|
|
6991
|
+
"};",
|
|
6992
|
+
"const upsertTask = (why) => {",
|
|
6993
|
+
" const description = [",
|
|
6994
|
+
" `OpenLoops could not complete network PR handoff for original task ${taskId}.`,",
|
|
6995
|
+
" `Reason: ${why}`,",
|
|
6996
|
+
" `Fingerprint: ${fingerprint}`,",
|
|
6997
|
+
" `Repository: ${repo || stringField('repo', 'remoteUrl') || 'unknown'}`,",
|
|
6998
|
+
" `Worktree: ${repoPath}`,",
|
|
6999
|
+
" `Branch: ${branch || 'unknown'}`,",
|
|
7000
|
+
" `Base: ${base}`,",
|
|
7001
|
+
" `Commit: ${commit || 'unknown'}`,",
|
|
7002
|
+
" `Artifact: ${artifactPath}`,",
|
|
7003
|
+
" artifact.validation ? `Validation: ${artifact.validation}` : undefined,",
|
|
7004
|
+
" artifact.error ? `Worker error: ${artifact.error}` : undefined,",
|
|
7005
|
+
" 'Do not rerun implementation work. Push the recorded commit/branch, open or update the PR, then comment the original task with the PR URL and validation evidence.',",
|
|
7006
|
+
" ].filter(Boolean).join('\\n\\n');",
|
|
7007
|
+
" const result = todos(",
|
|
7008
|
+
" 'task',",
|
|
7009
|
+
" 'upsert',",
|
|
7010
|
+
" '--fingerprint', fingerprint,",
|
|
7011
|
+
" '--title', `PR handoff for ${taskId}`,",
|
|
7012
|
+
" '-d', description,",
|
|
7013
|
+
" '-p', 'high',",
|
|
7014
|
+
" '-t', ['auto:route', 'pr-handoff', 'github', 'network', repoTag].join(','),",
|
|
7015
|
+
" '--metadata-json', JSON.stringify(metadata),",
|
|
7016
|
+
" '--working-dir', repoPath,",
|
|
7017
|
+
" );",
|
|
7018
|
+
" if (result.status !== 0) throw new Error(`todos task upsert failed: ${result.stderr || result.stdout || result.status}`);",
|
|
7019
|
+
" comment(`openloops:pr-handoff=pending task=${taskId} artifact=${artifactPath} fingerprint=${fingerprint} reason=${why}`);",
|
|
7020
|
+
" console.log(`queued PR handoff task fingerprint=${fingerprint}`);",
|
|
7021
|
+
"};",
|
|
7022
|
+
"const queueNetworkHandoff = (why) => { upsertTask(why); process.exit(0); };",
|
|
7023
|
+
"const invalidArtifact = (why) => {",
|
|
7024
|
+
" comment(`openloops:pr-handoff=invalid task=${taskId} artifact=${artifactPath} reason=${why}`);",
|
|
7025
|
+
" console.error(`invalid PR handoff artifact: ${why}`);",
|
|
7026
|
+
" process.exit(0);",
|
|
7027
|
+
"};",
|
|
7028
|
+
"const canonicalPath = (path) => {",
|
|
7029
|
+
" try { return realpathSync(path); } catch { return path; }",
|
|
7030
|
+
"};",
|
|
7031
|
+
"if (artifactTaskId && artifactTaskId !== taskId) invalidArtifact(`artifact task id ${artifactTaskId} does not match expected ${taskId}`);",
|
|
7032
|
+
"if (!branch || !commit) invalidArtifact('artifact missing branch or commit');",
|
|
7033
|
+
"const topLevel = run(gitBin, ['-C', repoPath, 'rev-parse', '--show-toplevel']);",
|
|
7034
|
+
"if (topLevel.status !== 0) invalidArtifact(`artifact repoPath is not a git worktree: ${String(topLevel.stderr || topLevel.stdout || topLevel.status).slice(0, 300)}`);",
|
|
7035
|
+
"const actualRoot = canonicalPath(String(topLevel.stdout || '').trim());",
|
|
7036
|
+
"const wantedRoot = canonicalPath(expectedRoot);",
|
|
7037
|
+
"if (actualRoot !== wantedRoot) invalidArtifact(`artifact repo root mismatch: expected ${wantedRoot}, got ${actualRoot}`);",
|
|
7038
|
+
"const currentBranch = run(gitBin, ['-C', repoPath, 'branch', '--show-current']);",
|
|
7039
|
+
"const actualBranch = String(currentBranch.stdout || '').trim();",
|
|
7040
|
+
"if (currentBranch.status !== 0 || !actualBranch) invalidArtifact(`could not resolve current branch for artifact repo: ${String(currentBranch.stderr || currentBranch.stdout || currentBranch.status).slice(0, 300)}`);",
|
|
7041
|
+
"if (expectedBranch && branch !== expectedBranch) invalidArtifact(`artifact branch ${branch} does not match expected ${expectedBranch}`);",
|
|
7042
|
+
"if (branch !== actualBranch) invalidArtifact(`artifact branch ${branch} does not match current worktree branch ${actualBranch}`);",
|
|
7043
|
+
"const resolvedCommit = run(gitBin, ['-C', repoPath, 'rev-parse', '--verify', `${commit}^{commit}`]);",
|
|
7044
|
+
"if (resolvedCommit.status !== 0) invalidArtifact(`artifact commit is not present in repo: ${String(resolvedCommit.stderr || resolvedCommit.stdout || resolvedCommit.status).slice(0, 300)}`);",
|
|
7045
|
+
"commit = String(resolvedCommit.stdout || commit).trim();",
|
|
7046
|
+
"const reachable = run(gitBin, ['-C', repoPath, 'merge-base', '--is-ancestor', commit, 'HEAD']);",
|
|
7047
|
+
"if (reachable.status !== 0) invalidArtifact(`artifact commit ${commit} is not reachable from HEAD`);",
|
|
7048
|
+
"if (prUrl) {",
|
|
7049
|
+
` const viewed = run(ghBin, ['pr', 'view', prUrl, '--json', 'url,headRefName', '--jq', '.url + "\\\\n" + .headRefName']);`,
|
|
7050
|
+
" if (viewed.status !== 0) queueNetworkHandoff(`could not verify existing PR URL: ${String(viewed.stderr || viewed.stdout || viewed.status).slice(0, 300)}`);",
|
|
7051
|
+
" const [verifiedUrl, verifiedHead] = String(viewed.stdout || '').trim().split(/\\r?\\n/);",
|
|
7052
|
+
" if (!verifiedUrl || !/^https?:\\/\\//.test(verifiedUrl)) invalidArtifact('verified PR URL was missing or invalid');",
|
|
7053
|
+
" if (verifiedHead && verifiedHead !== branch) invalidArtifact(`verified PR head ${verifiedHead} does not match artifact branch ${branch}`);",
|
|
7054
|
+
" comment(`openloops:pr-handoff=done task=${taskId} pr=${verifiedUrl} commit=${commit} branch=${branch}`);",
|
|
7055
|
+
" console.log(`PR handoff already complete: ${verifiedUrl}`);",
|
|
7056
|
+
" process.exit(0);",
|
|
7057
|
+
"}",
|
|
7058
|
+
"const push = run(gitBin, ['-C', repoPath, 'push', remote, `${commit}:refs/heads/${branch}`]);",
|
|
7059
|
+
"if (push.status !== 0) {",
|
|
7060
|
+
" upsertTask(`git push failed: ${String(push.stderr || push.stdout || push.status).slice(0, 300)}`);",
|
|
7061
|
+
" process.exit(0);",
|
|
7062
|
+
"}",
|
|
7063
|
+
"const ghRepoArgs = repo ? ['--repo', repo] : [];",
|
|
7064
|
+
"const existing = run(ghBin, ['pr', 'list', ...ghRepoArgs, '--head', branch, '--state', 'all', '--json', 'url', '--jq', '.[0].url']);",
|
|
7065
|
+
"let finalPrUrl = existing.status === 0 ? String(existing.stdout || '').trim() : '';",
|
|
7066
|
+
"if (!finalPrUrl) {",
|
|
7067
|
+
" const created = run(ghBin, ['pr', 'create', ...ghRepoArgs, '--base', base, '--head', branch, '--title', title, '--body', body], { cwd: repoPath });",
|
|
7068
|
+
" if (created.status !== 0) {",
|
|
7069
|
+
" upsertTask(`gh pr create failed: ${String(created.stderr || created.stdout || created.status).slice(0, 300)}`);",
|
|
7070
|
+
" process.exit(0);",
|
|
7071
|
+
" }",
|
|
7072
|
+
" finalPrUrl = String(created.stdout || '').trim().split(/\\r?\\n/).find((line) => /^https?:\\/\\//.test(line)) || String(created.stdout || '').trim();",
|
|
7073
|
+
"}",
|
|
7074
|
+
"comment(`openloops:pr-handoff=done task=${taskId} pr=${finalPrUrl} commit=${commit} branch=${branch}`);",
|
|
7075
|
+
"console.log(`PR handoff complete: ${finalPrUrl}`);",
|
|
7076
|
+
"BUN"
|
|
7077
|
+
].join(`
|
|
7078
|
+
`);
|
|
7079
|
+
}
|
|
7080
|
+
function sourceTaskGateCommand(todosProjectPath, taskId) {
|
|
7081
|
+
return [
|
|
7082
|
+
"set -euo pipefail",
|
|
7083
|
+
`todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(taskId)} >/dev/null`,
|
|
7084
|
+
`printf "source task %s resolved in todos project %s\\n" ${shellQuote2(taskId)} ${shellQuote2(todosProjectPath)}`
|
|
7085
|
+
].join(`
|
|
7086
|
+
`);
|
|
7087
|
+
}
|
|
6559
7088
|
function normalizeWorktreeMode(mode) {
|
|
6560
7089
|
const value = mode ?? "auto";
|
|
6561
7090
|
if (!["auto", "required", "off", "main"].includes(value)) {
|
|
@@ -6571,7 +7100,7 @@ function defaultWorktreeRoot(root) {
|
|
|
6571
7100
|
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
6572
7101
|
}
|
|
6573
7102
|
function gitRootFor(path) {
|
|
6574
|
-
if (!
|
|
7103
|
+
if (!existsSync5(path))
|
|
6575
7104
|
return;
|
|
6576
7105
|
try {
|
|
6577
7106
|
return execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], {
|
|
@@ -6583,7 +7112,7 @@ function gitRootFor(path) {
|
|
|
6583
7112
|
}
|
|
6584
7113
|
}
|
|
6585
7114
|
function gitCommonDirFor(path) {
|
|
6586
|
-
if (!
|
|
7115
|
+
if (!existsSync5(path))
|
|
6587
7116
|
return;
|
|
6588
7117
|
try {
|
|
6589
7118
|
const raw = execFileSync("git", ["-C", path, "rev-parse", "--git-common-dir"], {
|
|
@@ -6795,7 +7324,8 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6795
7324
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
6796
7325
|
},
|
|
6797
7326
|
account: accountForRole(input, role, seed),
|
|
6798
|
-
timeoutMs: agentTimeoutMs(input)
|
|
7327
|
+
timeoutMs: agentTimeoutMs(input),
|
|
7328
|
+
idleTimeoutMs: role === "verifier" ? verifierIdleTimeoutMs(input) : undefined
|
|
6799
7329
|
};
|
|
6800
7330
|
}
|
|
6801
7331
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -6975,7 +7505,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
6975
7505
|
function readCustomTemplateFile(file) {
|
|
6976
7506
|
let parsed;
|
|
6977
7507
|
try {
|
|
6978
|
-
parsed = JSON.parse(
|
|
7508
|
+
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
6979
7509
|
} catch (error) {
|
|
6980
7510
|
const message = error instanceof Error ? error.message : String(error);
|
|
6981
7511
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -7001,7 +7531,7 @@ function assertNoTemplateCollisions(entries) {
|
|
|
7001
7531
|
}
|
|
7002
7532
|
function loadCustomLoopTemplatesRaw() {
|
|
7003
7533
|
const dir = customLoopTemplatesDir();
|
|
7004
|
-
if (!
|
|
7534
|
+
if (!existsSync5(dir))
|
|
7005
7535
|
return [];
|
|
7006
7536
|
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
7007
7537
|
const file = join5(dir, entry.name);
|
|
@@ -7125,7 +7655,7 @@ function importCustomLoopTemplate(file, opts = {}) {
|
|
|
7125
7655
|
const entry = readCustomTemplateFile(source);
|
|
7126
7656
|
const dir = ensureCustomLoopTemplatesDir();
|
|
7127
7657
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
7128
|
-
const replaced =
|
|
7658
|
+
const replaced = existsSync5(destination);
|
|
7129
7659
|
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
7130
7660
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
7131
7661
|
if (replaced) {
|
|
@@ -7221,6 +7751,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
7221
7751
|
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
7222
7752
|
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
7223
7753
|
"Use fresh context. Inspect the task, repository state, commits, tests, and worker evidence. Act as an adversarial reviewer focused on correctness, regressions, missing tests, security, and incomplete requirements.",
|
|
7754
|
+
verifierRuntimeGuidance(input),
|
|
7224
7755
|
"If the work is valid, record verification evidence in todos and mark/leave the task in the correct completed state according to the todos CLI. If it is not valid, add precise follow-up tasks or comments and leave the original task open or blocked with clear evidence.",
|
|
7225
7756
|
"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.",
|
|
7226
7757
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
@@ -7233,10 +7764,24 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
7233
7764
|
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
7234
7765
|
version: 1,
|
|
7235
7766
|
steps: workflowStepsWithWorktree(plan, [
|
|
7767
|
+
{
|
|
7768
|
+
id: "source-task-gate",
|
|
7769
|
+
name: "Source Task Gate",
|
|
7770
|
+
description: "Fail before worker execution when the source todos task is not resolvable.",
|
|
7771
|
+
target: {
|
|
7772
|
+
type: "command",
|
|
7773
|
+
command: "bash",
|
|
7774
|
+
args: ["-lc", sourceTaskGateCommand(todosProjectPath, input.taskId)],
|
|
7775
|
+
cwd: plan.cwd,
|
|
7776
|
+
timeoutMs: 60000
|
|
7777
|
+
},
|
|
7778
|
+
timeoutMs: 60000
|
|
7779
|
+
},
|
|
7236
7780
|
{
|
|
7237
7781
|
id: "worker",
|
|
7238
7782
|
name: "Worker",
|
|
7239
7783
|
description: "Implement the todos task and record evidence.",
|
|
7784
|
+
dependsOn: ["source-task-gate"],
|
|
7240
7785
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7241
7786
|
timeoutMs: agentTimeoutMs(input)
|
|
7242
7787
|
},
|
|
@@ -7277,6 +7822,14 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7277
7822
|
reason: plan.reason
|
|
7278
7823
|
}
|
|
7279
7824
|
};
|
|
7825
|
+
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
7826
|
+
const prHandoffGuidance = input.prHandoff ? [
|
|
7827
|
+
"PR handoff mode is enabled for this lifecycle.",
|
|
7828
|
+
`If implementation and validation pass but git push or gh PR creation fails because DNS, network, or sandbox policy blocks GitHub access, write a JSON artifact to: ${handoffArtifactPath}`,
|
|
7829
|
+
"The artifact must include taskId, worktreePath or repoPath, branch, base, commit, remote, validation, and error. Include githubRepo, title, and body when known.",
|
|
7830
|
+
"After writing the artifact, comment the source task with the artifact path and exit without marking the task done. The bounded PR handoff step will push/open the PR or queue a network-enabled handoff task without rerunning implementation."
|
|
7831
|
+
].join(`
|
|
7832
|
+
`) : "";
|
|
7280
7833
|
const shared = [
|
|
7281
7834
|
worktreePrompt(plan),
|
|
7282
7835
|
`Todos project path: ${todosProjectPath}`,
|
|
@@ -7286,7 +7839,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7286
7839
|
"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.",
|
|
7287
7840
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
7288
7841
|
"",
|
|
7289
|
-
`Task context JSON: ${compactJson(taskContext)}
|
|
7842
|
+
`Task context JSON: ${compactJson(taskContext)}`,
|
|
7843
|
+
prHandoffGuidance
|
|
7290
7844
|
].join(`
|
|
7291
7845
|
`);
|
|
7292
7846
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
@@ -7330,7 +7884,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7330
7884
|
"const latestMarker = markers.at(-1)?.state;",
|
|
7331
7885
|
"const blockers = [];",
|
|
7332
7886
|
"if (blockedStatuses.has(status)) blockers.push(`task status is ${status}`);",
|
|
7333
|
-
"for (const tag of ['no-auto', 'manual', 'manual-required', 'approval-required']) {",
|
|
7887
|
+
"for (const tag of ['no-auto', 'manual', 'manual-required', 'approval-required', 'blocked', 'completed', 'done', 'cancelled', 'canceled', 'failed', 'archived']) {",
|
|
7334
7888
|
" if (tags.has(tag)) blockers.push(`task has disallowed tag ${tag}`);",
|
|
7335
7889
|
"}",
|
|
7336
7890
|
"for (const [key, source] of records.entries()) {",
|
|
@@ -7359,7 +7913,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7359
7913
|
"Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
|
|
7360
7914
|
`If the task should not proceed automatically, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
|
|
7361
7915
|
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
7362
|
-
"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."
|
|
7916
|
+
"The deterministic triage gate will stop later steps unless the latest triage marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state."
|
|
7363
7917
|
].join(`
|
|
7364
7918
|
`);
|
|
7365
7919
|
const plannerPrompt = [
|
|
@@ -7372,7 +7926,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7372
7926
|
"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.",
|
|
7373
7927
|
`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`,
|
|
7374
7928
|
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
7375
|
-
"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."
|
|
7929
|
+
"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/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state."
|
|
7376
7930
|
].join(`
|
|
7377
7931
|
`);
|
|
7378
7932
|
const workerPrompt = [
|
|
@@ -7382,8 +7936,9 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7382
7936
|
shared,
|
|
7383
7937
|
`- Claim/start if appropriate: todos --project ${todosProjectPath} start ${input.taskId}`,
|
|
7384
7938
|
"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.",
|
|
7939
|
+
input.prHandoff ? `When only GitHub network access is blocked after a successful commit/validation, record the handoff artifact at ${handoffArtifactPath} instead of repeatedly retrying push/PR creation.` : undefined,
|
|
7385
7940
|
"Do not mark the task complete in the worker step; the verifier step owns completion after independent validation."
|
|
7386
|
-
].join(`
|
|
7941
|
+
].filter(Boolean).join(`
|
|
7387
7942
|
`);
|
|
7388
7943
|
const verifierPrompt = [
|
|
7389
7944
|
`/goal Verify todos task ${input.taskId} after the full lifecycle worker step.`,
|
|
@@ -7393,75 +7948,108 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7393
7948
|
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
7394
7949
|
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
7395
7950
|
"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.",
|
|
7951
|
+
verifierRuntimeGuidance(input),
|
|
7952
|
+
input.prHandoff ? `If ${handoffArtifactPath} exists and there is no PR URL evidence, verify that the PR handoff step queued or completed a bounded handoff; leave the original task open or blocked until PR evidence is recorded.` : undefined,
|
|
7396
7953
|
"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.",
|
|
7397
7954
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks."
|
|
7398
|
-
].join(`
|
|
7955
|
+
].filter(Boolean).join(`
|
|
7399
7956
|
`);
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
{
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
timeoutMs:
|
|
7957
|
+
const steps = [
|
|
7958
|
+
{
|
|
7959
|
+
id: "source-task-gate",
|
|
7960
|
+
name: "Source Task Gate",
|
|
7961
|
+
description: "Fail before lifecycle agents execute when the source todos task is not resolvable.",
|
|
7962
|
+
target: {
|
|
7963
|
+
type: "command",
|
|
7964
|
+
command: "bash",
|
|
7965
|
+
args: ["-lc", sourceTaskGateCommand(todosProjectPath, input.taskId)],
|
|
7966
|
+
cwd: plan.cwd,
|
|
7967
|
+
timeoutMs: 60000
|
|
7411
7968
|
},
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7969
|
+
timeoutMs: 60000
|
|
7970
|
+
},
|
|
7971
|
+
{
|
|
7972
|
+
id: "triage",
|
|
7973
|
+
name: "Triage",
|
|
7974
|
+
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
7975
|
+
dependsOn: ["source-task-gate"],
|
|
7976
|
+
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
7977
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7978
|
+
},
|
|
7979
|
+
{
|
|
7980
|
+
id: "triage-gate",
|
|
7981
|
+
name: "Triage Gate",
|
|
7982
|
+
description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
|
|
7983
|
+
dependsOn: ["triage"],
|
|
7984
|
+
target: {
|
|
7985
|
+
type: "command",
|
|
7986
|
+
command: "bash",
|
|
7987
|
+
args: ["-lc", gateCommand("triage")],
|
|
7988
|
+
cwd: plan.cwd,
|
|
7424
7989
|
timeoutMs: 2 * 60000
|
|
7425
7990
|
},
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7991
|
+
timeoutMs: 2 * 60000
|
|
7992
|
+
},
|
|
7993
|
+
{
|
|
7994
|
+
id: "planner",
|
|
7995
|
+
name: "Planner",
|
|
7996
|
+
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
7997
|
+
dependsOn: ["triage-gate"],
|
|
7998
|
+
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
7999
|
+
timeoutMs: agentTimeoutMs(input)
|
|
8000
|
+
},
|
|
8001
|
+
{
|
|
8002
|
+
id: "planner-gate",
|
|
8003
|
+
name: "Planner Gate",
|
|
8004
|
+
description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
|
|
8005
|
+
dependsOn: ["planner"],
|
|
8006
|
+
target: {
|
|
8007
|
+
type: "command",
|
|
8008
|
+
command: "bash",
|
|
8009
|
+
args: ["-lc", gateCommand("planner")],
|
|
8010
|
+
cwd: plan.cwd,
|
|
7446
8011
|
timeoutMs: 2 * 60000
|
|
7447
8012
|
},
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
8013
|
+
timeoutMs: 2 * 60000
|
|
8014
|
+
},
|
|
8015
|
+
{
|
|
8016
|
+
id: "worker",
|
|
8017
|
+
name: "Worker",
|
|
8018
|
+
description: "Implement the todos task according to triage and planner evidence.",
|
|
8019
|
+
dependsOn: ["planner-gate"],
|
|
8020
|
+
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
8021
|
+
timeoutMs: agentTimeoutMs(input)
|
|
8022
|
+
}
|
|
8023
|
+
];
|
|
8024
|
+
if (input.prHandoff) {
|
|
8025
|
+
steps.push({
|
|
8026
|
+
id: "pr-handoff",
|
|
8027
|
+
name: "PR Handoff",
|
|
8028
|
+
description: "Push/open a PR from a worker handoff artifact or queue a bounded network handoff task.",
|
|
8029
|
+
dependsOn: ["worker"],
|
|
8030
|
+
target: {
|
|
8031
|
+
type: "command",
|
|
8032
|
+
command: "bash",
|
|
8033
|
+
args: ["-lc", prHandoffCommand(input, plan, todosProjectPath)],
|
|
8034
|
+
cwd: plan.cwd,
|
|
8035
|
+
timeoutMs: 2 * 60000
|
|
7455
8036
|
},
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
8037
|
+
timeoutMs: 2 * 60000
|
|
8038
|
+
});
|
|
8039
|
+
}
|
|
8040
|
+
steps.push({
|
|
8041
|
+
id: "verifier",
|
|
8042
|
+
name: "Verifier",
|
|
8043
|
+
description: "Adversarially verify worker output and update todos.",
|
|
8044
|
+
dependsOn: [input.prHandoff ? "pr-handoff" : "worker"],
|
|
8045
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
8046
|
+
timeoutMs: agentTimeoutMs(input)
|
|
8047
|
+
});
|
|
8048
|
+
return {
|
|
8049
|
+
name: `task-lifecycle-${input.taskId.slice(0, 8)}-triage-plan-worker-verifier`,
|
|
8050
|
+
description: `Full task lifecycle workflow for ${taskLabel(input)}`,
|
|
8051
|
+
version: 1,
|
|
8052
|
+
steps: workflowStepsWithWorktree(plan, steps)
|
|
7465
8053
|
};
|
|
7466
8054
|
}
|
|
7467
8055
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
@@ -7513,6 +8101,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
7513
8101
|
"You are the verifier agent for an event-triggered OpenLoops workflow.",
|
|
7514
8102
|
worktreePrompt(plan),
|
|
7515
8103
|
"Use fresh context. Inspect the event, repository/project state, worker evidence, tests, and any created tasks or notes. Act as an adversarial reviewer focused on correctness, regressions, security, missing evidence, and incomplete requirements.",
|
|
8104
|
+
verifierRuntimeGuidance(input),
|
|
7516
8105
|
"If the work is valid, record verification evidence in the relevant local system. If it is not valid, add precise follow-up tasks/comments and leave the event handling state open or blocked with clear evidence.",
|
|
7517
8106
|
"",
|
|
7518
8107
|
`Event context JSON: ${compactJson(eventContext)}`,
|
|
@@ -7567,6 +8156,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7567
8156
|
"You are the verifier step for a bounded OpenLoops agent workflow.",
|
|
7568
8157
|
worktreePrompt(plan),
|
|
7569
8158
|
"Use fresh context. Review the worker result for correctness, regressions, missing tests, safety, runaway-agent risk, output bounds, and incomplete evidence.",
|
|
8159
|
+
verifierRuntimeGuidance(input),
|
|
7570
8160
|
"If valid, record verification evidence. If invalid, create precise follow-up tasks or comments and leave the original work open. Do not make broad unrelated changes."
|
|
7571
8161
|
].join(`
|
|
7572
8162
|
`);
|
|
@@ -7617,7 +8207,8 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7617
8207
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
7618
8208
|
worktreeRoot: values.worktreeRoot,
|
|
7619
8209
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7620
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8210
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8211
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7621
8212
|
};
|
|
7622
8213
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
7623
8214
|
const taskId = values.taskId ?? "";
|
|
@@ -7651,10 +8242,12 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7651
8242
|
permissionMode: values.permissionMode,
|
|
7652
8243
|
sandbox: values.sandbox,
|
|
7653
8244
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
8245
|
+
prHandoff: booleanVar(values.prHandoff),
|
|
7654
8246
|
worktreeMode: values.worktreeMode ?? "required",
|
|
7655
8247
|
worktreeRoot: values.worktreeRoot,
|
|
7656
8248
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7657
8249
|
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8250
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout),
|
|
7658
8251
|
eventId: values.eventId,
|
|
7659
8252
|
eventType: values.eventType
|
|
7660
8253
|
});
|
|
@@ -7779,6 +8372,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7779
8372
|
worktreeRoot: values.worktreeRoot,
|
|
7780
8373
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7781
8374
|
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8375
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout),
|
|
7782
8376
|
eventId: values.eventId,
|
|
7783
8377
|
eventType: values.eventType
|
|
7784
8378
|
});
|
|
@@ -7811,7 +8405,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7811
8405
|
worktreeMode: values.worktreeMode,
|
|
7812
8406
|
worktreeRoot: values.worktreeRoot,
|
|
7813
8407
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7814
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8408
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8409
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7815
8410
|
});
|
|
7816
8411
|
}
|
|
7817
8412
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -7839,7 +8434,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7839
8434
|
worktreeMode: values.worktreeMode,
|
|
7840
8435
|
worktreeRoot: values.worktreeRoot,
|
|
7841
8436
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7842
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8437
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8438
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7843
8439
|
});
|
|
7844
8440
|
}
|
|
7845
8441
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -7946,7 +8542,7 @@ function normalizeWorkflowForStorage(body, context) {
|
|
|
7946
8542
|
function workflowBodyFromFile(file, fallbackName, context) {
|
|
7947
8543
|
try {
|
|
7948
8544
|
const resolved = resolve3(file);
|
|
7949
|
-
return workflowBodyFromJson(JSON.parse(
|
|
8545
|
+
return workflowBodyFromJson(JSON.parse(readFileSync5(resolved, "utf8")), fallbackName, { baseDir: dirname4(resolved) });
|
|
7950
8546
|
} catch (error) {
|
|
7951
8547
|
validationFailed(error, context);
|
|
7952
8548
|
}
|
|
@@ -8072,6 +8668,10 @@ function timeoutDuration(raw, label) {
|
|
|
8072
8668
|
throw new Error(`${label} must be a duration greater than zero, or none/unlimited`);
|
|
8073
8669
|
return value;
|
|
8074
8670
|
}
|
|
8671
|
+
function idleTimeoutDuration(raw, label) {
|
|
8672
|
+
const value = timeoutDuration(raw, label);
|
|
8673
|
+
return value === null ? undefined : value;
|
|
8674
|
+
}
|
|
8075
8675
|
function parsePolicy(opts) {
|
|
8076
8676
|
const catchUp = opts.catchUp ?? "latest";
|
|
8077
8677
|
if (!["none", "latest", "all"].includes(catchUp))
|
|
@@ -8088,6 +8688,45 @@ function parsePolicy(opts) {
|
|
|
8088
8688
|
leaseMs: positiveDuration(opts.lease, "--lease")
|
|
8089
8689
|
};
|
|
8090
8690
|
}
|
|
8691
|
+
function durationLabel(ms) {
|
|
8692
|
+
if (!ms || !Number.isFinite(ms))
|
|
8693
|
+
return "";
|
|
8694
|
+
const units = [
|
|
8695
|
+
[7 * 24 * 60 * 60 * 1000, "w"],
|
|
8696
|
+
[24 * 60 * 60 * 1000, "d"],
|
|
8697
|
+
[60 * 60 * 1000, "h"],
|
|
8698
|
+
[60 * 1000, "m"],
|
|
8699
|
+
[1000, "s"]
|
|
8700
|
+
];
|
|
8701
|
+
for (const [unitMs, label] of units) {
|
|
8702
|
+
if (ms % unitMs === 0)
|
|
8703
|
+
return `${ms / unitMs}${label}`;
|
|
8704
|
+
}
|
|
8705
|
+
return `${ms}ms`;
|
|
8706
|
+
}
|
|
8707
|
+
function scheduleLabel(schedule) {
|
|
8708
|
+
if (schedule.type === "once")
|
|
8709
|
+
return `once:${schedule.at}`;
|
|
8710
|
+
if (schedule.type === "interval")
|
|
8711
|
+
return `every:${durationLabel(schedule.everyMs) || `${schedule.everyMs}ms`}`;
|
|
8712
|
+
if (schedule.type === "cron")
|
|
8713
|
+
return `cron:${schedule.expression}`;
|
|
8714
|
+
return schedule.minIntervalMs ? `dynamic:min-${durationLabel(schedule.minIntervalMs) || `${schedule.minIntervalMs}ms`}` : "dynamic";
|
|
8715
|
+
}
|
|
8716
|
+
function targetLabel(target) {
|
|
8717
|
+
if (target.type === "command")
|
|
8718
|
+
return `runs command ${target.command}`;
|
|
8719
|
+
if (target.type === "agent")
|
|
8720
|
+
return `runs ${target.provider} agent${target.cwd ? ` in ${target.cwd}` : ""}`;
|
|
8721
|
+
return `runs workflow ${target.workflowId}`;
|
|
8722
|
+
}
|
|
8723
|
+
function defaultLoopDescription(name, schedule, target) {
|
|
8724
|
+
return [
|
|
8725
|
+
`Why: keep ${name} running as an OpenLoops scheduled automation.`,
|
|
8726
|
+
`How: ${targetLabel(target)} on cadence ${scheduleLabel(schedule)}.`,
|
|
8727
|
+
"Outcome: record each run, status, retries, and evidence in OpenLoops for operator review."
|
|
8728
|
+
].join(" ");
|
|
8729
|
+
}
|
|
8091
8730
|
function baseCreateInput(name, opts, target) {
|
|
8092
8731
|
const schedule = parseSchedule({
|
|
8093
8732
|
at: typeof opts.at === "string" ? opts.at : undefined,
|
|
@@ -8103,9 +8742,10 @@ function baseCreateInput(name, opts, target) {
|
|
|
8103
8742
|
retryDelay: typeof opts.retryDelay === "string" ? opts.retryDelay : undefined,
|
|
8104
8743
|
lease: typeof opts.lease === "string" ? opts.lease : undefined
|
|
8105
8744
|
});
|
|
8745
|
+
const explicitDescription = typeof opts.description === "string" && opts.description.trim() ? opts.description : undefined;
|
|
8106
8746
|
return {
|
|
8107
8747
|
name,
|
|
8108
|
-
description:
|
|
8748
|
+
description: explicitDescription ?? defaultLoopDescription(name, schedule, target),
|
|
8109
8749
|
schedule,
|
|
8110
8750
|
target,
|
|
8111
8751
|
goal: goalFromOpts(opts),
|
|
@@ -8228,7 +8868,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
8228
8868
|
return {
|
|
8229
8869
|
ok: result.status === 0,
|
|
8230
8870
|
status: result.status,
|
|
8231
|
-
stdout:
|
|
8871
|
+
stdout: readFileSync5(stdoutPath, "utf8"),
|
|
8232
8872
|
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
8233
8873
|
error: result.error ? String(result.error.message || result.error) : ""
|
|
8234
8874
|
};
|
|
@@ -8269,10 +8909,10 @@ function routeCursorsPath() {
|
|
|
8269
8909
|
}
|
|
8270
8910
|
function readRouteCursors() {
|
|
8271
8911
|
const path = routeCursorsPath();
|
|
8272
|
-
if (!
|
|
8912
|
+
if (!existsSync6(path))
|
|
8273
8913
|
return {};
|
|
8274
8914
|
try {
|
|
8275
|
-
const parsed = JSON.parse(
|
|
8915
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8276
8916
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
8277
8917
|
} catch {
|
|
8278
8918
|
return {};
|
|
@@ -8436,31 +9076,46 @@ function taskEventField(data, keys) {
|
|
|
8436
9076
|
}
|
|
8437
9077
|
return;
|
|
8438
9078
|
}
|
|
8439
|
-
function
|
|
9079
|
+
function objectField2(value) {
|
|
8440
9080
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
8441
9081
|
}
|
|
8442
9082
|
function nestedObject(input, key) {
|
|
8443
|
-
return
|
|
9083
|
+
return objectField2(input[key]);
|
|
8444
9084
|
}
|
|
8445
9085
|
function taskEventRecords(data, metadata) {
|
|
8446
9086
|
const records = [data];
|
|
8447
9087
|
const dataTask = nestedObject(data, "task");
|
|
8448
|
-
if (dataTask)
|
|
9088
|
+
if (dataTask) {
|
|
8449
9089
|
records.push(dataTask);
|
|
9090
|
+
const dataTaskMetadata = nestedObject(dataTask, "metadata");
|
|
9091
|
+
if (dataTaskMetadata)
|
|
9092
|
+
records.push(dataTaskMetadata);
|
|
9093
|
+
}
|
|
8450
9094
|
const dataPayload = nestedObject(data, "payload");
|
|
8451
9095
|
if (dataPayload) {
|
|
8452
9096
|
records.push(dataPayload);
|
|
9097
|
+
const payloadMetadata = nestedObject(dataPayload, "metadata");
|
|
9098
|
+
if (payloadMetadata)
|
|
9099
|
+
records.push(payloadMetadata);
|
|
8453
9100
|
const payloadTask = nestedObject(dataPayload, "task");
|
|
8454
|
-
if (payloadTask)
|
|
9101
|
+
if (payloadTask) {
|
|
8455
9102
|
records.push(payloadTask);
|
|
9103
|
+
const payloadTaskMetadata = nestedObject(payloadTask, "metadata");
|
|
9104
|
+
if (payloadTaskMetadata)
|
|
9105
|
+
records.push(payloadTaskMetadata);
|
|
9106
|
+
}
|
|
8456
9107
|
}
|
|
8457
9108
|
const dataMetadata = nestedObject(data, "metadata");
|
|
8458
9109
|
if (dataMetadata)
|
|
8459
9110
|
records.push(dataMetadata);
|
|
8460
9111
|
records.push(metadata);
|
|
8461
9112
|
const metadataTask = nestedObject(metadata, "task");
|
|
8462
|
-
if (metadataTask)
|
|
9113
|
+
if (metadataTask) {
|
|
8463
9114
|
records.push(metadataTask);
|
|
9115
|
+
const metadataTaskMetadata = nestedObject(metadataTask, "metadata");
|
|
9116
|
+
if (metadataTaskMetadata)
|
|
9117
|
+
records.push(metadataTaskMetadata);
|
|
9118
|
+
}
|
|
8464
9119
|
const metadataAutomation = nestedObject(metadata, "automation");
|
|
8465
9120
|
if (metadataAutomation)
|
|
8466
9121
|
records.push(metadataAutomation);
|
|
@@ -8472,6 +9127,15 @@ function booleanLike(value) {
|
|
|
8472
9127
|
function hasTruthyField(records, keys) {
|
|
8473
9128
|
return records.some((record) => keys.some((key) => booleanLike(record[key])));
|
|
8474
9129
|
}
|
|
9130
|
+
function firstTruthyField(records, keys) {
|
|
9131
|
+
for (const record of records) {
|
|
9132
|
+
for (const key of keys) {
|
|
9133
|
+
if (booleanLike(record[key]))
|
|
9134
|
+
return key;
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
return;
|
|
9138
|
+
}
|
|
8475
9139
|
function automationRecords(data, metadata) {
|
|
8476
9140
|
const records = [];
|
|
8477
9141
|
const dataAutomation = nestedObject(data, "automation");
|
|
@@ -8481,14 +9145,26 @@ function automationRecords(data, metadata) {
|
|
|
8481
9145
|
const dataTaskAutomation = dataTask ? nestedObject(dataTask, "automation") : undefined;
|
|
8482
9146
|
if (dataTaskAutomation)
|
|
8483
9147
|
records.push(dataTaskAutomation);
|
|
9148
|
+
const dataTaskMetadata = dataTask ? nestedObject(dataTask, "metadata") : undefined;
|
|
9149
|
+
const dataTaskMetadataAutomation = dataTaskMetadata ? nestedObject(dataTaskMetadata, "automation") : undefined;
|
|
9150
|
+
if (dataTaskMetadataAutomation)
|
|
9151
|
+
records.push(dataTaskMetadataAutomation);
|
|
8484
9152
|
const dataPayload = nestedObject(data, "payload");
|
|
8485
9153
|
const payloadAutomation = dataPayload ? nestedObject(dataPayload, "automation") : undefined;
|
|
8486
9154
|
if (payloadAutomation)
|
|
8487
9155
|
records.push(payloadAutomation);
|
|
9156
|
+
const payloadMetadata = dataPayload ? nestedObject(dataPayload, "metadata") : undefined;
|
|
9157
|
+
const payloadMetadataAutomation = payloadMetadata ? nestedObject(payloadMetadata, "automation") : undefined;
|
|
9158
|
+
if (payloadMetadataAutomation)
|
|
9159
|
+
records.push(payloadMetadataAutomation);
|
|
8488
9160
|
const payloadTask = dataPayload ? nestedObject(dataPayload, "task") : undefined;
|
|
8489
9161
|
const payloadTaskAutomation = payloadTask ? nestedObject(payloadTask, "automation") : undefined;
|
|
8490
9162
|
if (payloadTaskAutomation)
|
|
8491
9163
|
records.push(payloadTaskAutomation);
|
|
9164
|
+
const payloadTaskMetadata = payloadTask ? nestedObject(payloadTask, "metadata") : undefined;
|
|
9165
|
+
const payloadTaskMetadataAutomation = payloadTaskMetadata ? nestedObject(payloadTaskMetadata, "automation") : undefined;
|
|
9166
|
+
if (payloadTaskMetadataAutomation)
|
|
9167
|
+
records.push(payloadTaskMetadataAutomation);
|
|
8492
9168
|
const dataMetadata = nestedObject(data, "metadata");
|
|
8493
9169
|
const dataMetadataAutomation = dataMetadata ? nestedObject(dataMetadata, "automation") : undefined;
|
|
8494
9170
|
if (dataMetadataAutomation)
|
|
@@ -8500,9 +9176,13 @@ function automationRecords(data, metadata) {
|
|
|
8500
9176
|
const metadataTaskAutomation = metadataTask ? nestedObject(metadataTask, "automation") : undefined;
|
|
8501
9177
|
if (metadataTaskAutomation)
|
|
8502
9178
|
records.push(metadataTaskAutomation);
|
|
9179
|
+
const metadataTaskMetadata = metadataTask ? nestedObject(metadataTask, "metadata") : undefined;
|
|
9180
|
+
const metadataTaskMetadataAutomation = metadataTaskMetadata ? nestedObject(metadataTaskMetadata, "automation") : undefined;
|
|
9181
|
+
if (metadataTaskMetadataAutomation)
|
|
9182
|
+
records.push(metadataTaskMetadataAutomation);
|
|
8503
9183
|
return records;
|
|
8504
9184
|
}
|
|
8505
|
-
function
|
|
9185
|
+
function tagsFromValue2(value) {
|
|
8506
9186
|
if (Array.isArray(value))
|
|
8507
9187
|
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
8508
9188
|
if (typeof value === "string")
|
|
@@ -8512,40 +9192,381 @@ function tagsFromValue(value) {
|
|
|
8512
9192
|
function taskEventTags(records) {
|
|
8513
9193
|
const tags = new Set;
|
|
8514
9194
|
for (const record of records) {
|
|
8515
|
-
for (const tag of
|
|
9195
|
+
for (const tag of tagsFromValue2(record.tags ?? record.task_tags ?? record.taskTags))
|
|
8516
9196
|
tags.add(tag);
|
|
8517
9197
|
}
|
|
8518
9198
|
return [...tags];
|
|
8519
9199
|
}
|
|
9200
|
+
var ROUTE_DISALLOWED_TASK_TAGS = new Set([
|
|
9201
|
+
"no-auto",
|
|
9202
|
+
"manual",
|
|
9203
|
+
"manual-required",
|
|
9204
|
+
"approval-required",
|
|
9205
|
+
"blocked",
|
|
9206
|
+
"completed",
|
|
9207
|
+
"done",
|
|
9208
|
+
"cancelled",
|
|
9209
|
+
"canceled",
|
|
9210
|
+
"failed",
|
|
9211
|
+
"archived"
|
|
9212
|
+
]);
|
|
9213
|
+
var ROUTE_MANUAL_GATE_FIELDS = [
|
|
9214
|
+
"no_auto",
|
|
9215
|
+
"noAuto",
|
|
9216
|
+
"manual",
|
|
9217
|
+
"manual_required",
|
|
9218
|
+
"manualRequired",
|
|
9219
|
+
"requires_approval",
|
|
9220
|
+
"requiresApproval",
|
|
9221
|
+
"approval_required",
|
|
9222
|
+
"approvalRequired"
|
|
9223
|
+
];
|
|
8520
9224
|
function taskRouteEligibility(data, metadata) {
|
|
8521
9225
|
const records = taskEventRecords(data, metadata);
|
|
9226
|
+
const automation = automationRecords(data, metadata);
|
|
8522
9227
|
const tags = taskEventTags(records);
|
|
8523
|
-
const hasRouteOptIn = tags.includes("auto:route") || hasTruthyField(records, ["route_enabled", "routeEnabled", "automation_allowed", "automationAllowed"]) || hasTruthyField(
|
|
9228
|
+
const hasRouteOptIn = tags.includes("auto:route") || hasTruthyField(records, ["route_enabled", "routeEnabled", "automation_allowed", "automationAllowed"]) || hasTruthyField(automation, ["allowed"]);
|
|
8524
9229
|
if (!hasRouteOptIn)
|
|
8525
9230
|
return { eligible: false, reason: "missing explicit route opt-in", tags };
|
|
8526
9231
|
const status = taskEventField(data, ["status", "task_status", "taskStatus"])?.toLowerCase();
|
|
8527
9232
|
if (status && ["blocked", "completed", "done", "cancelled", "canceled", "failed", "archived"].includes(status)) {
|
|
8528
9233
|
return { eligible: false, reason: `task status is not routable: ${status}`, tags };
|
|
8529
9234
|
}
|
|
8530
|
-
const disallowedTags = tags.filter((tag) =>
|
|
9235
|
+
const disallowedTags = tags.filter((tag) => ROUTE_DISALLOWED_TASK_TAGS.has(tag.toLowerCase()));
|
|
8531
9236
|
if (disallowedTags.length)
|
|
8532
9237
|
return { eligible: false, reason: `task has disallowed tag: ${disallowedTags[0]}`, tags };
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
"manual",
|
|
8537
|
-
"manual_required",
|
|
8538
|
-
"manualRequired",
|
|
8539
|
-
"requires_approval",
|
|
8540
|
-
"requiresApproval",
|
|
8541
|
-
"approval_required",
|
|
8542
|
-
"approvalRequired"
|
|
8543
|
-
])) {
|
|
8544
|
-
return { eligible: false, reason: "task metadata requires manual or approval-gated handling", tags };
|
|
9238
|
+
const manualGateField = firstTruthyField([...records, ...automation], ROUTE_MANUAL_GATE_FIELDS);
|
|
9239
|
+
if (manualGateField) {
|
|
9240
|
+
return { eligible: false, reason: `task metadata requires manual or approval-gated handling: ${manualGateField}`, tags };
|
|
8545
9241
|
}
|
|
8546
9242
|
return { eligible: true, tags };
|
|
8547
9243
|
}
|
|
8548
|
-
|
|
9244
|
+
var PR_AUTHOR_FIELDS = [
|
|
9245
|
+
"github_author",
|
|
9246
|
+
"githubAuthor",
|
|
9247
|
+
"github_pr_author",
|
|
9248
|
+
"githubPrAuthor",
|
|
9249
|
+
"pr_author",
|
|
9250
|
+
"prAuthor",
|
|
9251
|
+
"pull_request_author",
|
|
9252
|
+
"pullRequestAuthor",
|
|
9253
|
+
"author_login",
|
|
9254
|
+
"authorLogin"
|
|
9255
|
+
];
|
|
9256
|
+
var PR_REVIEWER_FIELDS = [
|
|
9257
|
+
"github_reviewer",
|
|
9258
|
+
"githubReviewer",
|
|
9259
|
+
"github_review_actor",
|
|
9260
|
+
"githubReviewActor",
|
|
9261
|
+
"github_review_login",
|
|
9262
|
+
"githubReviewLogin",
|
|
9263
|
+
"github_actor",
|
|
9264
|
+
"githubActor",
|
|
9265
|
+
"reviewer_login",
|
|
9266
|
+
"reviewerLogin",
|
|
9267
|
+
"review_actor",
|
|
9268
|
+
"reviewActor",
|
|
9269
|
+
"merge_actor",
|
|
9270
|
+
"mergeActor"
|
|
9271
|
+
];
|
|
9272
|
+
var PR_REVIEWER_POOL_FIELDS = [
|
|
9273
|
+
"github_reviewer_pool",
|
|
9274
|
+
"githubReviewerPool",
|
|
9275
|
+
"github_review_pool",
|
|
9276
|
+
"githubReviewPool",
|
|
9277
|
+
"github_reviewers",
|
|
9278
|
+
"githubReviewers",
|
|
9279
|
+
"reviewer_pool",
|
|
9280
|
+
"reviewerPool",
|
|
9281
|
+
"reviewers"
|
|
9282
|
+
];
|
|
9283
|
+
var PR_REVIEW_REQUIRED_FIELDS = [
|
|
9284
|
+
"github_review_required",
|
|
9285
|
+
"githubReviewRequired",
|
|
9286
|
+
"pr_review_required",
|
|
9287
|
+
"prReviewRequired",
|
|
9288
|
+
"review_required",
|
|
9289
|
+
"reviewRequired",
|
|
9290
|
+
"requires_non_author_review",
|
|
9291
|
+
"requiresNonAuthorReview",
|
|
9292
|
+
"branch_protection_review_required",
|
|
9293
|
+
"branchProtectionReviewRequired"
|
|
9294
|
+
];
|
|
9295
|
+
function githubLogin(value) {
|
|
9296
|
+
if (!value?.trim())
|
|
9297
|
+
return;
|
|
9298
|
+
const login = value.trim().replace(/^@/, "");
|
|
9299
|
+
return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(login) ? login : undefined;
|
|
9300
|
+
}
|
|
9301
|
+
function githubLogins(values) {
|
|
9302
|
+
const seen = new Set;
|
|
9303
|
+
const logins = [];
|
|
9304
|
+
for (const value of values) {
|
|
9305
|
+
const login = githubLogin(value);
|
|
9306
|
+
if (!login)
|
|
9307
|
+
continue;
|
|
9308
|
+
const key = login.toLowerCase();
|
|
9309
|
+
if (seen.has(key))
|
|
9310
|
+
continue;
|
|
9311
|
+
seen.add(key);
|
|
9312
|
+
logins.push(login);
|
|
9313
|
+
}
|
|
9314
|
+
return logins;
|
|
9315
|
+
}
|
|
9316
|
+
function routeEvidenceText(records) {
|
|
9317
|
+
const fields = [
|
|
9318
|
+
"title",
|
|
9319
|
+
"task_title",
|
|
9320
|
+
"taskTitle",
|
|
9321
|
+
"description",
|
|
9322
|
+
"body",
|
|
9323
|
+
"reason",
|
|
9324
|
+
"message",
|
|
9325
|
+
"fingerprint",
|
|
9326
|
+
"reviewDecision",
|
|
9327
|
+
"review_decision",
|
|
9328
|
+
"mergeStateStatus",
|
|
9329
|
+
"merge_state_status"
|
|
9330
|
+
];
|
|
9331
|
+
const values = [];
|
|
9332
|
+
for (const record of records) {
|
|
9333
|
+
for (const field of fields)
|
|
9334
|
+
values.push(...recordFieldValues(record, field));
|
|
9335
|
+
values.push(...tagsFromValue2(record.tags ?? record.task_tags ?? record.taskTags));
|
|
9336
|
+
}
|
|
9337
|
+
return values.join(`
|
|
9338
|
+
`);
|
|
9339
|
+
}
|
|
9340
|
+
function authorFromPrText(text) {
|
|
9341
|
+
const patterns = [
|
|
9342
|
+
/\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
|
|
9343
|
+
/\bPR\s*#?\d+\s+author\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
|
|
9344
|
+
/\bgithub\s+author\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i
|
|
9345
|
+
];
|
|
9346
|
+
for (const pattern of patterns) {
|
|
9347
|
+
const match = pattern.exec(text);
|
|
9348
|
+
const login = githubLogin(match?.[1]);
|
|
9349
|
+
if (login)
|
|
9350
|
+
return login;
|
|
9351
|
+
}
|
|
9352
|
+
return;
|
|
9353
|
+
}
|
|
9354
|
+
function prReviewRoutingDecision(data, metadata, opts) {
|
|
9355
|
+
const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
|
|
9356
|
+
const text = routeEvidenceText(records);
|
|
9357
|
+
const signals = [];
|
|
9358
|
+
const hasPrReference = /github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+/i.test(text) || /\bgithub-pr:[^\s]+#\d+/i.test(text) || /\bpull request\b/i.test(text) || /\bpr\s*#?\d+\b/i.test(text);
|
|
9359
|
+
const reviewRequiredByField = hasTruthyField(records, PR_REVIEW_REQUIRED_FIELDS);
|
|
9360
|
+
const reviewRequiredByText = /reviewdecision\s*[:=]\s*review_required/i.test(text) || /\breview_required\b/i.test(text) || /\breview required\b/i.test(text) || /\brequires?\s+\d+\s+approving review/i.test(text) || /\bbranch protection\b[\s\S]{0,160}\bapproving review/i.test(text);
|
|
9361
|
+
const mergeBlockedByText = /mergestatestatus\s*[:=]\s*blocked/i.test(text) || /\bmerge state status\b[\s:=]+blocked/i.test(text);
|
|
9362
|
+
const approvalIntent = /\b(approve|approval|merge|review)\b/i.test(text);
|
|
9363
|
+
if (reviewRequiredByField)
|
|
9364
|
+
signals.push("review-required-field");
|
|
9365
|
+
if (reviewRequiredByText)
|
|
9366
|
+
signals.push("review-required-text");
|
|
9367
|
+
if (mergeBlockedByText)
|
|
9368
|
+
signals.push("merge-blocked-text");
|
|
9369
|
+
if (approvalIntent && hasPrReference)
|
|
9370
|
+
signals.push("pr-approval-intent");
|
|
9371
|
+
const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
|
|
9372
|
+
if (!required)
|
|
9373
|
+
return { required: false, allowed: true, reviewers: [], signals };
|
|
9374
|
+
const author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
|
|
9375
|
+
const reviewers = githubLogins([
|
|
9376
|
+
opts.githubReviewer,
|
|
9377
|
+
...splitList(opts.githubReviewerPool) ?? [],
|
|
9378
|
+
...PR_REVIEWER_FIELDS.flatMap((field) => routeFieldValues(records, field)),
|
|
9379
|
+
...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field))
|
|
9380
|
+
]);
|
|
9381
|
+
const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
|
|
9382
|
+
if (!author) {
|
|
9383
|
+
return {
|
|
9384
|
+
required: true,
|
|
9385
|
+
allowed: false,
|
|
9386
|
+
reason: "PR approval/merge route requires PR author evidence before selecting a non-author GitHub reviewer",
|
|
9387
|
+
reviewers,
|
|
9388
|
+
signals
|
|
9389
|
+
};
|
|
9390
|
+
}
|
|
9391
|
+
if (!reviewers.length) {
|
|
9392
|
+
return {
|
|
9393
|
+
required: true,
|
|
9394
|
+
allowed: false,
|
|
9395
|
+
reason: "PR approval/merge route requires --github-reviewer, --github-reviewer-pool, or task metadata github_reviewer/github_reviewer_pool with a login different from the PR author",
|
|
9396
|
+
author,
|
|
9397
|
+
reviewers,
|
|
9398
|
+
signals
|
|
9399
|
+
};
|
|
9400
|
+
}
|
|
9401
|
+
if (!selectedReviewer) {
|
|
9402
|
+
return {
|
|
9403
|
+
required: true,
|
|
9404
|
+
allowed: false,
|
|
9405
|
+
reason: `PR approval/merge route reviewer candidates match PR author ${author}; self-review is not routable`,
|
|
9406
|
+
author,
|
|
9407
|
+
reviewers,
|
|
9408
|
+
signals
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
return {
|
|
9412
|
+
required: true,
|
|
9413
|
+
allowed: true,
|
|
9414
|
+
author,
|
|
9415
|
+
reviewers,
|
|
9416
|
+
selectedReviewer,
|
|
9417
|
+
signals
|
|
9418
|
+
};
|
|
9419
|
+
}
|
|
9420
|
+
var SUPPORTED_AGENT_PROVIDERS = new Set(["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"]);
|
|
9421
|
+
var PROVIDER_HINT_FIELDS = ["provider_hint", "providerHint", "route_provider", "routeProvider", "agent_provider", "agentProvider"];
|
|
9422
|
+
var AUTH_PROFILE_HINT_FIELDS = ["auth_profile", "authProfile", "codewith_profile", "codewithProfile", "profile"];
|
|
9423
|
+
var AUTH_PROFILE_POOL_HINT_FIELDS = ["auth_profile_pool", "authProfilePool", "codewith_profile_pool", "codewithProfilePool", "profile_pool", "profilePool"];
|
|
9424
|
+
var ACCOUNT_HINT_FIELDS = ["account", "account_profile", "accountProfile", "openaccounts_profile", "openaccountsProfile"];
|
|
9425
|
+
var ACCOUNT_POOL_HINT_FIELDS = ["account_pool", "accountPool", "openaccounts_pool", "openaccountsPool"];
|
|
9426
|
+
var ACCOUNT_TOOL_HINT_FIELDS = ["account_tool", "accountTool", "openaccounts_tool", "openaccountsTool"];
|
|
9427
|
+
function canonicalRouteField(value) {
|
|
9428
|
+
return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
9429
|
+
}
|
|
9430
|
+
function normalizeAgentProvider(value, source) {
|
|
9431
|
+
const provider = value?.trim().toLowerCase();
|
|
9432
|
+
if (provider && SUPPORTED_AGENT_PROVIDERS.has(provider))
|
|
9433
|
+
return provider;
|
|
9434
|
+
const expected = [...SUPPORTED_AGENT_PROVIDERS].join(", ");
|
|
9435
|
+
throw new Error(`unsupported provider${provider ? ` "${provider}"` : ""} from ${source}; expected one of: ${expected}`);
|
|
9436
|
+
}
|
|
9437
|
+
function stringValuesFromUnknown(value) {
|
|
9438
|
+
if (Array.isArray(value))
|
|
9439
|
+
return value.flatMap((entry) => stringValuesFromUnknown(entry));
|
|
9440
|
+
if (typeof value === "string")
|
|
9441
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
9442
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
9443
|
+
return [String(value)];
|
|
9444
|
+
return [];
|
|
9445
|
+
}
|
|
9446
|
+
function recordFieldValues(record, field) {
|
|
9447
|
+
const expected = canonicalRouteField(field);
|
|
9448
|
+
const values = [];
|
|
9449
|
+
for (const [key, value] of Object.entries(record)) {
|
|
9450
|
+
if (canonicalRouteField(key) === expected)
|
|
9451
|
+
values.push(...stringValuesFromUnknown(value));
|
|
9452
|
+
}
|
|
9453
|
+
return values;
|
|
9454
|
+
}
|
|
9455
|
+
function routeFieldValues(records, field) {
|
|
9456
|
+
if (canonicalRouteField(field) === "tags")
|
|
9457
|
+
return taskEventTags(records);
|
|
9458
|
+
return records.flatMap((record) => recordFieldValues(record, field));
|
|
9459
|
+
}
|
|
9460
|
+
function firstRouteField(records, fields) {
|
|
9461
|
+
for (const field of fields) {
|
|
9462
|
+
const value = routeFieldValues(records, field).find(Boolean);
|
|
9463
|
+
if (value)
|
|
9464
|
+
return value;
|
|
9465
|
+
}
|
|
9466
|
+
return;
|
|
9467
|
+
}
|
|
9468
|
+
function routeFieldList(records, fields) {
|
|
9469
|
+
for (const field of fields) {
|
|
9470
|
+
const values = routeFieldValues(records, field);
|
|
9471
|
+
if (values.length)
|
|
9472
|
+
return values;
|
|
9473
|
+
}
|
|
9474
|
+
return;
|
|
9475
|
+
}
|
|
9476
|
+
function parseProviderRoutingRule(raw) {
|
|
9477
|
+
const rule = raw.trim();
|
|
9478
|
+
const selectorIndex = rule.indexOf(":");
|
|
9479
|
+
if (selectorIndex <= 0)
|
|
9480
|
+
throw new Error(`invalid --provider-rule "${raw}", expected field=value:provider[:profile1,profile2]`);
|
|
9481
|
+
const selector = rule.slice(0, selectorIndex).trim();
|
|
9482
|
+
const route = rule.slice(selectorIndex + 1).trim();
|
|
9483
|
+
const equalsIndex = selector.indexOf("=");
|
|
9484
|
+
if (equalsIndex <= 0)
|
|
9485
|
+
throw new Error(`invalid --provider-rule "${raw}", expected field=value:provider[:profile1,profile2]`);
|
|
9486
|
+
const field = selector.slice(0, equalsIndex).trim();
|
|
9487
|
+
const value = selector.slice(equalsIndex + 1).trim();
|
|
9488
|
+
const providerIndex = route.indexOf(":");
|
|
9489
|
+
const providerRaw = providerIndex >= 0 ? route.slice(0, providerIndex).trim() : route;
|
|
9490
|
+
const profilesRaw = providerIndex >= 0 ? route.slice(providerIndex + 1).trim() : undefined;
|
|
9491
|
+
if (!field || !value || !providerRaw)
|
|
9492
|
+
throw new Error(`invalid --provider-rule "${raw}", expected field=value:provider[:profile1,profile2]`);
|
|
9493
|
+
return {
|
|
9494
|
+
raw,
|
|
9495
|
+
field,
|
|
9496
|
+
value,
|
|
9497
|
+
provider: normalizeAgentProvider(providerRaw, `provider rule ${field}=${value}`),
|
|
9498
|
+
profiles: splitList(profilesRaw)
|
|
9499
|
+
};
|
|
9500
|
+
}
|
|
9501
|
+
function parseProviderRoutingRules(values) {
|
|
9502
|
+
return (values ?? []).map(parseProviderRoutingRule);
|
|
9503
|
+
}
|
|
9504
|
+
function providerRuleMatches(rule, records) {
|
|
9505
|
+
const expected = rule.value.trim().toLowerCase();
|
|
9506
|
+
return routeFieldValues(records, rule.field).some((value) => value.trim().toLowerCase() === expected);
|
|
9507
|
+
}
|
|
9508
|
+
function accountRef(profile, tool) {
|
|
9509
|
+
return profile ? { profile, tool } : undefined;
|
|
9510
|
+
}
|
|
9511
|
+
function accountRefs(profiles, tool) {
|
|
9512
|
+
return profiles?.length ? profiles.map((profile) => ({ profile, tool })) : undefined;
|
|
9513
|
+
}
|
|
9514
|
+
function providerRoutingPublic(decision) {
|
|
9515
|
+
return {
|
|
9516
|
+
provider: decision.provider,
|
|
9517
|
+
source: decision.source,
|
|
9518
|
+
reason: decision.reason,
|
|
9519
|
+
rule: decision.rule,
|
|
9520
|
+
authProfile: decision.authProfile,
|
|
9521
|
+
authProfilePool: decision.authProfilePool,
|
|
9522
|
+
account: decision.account,
|
|
9523
|
+
accountPool: decision.accountPool
|
|
9524
|
+
};
|
|
9525
|
+
}
|
|
9526
|
+
function resolveProviderRouting(data, metadata, opts) {
|
|
9527
|
+
const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
|
|
9528
|
+
const matchedRule = parseProviderRoutingRules(opts.providerRule).find((rule) => providerRuleMatches(rule, records));
|
|
9529
|
+
const metadataProvider = matchedRule || opts.provider ? undefined : firstRouteField(records, PROVIDER_HINT_FIELDS);
|
|
9530
|
+
const provider = matchedRule ? matchedRule.provider : opts.provider ? normalizeAgentProvider(opts.provider, "--provider") : metadataProvider ? normalizeAgentProvider(metadataProvider, "task metadata provider hint") : "codewith";
|
|
9531
|
+
const source = matchedRule ? "rule" : opts.provider ? "option" : metadataProvider ? "metadata" : "default";
|
|
9532
|
+
const metadataProfilePool = routeFieldList(records, provider === "codewith" ? AUTH_PROFILE_POOL_HINT_FIELDS : [...ACCOUNT_POOL_HINT_FIELDS, ...AUTH_PROFILE_POOL_HINT_FIELDS]);
|
|
9533
|
+
const metadataProfile = firstRouteField(records, provider === "codewith" ? AUTH_PROFILE_HINT_FIELDS : [...ACCOUNT_HINT_FIELDS, ...AUTH_PROFILE_HINT_FIELDS]);
|
|
9534
|
+
const profilePool = matchedRule?.profiles ?? metadataProfilePool;
|
|
9535
|
+
const profile = metadataProfile;
|
|
9536
|
+
const metadataAccountTool = firstRouteField(records, ACCOUNT_TOOL_HINT_FIELDS);
|
|
9537
|
+
const accountTool = opts.accountTool ?? (matchedRule?.profiles ? undefined : metadataAccountTool) ?? (provider === "codewith" ? undefined : provider);
|
|
9538
|
+
const hasResolvedAccountPool = provider !== "codewith" && Boolean((profilePool?.length ?? 0) || profile);
|
|
9539
|
+
if (opts.accountTool && !opts.account && !opts.accountPool && !opts.triageAccount && !opts.plannerAccount && !opts.workerAccount && !opts.verifierAccount && !hasResolvedAccountPool) {
|
|
9540
|
+
throw new Error("--account-tool requires --account, --account-pool, --triage-account, --planner-account, --worker-account, --verifier-account, metadata account hints, or provider-rule profiles");
|
|
9541
|
+
}
|
|
9542
|
+
if (provider === "codewith") {
|
|
9543
|
+
const cliAuthProfilePool = splitList(opts.authProfilePool);
|
|
9544
|
+
return {
|
|
9545
|
+
provider,
|
|
9546
|
+
source,
|
|
9547
|
+
reason: matchedRule ? `matched provider rule ${matchedRule.field}=${matchedRule.value}` : metadataProvider ? "selected provider from task metadata" : opts.provider ? "selected provider from --provider" : "selected default provider",
|
|
9548
|
+
rule: matchedRule,
|
|
9549
|
+
authProfile: opts.authProfile ?? profile,
|
|
9550
|
+
authProfilePool: matchedRule?.profiles ?? cliAuthProfilePool ?? (opts.authProfile ? undefined : metadataProfilePool),
|
|
9551
|
+
account: accountRef(opts.account, opts.accountTool),
|
|
9552
|
+
accountPool: accountPoolFromOpts(opts)
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
9555
|
+
if (opts.authProfile || opts.authProfilePool) {
|
|
9556
|
+
throw new Error(`--auth-profile and --auth-profile-pool are supported only for --provider codewith; use OpenAccounts account profiles for ${provider}`);
|
|
9557
|
+
}
|
|
9558
|
+
const cliAccountPool = accountPoolFromOpts(opts);
|
|
9559
|
+
const account = opts.account ? accountRef(opts.account, opts.accountTool ?? provider) : accountRef(profile, accountTool);
|
|
9560
|
+
return {
|
|
9561
|
+
provider,
|
|
9562
|
+
source,
|
|
9563
|
+
reason: matchedRule ? `matched provider rule ${matchedRule.field}=${matchedRule.value}` : metadataProvider ? "selected provider from task metadata" : opts.provider ? "selected provider from --provider" : "selected default provider",
|
|
9564
|
+
rule: matchedRule,
|
|
9565
|
+
account,
|
|
9566
|
+
accountPool: matchedRule?.profiles ? accountRefs(matchedRule.profiles, accountTool) : cliAccountPool ?? (opts.account ? undefined : accountRefs(metadataProfilePool, accountTool))
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9569
|
+
function skippedDrainTask(task, event, reason, extra = {}) {
|
|
8549
9570
|
const taskId = taskField(task, ["id", "task_id", "taskId"]) ?? event?.subject ?? "unknown";
|
|
8550
9571
|
return {
|
|
8551
9572
|
kind: "skipped",
|
|
@@ -8554,7 +9575,8 @@ function skippedDrainTask(task, event, reason) {
|
|
|
8554
9575
|
reason,
|
|
8555
9576
|
taskId,
|
|
8556
9577
|
event,
|
|
8557
|
-
routeError: true
|
|
9578
|
+
routeError: true,
|
|
9579
|
+
...extra
|
|
8558
9580
|
},
|
|
8559
9581
|
human: `skipped task ${taskId}: ${reason}`
|
|
8560
9582
|
};
|
|
@@ -8562,6 +9584,32 @@ function skippedDrainTask(task, event, reason) {
|
|
|
8562
9584
|
function isSkippableDrainRouteError(message) {
|
|
8563
9585
|
return message.startsWith("worktreeMode=required but projectPath is not an existing git repository:");
|
|
8564
9586
|
}
|
|
9587
|
+
function todosMutationSummary(result) {
|
|
9588
|
+
return {
|
|
9589
|
+
ok: result.ok,
|
|
9590
|
+
status: result.status,
|
|
9591
|
+
error: result.ok ? undefined : redact(result.stderr || result.error || "todos mutation failed", 320)
|
|
9592
|
+
};
|
|
9593
|
+
}
|
|
9594
|
+
function markInvalidDrainTaskNonRouteable(todosProject, task, reason) {
|
|
9595
|
+
const taskId = taskField(task, ["id", "task_id", "taskId"]);
|
|
9596
|
+
if (!taskId)
|
|
9597
|
+
return { attempted: false, reason: "task id missing" };
|
|
9598
|
+
const comment = `OpenLoops route blocked for task ${taskId}: ${reason}. Added no-auto and removed auto:route so route drains do not repeatedly route this task until its project path is fixed.`;
|
|
9599
|
+
const commentResult = runLocalCommand("todos", ["--project", todosProject, "comment", taskId, comment], { timeoutMs: 30000 });
|
|
9600
|
+
const tagResult = runLocalCommand("todos", ["--project", todosProject, "tag", taskId, "no-auto"], { timeoutMs: 30000 });
|
|
9601
|
+
const untagResult = runLocalCommand("todos", ["--project", todosProject, "untag", taskId, "auto:route"], { timeoutMs: 30000 });
|
|
9602
|
+
const ok = commentResult.ok && tagResult.ok && untagResult.ok;
|
|
9603
|
+
return {
|
|
9604
|
+
ok,
|
|
9605
|
+
attempted: true,
|
|
9606
|
+
taskId,
|
|
9607
|
+
error: ok ? undefined : "one or more source task updates failed; inspect per-command results",
|
|
9608
|
+
comment: todosMutationSummary(commentResult),
|
|
9609
|
+
tagNoAuto: todosMutationSummary(tagResult),
|
|
9610
|
+
untagAutoRoute: todosMutationSummary(untagResult)
|
|
9611
|
+
};
|
|
9612
|
+
}
|
|
8565
9613
|
function generatedRouteSandboxPreflight(workflow) {
|
|
8566
9614
|
const checks = [];
|
|
8567
9615
|
for (const step of workflow.steps) {
|
|
@@ -8687,6 +9735,28 @@ var TODOS_TASK_ROUTE_TEMPLATE_IDS = new Set([
|
|
|
8687
9735
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
8688
9736
|
TASK_LIFECYCLE_TEMPLATE_ID
|
|
8689
9737
|
]);
|
|
9738
|
+
var UNCLEARED_ROUTE_WORK_ITEM_STATUSES = new Set([
|
|
9739
|
+
"admitted",
|
|
9740
|
+
"running",
|
|
9741
|
+
"succeeded",
|
|
9742
|
+
"failed",
|
|
9743
|
+
"dead_letter",
|
|
9744
|
+
"cancelled"
|
|
9745
|
+
]);
|
|
9746
|
+
function isUnclearedRouteWorkItem(item) {
|
|
9747
|
+
return UNCLEARED_ROUTE_WORK_ITEM_STATUSES.has(item.status);
|
|
9748
|
+
}
|
|
9749
|
+
function isExistingGitProjectPath(path) {
|
|
9750
|
+
const result = spawnSync5("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
|
|
9751
|
+
return result.status === 0;
|
|
9752
|
+
}
|
|
9753
|
+
function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
|
|
9754
|
+
if ((opts.worktreeMode ?? "auto") !== "required")
|
|
9755
|
+
return;
|
|
9756
|
+
if (!isExistingGitProjectPath(projectPath)) {
|
|
9757
|
+
throw new Error(`worktreeMode=required but projectPath is not an existing git repository: ${projectPath}`);
|
|
9758
|
+
}
|
|
9759
|
+
}
|
|
8690
9760
|
function todosTaskRouteTemplateId(opts) {
|
|
8691
9761
|
const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
|
|
8692
9762
|
if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
|
|
@@ -8695,7 +9765,7 @@ function todosTaskRouteTemplateId(opts) {
|
|
|
8695
9765
|
return id;
|
|
8696
9766
|
}
|
|
8697
9767
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8698
|
-
const raw = opts.eventJson ?? (opts.eventFile ?
|
|
9768
|
+
const raw = opts.eventJson ?? (opts.eventFile ? readFileSync5(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
|
|
8699
9769
|
const event = JSON.parse(raw);
|
|
8700
9770
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
8701
9771
|
throw new Error("event JSON must be an object");
|
|
@@ -8748,8 +9818,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8748
9818
|
const store2 = new Store;
|
|
8749
9819
|
try {
|
|
8750
9820
|
const existingItem = store2.findWorkflowWorkItem("todos-task", idempotencyKey);
|
|
8751
|
-
if (existingItem
|
|
8752
|
-
const existingLoop = store2.getLoop(existingItem.loopId);
|
|
9821
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
9822
|
+
const existingLoop = existingItem.loopId ? store2.getLoop(existingItem.loopId) : undefined;
|
|
8753
9823
|
const existingWorkflow = existingItem.workflowId ? store2.getWorkflow(existingItem.workflowId) : undefined;
|
|
8754
9824
|
const existingInvocation = store2.getWorkflowInvocation(existingItem.invocationId);
|
|
8755
9825
|
return {
|
|
@@ -8771,12 +9841,27 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8771
9841
|
store2.close();
|
|
8772
9842
|
}
|
|
8773
9843
|
}
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
9844
|
+
validateRequiredRouteWorktreeProjectPath(opts, projectPath);
|
|
9845
|
+
const prReviewRouting = prReviewRoutingDecision(data, metadata, opts);
|
|
9846
|
+
if (prReviewRouting.required && !prReviewRouting.allowed) {
|
|
9847
|
+
return {
|
|
9848
|
+
kind: "skipped",
|
|
9849
|
+
value: {
|
|
9850
|
+
skipped: true,
|
|
9851
|
+
reason: prReviewRouting.reason,
|
|
9852
|
+
event,
|
|
9853
|
+
taskId,
|
|
9854
|
+
routeError: true,
|
|
9855
|
+
prReviewRouting
|
|
9856
|
+
},
|
|
9857
|
+
human: `skipped task ${taskId}: ${prReviewRouting.reason}`
|
|
9858
|
+
};
|
|
9859
|
+
}
|
|
9860
|
+
const providerRouting = resolveProviderRouting(data, metadata, opts);
|
|
9861
|
+
const provider = providerRouting.provider;
|
|
8777
9862
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
8778
9863
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8779
|
-
const authProfile = providerAuthProfileFromOpts({ authProfile:
|
|
9864
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: providerRouting.authProfile }, provider);
|
|
8780
9865
|
const templateId = todosTaskRouteTemplateId(opts);
|
|
8781
9866
|
const workflowInput = {
|
|
8782
9867
|
taskId,
|
|
@@ -8787,13 +9872,13 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8787
9872
|
projectGroup,
|
|
8788
9873
|
provider,
|
|
8789
9874
|
authProfile,
|
|
8790
|
-
authProfilePool:
|
|
9875
|
+
authProfilePool: providerRouting.authProfilePool,
|
|
8791
9876
|
triageAuthProfile: opts.triageAuthProfile,
|
|
8792
9877
|
plannerAuthProfile: opts.plannerAuthProfile,
|
|
8793
9878
|
workerAuthProfile: opts.workerAuthProfile,
|
|
8794
9879
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
8795
|
-
account:
|
|
8796
|
-
accountPool:
|
|
9880
|
+
account: providerRouting.account,
|
|
9881
|
+
accountPool: providerRouting.accountPool,
|
|
8797
9882
|
triageAccount: roleAccountFromOpts(opts, opts.triageAccount),
|
|
8798
9883
|
plannerAccount: roleAccountFromOpts(opts, opts.plannerAccount),
|
|
8799
9884
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
@@ -8803,12 +9888,14 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8803
9888
|
agent: opts.agent,
|
|
8804
9889
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
8805
9890
|
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9891
|
+
verifierIdleTimeoutMs: idleTimeoutDuration(opts.verifierIdleTimeout, "--verifier-idle-timeout"),
|
|
8806
9892
|
permissionMode,
|
|
8807
9893
|
sandbox,
|
|
8808
9894
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8809
9895
|
worktreeMode: opts.worktreeMode ?? "auto",
|
|
8810
9896
|
worktreeRoot: opts.worktreeRoot,
|
|
8811
9897
|
worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
|
|
9898
|
+
prHandoff: templateId === TASK_LIFECYCLE_TEMPLATE_ID ? Boolean(opts.prHandoff) : false,
|
|
8812
9899
|
eventId: event.id,
|
|
8813
9900
|
eventType: event.type,
|
|
8814
9901
|
todosProjectPath: opts.todosProject
|
|
@@ -8844,7 +9931,10 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8844
9931
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
8845
9932
|
permissions: permissionMode,
|
|
8846
9933
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8847
|
-
|
|
9934
|
+
prHandoff: templateId === TASK_LIFECYCLE_TEMPLATE_ID ? Boolean(opts.prHandoff) : false,
|
|
9935
|
+
accountPolicy: providerRouting.authProfilePool?.length || providerRouting.accountPool?.length ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
|
|
9936
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9937
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
8848
9938
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
8849
9939
|
},
|
|
8850
9940
|
outputPolicy: {
|
|
@@ -8883,7 +9973,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8883
9973
|
}, {}) : undefined;
|
|
8884
9974
|
return {
|
|
8885
9975
|
kind: "created",
|
|
8886
|
-
value: { deduped: false, idempotencyKey, event, invocation: invocationInput, workItem: workItemInput, workflow: workflowBody, loop: loopInput, throttle, sandboxPreflight, preflight },
|
|
9976
|
+
value: { deduped: false, idempotencyKey, event, providerRouting: providerRoutingPublic(providerRouting), prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined, invocation: invocationInput, workItem: workItemInput, workflow: workflowBody, loop: loopInput, throttle, sandboxPreflight, preflight },
|
|
8887
9977
|
human: `dry-run ${loopName}`
|
|
8888
9978
|
};
|
|
8889
9979
|
}
|
|
@@ -8899,8 +9989,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8899
9989
|
const outcome = store.writeTransaction(() => {
|
|
8900
9990
|
const invocation = store.createWorkflowInvocation(invocationInput);
|
|
8901
9991
|
const existingItem = store.findWorkflowWorkItem("todos-task", idempotencyKey);
|
|
8902
|
-
if (existingItem
|
|
8903
|
-
const existingLoop = store.getLoop(existingItem.loopId);
|
|
9992
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
9993
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
8904
9994
|
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
8905
9995
|
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
8906
9996
|
}
|
|
@@ -8911,6 +10001,11 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8911
10001
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
8912
10002
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
8913
10003
|
});
|
|
10004
|
+
const requeue = workItem.attempts > 0 && workItem.status === "queued" ? {
|
|
10005
|
+
previousWorkItemId: workItem.id,
|
|
10006
|
+
previousAttempts: workItem.attempts,
|
|
10007
|
+
reason: workItem.lastReason
|
|
10008
|
+
} : undefined;
|
|
8914
10009
|
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
8915
10010
|
if (throttle && !throttle.allowed)
|
|
8916
10011
|
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
@@ -8927,7 +10022,15 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8927
10022
|
}
|
|
8928
10023
|
});
|
|
8929
10024
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by todos-task route" });
|
|
8930
|
-
return {
|
|
10025
|
+
return {
|
|
10026
|
+
kind: "created",
|
|
10027
|
+
invocation: refreshedInvocation,
|
|
10028
|
+
workItem: admitted,
|
|
10029
|
+
workflow,
|
|
10030
|
+
loop,
|
|
10031
|
+
throttle,
|
|
10032
|
+
requeue: requeue ? { ...requeue, attempt: admitted.attempts, newWorkflowId: workflow.id, newLoopId: loop.id } : undefined
|
|
10033
|
+
};
|
|
8931
10034
|
});
|
|
8932
10035
|
if (outcome.kind === "deduped") {
|
|
8933
10036
|
return {
|
|
@@ -8956,6 +10059,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8956
10059
|
event,
|
|
8957
10060
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8958
10061
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
10062
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
10063
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
8959
10064
|
throttle: outcome.throttle,
|
|
8960
10065
|
workflow: workflowBody,
|
|
8961
10066
|
loop: loopInput
|
|
@@ -8971,8 +10076,11 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8971
10076
|
event,
|
|
8972
10077
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8973
10078
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
10079
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
10080
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
8974
10081
|
workflow: publicWorkflow(outcome.workflow),
|
|
8975
10082
|
loop: publicLoop(outcome.loop),
|
|
10083
|
+
requeue: outcome.requeue,
|
|
8976
10084
|
throttle: outcome.throttle,
|
|
8977
10085
|
sandboxPreflight,
|
|
8978
10086
|
preflight
|
|
@@ -8996,12 +10104,11 @@ function routeGenericEvent(event, opts) {
|
|
|
8996
10104
|
const workflowName = `${opts.namePrefix ?? "event:generic"}:${source}:${type}:${eventSuffix}:workflow`;
|
|
8997
10105
|
const loopName = `${opts.namePrefix ?? "event:generic"}:${source}:${type}:${eventSuffix}:run`;
|
|
8998
10106
|
const idempotencyKey = `generic-event:${event.source}:${event.type}:${event.id}`;
|
|
8999
|
-
const
|
|
9000
|
-
|
|
9001
|
-
throw new Error("unsupported provider");
|
|
10107
|
+
const providerRouting = resolveProviderRouting(data, metadata, opts);
|
|
10108
|
+
const provider = providerRouting.provider;
|
|
9002
10109
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
9003
10110
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
9004
|
-
const authProfile = providerAuthProfileFromOpts({ authProfile:
|
|
10111
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: providerRouting.authProfile }, provider);
|
|
9005
10112
|
let workflowBody = renderEventWorkerVerifierWorkflow({
|
|
9006
10113
|
eventId: event.id,
|
|
9007
10114
|
eventType: event.type,
|
|
@@ -9014,11 +10121,11 @@ function routeGenericEvent(event, opts) {
|
|
|
9014
10121
|
projectGroup,
|
|
9015
10122
|
provider,
|
|
9016
10123
|
authProfile,
|
|
9017
|
-
authProfilePool:
|
|
10124
|
+
authProfilePool: providerRouting.authProfilePool,
|
|
9018
10125
|
workerAuthProfile: opts.workerAuthProfile,
|
|
9019
10126
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
9020
|
-
account:
|
|
9021
|
-
accountPool:
|
|
10127
|
+
account: providerRouting.account,
|
|
10128
|
+
accountPool: providerRouting.accountPool,
|
|
9022
10129
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
9023
10130
|
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
9024
10131
|
model: opts.model,
|
|
@@ -9026,6 +10133,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9026
10133
|
agent: opts.agent,
|
|
9027
10134
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9028
10135
|
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
10136
|
+
verifierIdleTimeoutMs: idleTimeoutDuration(opts.verifierIdleTimeout, "--verifier-idle-timeout"),
|
|
9029
10137
|
permissionMode,
|
|
9030
10138
|
sandbox,
|
|
9031
10139
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -9041,6 +10149,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9041
10149
|
event: event.id
|
|
9042
10150
|
});
|
|
9043
10151
|
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
10152
|
+
const hasExplicitRoleAccount = Boolean(opts.workerAuthProfile || opts.verifierAuthProfile || opts.workerAccount || opts.verifierAccount);
|
|
9044
10153
|
const invocationInput = {
|
|
9045
10154
|
templateId: "event-worker-verifier",
|
|
9046
10155
|
sourceRef: {
|
|
@@ -9062,7 +10171,8 @@ function routeGenericEvent(event, opts) {
|
|
|
9062
10171
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
9063
10172
|
permissions: permissionMode,
|
|
9064
10173
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
9065
|
-
accountPolicy:
|
|
10174
|
+
accountPolicy: providerRouting.authProfilePool?.length || providerRouting.accountPool?.length ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
|
|
10175
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9066
10176
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
9067
10177
|
},
|
|
9068
10178
|
outputPolicy: {
|
|
@@ -9101,7 +10211,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9101
10211
|
}, {}) : undefined;
|
|
9102
10212
|
return {
|
|
9103
10213
|
kind: "created",
|
|
9104
|
-
value: { event, idempotencyKey, invocation: invocationInput, workItem: workItemInput, workflow: workflowBody, loop: loopInput, throttle, sandboxPreflight, preflight },
|
|
10214
|
+
value: { event, idempotencyKey, providerRouting: providerRoutingPublic(providerRouting), invocation: invocationInput, workItem: workItemInput, workflow: workflowBody, loop: loopInput, throttle, sandboxPreflight, preflight },
|
|
9105
10215
|
human: `dry-run ${loopName}`
|
|
9106
10216
|
};
|
|
9107
10217
|
}
|
|
@@ -9117,8 +10227,8 @@ function routeGenericEvent(event, opts) {
|
|
|
9117
10227
|
const outcome = store.writeTransaction(() => {
|
|
9118
10228
|
const invocation = store.createWorkflowInvocation(invocationInput);
|
|
9119
10229
|
const existingItem = store.findWorkflowWorkItem("generic-event", idempotencyKey);
|
|
9120
|
-
if (existingItem
|
|
9121
|
-
const existingLoop = store.getLoop(existingItem.loopId);
|
|
10230
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
10231
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
9122
10232
|
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
9123
10233
|
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
9124
10234
|
}
|
|
@@ -9129,6 +10239,11 @@ function routeGenericEvent(event, opts) {
|
|
|
9129
10239
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
9130
10240
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
9131
10241
|
});
|
|
10242
|
+
const requeue = workItem.attempts > 0 && workItem.status === "queued" ? {
|
|
10243
|
+
previousWorkItemId: workItem.id,
|
|
10244
|
+
previousAttempts: workItem.attempts,
|
|
10245
|
+
reason: workItem.lastReason
|
|
10246
|
+
} : undefined;
|
|
9132
10247
|
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
9133
10248
|
if (throttle && !throttle.allowed)
|
|
9134
10249
|
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
@@ -9145,7 +10260,15 @@ function routeGenericEvent(event, opts) {
|
|
|
9145
10260
|
}
|
|
9146
10261
|
});
|
|
9147
10262
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by generic-event route" });
|
|
9148
|
-
return {
|
|
10263
|
+
return {
|
|
10264
|
+
kind: "created",
|
|
10265
|
+
invocation: refreshedInvocation,
|
|
10266
|
+
workItem: admitted,
|
|
10267
|
+
workflow,
|
|
10268
|
+
loop,
|
|
10269
|
+
throttle,
|
|
10270
|
+
requeue: requeue ? { ...requeue, attempt: admitted.attempts, newWorkflowId: workflow.id, newLoopId: loop.id } : undefined
|
|
10271
|
+
};
|
|
9149
10272
|
});
|
|
9150
10273
|
if (outcome.kind === "deduped") {
|
|
9151
10274
|
return {
|
|
@@ -9155,6 +10278,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9155
10278
|
idempotencyKey,
|
|
9156
10279
|
dedupedBy: "work-item",
|
|
9157
10280
|
event,
|
|
10281
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9158
10282
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9159
10283
|
workItem: publicWorkflowWorkItem(outcome.existingItem),
|
|
9160
10284
|
workflow: outcome.existingWorkflow ? publicWorkflow(outcome.existingWorkflow) : undefined,
|
|
@@ -9172,6 +10296,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9172
10296
|
reason: outcome.throttle.reason,
|
|
9173
10297
|
idempotencyKey,
|
|
9174
10298
|
event,
|
|
10299
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9175
10300
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9176
10301
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
9177
10302
|
throttle: outcome.throttle,
|
|
@@ -9187,10 +10312,12 @@ function routeGenericEvent(event, opts) {
|
|
|
9187
10312
|
deduped: false,
|
|
9188
10313
|
idempotencyKey,
|
|
9189
10314
|
event,
|
|
10315
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9190
10316
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9191
10317
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
9192
10318
|
workflow: publicWorkflow(outcome.workflow),
|
|
9193
10319
|
loop: publicLoop(outcome.loop),
|
|
10320
|
+
requeue: outcome.requeue,
|
|
9194
10321
|
throttle: outcome.throttle,
|
|
9195
10322
|
sandboxPreflight,
|
|
9196
10323
|
preflight
|
|
@@ -9221,14 +10348,14 @@ function taskDescriptionProjectPath(task) {
|
|
|
9221
10348
|
return match?.[1]?.trim();
|
|
9222
10349
|
}
|
|
9223
10350
|
function taskProjectPath(task) {
|
|
9224
|
-
const metadata =
|
|
10351
|
+
const metadata = objectField2(task.metadata) ?? {};
|
|
9225
10352
|
return taskField(task, ["project_path", "projectPath"]) ?? taskEventField(metadata, ["project_path", "projectPath", "project_canonical_path"]) ?? taskDescriptionProjectPath(task) ?? taskField(task, ["working_dir", "workingDir", "cwd"]) ?? taskEventField(metadata, ["working_dir", "workingDir", "cwd"]);
|
|
9226
10353
|
}
|
|
9227
10354
|
function taskDrainEvent(task) {
|
|
9228
10355
|
const taskId = taskField(task, ["id", "task_id", "taskId"]);
|
|
9229
10356
|
if (!taskId)
|
|
9230
10357
|
throw new Error("todos ready returned a task without an id");
|
|
9231
|
-
const metadata =
|
|
10358
|
+
const metadata = objectField2(task.metadata) ?? {};
|
|
9232
10359
|
const workingDir = taskProjectPath(task);
|
|
9233
10360
|
const data = {
|
|
9234
10361
|
...task,
|
|
@@ -9236,7 +10363,7 @@ function taskDrainEvent(task) {
|
|
|
9236
10363
|
title: taskField(task, ["title"]),
|
|
9237
10364
|
description: taskField(task, ["description", "body"]),
|
|
9238
10365
|
status: taskField(task, ["status"]),
|
|
9239
|
-
tags:
|
|
10366
|
+
tags: tagsFromValue2(task.tags),
|
|
9240
10367
|
metadata
|
|
9241
10368
|
};
|
|
9242
10369
|
if (workingDir) {
|
|
@@ -9264,10 +10391,12 @@ function taskDrainEvent(task) {
|
|
|
9264
10391
|
}
|
|
9265
10392
|
function compactDrainResult(result) {
|
|
9266
10393
|
const value = result.value;
|
|
9267
|
-
const event =
|
|
9268
|
-
const loop =
|
|
9269
|
-
const workflow =
|
|
9270
|
-
const throttle =
|
|
10394
|
+
const event = objectField2(value.event);
|
|
10395
|
+
const loop = objectField2(value.loop);
|
|
10396
|
+
const workflow = objectField2(value.workflow);
|
|
10397
|
+
const throttle = objectField2(value.throttle);
|
|
10398
|
+
const requeue = objectField2(value.requeue);
|
|
10399
|
+
const providerRouting = objectField2(value.providerRouting);
|
|
9271
10400
|
return {
|
|
9272
10401
|
kind: result.kind,
|
|
9273
10402
|
taskId: event?.subject,
|
|
@@ -9278,6 +10407,8 @@ function compactDrainResult(result) {
|
|
|
9278
10407
|
loopName: stringField(loop?.name),
|
|
9279
10408
|
workflowId: stringField(workflow?.id),
|
|
9280
10409
|
workflowName: stringField(workflow?.name),
|
|
10410
|
+
providerRouting,
|
|
10411
|
+
requeue,
|
|
9281
10412
|
queuedAtSource: value.queuedAtSource
|
|
9282
10413
|
};
|
|
9283
10414
|
}
|
|
@@ -9324,7 +10455,7 @@ function taskMatchesDrainFilters(task, filters) {
|
|
|
9324
10455
|
return false;
|
|
9325
10456
|
}
|
|
9326
10457
|
if (filters.tags.length) {
|
|
9327
|
-
const taskTags = new Set(
|
|
10458
|
+
const taskTags = new Set(tagsFromValue2(task.tags));
|
|
9328
10459
|
for (const tag of filters.tags) {
|
|
9329
10460
|
if (!taskTags.has(tag))
|
|
9330
10461
|
return false;
|
|
@@ -9456,10 +10587,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
9456
10587
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
9457
10588
|
var goal = program.command("goal").description("inspect goal runs");
|
|
9458
10589
|
function addRouteEventOptions(command) {
|
|
9459
|
-
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", "
|
|
10590
|
+
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; defaults to codewith").option("--provider-rule <rule>", "task metadata provider routing rule field=value:provider[:profile1,profile2]; may be repeated", collectValues, []).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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--verifier-idle-timeout <duration>", "verifier idle watchdog; use none/off to disable when an external heartbeat exists", "15m").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("--pr-handoff", "for task-lifecycle routes, add a bounded PR handoff step after the worker").option("--github-reviewer <login>", "GitHub login expected to review/merge review-required PR routes; must differ from PR author").option("--github-reviewer-pool <logins>", "comma-separated GitHub logins eligible to review/merge review-required PR routes").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
|
|
9460
10591
|
}
|
|
9461
10592
|
function addTodosDrainOptions(command, opts = {}) {
|
|
9462
|
-
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", "
|
|
10593
|
+
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; defaults to codewith").option("--provider-rule <rule>", "task metadata provider routing rule field=value:provider[:profile1,profile2]; may be repeated", collectValues, []).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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--verifier-idle-timeout <duration>", "verifier idle watchdog; use none/off to disable when an external heartbeat exists", "15m").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("--pr-handoff", "for task-lifecycle routes, add a bounded PR handoff step after the worker").option("--github-reviewer <login>", "GitHub login expected to review/merge review-required PR routes; must differ from PR author").option("--github-reviewer-pool <logins>", "comma-separated GitHub logins eligible to review/merge review-required PR routes").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
|
|
9463
10594
|
if (opts.includeDryRun ?? true) {
|
|
9464
10595
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
9465
10596
|
}
|
|
@@ -9494,6 +10625,8 @@ function routeDrainArgs(opts) {
|
|
|
9494
10625
|
addBool("--compact", opts.compact);
|
|
9495
10626
|
add("--template", opts.template);
|
|
9496
10627
|
add("--provider", opts.provider);
|
|
10628
|
+
for (const rule of opts.providerRule ?? [])
|
|
10629
|
+
add("--provider-rule", rule);
|
|
9497
10630
|
add("--auth-profile", opts.authProfile);
|
|
9498
10631
|
add("--auth-profile-pool", opts.authProfilePool);
|
|
9499
10632
|
add("--triage-auth-profile", opts.triageAuthProfile);
|
|
@@ -9513,6 +10646,7 @@ function routeDrainArgs(opts) {
|
|
|
9513
10646
|
for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
|
|
9514
10647
|
add("--add-dir", dir);
|
|
9515
10648
|
add("--timeout", opts.timeout);
|
|
10649
|
+
add("--verifier-idle-timeout", opts.verifierIdleTimeout);
|
|
9516
10650
|
add("--permission-mode", opts.permissionMode);
|
|
9517
10651
|
add("--sandbox", opts.sandbox);
|
|
9518
10652
|
addBool("--manual-break-glass", opts.manualBreakGlass);
|
|
@@ -9524,6 +10658,9 @@ function routeDrainArgs(opts) {
|
|
|
9524
10658
|
add("--worktree-mode", opts.worktreeMode);
|
|
9525
10659
|
add("--worktree-root", opts.worktreeRoot);
|
|
9526
10660
|
add("--worktree-branch-prefix", opts.worktreeBranchPrefix);
|
|
10661
|
+
addBool("--pr-handoff", opts.prHandoff);
|
|
10662
|
+
add("--github-reviewer", opts.githubReviewer);
|
|
10663
|
+
add("--github-reviewer-pool", opts.githubReviewerPool);
|
|
9527
10664
|
add("--name-prefix", opts.namePrefix);
|
|
9528
10665
|
addBool("--preflight", opts.preflight);
|
|
9529
10666
|
return args;
|
|
@@ -9559,7 +10696,8 @@ function drainTodosTaskRoutes(opts) {
|
|
|
9559
10696
|
const message = error instanceof Error ? error.message : String(error);
|
|
9560
10697
|
if (!isSkippableDrainRouteError(message))
|
|
9561
10698
|
throw error;
|
|
9562
|
-
|
|
10699
|
+
const sourceTaskUpdate = opts.dryRun ? { attempted: false, reason: "dry-run" } : markInvalidDrainTaskNonRouteable(todosProject, task, message);
|
|
10700
|
+
result = skippedDrainTask(task, event, redact(message, 640) ?? "route task failed", { sourceTaskUpdate });
|
|
9563
10701
|
}
|
|
9564
10702
|
results.push(result);
|
|
9565
10703
|
if (result.kind === "created" && !opts.dryRun)
|
|
@@ -9770,6 +10908,18 @@ routes.command("show <id>").description("show one admission work item").action((
|
|
|
9770
10908
|
store.close();
|
|
9771
10909
|
}
|
|
9772
10910
|
});
|
|
10911
|
+
routes.command("requeue <id>").description("requeue a terminal admission work item for the next task/event delivery").option("--reason <text>", "operator reason recorded on the work item").action((id, opts) => {
|
|
10912
|
+
const store = new Store;
|
|
10913
|
+
try {
|
|
10914
|
+
const reason = stringField(opts.reason);
|
|
10915
|
+
if (!reason)
|
|
10916
|
+
throw new Error("routes requeue requires --reason <text>");
|
|
10917
|
+
const item = store.requeueWorkflowWorkItem(id, { reason });
|
|
10918
|
+
print(publicWorkflowWorkItem(item), `requeued route work item ${item.id} (${item.routeKey})`);
|
|
10919
|
+
} finally {
|
|
10920
|
+
store.close();
|
|
10921
|
+
}
|
|
10922
|
+
});
|
|
9773
10923
|
routes.command("invocations").description("list workflow invocations").option("--limit <n>", "maximum rows", "50").action((opts) => {
|
|
9774
10924
|
const store = new Store;
|
|
9775
10925
|
try {
|
|
@@ -9824,7 +10974,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9824
10974
|
}
|
|
9825
10975
|
});
|
|
9826
10976
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
9827
|
-
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", "
|
|
10977
|
+
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; defaults to codewith").option("--provider-rule <rule>", "task metadata provider routing rule field=value:provider[:profile1,profile2]; may be repeated", collectValues, []).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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--verifier-idle-timeout <duration>", "verifier idle watchdog; use none/off to disable when an external heartbeat exists", "15m").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("--github-reviewer <login>", "GitHub login expected to review/merge review-required PR routes; must differ from PR author").option("--github-reviewer-pool <logins>", "comma-separated GitHub logins eligible to review/merge review-required PR routes").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) => {
|
|
9828
10978
|
const event = await readEventEnvelopeFromStdin();
|
|
9829
10979
|
const result = routeTodosTaskEvent(event, opts);
|
|
9830
10980
|
print(result.value, result.human);
|
|
@@ -9833,7 +10983,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
|
|
|
9833
10983
|
addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
|
|
9834
10984
|
drainTodosTaskRoutes(opts);
|
|
9835
10985
|
});
|
|
9836
|
-
eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").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 event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").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) => {
|
|
10986
|
+
eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--provider-rule <rule>", "event metadata provider routing rule field=value:provider[:profile1,profile2]; may be repeated", collectValues, []).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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--verifier-idle-timeout <duration>", "verifier idle watchdog; use none/off to disable when an external heartbeat exists", "15m").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 event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").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) => {
|
|
9837
10987
|
const event = await readEventEnvelopeFromStdin();
|
|
9838
10988
|
const result = routeGenericEvent(event, opts);
|
|
9839
10989
|
print(result.value, result.human);
|
|
@@ -10139,7 +11289,7 @@ program.command("list").alias("ls").option("--status <status>", "filter by statu
|
|
|
10139
11289
|
for (const loop of loops) {
|
|
10140
11290
|
const machine = loop.machine ? ` machine=${loop.machine.id}` : "";
|
|
10141
11291
|
const archive = loop.archivedAt ? ` archived=${loop.archivedAt} from=${loop.archivedFromStatus ?? "-"}` : "";
|
|
10142
|
-
console.log(`${loop.id} ${loop.status.padEnd(7)} next=${loop.nextRunAt ?? "-"} ${loop.name}${machine}${archive}`);
|
|
11292
|
+
console.log(`${loop.id} ${loop.status.padEnd(7)} cadence=${scheduleLabel(loop.schedule)} next=${loop.nextRunAt ?? "-"} ${loop.name}${machine}${archive}`);
|
|
10143
11293
|
}
|
|
10144
11294
|
}
|
|
10145
11295
|
} finally {
|
|
@@ -10779,11 +11929,11 @@ ${result.instructions.join(`
|
|
|
10779
11929
|
});
|
|
10780
11930
|
daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
|
|
10781
11931
|
const path = daemonLogPath();
|
|
10782
|
-
if (!
|
|
11932
|
+
if (!existsSync6(path)) {
|
|
10783
11933
|
console.log("");
|
|
10784
11934
|
return;
|
|
10785
11935
|
}
|
|
10786
|
-
const lines =
|
|
11936
|
+
const lines = readFileSync5(path, "utf8").trimEnd().split(`
|
|
10787
11937
|
`);
|
|
10788
11938
|
console.log(lines.slice(-Number(opts.lines)).join(`
|
|
10789
11939
|
`));
|