@hasna/loops 0.3.55 → 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 +1345 -175
- package/dist/daemon/index.js +143 -19
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4024 -2849
- 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"], {
|
|
@@ -6582,6 +7111,21 @@ function gitRootFor(path) {
|
|
|
6582
7111
|
return;
|
|
6583
7112
|
}
|
|
6584
7113
|
}
|
|
7114
|
+
function gitCommonDirFor(path) {
|
|
7115
|
+
if (!existsSync5(path))
|
|
7116
|
+
return;
|
|
7117
|
+
try {
|
|
7118
|
+
const raw = execFileSync("git", ["-C", path, "rev-parse", "--git-common-dir"], {
|
|
7119
|
+
encoding: "utf8",
|
|
7120
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
7121
|
+
}).trim();
|
|
7122
|
+
if (!raw)
|
|
7123
|
+
return;
|
|
7124
|
+
return isAbsolute2(raw) ? raw : resolve2(path, raw);
|
|
7125
|
+
} catch {
|
|
7126
|
+
return;
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
6585
7129
|
function prepareWorktreeCommand(plan) {
|
|
6586
7130
|
const repo = shellQuote2(plan.repoRoot);
|
|
6587
7131
|
const path = shellQuote2(plan.path);
|
|
@@ -6698,6 +7242,7 @@ function worktreePlan(input, seed) {
|
|
|
6698
7242
|
root,
|
|
6699
7243
|
path: worktreePath,
|
|
6700
7244
|
branch,
|
|
7245
|
+
gitMetadataDir: gitCommonDirFor(repoRoot),
|
|
6701
7246
|
prepareStep
|
|
6702
7247
|
};
|
|
6703
7248
|
}
|
|
@@ -6745,6 +7290,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6745
7290
|
assertNativeAuthProfileSupport(input, provider);
|
|
6746
7291
|
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "workspace-write" : provider === "cursor" ? "enabled" : undefined);
|
|
6747
7292
|
failClosedSandbox(input, provider, sandbox);
|
|
7293
|
+
const addDirs = [...input.addDirs ?? []];
|
|
7294
|
+
if (plan.enabled && plan.gitMetadataDir && (provider === "codewith" || provider === "codex") && sandbox === "workspace-write") {
|
|
7295
|
+
addDirs.push(plan.gitMetadataDir);
|
|
7296
|
+
}
|
|
6748
7297
|
return {
|
|
6749
7298
|
type: "agent",
|
|
6750
7299
|
provider,
|
|
@@ -6753,7 +7302,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6753
7302
|
model: input.model,
|
|
6754
7303
|
variant: input.variant,
|
|
6755
7304
|
agent: input.agent,
|
|
6756
|
-
addDirs:
|
|
7305
|
+
addDirs: addDirs.length ? [...new Set(addDirs)] : undefined,
|
|
6757
7306
|
authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
|
|
6758
7307
|
configIsolation: "safe",
|
|
6759
7308
|
permissionMode: input.permissionMode ?? "bypass",
|
|
@@ -6775,7 +7324,8 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6775
7324
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
6776
7325
|
},
|
|
6777
7326
|
account: accountForRole(input, role, seed),
|
|
6778
|
-
timeoutMs: agentTimeoutMs(input)
|
|
7327
|
+
timeoutMs: agentTimeoutMs(input),
|
|
7328
|
+
idleTimeoutMs: role === "verifier" ? verifierIdleTimeoutMs(input) : undefined
|
|
6779
7329
|
};
|
|
6780
7330
|
}
|
|
6781
7331
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -6955,7 +7505,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
6955
7505
|
function readCustomTemplateFile(file) {
|
|
6956
7506
|
let parsed;
|
|
6957
7507
|
try {
|
|
6958
|
-
parsed = JSON.parse(
|
|
7508
|
+
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
6959
7509
|
} catch (error) {
|
|
6960
7510
|
const message = error instanceof Error ? error.message : String(error);
|
|
6961
7511
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -6981,7 +7531,7 @@ function assertNoTemplateCollisions(entries) {
|
|
|
6981
7531
|
}
|
|
6982
7532
|
function loadCustomLoopTemplatesRaw() {
|
|
6983
7533
|
const dir = customLoopTemplatesDir();
|
|
6984
|
-
if (!
|
|
7534
|
+
if (!existsSync5(dir))
|
|
6985
7535
|
return [];
|
|
6986
7536
|
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
6987
7537
|
const file = join5(dir, entry.name);
|
|
@@ -7105,7 +7655,7 @@ function importCustomLoopTemplate(file, opts = {}) {
|
|
|
7105
7655
|
const entry = readCustomTemplateFile(source);
|
|
7106
7656
|
const dir = ensureCustomLoopTemplatesDir();
|
|
7107
7657
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
7108
|
-
const replaced =
|
|
7658
|
+
const replaced = existsSync5(destination);
|
|
7109
7659
|
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
7110
7660
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
7111
7661
|
if (replaced) {
|
|
@@ -7201,6 +7751,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
7201
7751
|
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
7202
7752
|
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
7203
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),
|
|
7204
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.",
|
|
7205
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.",
|
|
7206
7757
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
@@ -7213,10 +7764,24 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
7213
7764
|
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
7214
7765
|
version: 1,
|
|
7215
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
|
+
},
|
|
7216
7780
|
{
|
|
7217
7781
|
id: "worker",
|
|
7218
7782
|
name: "Worker",
|
|
7219
7783
|
description: "Implement the todos task and record evidence.",
|
|
7784
|
+
dependsOn: ["source-task-gate"],
|
|
7220
7785
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7221
7786
|
timeoutMs: agentTimeoutMs(input)
|
|
7222
7787
|
},
|
|
@@ -7257,6 +7822,14 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7257
7822
|
reason: plan.reason
|
|
7258
7823
|
}
|
|
7259
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
|
+
`) : "";
|
|
7260
7833
|
const shared = [
|
|
7261
7834
|
worktreePrompt(plan),
|
|
7262
7835
|
`Todos project path: ${todosProjectPath}`,
|
|
@@ -7266,7 +7839,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7266
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.",
|
|
7267
7840
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
7268
7841
|
"",
|
|
7269
|
-
`Task context JSON: ${compactJson(taskContext)}
|
|
7842
|
+
`Task context JSON: ${compactJson(taskContext)}`,
|
|
7843
|
+
prHandoffGuidance
|
|
7270
7844
|
].join(`
|
|
7271
7845
|
`);
|
|
7272
7846
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
@@ -7310,7 +7884,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7310
7884
|
"const latestMarker = markers.at(-1)?.state;",
|
|
7311
7885
|
"const blockers = [];",
|
|
7312
7886
|
"if (blockedStatuses.has(status)) blockers.push(`task status is ${status}`);",
|
|
7313
|
-
"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']) {",
|
|
7314
7888
|
" if (tags.has(tag)) blockers.push(`task has disallowed tag ${tag}`);",
|
|
7315
7889
|
"}",
|
|
7316
7890
|
"for (const [key, source] of records.entries()) {",
|
|
@@ -7339,7 +7913,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7339
7913
|
"Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
|
|
7340
7914
|
`If the task should not proceed automatically, run: todos --project ${todosProjectPath} update ${input.taskId} --status blocked`,
|
|
7341
7915
|
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
7342
|
-
"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."
|
|
7343
7917
|
].join(`
|
|
7344
7918
|
`);
|
|
7345
7919
|
const plannerPrompt = [
|
|
@@ -7352,7 +7926,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7352
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.",
|
|
7353
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`,
|
|
7354
7928
|
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
7355
|
-
"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."
|
|
7356
7930
|
].join(`
|
|
7357
7931
|
`);
|
|
7358
7932
|
const workerPrompt = [
|
|
@@ -7362,8 +7936,9 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7362
7936
|
shared,
|
|
7363
7937
|
`- Claim/start if appropriate: todos --project ${todosProjectPath} start ${input.taskId}`,
|
|
7364
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,
|
|
7365
7940
|
"Do not mark the task complete in the worker step; the verifier step owns completion after independent validation."
|
|
7366
|
-
].join(`
|
|
7941
|
+
].filter(Boolean).join(`
|
|
7367
7942
|
`);
|
|
7368
7943
|
const verifierPrompt = [
|
|
7369
7944
|
`/goal Verify todos task ${input.taskId} after the full lifecycle worker step.`,
|
|
@@ -7373,75 +7948,108 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7373
7948
|
`- Record verification: todos --project ${todosProjectPath} comment ${input.taskId} "<verification evidence or blocker>"`,
|
|
7374
7949
|
`- If valid and complete: todos --project ${todosProjectPath} done ${input.taskId}`,
|
|
7375
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,
|
|
7376
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.",
|
|
7377
7954
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks."
|
|
7378
|
-
].join(`
|
|
7955
|
+
].filter(Boolean).join(`
|
|
7379
7956
|
`);
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
{
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
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
|
|
7391
7968
|
},
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
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,
|
|
7404
7989
|
timeoutMs: 2 * 60000
|
|
7405
7990
|
},
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
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,
|
|
7426
8011
|
timeoutMs: 2 * 60000
|
|
7427
8012
|
},
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
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
|
|
7435
8036
|
},
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
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)
|
|
7445
8053
|
};
|
|
7446
8054
|
}
|
|
7447
8055
|
function renderEventWorkerVerifierWorkflow(input) {
|
|
@@ -7493,6 +8101,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
7493
8101
|
"You are the verifier agent for an event-triggered OpenLoops workflow.",
|
|
7494
8102
|
worktreePrompt(plan),
|
|
7495
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),
|
|
7496
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.",
|
|
7497
8106
|
"",
|
|
7498
8107
|
`Event context JSON: ${compactJson(eventContext)}`,
|
|
@@ -7547,6 +8156,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7547
8156
|
"You are the verifier step for a bounded OpenLoops agent workflow.",
|
|
7548
8157
|
worktreePrompt(plan),
|
|
7549
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),
|
|
7550
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."
|
|
7551
8161
|
].join(`
|
|
7552
8162
|
`);
|
|
@@ -7597,7 +8207,8 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7597
8207
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
7598
8208
|
worktreeRoot: values.worktreeRoot,
|
|
7599
8209
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7600
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8210
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8211
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7601
8212
|
};
|
|
7602
8213
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
7603
8214
|
const taskId = values.taskId ?? "";
|
|
@@ -7631,10 +8242,12 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7631
8242
|
permissionMode: values.permissionMode,
|
|
7632
8243
|
sandbox: values.sandbox,
|
|
7633
8244
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
8245
|
+
prHandoff: booleanVar(values.prHandoff),
|
|
7634
8246
|
worktreeMode: values.worktreeMode ?? "required",
|
|
7635
8247
|
worktreeRoot: values.worktreeRoot,
|
|
7636
8248
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7637
8249
|
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8250
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout),
|
|
7638
8251
|
eventId: values.eventId,
|
|
7639
8252
|
eventType: values.eventType
|
|
7640
8253
|
});
|
|
@@ -7759,6 +8372,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7759
8372
|
worktreeRoot: values.worktreeRoot,
|
|
7760
8373
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7761
8374
|
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8375
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout),
|
|
7762
8376
|
eventId: values.eventId,
|
|
7763
8377
|
eventType: values.eventType
|
|
7764
8378
|
});
|
|
@@ -7791,7 +8405,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7791
8405
|
worktreeMode: values.worktreeMode,
|
|
7792
8406
|
worktreeRoot: values.worktreeRoot,
|
|
7793
8407
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7794
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8408
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8409
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7795
8410
|
});
|
|
7796
8411
|
}
|
|
7797
8412
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -7819,7 +8434,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7819
8434
|
worktreeMode: values.worktreeMode,
|
|
7820
8435
|
worktreeRoot: values.worktreeRoot,
|
|
7821
8436
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7822
|
-
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
8437
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
8438
|
+
verifierIdleTimeoutMs: parseTemplateIdleTimeoutMs(values.verifierIdleTimeoutMs ?? values.verifierIdleTimeout)
|
|
7823
8439
|
});
|
|
7824
8440
|
}
|
|
7825
8441
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -7926,7 +8542,7 @@ function normalizeWorkflowForStorage(body, context) {
|
|
|
7926
8542
|
function workflowBodyFromFile(file, fallbackName, context) {
|
|
7927
8543
|
try {
|
|
7928
8544
|
const resolved = resolve3(file);
|
|
7929
|
-
return workflowBodyFromJson(JSON.parse(
|
|
8545
|
+
return workflowBodyFromJson(JSON.parse(readFileSync5(resolved, "utf8")), fallbackName, { baseDir: dirname4(resolved) });
|
|
7930
8546
|
} catch (error) {
|
|
7931
8547
|
validationFailed(error, context);
|
|
7932
8548
|
}
|
|
@@ -8052,6 +8668,10 @@ function timeoutDuration(raw, label) {
|
|
|
8052
8668
|
throw new Error(`${label} must be a duration greater than zero, or none/unlimited`);
|
|
8053
8669
|
return value;
|
|
8054
8670
|
}
|
|
8671
|
+
function idleTimeoutDuration(raw, label) {
|
|
8672
|
+
const value = timeoutDuration(raw, label);
|
|
8673
|
+
return value === null ? undefined : value;
|
|
8674
|
+
}
|
|
8055
8675
|
function parsePolicy(opts) {
|
|
8056
8676
|
const catchUp = opts.catchUp ?? "latest";
|
|
8057
8677
|
if (!["none", "latest", "all"].includes(catchUp))
|
|
@@ -8068,6 +8688,45 @@ function parsePolicy(opts) {
|
|
|
8068
8688
|
leaseMs: positiveDuration(opts.lease, "--lease")
|
|
8069
8689
|
};
|
|
8070
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
|
+
}
|
|
8071
8730
|
function baseCreateInput(name, opts, target) {
|
|
8072
8731
|
const schedule = parseSchedule({
|
|
8073
8732
|
at: typeof opts.at === "string" ? opts.at : undefined,
|
|
@@ -8083,9 +8742,10 @@ function baseCreateInput(name, opts, target) {
|
|
|
8083
8742
|
retryDelay: typeof opts.retryDelay === "string" ? opts.retryDelay : undefined,
|
|
8084
8743
|
lease: typeof opts.lease === "string" ? opts.lease : undefined
|
|
8085
8744
|
});
|
|
8745
|
+
const explicitDescription = typeof opts.description === "string" && opts.description.trim() ? opts.description : undefined;
|
|
8086
8746
|
return {
|
|
8087
8747
|
name,
|
|
8088
|
-
description:
|
|
8748
|
+
description: explicitDescription ?? defaultLoopDescription(name, schedule, target),
|
|
8089
8749
|
schedule,
|
|
8090
8750
|
target,
|
|
8091
8751
|
goal: goalFromOpts(opts),
|
|
@@ -8208,7 +8868,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
8208
8868
|
return {
|
|
8209
8869
|
ok: result.status === 0,
|
|
8210
8870
|
status: result.status,
|
|
8211
|
-
stdout:
|
|
8871
|
+
stdout: readFileSync5(stdoutPath, "utf8"),
|
|
8212
8872
|
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
8213
8873
|
error: result.error ? String(result.error.message || result.error) : ""
|
|
8214
8874
|
};
|
|
@@ -8249,10 +8909,10 @@ function routeCursorsPath() {
|
|
|
8249
8909
|
}
|
|
8250
8910
|
function readRouteCursors() {
|
|
8251
8911
|
const path = routeCursorsPath();
|
|
8252
|
-
if (!
|
|
8912
|
+
if (!existsSync6(path))
|
|
8253
8913
|
return {};
|
|
8254
8914
|
try {
|
|
8255
|
-
const parsed = JSON.parse(
|
|
8915
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8256
8916
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
8257
8917
|
} catch {
|
|
8258
8918
|
return {};
|
|
@@ -8416,31 +9076,46 @@ function taskEventField(data, keys) {
|
|
|
8416
9076
|
}
|
|
8417
9077
|
return;
|
|
8418
9078
|
}
|
|
8419
|
-
function
|
|
9079
|
+
function objectField2(value) {
|
|
8420
9080
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
8421
9081
|
}
|
|
8422
9082
|
function nestedObject(input, key) {
|
|
8423
|
-
return
|
|
9083
|
+
return objectField2(input[key]);
|
|
8424
9084
|
}
|
|
8425
9085
|
function taskEventRecords(data, metadata) {
|
|
8426
9086
|
const records = [data];
|
|
8427
9087
|
const dataTask = nestedObject(data, "task");
|
|
8428
|
-
if (dataTask)
|
|
9088
|
+
if (dataTask) {
|
|
8429
9089
|
records.push(dataTask);
|
|
9090
|
+
const dataTaskMetadata = nestedObject(dataTask, "metadata");
|
|
9091
|
+
if (dataTaskMetadata)
|
|
9092
|
+
records.push(dataTaskMetadata);
|
|
9093
|
+
}
|
|
8430
9094
|
const dataPayload = nestedObject(data, "payload");
|
|
8431
9095
|
if (dataPayload) {
|
|
8432
9096
|
records.push(dataPayload);
|
|
9097
|
+
const payloadMetadata = nestedObject(dataPayload, "metadata");
|
|
9098
|
+
if (payloadMetadata)
|
|
9099
|
+
records.push(payloadMetadata);
|
|
8433
9100
|
const payloadTask = nestedObject(dataPayload, "task");
|
|
8434
|
-
if (payloadTask)
|
|
9101
|
+
if (payloadTask) {
|
|
8435
9102
|
records.push(payloadTask);
|
|
9103
|
+
const payloadTaskMetadata = nestedObject(payloadTask, "metadata");
|
|
9104
|
+
if (payloadTaskMetadata)
|
|
9105
|
+
records.push(payloadTaskMetadata);
|
|
9106
|
+
}
|
|
8436
9107
|
}
|
|
8437
9108
|
const dataMetadata = nestedObject(data, "metadata");
|
|
8438
9109
|
if (dataMetadata)
|
|
8439
9110
|
records.push(dataMetadata);
|
|
8440
9111
|
records.push(metadata);
|
|
8441
9112
|
const metadataTask = nestedObject(metadata, "task");
|
|
8442
|
-
if (metadataTask)
|
|
9113
|
+
if (metadataTask) {
|
|
8443
9114
|
records.push(metadataTask);
|
|
9115
|
+
const metadataTaskMetadata = nestedObject(metadataTask, "metadata");
|
|
9116
|
+
if (metadataTaskMetadata)
|
|
9117
|
+
records.push(metadataTaskMetadata);
|
|
9118
|
+
}
|
|
8444
9119
|
const metadataAutomation = nestedObject(metadata, "automation");
|
|
8445
9120
|
if (metadataAutomation)
|
|
8446
9121
|
records.push(metadataAutomation);
|
|
@@ -8452,6 +9127,15 @@ function booleanLike(value) {
|
|
|
8452
9127
|
function hasTruthyField(records, keys) {
|
|
8453
9128
|
return records.some((record) => keys.some((key) => booleanLike(record[key])));
|
|
8454
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
|
+
}
|
|
8455
9139
|
function automationRecords(data, metadata) {
|
|
8456
9140
|
const records = [];
|
|
8457
9141
|
const dataAutomation = nestedObject(data, "automation");
|
|
@@ -8461,14 +9145,26 @@ function automationRecords(data, metadata) {
|
|
|
8461
9145
|
const dataTaskAutomation = dataTask ? nestedObject(dataTask, "automation") : undefined;
|
|
8462
9146
|
if (dataTaskAutomation)
|
|
8463
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);
|
|
8464
9152
|
const dataPayload = nestedObject(data, "payload");
|
|
8465
9153
|
const payloadAutomation = dataPayload ? nestedObject(dataPayload, "automation") : undefined;
|
|
8466
9154
|
if (payloadAutomation)
|
|
8467
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);
|
|
8468
9160
|
const payloadTask = dataPayload ? nestedObject(dataPayload, "task") : undefined;
|
|
8469
9161
|
const payloadTaskAutomation = payloadTask ? nestedObject(payloadTask, "automation") : undefined;
|
|
8470
9162
|
if (payloadTaskAutomation)
|
|
8471
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);
|
|
8472
9168
|
const dataMetadata = nestedObject(data, "metadata");
|
|
8473
9169
|
const dataMetadataAutomation = dataMetadata ? nestedObject(dataMetadata, "automation") : undefined;
|
|
8474
9170
|
if (dataMetadataAutomation)
|
|
@@ -8480,9 +9176,13 @@ function automationRecords(data, metadata) {
|
|
|
8480
9176
|
const metadataTaskAutomation = metadataTask ? nestedObject(metadataTask, "automation") : undefined;
|
|
8481
9177
|
if (metadataTaskAutomation)
|
|
8482
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);
|
|
8483
9183
|
return records;
|
|
8484
9184
|
}
|
|
8485
|
-
function
|
|
9185
|
+
function tagsFromValue2(value) {
|
|
8486
9186
|
if (Array.isArray(value))
|
|
8487
9187
|
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
8488
9188
|
if (typeof value === "string")
|
|
@@ -8492,40 +9192,381 @@ function tagsFromValue(value) {
|
|
|
8492
9192
|
function taskEventTags(records) {
|
|
8493
9193
|
const tags = new Set;
|
|
8494
9194
|
for (const record of records) {
|
|
8495
|
-
for (const tag of
|
|
9195
|
+
for (const tag of tagsFromValue2(record.tags ?? record.task_tags ?? record.taskTags))
|
|
8496
9196
|
tags.add(tag);
|
|
8497
9197
|
}
|
|
8498
9198
|
return [...tags];
|
|
8499
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
|
+
];
|
|
8500
9224
|
function taskRouteEligibility(data, metadata) {
|
|
8501
9225
|
const records = taskEventRecords(data, metadata);
|
|
9226
|
+
const automation = automationRecords(data, metadata);
|
|
8502
9227
|
const tags = taskEventTags(records);
|
|
8503
|
-
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"]);
|
|
8504
9229
|
if (!hasRouteOptIn)
|
|
8505
9230
|
return { eligible: false, reason: "missing explicit route opt-in", tags };
|
|
8506
9231
|
const status = taskEventField(data, ["status", "task_status", "taskStatus"])?.toLowerCase();
|
|
8507
9232
|
if (status && ["blocked", "completed", "done", "cancelled", "canceled", "failed", "archived"].includes(status)) {
|
|
8508
9233
|
return { eligible: false, reason: `task status is not routable: ${status}`, tags };
|
|
8509
9234
|
}
|
|
8510
|
-
const disallowedTags = tags.filter((tag) =>
|
|
9235
|
+
const disallowedTags = tags.filter((tag) => ROUTE_DISALLOWED_TASK_TAGS.has(tag.toLowerCase()));
|
|
8511
9236
|
if (disallowedTags.length)
|
|
8512
9237
|
return { eligible: false, reason: `task has disallowed tag: ${disallowedTags[0]}`, tags };
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
"manual",
|
|
8517
|
-
"manual_required",
|
|
8518
|
-
"manualRequired",
|
|
8519
|
-
"requires_approval",
|
|
8520
|
-
"requiresApproval",
|
|
8521
|
-
"approval_required",
|
|
8522
|
-
"approvalRequired"
|
|
8523
|
-
])) {
|
|
8524
|
-
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 };
|
|
8525
9241
|
}
|
|
8526
9242
|
return { eligible: true, tags };
|
|
8527
9243
|
}
|
|
8528
|
-
|
|
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 = {}) {
|
|
8529
9570
|
const taskId = taskField(task, ["id", "task_id", "taskId"]) ?? event?.subject ?? "unknown";
|
|
8530
9571
|
return {
|
|
8531
9572
|
kind: "skipped",
|
|
@@ -8534,7 +9575,8 @@ function skippedDrainTask(task, event, reason) {
|
|
|
8534
9575
|
reason,
|
|
8535
9576
|
taskId,
|
|
8536
9577
|
event,
|
|
8537
|
-
routeError: true
|
|
9578
|
+
routeError: true,
|
|
9579
|
+
...extra
|
|
8538
9580
|
},
|
|
8539
9581
|
human: `skipped task ${taskId}: ${reason}`
|
|
8540
9582
|
};
|
|
@@ -8542,6 +9584,32 @@ function skippedDrainTask(task, event, reason) {
|
|
|
8542
9584
|
function isSkippableDrainRouteError(message) {
|
|
8543
9585
|
return message.startsWith("worktreeMode=required but projectPath is not an existing git repository:");
|
|
8544
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
|
+
}
|
|
8545
9613
|
function generatedRouteSandboxPreflight(workflow) {
|
|
8546
9614
|
const checks = [];
|
|
8547
9615
|
for (const step of workflow.steps) {
|
|
@@ -8667,6 +9735,28 @@ var TODOS_TASK_ROUTE_TEMPLATE_IDS = new Set([
|
|
|
8667
9735
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
8668
9736
|
TASK_LIFECYCLE_TEMPLATE_ID
|
|
8669
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
|
+
}
|
|
8670
9760
|
function todosTaskRouteTemplateId(opts) {
|
|
8671
9761
|
const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
|
|
8672
9762
|
if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
|
|
@@ -8675,7 +9765,7 @@ function todosTaskRouteTemplateId(opts) {
|
|
|
8675
9765
|
return id;
|
|
8676
9766
|
}
|
|
8677
9767
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8678
|
-
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());
|
|
8679
9769
|
const event = JSON.parse(raw);
|
|
8680
9770
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
8681
9771
|
throw new Error("event JSON must be an object");
|
|
@@ -8728,8 +9818,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8728
9818
|
const store2 = new Store;
|
|
8729
9819
|
try {
|
|
8730
9820
|
const existingItem = store2.findWorkflowWorkItem("todos-task", idempotencyKey);
|
|
8731
|
-
if (existingItem
|
|
8732
|
-
const existingLoop = store2.getLoop(existingItem.loopId);
|
|
9821
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
9822
|
+
const existingLoop = existingItem.loopId ? store2.getLoop(existingItem.loopId) : undefined;
|
|
8733
9823
|
const existingWorkflow = existingItem.workflowId ? store2.getWorkflow(existingItem.workflowId) : undefined;
|
|
8734
9824
|
const existingInvocation = store2.getWorkflowInvocation(existingItem.invocationId);
|
|
8735
9825
|
return {
|
|
@@ -8751,12 +9841,27 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8751
9841
|
store2.close();
|
|
8752
9842
|
}
|
|
8753
9843
|
}
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
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;
|
|
8757
9862
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
8758
9863
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8759
|
-
const authProfile = providerAuthProfileFromOpts({ authProfile:
|
|
9864
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: providerRouting.authProfile }, provider);
|
|
8760
9865
|
const templateId = todosTaskRouteTemplateId(opts);
|
|
8761
9866
|
const workflowInput = {
|
|
8762
9867
|
taskId,
|
|
@@ -8767,13 +9872,13 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8767
9872
|
projectGroup,
|
|
8768
9873
|
provider,
|
|
8769
9874
|
authProfile,
|
|
8770
|
-
authProfilePool:
|
|
9875
|
+
authProfilePool: providerRouting.authProfilePool,
|
|
8771
9876
|
triageAuthProfile: opts.triageAuthProfile,
|
|
8772
9877
|
plannerAuthProfile: opts.plannerAuthProfile,
|
|
8773
9878
|
workerAuthProfile: opts.workerAuthProfile,
|
|
8774
9879
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
8775
|
-
account:
|
|
8776
|
-
accountPool:
|
|
9880
|
+
account: providerRouting.account,
|
|
9881
|
+
accountPool: providerRouting.accountPool,
|
|
8777
9882
|
triageAccount: roleAccountFromOpts(opts, opts.triageAccount),
|
|
8778
9883
|
plannerAccount: roleAccountFromOpts(opts, opts.plannerAccount),
|
|
8779
9884
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
@@ -8783,12 +9888,14 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8783
9888
|
agent: opts.agent,
|
|
8784
9889
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
8785
9890
|
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9891
|
+
verifierIdleTimeoutMs: idleTimeoutDuration(opts.verifierIdleTimeout, "--verifier-idle-timeout"),
|
|
8786
9892
|
permissionMode,
|
|
8787
9893
|
sandbox,
|
|
8788
9894
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8789
9895
|
worktreeMode: opts.worktreeMode ?? "auto",
|
|
8790
9896
|
worktreeRoot: opts.worktreeRoot,
|
|
8791
9897
|
worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
|
|
9898
|
+
prHandoff: templateId === TASK_LIFECYCLE_TEMPLATE_ID ? Boolean(opts.prHandoff) : false,
|
|
8792
9899
|
eventId: event.id,
|
|
8793
9900
|
eventType: event.type,
|
|
8794
9901
|
todosProjectPath: opts.todosProject
|
|
@@ -8824,7 +9931,10 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8824
9931
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
8825
9932
|
permissions: permissionMode,
|
|
8826
9933
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8827
|
-
|
|
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,
|
|
8828
9938
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
8829
9939
|
},
|
|
8830
9940
|
outputPolicy: {
|
|
@@ -8863,7 +9973,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8863
9973
|
}, {}) : undefined;
|
|
8864
9974
|
return {
|
|
8865
9975
|
kind: "created",
|
|
8866
|
-
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 },
|
|
8867
9977
|
human: `dry-run ${loopName}`
|
|
8868
9978
|
};
|
|
8869
9979
|
}
|
|
@@ -8879,8 +9989,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8879
9989
|
const outcome = store.writeTransaction(() => {
|
|
8880
9990
|
const invocation = store.createWorkflowInvocation(invocationInput);
|
|
8881
9991
|
const existingItem = store.findWorkflowWorkItem("todos-task", idempotencyKey);
|
|
8882
|
-
if (existingItem
|
|
8883
|
-
const existingLoop = store.getLoop(existingItem.loopId);
|
|
9992
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
9993
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
8884
9994
|
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
8885
9995
|
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
8886
9996
|
}
|
|
@@ -8891,6 +10001,11 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8891
10001
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
8892
10002
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
8893
10003
|
});
|
|
10004
|
+
const requeue = workItem.attempts > 0 && workItem.status === "queued" ? {
|
|
10005
|
+
previousWorkItemId: workItem.id,
|
|
10006
|
+
previousAttempts: workItem.attempts,
|
|
10007
|
+
reason: workItem.lastReason
|
|
10008
|
+
} : undefined;
|
|
8894
10009
|
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
8895
10010
|
if (throttle && !throttle.allowed)
|
|
8896
10011
|
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
@@ -8907,7 +10022,15 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8907
10022
|
}
|
|
8908
10023
|
});
|
|
8909
10024
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by todos-task route" });
|
|
8910
|
-
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
|
+
};
|
|
8911
10034
|
});
|
|
8912
10035
|
if (outcome.kind === "deduped") {
|
|
8913
10036
|
return {
|
|
@@ -8936,6 +10059,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8936
10059
|
event,
|
|
8937
10060
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8938
10061
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
10062
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
10063
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
8939
10064
|
throttle: outcome.throttle,
|
|
8940
10065
|
workflow: workflowBody,
|
|
8941
10066
|
loop: loopInput
|
|
@@ -8951,8 +10076,11 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8951
10076
|
event,
|
|
8952
10077
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8953
10078
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
10079
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
10080
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
8954
10081
|
workflow: publicWorkflow(outcome.workflow),
|
|
8955
10082
|
loop: publicLoop(outcome.loop),
|
|
10083
|
+
requeue: outcome.requeue,
|
|
8956
10084
|
throttle: outcome.throttle,
|
|
8957
10085
|
sandboxPreflight,
|
|
8958
10086
|
preflight
|
|
@@ -8976,12 +10104,11 @@ function routeGenericEvent(event, opts) {
|
|
|
8976
10104
|
const workflowName = `${opts.namePrefix ?? "event:generic"}:${source}:${type}:${eventSuffix}:workflow`;
|
|
8977
10105
|
const loopName = `${opts.namePrefix ?? "event:generic"}:${source}:${type}:${eventSuffix}:run`;
|
|
8978
10106
|
const idempotencyKey = `generic-event:${event.source}:${event.type}:${event.id}`;
|
|
8979
|
-
const
|
|
8980
|
-
|
|
8981
|
-
throw new Error("unsupported provider");
|
|
10107
|
+
const providerRouting = resolveProviderRouting(data, metadata, opts);
|
|
10108
|
+
const provider = providerRouting.provider;
|
|
8982
10109
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
8983
10110
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8984
|
-
const authProfile = providerAuthProfileFromOpts({ authProfile:
|
|
10111
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: providerRouting.authProfile }, provider);
|
|
8985
10112
|
let workflowBody = renderEventWorkerVerifierWorkflow({
|
|
8986
10113
|
eventId: event.id,
|
|
8987
10114
|
eventType: event.type,
|
|
@@ -8994,11 +10121,11 @@ function routeGenericEvent(event, opts) {
|
|
|
8994
10121
|
projectGroup,
|
|
8995
10122
|
provider,
|
|
8996
10123
|
authProfile,
|
|
8997
|
-
authProfilePool:
|
|
10124
|
+
authProfilePool: providerRouting.authProfilePool,
|
|
8998
10125
|
workerAuthProfile: opts.workerAuthProfile,
|
|
8999
10126
|
verifierAuthProfile: opts.verifierAuthProfile,
|
|
9000
|
-
account:
|
|
9001
|
-
accountPool:
|
|
10127
|
+
account: providerRouting.account,
|
|
10128
|
+
accountPool: providerRouting.accountPool,
|
|
9002
10129
|
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
9003
10130
|
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
9004
10131
|
model: opts.model,
|
|
@@ -9006,6 +10133,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9006
10133
|
agent: opts.agent,
|
|
9007
10134
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9008
10135
|
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
10136
|
+
verifierIdleTimeoutMs: idleTimeoutDuration(opts.verifierIdleTimeout, "--verifier-idle-timeout"),
|
|
9009
10137
|
permissionMode,
|
|
9010
10138
|
sandbox,
|
|
9011
10139
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -9021,6 +10149,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9021
10149
|
event: event.id
|
|
9022
10150
|
});
|
|
9023
10151
|
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
10152
|
+
const hasExplicitRoleAccount = Boolean(opts.workerAuthProfile || opts.verifierAuthProfile || opts.workerAccount || opts.verifierAccount);
|
|
9024
10153
|
const invocationInput = {
|
|
9025
10154
|
templateId: "event-worker-verifier",
|
|
9026
10155
|
sourceRef: {
|
|
@@ -9042,7 +10171,8 @@ function routeGenericEvent(event, opts) {
|
|
|
9042
10171
|
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
9043
10172
|
permissions: permissionMode,
|
|
9044
10173
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
9045
|
-
accountPolicy:
|
|
10174
|
+
accountPolicy: providerRouting.authProfilePool?.length || providerRouting.accountPool?.length ? "pool" : hasExplicitRoleAccount ? "role-explicit" : "single",
|
|
10175
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9046
10176
|
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
9047
10177
|
},
|
|
9048
10178
|
outputPolicy: {
|
|
@@ -9081,7 +10211,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9081
10211
|
}, {}) : undefined;
|
|
9082
10212
|
return {
|
|
9083
10213
|
kind: "created",
|
|
9084
|
-
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 },
|
|
9085
10215
|
human: `dry-run ${loopName}`
|
|
9086
10216
|
};
|
|
9087
10217
|
}
|
|
@@ -9097,8 +10227,8 @@ function routeGenericEvent(event, opts) {
|
|
|
9097
10227
|
const outcome = store.writeTransaction(() => {
|
|
9098
10228
|
const invocation = store.createWorkflowInvocation(invocationInput);
|
|
9099
10229
|
const existingItem = store.findWorkflowWorkItem("generic-event", idempotencyKey);
|
|
9100
|
-
if (existingItem
|
|
9101
|
-
const existingLoop = store.getLoop(existingItem.loopId);
|
|
10230
|
+
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
10231
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
9102
10232
|
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
9103
10233
|
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
9104
10234
|
}
|
|
@@ -9109,6 +10239,11 @@ function routeGenericEvent(event, opts) {
|
|
|
9109
10239
|
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
9110
10240
|
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
9111
10241
|
});
|
|
10242
|
+
const requeue = workItem.attempts > 0 && workItem.status === "queued" ? {
|
|
10243
|
+
previousWorkItemId: workItem.id,
|
|
10244
|
+
previousAttempts: workItem.attempts,
|
|
10245
|
+
reason: workItem.lastReason
|
|
10246
|
+
} : undefined;
|
|
9112
10247
|
const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
|
|
9113
10248
|
if (throttle && !throttle.allowed)
|
|
9114
10249
|
return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
|
|
@@ -9125,7 +10260,15 @@ function routeGenericEvent(event, opts) {
|
|
|
9125
10260
|
}
|
|
9126
10261
|
});
|
|
9127
10262
|
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by generic-event route" });
|
|
9128
|
-
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
|
+
};
|
|
9129
10272
|
});
|
|
9130
10273
|
if (outcome.kind === "deduped") {
|
|
9131
10274
|
return {
|
|
@@ -9135,6 +10278,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9135
10278
|
idempotencyKey,
|
|
9136
10279
|
dedupedBy: "work-item",
|
|
9137
10280
|
event,
|
|
10281
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9138
10282
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9139
10283
|
workItem: publicWorkflowWorkItem(outcome.existingItem),
|
|
9140
10284
|
workflow: outcome.existingWorkflow ? publicWorkflow(outcome.existingWorkflow) : undefined,
|
|
@@ -9152,6 +10296,7 @@ function routeGenericEvent(event, opts) {
|
|
|
9152
10296
|
reason: outcome.throttle.reason,
|
|
9153
10297
|
idempotencyKey,
|
|
9154
10298
|
event,
|
|
10299
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9155
10300
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9156
10301
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
9157
10302
|
throttle: outcome.throttle,
|
|
@@ -9167,10 +10312,12 @@ function routeGenericEvent(event, opts) {
|
|
|
9167
10312
|
deduped: false,
|
|
9168
10313
|
idempotencyKey,
|
|
9169
10314
|
event,
|
|
10315
|
+
providerRouting: providerRoutingPublic(providerRouting),
|
|
9170
10316
|
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
9171
10317
|
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
9172
10318
|
workflow: publicWorkflow(outcome.workflow),
|
|
9173
10319
|
loop: publicLoop(outcome.loop),
|
|
10320
|
+
requeue: outcome.requeue,
|
|
9174
10321
|
throttle: outcome.throttle,
|
|
9175
10322
|
sandboxPreflight,
|
|
9176
10323
|
preflight
|
|
@@ -9201,14 +10348,14 @@ function taskDescriptionProjectPath(task) {
|
|
|
9201
10348
|
return match?.[1]?.trim();
|
|
9202
10349
|
}
|
|
9203
10350
|
function taskProjectPath(task) {
|
|
9204
|
-
const metadata =
|
|
10351
|
+
const metadata = objectField2(task.metadata) ?? {};
|
|
9205
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"]);
|
|
9206
10353
|
}
|
|
9207
10354
|
function taskDrainEvent(task) {
|
|
9208
10355
|
const taskId = taskField(task, ["id", "task_id", "taskId"]);
|
|
9209
10356
|
if (!taskId)
|
|
9210
10357
|
throw new Error("todos ready returned a task without an id");
|
|
9211
|
-
const metadata =
|
|
10358
|
+
const metadata = objectField2(task.metadata) ?? {};
|
|
9212
10359
|
const workingDir = taskProjectPath(task);
|
|
9213
10360
|
const data = {
|
|
9214
10361
|
...task,
|
|
@@ -9216,7 +10363,7 @@ function taskDrainEvent(task) {
|
|
|
9216
10363
|
title: taskField(task, ["title"]),
|
|
9217
10364
|
description: taskField(task, ["description", "body"]),
|
|
9218
10365
|
status: taskField(task, ["status"]),
|
|
9219
|
-
tags:
|
|
10366
|
+
tags: tagsFromValue2(task.tags),
|
|
9220
10367
|
metadata
|
|
9221
10368
|
};
|
|
9222
10369
|
if (workingDir) {
|
|
@@ -9244,10 +10391,12 @@ function taskDrainEvent(task) {
|
|
|
9244
10391
|
}
|
|
9245
10392
|
function compactDrainResult(result) {
|
|
9246
10393
|
const value = result.value;
|
|
9247
|
-
const event =
|
|
9248
|
-
const loop =
|
|
9249
|
-
const workflow =
|
|
9250
|
-
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);
|
|
9251
10400
|
return {
|
|
9252
10401
|
kind: result.kind,
|
|
9253
10402
|
taskId: event?.subject,
|
|
@@ -9258,6 +10407,8 @@ function compactDrainResult(result) {
|
|
|
9258
10407
|
loopName: stringField(loop?.name),
|
|
9259
10408
|
workflowId: stringField(workflow?.id),
|
|
9260
10409
|
workflowName: stringField(workflow?.name),
|
|
10410
|
+
providerRouting,
|
|
10411
|
+
requeue,
|
|
9261
10412
|
queuedAtSource: value.queuedAtSource
|
|
9262
10413
|
};
|
|
9263
10414
|
}
|
|
@@ -9304,7 +10455,7 @@ function taskMatchesDrainFilters(task, filters) {
|
|
|
9304
10455
|
return false;
|
|
9305
10456
|
}
|
|
9306
10457
|
if (filters.tags.length) {
|
|
9307
|
-
const taskTags = new Set(
|
|
10458
|
+
const taskTags = new Set(tagsFromValue2(task.tags));
|
|
9308
10459
|
for (const tag of filters.tags) {
|
|
9309
10460
|
if (!taskTags.has(tag))
|
|
9310
10461
|
return false;
|
|
@@ -9436,10 +10587,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
9436
10587
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
9437
10588
|
var goal = program.command("goal").description("inspect goal runs");
|
|
9438
10589
|
function addRouteEventOptions(command) {
|
|
9439
|
-
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");
|
|
9440
10591
|
}
|
|
9441
10592
|
function addTodosDrainOptions(command, opts = {}) {
|
|
9442
|
-
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");
|
|
9443
10594
|
if (opts.includeDryRun ?? true) {
|
|
9444
10595
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
9445
10596
|
}
|
|
@@ -9474,6 +10625,8 @@ function routeDrainArgs(opts) {
|
|
|
9474
10625
|
addBool("--compact", opts.compact);
|
|
9475
10626
|
add("--template", opts.template);
|
|
9476
10627
|
add("--provider", opts.provider);
|
|
10628
|
+
for (const rule of opts.providerRule ?? [])
|
|
10629
|
+
add("--provider-rule", rule);
|
|
9477
10630
|
add("--auth-profile", opts.authProfile);
|
|
9478
10631
|
add("--auth-profile-pool", opts.authProfilePool);
|
|
9479
10632
|
add("--triage-auth-profile", opts.triageAuthProfile);
|
|
@@ -9493,6 +10646,7 @@ function routeDrainArgs(opts) {
|
|
|
9493
10646
|
for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
|
|
9494
10647
|
add("--add-dir", dir);
|
|
9495
10648
|
add("--timeout", opts.timeout);
|
|
10649
|
+
add("--verifier-idle-timeout", opts.verifierIdleTimeout);
|
|
9496
10650
|
add("--permission-mode", opts.permissionMode);
|
|
9497
10651
|
add("--sandbox", opts.sandbox);
|
|
9498
10652
|
addBool("--manual-break-glass", opts.manualBreakGlass);
|
|
@@ -9504,6 +10658,9 @@ function routeDrainArgs(opts) {
|
|
|
9504
10658
|
add("--worktree-mode", opts.worktreeMode);
|
|
9505
10659
|
add("--worktree-root", opts.worktreeRoot);
|
|
9506
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);
|
|
9507
10664
|
add("--name-prefix", opts.namePrefix);
|
|
9508
10665
|
addBool("--preflight", opts.preflight);
|
|
9509
10666
|
return args;
|
|
@@ -9539,7 +10696,8 @@ function drainTodosTaskRoutes(opts) {
|
|
|
9539
10696
|
const message = error instanceof Error ? error.message : String(error);
|
|
9540
10697
|
if (!isSkippableDrainRouteError(message))
|
|
9541
10698
|
throw error;
|
|
9542
|
-
|
|
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 });
|
|
9543
10701
|
}
|
|
9544
10702
|
results.push(result);
|
|
9545
10703
|
if (result.kind === "created" && !opts.dryRun)
|
|
@@ -9750,6 +10908,18 @@ routes.command("show <id>").description("show one admission work item").action((
|
|
|
9750
10908
|
store.close();
|
|
9751
10909
|
}
|
|
9752
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
|
+
});
|
|
9753
10923
|
routes.command("invocations").description("list workflow invocations").option("--limit <n>", "maximum rows", "50").action((opts) => {
|
|
9754
10924
|
const store = new Store;
|
|
9755
10925
|
try {
|
|
@@ -9804,7 +10974,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9804
10974
|
}
|
|
9805
10975
|
});
|
|
9806
10976
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
9807
|
-
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) => {
|
|
9808
10978
|
const event = await readEventEnvelopeFromStdin();
|
|
9809
10979
|
const result = routeTodosTaskEvent(event, opts);
|
|
9810
10980
|
print(result.value, result.human);
|
|
@@ -9813,7 +10983,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
|
|
|
9813
10983
|
addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
|
|
9814
10984
|
drainTodosTaskRoutes(opts);
|
|
9815
10985
|
});
|
|
9816
|
-
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) => {
|
|
9817
10987
|
const event = await readEventEnvelopeFromStdin();
|
|
9818
10988
|
const result = routeGenericEvent(event, opts);
|
|
9819
10989
|
print(result.value, result.human);
|
|
@@ -10119,7 +11289,7 @@ program.command("list").alias("ls").option("--status <status>", "filter by statu
|
|
|
10119
11289
|
for (const loop of loops) {
|
|
10120
11290
|
const machine = loop.machine ? ` machine=${loop.machine.id}` : "";
|
|
10121
11291
|
const archive = loop.archivedAt ? ` archived=${loop.archivedAt} from=${loop.archivedFromStatus ?? "-"}` : "";
|
|
10122
|
-
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}`);
|
|
10123
11293
|
}
|
|
10124
11294
|
}
|
|
10125
11295
|
} finally {
|
|
@@ -10759,11 +11929,11 @@ ${result.instructions.join(`
|
|
|
10759
11929
|
});
|
|
10760
11930
|
daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
|
|
10761
11931
|
const path = daemonLogPath();
|
|
10762
|
-
if (!
|
|
11932
|
+
if (!existsSync6(path)) {
|
|
10763
11933
|
console.log("");
|
|
10764
11934
|
return;
|
|
10765
11935
|
}
|
|
10766
|
-
const lines =
|
|
11936
|
+
const lines = readFileSync5(path, "utf8").trimEnd().split(`
|
|
10767
11937
|
`);
|
|
10768
11938
|
console.log(lines.slice(-Number(opts.lines)).join(`
|
|
10769
11939
|
`));
|