@hasna/loops 0.4.6 → 0.4.7
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/CHANGELOG.md +13 -0
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +247 -40
- package/dist/daemon/index.js +51 -9
- package/dist/index.js +75 -9
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/types.d.ts +6 -0
- package/dist/lib/storage/index.js +1 -1
- package/dist/lib/storage/sqlite.js +1 -1
- package/dist/lib/store.js +1 -1
- package/dist/lib/templates.d.ts +10 -0
- package/dist/mcp/index.js +51 -9
- package/dist/runner/index.js +51 -9
- package/dist/sdk/index.js +51 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ documented in this file. Version entries are generated from the
|
|
|
5
5
|
conventional-commit git history; one commit maps to one released patch version
|
|
6
6
|
unless noted.
|
|
7
7
|
|
|
8
|
+
## 0.4.7 (2026-07-04)
|
|
9
|
+
|
|
10
|
+
Routing hardening for PR merge/drain automation.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Todos drain:** admit registered repo-project task sources while pinning
|
|
15
|
+
routed work to the scanned source project, preventing task-controlled nested
|
|
16
|
+
route paths from moving worker execution into another repository.
|
|
17
|
+
- **PR lifecycle routing:** propagate PR author and reviewer evidence into
|
|
18
|
+
reviewer and merger lifecycle prompts so merge routing can select a valid
|
|
19
|
+
non-author reviewer and keep reviewer/merger steps separate.
|
|
20
|
+
|
|
8
21
|
## 0.4.6 (2026-07-03)
|
|
9
22
|
|
|
10
23
|
Reliability hardening from a full audit of the runtime, control surfaces, and
|
package/dist/api/index.js
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -726,7 +726,7 @@ function buildAgentInvocation(target) {
|
|
|
726
726
|
if (target.model)
|
|
727
727
|
args.push("--model", target.model);
|
|
728
728
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
args.push("agent", "start");
|
|
729
|
+
args.push("agent", "start", "--json");
|
|
730
730
|
if (target.cwd)
|
|
731
731
|
args.push("--cwd", target.cwd);
|
|
732
732
|
args.push(target.prompt);
|
|
@@ -4219,7 +4219,7 @@ class Store {
|
|
|
4219
4219
|
// package.json
|
|
4220
4220
|
var package_default = {
|
|
4221
4221
|
name: "@hasna/loops",
|
|
4222
|
-
version: "0.4.
|
|
4222
|
+
version: "0.4.7",
|
|
4223
4223
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4224
4224
|
type: "module",
|
|
4225
4225
|
main: "dist/index.js",
|
|
@@ -5264,18 +5264,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5264
5264
|
"agent",
|
|
5265
5265
|
command,
|
|
5266
5266
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5267
|
+
"--json",
|
|
5267
5268
|
agentId
|
|
5268
5269
|
];
|
|
5269
5270
|
}
|
|
5271
|
+
function extractFirstJsonObject(text) {
|
|
5272
|
+
let depth = 0;
|
|
5273
|
+
let start = -1;
|
|
5274
|
+
let inString = false;
|
|
5275
|
+
let escaped = false;
|
|
5276
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5277
|
+
const ch = text[i];
|
|
5278
|
+
if (inString) {
|
|
5279
|
+
if (escaped)
|
|
5280
|
+
escaped = false;
|
|
5281
|
+
else if (ch === "\\")
|
|
5282
|
+
escaped = true;
|
|
5283
|
+
else if (ch === '"')
|
|
5284
|
+
inString = false;
|
|
5285
|
+
continue;
|
|
5286
|
+
}
|
|
5287
|
+
if (ch === '"') {
|
|
5288
|
+
inString = true;
|
|
5289
|
+
} else if (ch === "{") {
|
|
5290
|
+
if (depth === 0)
|
|
5291
|
+
start = i;
|
|
5292
|
+
depth += 1;
|
|
5293
|
+
} else if (ch === "}") {
|
|
5294
|
+
if (depth > 0) {
|
|
5295
|
+
depth -= 1;
|
|
5296
|
+
if (depth === 0 && start !== -1)
|
|
5297
|
+
return text.slice(start, i + 1);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
return;
|
|
5302
|
+
}
|
|
5270
5303
|
function parseJsonOutput(stdout, label) {
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5304
|
+
const raw = stdout || "{}";
|
|
5305
|
+
const candidates = [raw.trim()];
|
|
5306
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5307
|
+
if (scanned && scanned !== raw.trim())
|
|
5308
|
+
candidates.push(scanned);
|
|
5309
|
+
let lastError;
|
|
5310
|
+
for (const candidate of candidates) {
|
|
5311
|
+
try {
|
|
5312
|
+
const value = JSON.parse(candidate);
|
|
5313
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5314
|
+
throw new Error("not an object");
|
|
5315
|
+
return value;
|
|
5316
|
+
} catch (error) {
|
|
5317
|
+
lastError = error;
|
|
5318
|
+
}
|
|
5278
5319
|
}
|
|
5320
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5279
5321
|
}
|
|
5280
5322
|
function recordField(value, key) {
|
|
5281
5323
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -9973,6 +10015,28 @@ function importCustomLoopTemplate(file, reservedKeys, opts = {}) {
|
|
|
9973
10015
|
function compactJson(value) {
|
|
9974
10016
|
return JSON.stringify(value);
|
|
9975
10017
|
}
|
|
10018
|
+
function prReviewRoutingContext(input) {
|
|
10019
|
+
return input.prReviewRouting?.required ? input.prReviewRouting : undefined;
|
|
10020
|
+
}
|
|
10021
|
+
function prReviewFollowUpFragment(input) {
|
|
10022
|
+
const routing = prReviewRoutingContext(input);
|
|
10023
|
+
const author = routing?.author?.trim();
|
|
10024
|
+
const reviewers = routing?.reviewers?.map((reviewer) => reviewer.trim()).filter(Boolean) ?? [];
|
|
10025
|
+
const evidenceLines = [
|
|
10026
|
+
author ? `- Source PR author evidence: GitHub author is ${author}` : undefined,
|
|
10027
|
+
reviewers.length ? `- Source PR reviewer evidence: GitHub reviewer pool: ${reviewers.join(", ")}` : undefined,
|
|
10028
|
+
routing?.selectedReviewer ? `- Selected non-author reviewer: ${routing.selectedReviewer}` : undefined
|
|
10029
|
+
].filter(Boolean);
|
|
10030
|
+
return [
|
|
10031
|
+
"PR-derived follow-up todos: If any lifecycle step creates a follow-up todo that references a GitHub PR, PR approval, PR review, or PR merge work, the todo description must include parser-compatible routing evidence so downstream drains can select a non-author reviewer.",
|
|
10032
|
+
...evidenceLines,
|
|
10033
|
+
"Copy these exact evidence lines from the source task when present, or derive them from the referenced PR before creating the follow-up todo:",
|
|
10034
|
+
"GitHub author is <login>",
|
|
10035
|
+
"GitHub reviewer pool: <login>, <login>",
|
|
10036
|
+
"When the source PR author or reviewer pool cannot be determined, do not create an auto-routable PR-derived follow-up todo; comment the source task with the blocker instead."
|
|
10037
|
+
].join(`
|
|
10038
|
+
`);
|
|
10039
|
+
}
|
|
9976
10040
|
function taskLabel(input) {
|
|
9977
10041
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
9978
10042
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
@@ -10428,6 +10492,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10428
10492
|
routeProjectPath: input.routeProjectPath,
|
|
10429
10493
|
projectGroup: input.projectGroup,
|
|
10430
10494
|
todosProjectPath,
|
|
10495
|
+
prReviewRouting: prReviewRoutingContext(input),
|
|
10431
10496
|
worktree: worktreeContextFragment(plan)
|
|
10432
10497
|
};
|
|
10433
10498
|
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
@@ -10445,6 +10510,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10445
10510
|
]),
|
|
10446
10511
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
10447
10512
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
10513
|
+
prReviewFollowUpFragment(input),
|
|
10448
10514
|
"",
|
|
10449
10515
|
`Task context JSON: ${compactJson(taskContext)}`,
|
|
10450
10516
|
prHandoffGuidance
|
|
@@ -11630,12 +11696,30 @@ function routeEvidenceText(records) {
|
|
|
11630
11696
|
const values = [];
|
|
11631
11697
|
for (const record of records) {
|
|
11632
11698
|
for (const field of fields)
|
|
11633
|
-
values.push(...
|
|
11699
|
+
values.push(...routeTextFieldValues(record, field));
|
|
11634
11700
|
values.push(...tagsFromValue2(record.tags ?? record.task_tags ?? record.taskTags));
|
|
11635
11701
|
}
|
|
11636
11702
|
return values.join(`
|
|
11637
11703
|
`);
|
|
11638
11704
|
}
|
|
11705
|
+
function textValuesFromUnknown(value) {
|
|
11706
|
+
if (Array.isArray(value))
|
|
11707
|
+
return value.flatMap((entry) => textValuesFromUnknown(entry));
|
|
11708
|
+
if (typeof value === "string" && value.trim())
|
|
11709
|
+
return [value.trim()];
|
|
11710
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
11711
|
+
return [String(value)];
|
|
11712
|
+
return [];
|
|
11713
|
+
}
|
|
11714
|
+
function routeTextFieldValues(record, field) {
|
|
11715
|
+
const expected = canonicalRouteField(field);
|
|
11716
|
+
const values = [];
|
|
11717
|
+
for (const [key, value] of Object.entries(record)) {
|
|
11718
|
+
if (canonicalRouteField(key) === expected)
|
|
11719
|
+
values.push(...textValuesFromUnknown(value));
|
|
11720
|
+
}
|
|
11721
|
+
return values;
|
|
11722
|
+
}
|
|
11639
11723
|
function authorFromPrText(text) {
|
|
11640
11724
|
const patterns = [
|
|
11641
11725
|
/\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
|
|
@@ -11650,6 +11734,18 @@ function authorFromPrText(text) {
|
|
|
11650
11734
|
}
|
|
11651
11735
|
return;
|
|
11652
11736
|
}
|
|
11737
|
+
function reviewersFromPrText(text) {
|
|
11738
|
+
const reviewers = [];
|
|
11739
|
+
const patterns = [
|
|
11740
|
+
/\bgithub\s+reviewer\s+pool\s*(?:is|=|:)\s*([^\r\n]+)/gi,
|
|
11741
|
+
/\bgithub\s+reviewers?\s*(?:are|is|=|:)\s*([^\r\n]+)/gi
|
|
11742
|
+
];
|
|
11743
|
+
for (const pattern of patterns) {
|
|
11744
|
+
for (const match of text.matchAll(pattern))
|
|
11745
|
+
reviewers.push(...splitList(match[1]) ?? []);
|
|
11746
|
+
}
|
|
11747
|
+
return githubLogins(reviewers);
|
|
11748
|
+
}
|
|
11653
11749
|
function prReviewRoutingDecision(data, metadata, opts) {
|
|
11654
11750
|
const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
|
|
11655
11751
|
const text = routeEvidenceText(records);
|
|
@@ -11675,7 +11771,8 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11675
11771
|
opts.githubReviewer,
|
|
11676
11772
|
...splitList(opts.githubReviewerPool) ?? [],
|
|
11677
11773
|
...PR_REVIEWER_FIELDS.flatMap((field) => routeFieldValues(records, field)),
|
|
11678
|
-
...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field))
|
|
11774
|
+
...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
|
|
11775
|
+
...reviewersFromPrText(text)
|
|
11679
11776
|
]);
|
|
11680
11777
|
const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
|
|
11681
11778
|
if (!author) {
|
|
@@ -12063,7 +12160,16 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
12063
12160
|
}
|
|
12064
12161
|
const taskTitle = taskEventField(data, ["title", "task_title", "taskTitle"]);
|
|
12065
12162
|
const taskDescription = taskEventField(data, ["description", "body"]);
|
|
12066
|
-
const
|
|
12163
|
+
const sourceTodosProjectPath = opts.sourceTodosProjectPath?.trim();
|
|
12164
|
+
const dataProjectPath = taskEventField(data, [
|
|
12165
|
+
"route_project_path",
|
|
12166
|
+
"routeProjectPath",
|
|
12167
|
+
"project_path",
|
|
12168
|
+
"projectPath",
|
|
12169
|
+
"working_dir",
|
|
12170
|
+
"workingDir",
|
|
12171
|
+
"cwd"
|
|
12172
|
+
]);
|
|
12067
12173
|
const metadataProjectPath = taskEventField(metadata, [
|
|
12068
12174
|
"working_dir",
|
|
12069
12175
|
"workingDir",
|
|
@@ -12076,7 +12182,8 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
12076
12182
|
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve7(projectPath);
|
|
12077
12183
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
12078
12184
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
12079
|
-
const
|
|
12185
|
+
const sourceProjectIdempotencyPrefix = sourceTodosProjectPath ? normalizeRoutePath(sourceTodosProjectPath) ?? resolve7(sourceTodosProjectPath) : undefined;
|
|
12186
|
+
const idempotencyKey = sourceProjectIdempotencyPrefix ? `todos-task:${sourceProjectIdempotencyPrefix}:${taskId}` : `todos-task:${taskId}`;
|
|
12080
12187
|
const idempotencySuffix = stableSuffix(idempotencyKey);
|
|
12081
12188
|
const namePrefix = opts.namePrefix ?? "event:todos-task";
|
|
12082
12189
|
const workflowName = `${namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:workflow`;
|
|
@@ -12150,9 +12257,10 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
12150
12257
|
worktreeRoot: opts.worktreeRoot,
|
|
12151
12258
|
worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
|
|
12152
12259
|
prHandoff: templateId === TASK_LIFECYCLE_TEMPLATE_ID ? Boolean(opts.prHandoff) : false,
|
|
12260
|
+
prReviewRouting: prReviewRouting.required ? prReviewRouting : undefined,
|
|
12153
12261
|
eventId: event.id,
|
|
12154
12262
|
eventType: event.type,
|
|
12155
|
-
todosProjectPath: opts.todosProject
|
|
12263
|
+
todosProjectPath: sourceTodosProjectPath || opts.todosProject
|
|
12156
12264
|
};
|
|
12157
12265
|
const workflowContext = {
|
|
12158
12266
|
name: workflowName,
|
|
@@ -12420,8 +12528,8 @@ function taskField(task, keys) {
|
|
|
12420
12528
|
}
|
|
12421
12529
|
return;
|
|
12422
12530
|
}
|
|
12423
|
-
function
|
|
12424
|
-
return
|
|
12531
|
+
function taskRoutePathFromRegistry(sourceProjectPath) {
|
|
12532
|
+
return normalizeRoutePath(sourceProjectPath) ?? resolve8(sourceProjectPath);
|
|
12425
12533
|
}
|
|
12426
12534
|
function taskProjectId(task) {
|
|
12427
12535
|
return taskField(task, ["project_id", "projectId"]);
|
|
@@ -12435,12 +12543,28 @@ function taskProjectPath(task) {
|
|
|
12435
12543
|
const metadata = objectField2(task.metadata) ?? {};
|
|
12436
12544
|
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"]);
|
|
12437
12545
|
}
|
|
12438
|
-
function
|
|
12546
|
+
function taskListValues(task) {
|
|
12547
|
+
const taskList = objectField2(task.task_list);
|
|
12548
|
+
return [
|
|
12549
|
+
taskField(task, ["task_list_id", "taskListId"]),
|
|
12550
|
+
stringField2(task.task_list?.id),
|
|
12551
|
+
stringField2(task.task_list?.slug),
|
|
12552
|
+
stringField2(taskList?.name),
|
|
12553
|
+
taskField(task, ["task_list", "taskList"])
|
|
12554
|
+
].filter((value) => Boolean(value));
|
|
12555
|
+
}
|
|
12556
|
+
function taskDrainEvent(task, sourceProjectPath) {
|
|
12439
12557
|
const taskId = taskField(task, ["id", "task_id", "taskId"]);
|
|
12440
12558
|
if (!taskId)
|
|
12441
12559
|
throw new Error("todos ready returned a task without an id");
|
|
12442
|
-
const metadata = objectField2(task.metadata) ?? {};
|
|
12560
|
+
const metadata = { ...objectField2(task.metadata) ?? {} };
|
|
12443
12561
|
const workingDir = taskProjectPath(task);
|
|
12562
|
+
const routeWorkingDir = taskField(task, ["project_path", "projectPath"]) ?? workingDir;
|
|
12563
|
+
const sourceProject = sourceProjectPath?.trim();
|
|
12564
|
+
if (!sourceProject) {
|
|
12565
|
+
delete metadata.source_project_path;
|
|
12566
|
+
delete metadata.sourceProjectPath;
|
|
12567
|
+
}
|
|
12444
12568
|
const data = {
|
|
12445
12569
|
...task,
|
|
12446
12570
|
id: taskId,
|
|
@@ -12450,10 +12574,23 @@ function taskDrainEvent(task) {
|
|
|
12450
12574
|
tags: tagsFromValue2(task.tags),
|
|
12451
12575
|
metadata
|
|
12452
12576
|
};
|
|
12577
|
+
if (!sourceProject) {
|
|
12578
|
+
delete data.source_project_path;
|
|
12579
|
+
delete data.sourceProjectPath;
|
|
12580
|
+
}
|
|
12453
12581
|
if (workingDir) {
|
|
12454
|
-
data.working_dir =
|
|
12455
|
-
data.project_path =
|
|
12456
|
-
data.cwd = taskField(task, ["cwd"]) ??
|
|
12582
|
+
data.working_dir = routeWorkingDir;
|
|
12583
|
+
data.project_path = routeWorkingDir;
|
|
12584
|
+
data.cwd = taskField(task, ["cwd"]) ?? routeWorkingDir;
|
|
12585
|
+
}
|
|
12586
|
+
if (sourceProject) {
|
|
12587
|
+
data.source_project_path = sourceProject;
|
|
12588
|
+
if (!data.project_path)
|
|
12589
|
+
data.project_path = sourceProject;
|
|
12590
|
+
data.route_project_path = data.project_path;
|
|
12591
|
+
data.routeProjectPath = data.project_path;
|
|
12592
|
+
metadata.route_project_path = data.project_path;
|
|
12593
|
+
metadata.routeProjectPath = data.project_path;
|
|
12457
12594
|
}
|
|
12458
12595
|
const time = new Date().toISOString();
|
|
12459
12596
|
return {
|
|
@@ -12467,6 +12604,7 @@ function taskDrainEvent(task) {
|
|
|
12467
12604
|
schemaVersion: "1.0",
|
|
12468
12605
|
metadata: {
|
|
12469
12606
|
...metadata,
|
|
12607
|
+
...sourceProject ? { source_project_path: sourceProject } : {},
|
|
12470
12608
|
...workingDir ? { working_dir: workingDir, project_path: data.project_path, cwd: data.cwd } : {},
|
|
12471
12609
|
drained_by: "@hasna/loops",
|
|
12472
12610
|
drained_from: "todos ready"
|
|
@@ -12497,23 +12635,74 @@ function compactDrainResult(result) {
|
|
|
12497
12635
|
fatal: value.fatal === true ? true : undefined
|
|
12498
12636
|
};
|
|
12499
12637
|
}
|
|
12500
|
-
function
|
|
12501
|
-
const todosProject = opts.todosProject ?? defaultLoopsProject();
|
|
12502
|
-
const args = ["--project", todosProject, "--json", "ready", "--limit", String(scanLimit)];
|
|
12503
|
-
const result = runLocalCommandWithStdoutFile("todos", args, { timeoutMs: 60000, maxBuffer: 64 * 1024 * 1024 });
|
|
12504
|
-
if (!result.ok)
|
|
12505
|
-
throw new Error(result.stderr || result.error || "todos ready failed");
|
|
12506
|
-
let parsed;
|
|
12638
|
+
function parseTodosReadyJson(stdout) {
|
|
12507
12639
|
try {
|
|
12508
|
-
|
|
12640
|
+
return JSON.parse(stdout || "[]");
|
|
12509
12641
|
} catch (error) {
|
|
12510
12642
|
const message = error instanceof Error ? error.message : String(error);
|
|
12511
|
-
throw new Error(`failed to parse todos ready --json output (${
|
|
12643
|
+
throw new Error(`failed to parse todos ready --json output (${stdout.length} bytes): ${message}`);
|
|
12512
12644
|
}
|
|
12645
|
+
}
|
|
12646
|
+
function parseTodoProjectsJson(stdout) {
|
|
12647
|
+
try {
|
|
12648
|
+
return JSON.parse(stdout || "[]");
|
|
12649
|
+
} catch (error) {
|
|
12650
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12651
|
+
throw new Error(`failed to parse todos projects --json output (${stdout.length} bytes): ${message}`);
|
|
12652
|
+
}
|
|
12653
|
+
}
|
|
12654
|
+
function loadTodoProjectPathsFromRegistry(opts) {
|
|
12655
|
+
const result = runLocalCommand("todos", ["projects", "--json"], { timeoutMs: 60000 });
|
|
12656
|
+
if (!result.ok)
|
|
12657
|
+
throw new Error(result.stderr || result.error || "todos projects failed");
|
|
12658
|
+
const payload = parseTodoProjectsJson(result.stdout);
|
|
12659
|
+
const projectsPayload = Array.isArray(payload) ? payload : objectField2(payload)?.projects;
|
|
12660
|
+
if (!Array.isArray(projectsPayload))
|
|
12661
|
+
throw new Error("todos projects --json returned a non-array value");
|
|
12662
|
+
const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
|
|
12663
|
+
const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
|
|
12664
|
+
const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
|
|
12665
|
+
const path = stringField2(project.path) ?? stringField2(project.projectPath) ?? stringField2(project.project_path) ?? stringField2(project.dir) ?? stringField2(project.root) ?? stringField2(project.cwd);
|
|
12666
|
+
if (!path)
|
|
12667
|
+
return;
|
|
12668
|
+
return { path };
|
|
12669
|
+
}).filter((project) => Boolean(project));
|
|
12670
|
+
const paths = fromProjects.map((project) => project.path).filter(Boolean).filter((path) => {
|
|
12671
|
+
const normalizedPath = normalizeRoutePath(path) ?? resolve8(path);
|
|
12672
|
+
if (includesPrefix && !(normalizedPath === includesPrefix || normalizedPath.startsWith(`${includesPrefix}/`)))
|
|
12673
|
+
return false;
|
|
12674
|
+
if (includeFilters.length === 0)
|
|
12675
|
+
return true;
|
|
12676
|
+
return includeFilters.some((prefix) => normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`));
|
|
12677
|
+
});
|
|
12678
|
+
return [...new Set(paths)];
|
|
12679
|
+
}
|
|
12680
|
+
function loadReadyTodosTasksForProject(projectPath, scanLimit) {
|
|
12681
|
+
const args = ["--project", projectPath, "--json", "ready", "--limit", String(scanLimit)];
|
|
12682
|
+
const result = runLocalCommandWithStdoutFile("todos", args, { timeoutMs: 60000, maxBuffer: 64 * 1024 * 1024 });
|
|
12683
|
+
if (!result.ok)
|
|
12684
|
+
throw new Error(result.stderr || result.error || "todos ready failed");
|
|
12685
|
+
const parsed = parseTodosReadyJson(result.stdout);
|
|
12513
12686
|
if (!Array.isArray(parsed))
|
|
12514
12687
|
throw new Error("todos ready --json returned a non-array value");
|
|
12515
12688
|
return parsed;
|
|
12516
12689
|
}
|
|
12690
|
+
function loadReadyTodosTasks(opts, scanLimit) {
|
|
12691
|
+
if (!opts.todosProjectsFromRegistry) {
|
|
12692
|
+
const todosProject = opts.todosProject ?? defaultLoopsProject();
|
|
12693
|
+
return loadReadyTodosTasksForProject(todosProject, scanLimit);
|
|
12694
|
+
}
|
|
12695
|
+
const projectPaths = loadTodoProjectPathsFromRegistry(opts);
|
|
12696
|
+
const ready = projectPaths.flatMap((projectPath) => loadReadyTodosTasksForProject(projectPath, scanLimit).map((task) => {
|
|
12697
|
+
const sourceProject = projectPath;
|
|
12698
|
+
return {
|
|
12699
|
+
...task,
|
|
12700
|
+
source_project_path: sourceProject,
|
|
12701
|
+
project_path: taskRoutePathFromRegistry(sourceProject)
|
|
12702
|
+
};
|
|
12703
|
+
}));
|
|
12704
|
+
return ready;
|
|
12705
|
+
}
|
|
12517
12706
|
function resolveTaskListFilter(todosProject, filter) {
|
|
12518
12707
|
const wanted = filter?.trim();
|
|
12519
12708
|
if (!wanted)
|
|
@@ -12528,7 +12717,7 @@ function resolveTaskListFilter(todosProject, filter) {
|
|
|
12528
12717
|
function taskMatchesDrainFilters(task, filters) {
|
|
12529
12718
|
if (filters.projectId && taskProjectId(task) !== filters.projectId)
|
|
12530
12719
|
return false;
|
|
12531
|
-
if (filters.
|
|
12720
|
+
if (filters.taskList && !taskListValues(task).includes(filters.taskList))
|
|
12532
12721
|
return false;
|
|
12533
12722
|
if (filters.projectPathPrefix) {
|
|
12534
12723
|
const path = taskProjectPath(task);
|
|
@@ -12566,14 +12755,14 @@ function skippedDrainTask(task, event, reason, extra = {}) {
|
|
|
12566
12755
|
function isSkippableDrainRouteError(message) {
|
|
12567
12756
|
return message.startsWith("worktreeMode=required but projectPath is not an existing git repository:");
|
|
12568
12757
|
}
|
|
12569
|
-
function markInvalidDrainTaskNonRouteable(
|
|
12758
|
+
function markInvalidDrainTaskNonRouteable(sourceTodosProject, task, reason) {
|
|
12570
12759
|
const taskId = taskField(task, ["id", "task_id", "taskId"]);
|
|
12571
12760
|
if (!taskId)
|
|
12572
12761
|
return { attempted: false, reason: "task id missing" };
|
|
12573
12762
|
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.`;
|
|
12574
|
-
const commentResult = runLocalCommand("todos", ["--project",
|
|
12575
|
-
const tagResult = runLocalCommand("todos", ["--project",
|
|
12576
|
-
const untagResult = runLocalCommand("todos", ["--project",
|
|
12763
|
+
const commentResult = runLocalCommand("todos", ["--project", sourceTodosProject, "comment", taskId, comment], { timeoutMs: 30000 });
|
|
12764
|
+
const tagResult = runLocalCommand("todos", ["--project", sourceTodosProject, "tag", taskId, "no-auto"], { timeoutMs: 30000 });
|
|
12765
|
+
const untagResult = runLocalCommand("todos", ["--project", sourceTodosProject, "untag", taskId, "auto:route"], { timeoutMs: 30000 });
|
|
12577
12766
|
const ok = commentResult.ok && tagResult.ok && untagResult.ok;
|
|
12578
12767
|
return {
|
|
12579
12768
|
ok,
|
|
@@ -12589,7 +12778,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
12589
12778
|
const maxDispatch = positiveInteger(opts.maxDispatch ?? "1", "--max-dispatch") ?? 1;
|
|
12590
12779
|
const todosProject = opts.todosProject ?? defaultLoopsProject();
|
|
12591
12780
|
const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
|
|
12592
|
-
const taskListFilter = resolveTaskListFilter(todosProject, opts.taskList);
|
|
12781
|
+
const taskListFilter = opts.todosProjectsFromRegistry ? opts.taskList?.trim() : resolveTaskListFilter(todosProject, opts.taskList);
|
|
12593
12782
|
const candidateLimit = positiveInteger(opts.limit ?? "50", "--limit") ?? 50;
|
|
12594
12783
|
const hasPostFilters = Boolean(opts.todosProjectId || taskListFilter || opts.projectPathPrefix || requiredTags.length);
|
|
12595
12784
|
const defaultScanLimit = hasPostFilters ? Math.max(candidateLimit, 500) : candidateLimit;
|
|
@@ -12597,7 +12786,7 @@ function drainTodosTaskRoutes(opts) {
|
|
|
12597
12786
|
const ready = loadReadyTodosTasks(opts, scanLimit);
|
|
12598
12787
|
const filteredCandidates = ready.filter((task) => taskMatchesDrainFilters(task, {
|
|
12599
12788
|
projectId: opts.todosProjectId,
|
|
12600
|
-
|
|
12789
|
+
taskList: taskListFilter,
|
|
12601
12790
|
projectPathPrefix: opts.projectPathPrefix,
|
|
12602
12791
|
tags: requiredTags
|
|
12603
12792
|
}));
|
|
@@ -12610,12 +12799,17 @@ function drainTodosTaskRoutes(opts) {
|
|
|
12610
12799
|
let event;
|
|
12611
12800
|
let result;
|
|
12612
12801
|
try {
|
|
12613
|
-
|
|
12614
|
-
|
|
12802
|
+
const sourceProject = opts.todosProjectsFromRegistry ? taskField(task, ["source_project_path", "sourceProjectPath"]) : undefined;
|
|
12803
|
+
event = taskDrainEvent(task, sourceProject);
|
|
12804
|
+
result = routeTodosTaskEvent(event, {
|
|
12805
|
+
...opts,
|
|
12806
|
+
...sourceProject ? { sourceTodosProjectPath: sourceProject } : {}
|
|
12807
|
+
});
|
|
12615
12808
|
} catch (error) {
|
|
12616
12809
|
const message = error instanceof Error ? error.message : String(error);
|
|
12617
12810
|
if (isSkippableDrainRouteError(message)) {
|
|
12618
|
-
const
|
|
12811
|
+
const sourceTaskProject = opts.todosProjectsFromRegistry ? taskField(task, ["source_project_path", "sourceProjectPath"]) ?? todosProject : todosProject;
|
|
12812
|
+
const sourceTaskUpdate = opts.dryRun ? { attempted: false, reason: "dry-run" } : markInvalidDrainTaskNonRouteable(sourceTaskProject, task, message);
|
|
12619
12813
|
result = skippedDrainTask(task, event, redact(message, 640) ?? "route task failed", { sourceTaskUpdate });
|
|
12620
12814
|
} else {
|
|
12621
12815
|
result = skippedDrainTask(task, event, redact(message, 640) ?? "route task failed", { fatal: true });
|
|
@@ -12960,6 +13154,19 @@ var EVENT_INPUT_OPTION_SPECS = [
|
|
|
12960
13154
|
{ flags: "--event-json <json>", key: "eventJson", kind: "value", description: "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON" }
|
|
12961
13155
|
];
|
|
12962
13156
|
var DRAIN_FILTER_OPTION_SPECS = [
|
|
13157
|
+
{
|
|
13158
|
+
flags: "--todos-projects-from-registry",
|
|
13159
|
+
key: "todosProjectsFromRegistry",
|
|
13160
|
+
kind: "boolean",
|
|
13161
|
+
description: "scan registered todos projects from `todos projects --json` instead of one --todos-project"
|
|
13162
|
+
},
|
|
13163
|
+
{
|
|
13164
|
+
flags: "--todos-project-include <path>",
|
|
13165
|
+
key: "todosProjectInclude",
|
|
13166
|
+
kind: "repeat",
|
|
13167
|
+
description: "include additional registered project path prefixes when scanning via --todos-projects-from-registry",
|
|
13168
|
+
serializeValue: (opts) => listFromRepeatedOpts(opts.todosProjectInclude)
|
|
13169
|
+
},
|
|
12963
13170
|
{ flags: "--todos-project-id <id>", key: "todosProjectId", kind: "value", description: "filter todos ready output to one todos project id" },
|
|
12964
13171
|
{ flags: "--task-list <id-or-slug>", key: "taskList", kind: "value", description: "filter ready tasks to one task-list id, slug, or name" },
|
|
12965
13172
|
{ flags: "--project-path-prefix <path>", key: "projectPathPrefix", kind: "value", description: "filter ready tasks to a project/repo path prefix" },
|
package/dist/daemon/index.js
CHANGED
|
@@ -726,7 +726,7 @@ function buildAgentInvocation(target) {
|
|
|
726
726
|
if (target.model)
|
|
727
727
|
args.push("--model", target.model);
|
|
728
728
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
args.push("agent", "start");
|
|
729
|
+
args.push("agent", "start", "--json");
|
|
730
730
|
if (target.cwd)
|
|
731
731
|
args.push("--cwd", target.cwd);
|
|
732
732
|
args.push(target.prompt);
|
|
@@ -5425,18 +5425,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5425
5425
|
"agent",
|
|
5426
5426
|
command,
|
|
5427
5427
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5428
|
+
"--json",
|
|
5428
5429
|
agentId
|
|
5429
5430
|
];
|
|
5430
5431
|
}
|
|
5432
|
+
function extractFirstJsonObject(text) {
|
|
5433
|
+
let depth = 0;
|
|
5434
|
+
let start = -1;
|
|
5435
|
+
let inString = false;
|
|
5436
|
+
let escaped = false;
|
|
5437
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5438
|
+
const ch = text[i];
|
|
5439
|
+
if (inString) {
|
|
5440
|
+
if (escaped)
|
|
5441
|
+
escaped = false;
|
|
5442
|
+
else if (ch === "\\")
|
|
5443
|
+
escaped = true;
|
|
5444
|
+
else if (ch === '"')
|
|
5445
|
+
inString = false;
|
|
5446
|
+
continue;
|
|
5447
|
+
}
|
|
5448
|
+
if (ch === '"') {
|
|
5449
|
+
inString = true;
|
|
5450
|
+
} else if (ch === "{") {
|
|
5451
|
+
if (depth === 0)
|
|
5452
|
+
start = i;
|
|
5453
|
+
depth += 1;
|
|
5454
|
+
} else if (ch === "}") {
|
|
5455
|
+
if (depth > 0) {
|
|
5456
|
+
depth -= 1;
|
|
5457
|
+
if (depth === 0 && start !== -1)
|
|
5458
|
+
return text.slice(start, i + 1);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
return;
|
|
5463
|
+
}
|
|
5431
5464
|
function parseJsonOutput(stdout, label) {
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5465
|
+
const raw = stdout || "{}";
|
|
5466
|
+
const candidates = [raw.trim()];
|
|
5467
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5468
|
+
if (scanned && scanned !== raw.trim())
|
|
5469
|
+
candidates.push(scanned);
|
|
5470
|
+
let lastError;
|
|
5471
|
+
for (const candidate of candidates) {
|
|
5472
|
+
try {
|
|
5473
|
+
const value = JSON.parse(candidate);
|
|
5474
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5475
|
+
throw new Error("not an object");
|
|
5476
|
+
return value;
|
|
5477
|
+
} catch (error) {
|
|
5478
|
+
lastError = error;
|
|
5479
|
+
}
|
|
5439
5480
|
}
|
|
5481
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5440
5482
|
}
|
|
5441
5483
|
function recordField(value, key) {
|
|
5442
5484
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -7961,7 +8003,7 @@ function enableStartup(result) {
|
|
|
7961
8003
|
// package.json
|
|
7962
8004
|
var package_default = {
|
|
7963
8005
|
name: "@hasna/loops",
|
|
7964
|
-
version: "0.4.
|
|
8006
|
+
version: "0.4.7",
|
|
7965
8007
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7966
8008
|
type: "module",
|
|
7967
8009
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -4217,7 +4217,7 @@ class Store {
|
|
|
4217
4217
|
// package.json
|
|
4218
4218
|
var package_default = {
|
|
4219
4219
|
name: "@hasna/loops",
|
|
4220
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.7",
|
|
4221
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4222
4222
|
type: "module",
|
|
4223
4223
|
main: "dist/index.js",
|
|
@@ -5371,18 +5371,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5371
5371
|
"agent",
|
|
5372
5372
|
command,
|
|
5373
5373
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5374
|
+
"--json",
|
|
5374
5375
|
agentId
|
|
5375
5376
|
];
|
|
5376
5377
|
}
|
|
5378
|
+
function extractFirstJsonObject(text) {
|
|
5379
|
+
let depth = 0;
|
|
5380
|
+
let start = -1;
|
|
5381
|
+
let inString = false;
|
|
5382
|
+
let escaped = false;
|
|
5383
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5384
|
+
const ch = text[i];
|
|
5385
|
+
if (inString) {
|
|
5386
|
+
if (escaped)
|
|
5387
|
+
escaped = false;
|
|
5388
|
+
else if (ch === "\\")
|
|
5389
|
+
escaped = true;
|
|
5390
|
+
else if (ch === '"')
|
|
5391
|
+
inString = false;
|
|
5392
|
+
continue;
|
|
5393
|
+
}
|
|
5394
|
+
if (ch === '"') {
|
|
5395
|
+
inString = true;
|
|
5396
|
+
} else if (ch === "{") {
|
|
5397
|
+
if (depth === 0)
|
|
5398
|
+
start = i;
|
|
5399
|
+
depth += 1;
|
|
5400
|
+
} else if (ch === "}") {
|
|
5401
|
+
if (depth > 0) {
|
|
5402
|
+
depth -= 1;
|
|
5403
|
+
if (depth === 0 && start !== -1)
|
|
5404
|
+
return text.slice(start, i + 1);
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5377
5410
|
function parseJsonOutput(stdout, label) {
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5411
|
+
const raw = stdout || "{}";
|
|
5412
|
+
const candidates = [raw.trim()];
|
|
5413
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5414
|
+
if (scanned && scanned !== raw.trim())
|
|
5415
|
+
candidates.push(scanned);
|
|
5416
|
+
let lastError;
|
|
5417
|
+
for (const candidate of candidates) {
|
|
5418
|
+
try {
|
|
5419
|
+
const value = JSON.parse(candidate);
|
|
5420
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5421
|
+
throw new Error("not an object");
|
|
5422
|
+
return value;
|
|
5423
|
+
} catch (error) {
|
|
5424
|
+
lastError = error;
|
|
5425
|
+
}
|
|
5385
5426
|
}
|
|
5427
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5386
5428
|
}
|
|
5387
5429
|
function recordField(value, key) {
|
|
5388
5430
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -10729,6 +10771,28 @@ function importCustomLoopTemplate(file, reservedKeys, opts = {}) {
|
|
|
10729
10771
|
function compactJson(value) {
|
|
10730
10772
|
return JSON.stringify(value);
|
|
10731
10773
|
}
|
|
10774
|
+
function prReviewRoutingContext(input) {
|
|
10775
|
+
return input.prReviewRouting?.required ? input.prReviewRouting : undefined;
|
|
10776
|
+
}
|
|
10777
|
+
function prReviewFollowUpFragment(input) {
|
|
10778
|
+
const routing = prReviewRoutingContext(input);
|
|
10779
|
+
const author = routing?.author?.trim();
|
|
10780
|
+
const reviewers = routing?.reviewers?.map((reviewer) => reviewer.trim()).filter(Boolean) ?? [];
|
|
10781
|
+
const evidenceLines = [
|
|
10782
|
+
author ? `- Source PR author evidence: GitHub author is ${author}` : undefined,
|
|
10783
|
+
reviewers.length ? `- Source PR reviewer evidence: GitHub reviewer pool: ${reviewers.join(", ")}` : undefined,
|
|
10784
|
+
routing?.selectedReviewer ? `- Selected non-author reviewer: ${routing.selectedReviewer}` : undefined
|
|
10785
|
+
].filter(Boolean);
|
|
10786
|
+
return [
|
|
10787
|
+
"PR-derived follow-up todos: If any lifecycle step creates a follow-up todo that references a GitHub PR, PR approval, PR review, or PR merge work, the todo description must include parser-compatible routing evidence so downstream drains can select a non-author reviewer.",
|
|
10788
|
+
...evidenceLines,
|
|
10789
|
+
"Copy these exact evidence lines from the source task when present, or derive them from the referenced PR before creating the follow-up todo:",
|
|
10790
|
+
"GitHub author is <login>",
|
|
10791
|
+
"GitHub reviewer pool: <login>, <login>",
|
|
10792
|
+
"When the source PR author or reviewer pool cannot be determined, do not create an auto-routable PR-derived follow-up todo; comment the source task with the blocker instead."
|
|
10793
|
+
].join(`
|
|
10794
|
+
`);
|
|
10795
|
+
}
|
|
10732
10796
|
function taskLabel(input) {
|
|
10733
10797
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
10734
10798
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
@@ -11184,6 +11248,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11184
11248
|
routeProjectPath: input.routeProjectPath,
|
|
11185
11249
|
projectGroup: input.projectGroup,
|
|
11186
11250
|
todosProjectPath,
|
|
11251
|
+
prReviewRouting: prReviewRoutingContext(input),
|
|
11187
11252
|
worktree: worktreeContextFragment(plan)
|
|
11188
11253
|
};
|
|
11189
11254
|
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
@@ -11201,6 +11266,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11201
11266
|
]),
|
|
11202
11267
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
11203
11268
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
11269
|
+
prReviewFollowUpFragment(input),
|
|
11204
11270
|
"",
|
|
11205
11271
|
`Task context JSON: ${compactJson(taskContext)}`,
|
|
11206
11272
|
prHandoffGuidance
|
package/dist/lib/mode.js
CHANGED
|
@@ -42,11 +42,15 @@ export interface TodosTaskRouteOptions {
|
|
|
42
42
|
preflight?: boolean;
|
|
43
43
|
dryRun?: boolean;
|
|
44
44
|
todosProject?: string;
|
|
45
|
+
/** Internal drain context; never derived from CLI/event fields. */
|
|
46
|
+
sourceTodosProjectPath?: string;
|
|
45
47
|
}
|
|
46
48
|
export interface TodosReadyTask {
|
|
47
49
|
id?: string;
|
|
48
50
|
task_id?: string;
|
|
49
51
|
taskId?: string;
|
|
52
|
+
source_project_path?: string;
|
|
53
|
+
sourceProjectPath?: string;
|
|
50
54
|
title?: string;
|
|
51
55
|
description?: string;
|
|
52
56
|
body?: string;
|
|
@@ -70,6 +74,8 @@ export interface TodosReadyTask {
|
|
|
70
74
|
}
|
|
71
75
|
export interface TodosDrainOptions extends TodosTaskRouteOptions {
|
|
72
76
|
todosProject?: string;
|
|
77
|
+
todosProjectsFromRegistry?: boolean;
|
|
78
|
+
todosProjectInclude?: string[];
|
|
73
79
|
todosProjectId?: string;
|
|
74
80
|
taskList?: string;
|
|
75
81
|
tags?: string;
|
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
package/dist/lib/store.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -39,9 +39,19 @@ export interface TodosTaskWorkflowTemplateInput extends AgentWorkflowTemplateBas
|
|
|
39
39
|
taskDescription?: string;
|
|
40
40
|
todosProjectPath?: string;
|
|
41
41
|
prHandoff?: boolean;
|
|
42
|
+
prReviewRouting?: PrReviewRoutingTemplateContext;
|
|
42
43
|
eventId?: string;
|
|
43
44
|
eventType?: string;
|
|
44
45
|
}
|
|
46
|
+
export interface PrReviewRoutingTemplateContext {
|
|
47
|
+
required?: boolean;
|
|
48
|
+
allowed?: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
author?: string;
|
|
51
|
+
reviewers?: string[];
|
|
52
|
+
selectedReviewer?: string;
|
|
53
|
+
signals?: string[];
|
|
54
|
+
}
|
|
45
55
|
export interface EventWorkflowTemplateInput extends AgentWorkflowTemplateBaseInput {
|
|
46
56
|
eventId: string;
|
|
47
57
|
eventType: string;
|
package/dist/mcp/index.js
CHANGED
|
@@ -726,7 +726,7 @@ function buildAgentInvocation(target) {
|
|
|
726
726
|
if (target.model)
|
|
727
727
|
args.push("--model", target.model);
|
|
728
728
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
args.push("agent", "start");
|
|
729
|
+
args.push("agent", "start", "--json");
|
|
730
730
|
if (target.cwd)
|
|
731
731
|
args.push("--cwd", target.cwd);
|
|
732
732
|
args.push(target.prompt);
|
|
@@ -5106,18 +5106,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5106
5106
|
"agent",
|
|
5107
5107
|
command,
|
|
5108
5108
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5109
|
+
"--json",
|
|
5109
5110
|
agentId
|
|
5110
5111
|
];
|
|
5111
5112
|
}
|
|
5113
|
+
function extractFirstJsonObject(text) {
|
|
5114
|
+
let depth = 0;
|
|
5115
|
+
let start = -1;
|
|
5116
|
+
let inString = false;
|
|
5117
|
+
let escaped = false;
|
|
5118
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5119
|
+
const ch = text[i];
|
|
5120
|
+
if (inString) {
|
|
5121
|
+
if (escaped)
|
|
5122
|
+
escaped = false;
|
|
5123
|
+
else if (ch === "\\")
|
|
5124
|
+
escaped = true;
|
|
5125
|
+
else if (ch === '"')
|
|
5126
|
+
inString = false;
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (ch === '"') {
|
|
5130
|
+
inString = true;
|
|
5131
|
+
} else if (ch === "{") {
|
|
5132
|
+
if (depth === 0)
|
|
5133
|
+
start = i;
|
|
5134
|
+
depth += 1;
|
|
5135
|
+
} else if (ch === "}") {
|
|
5136
|
+
if (depth > 0) {
|
|
5137
|
+
depth -= 1;
|
|
5138
|
+
if (depth === 0 && start !== -1)
|
|
5139
|
+
return text.slice(start, i + 1);
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5112
5145
|
function parseJsonOutput(stdout, label) {
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5146
|
+
const raw = stdout || "{}";
|
|
5147
|
+
const candidates = [raw.trim()];
|
|
5148
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5149
|
+
if (scanned && scanned !== raw.trim())
|
|
5150
|
+
candidates.push(scanned);
|
|
5151
|
+
let lastError;
|
|
5152
|
+
for (const candidate of candidates) {
|
|
5153
|
+
try {
|
|
5154
|
+
const value = JSON.parse(candidate);
|
|
5155
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5156
|
+
throw new Error("not an object");
|
|
5157
|
+
return value;
|
|
5158
|
+
} catch (error) {
|
|
5159
|
+
lastError = error;
|
|
5160
|
+
}
|
|
5120
5161
|
}
|
|
5162
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5121
5163
|
}
|
|
5122
5164
|
function recordField(value, key) {
|
|
5123
5165
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -7677,7 +7719,7 @@ async function tick(deps) {
|
|
|
7677
7719
|
// package.json
|
|
7678
7720
|
var package_default = {
|
|
7679
7721
|
name: "@hasna/loops",
|
|
7680
|
-
version: "0.4.
|
|
7722
|
+
version: "0.4.7",
|
|
7681
7723
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7682
7724
|
type: "module",
|
|
7683
7725
|
main: "dist/index.js",
|
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.7",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -510,7 +510,7 @@ function buildAgentInvocation(target) {
|
|
|
510
510
|
if (target.model)
|
|
511
511
|
args.push("--model", target.model);
|
|
512
512
|
args.push(...target.extraArgs ?? []);
|
|
513
|
-
args.push("agent", "start");
|
|
513
|
+
args.push("agent", "start", "--json");
|
|
514
514
|
if (target.cwd)
|
|
515
515
|
args.push("--cwd", target.cwd);
|
|
516
516
|
args.push(target.prompt);
|
|
@@ -1403,18 +1403,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
1403
1403
|
"agent",
|
|
1404
1404
|
command,
|
|
1405
1405
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
1406
|
+
"--json",
|
|
1406
1407
|
agentId
|
|
1407
1408
|
];
|
|
1408
1409
|
}
|
|
1410
|
+
function extractFirstJsonObject(text) {
|
|
1411
|
+
let depth = 0;
|
|
1412
|
+
let start = -1;
|
|
1413
|
+
let inString = false;
|
|
1414
|
+
let escaped = false;
|
|
1415
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
1416
|
+
const ch = text[i];
|
|
1417
|
+
if (inString) {
|
|
1418
|
+
if (escaped)
|
|
1419
|
+
escaped = false;
|
|
1420
|
+
else if (ch === "\\")
|
|
1421
|
+
escaped = true;
|
|
1422
|
+
else if (ch === '"')
|
|
1423
|
+
inString = false;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (ch === '"') {
|
|
1427
|
+
inString = true;
|
|
1428
|
+
} else if (ch === "{") {
|
|
1429
|
+
if (depth === 0)
|
|
1430
|
+
start = i;
|
|
1431
|
+
depth += 1;
|
|
1432
|
+
} else if (ch === "}") {
|
|
1433
|
+
if (depth > 0) {
|
|
1434
|
+
depth -= 1;
|
|
1435
|
+
if (depth === 0 && start !== -1)
|
|
1436
|
+
return text.slice(start, i + 1);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1409
1442
|
function parseJsonOutput(stdout, label) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1443
|
+
const raw = stdout || "{}";
|
|
1444
|
+
const candidates = [raw.trim()];
|
|
1445
|
+
const scanned = extractFirstJsonObject(raw);
|
|
1446
|
+
if (scanned && scanned !== raw.trim())
|
|
1447
|
+
candidates.push(scanned);
|
|
1448
|
+
let lastError;
|
|
1449
|
+
for (const candidate of candidates) {
|
|
1450
|
+
try {
|
|
1451
|
+
const value = JSON.parse(candidate);
|
|
1452
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1453
|
+
throw new Error("not an object");
|
|
1454
|
+
return value;
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
lastError = error;
|
|
1457
|
+
}
|
|
1417
1458
|
}
|
|
1459
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
1418
1460
|
}
|
|
1419
1461
|
function recordField(value, key) {
|
|
1420
1462
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
package/dist/sdk/index.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -4217,7 +4217,7 @@ class Store {
|
|
|
4217
4217
|
// package.json
|
|
4218
4218
|
var package_default = {
|
|
4219
4219
|
name: "@hasna/loops",
|
|
4220
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.7",
|
|
4221
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4222
4222
|
type: "module",
|
|
4223
4223
|
main: "dist/index.js",
|
|
@@ -5366,18 +5366,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5366
5366
|
"agent",
|
|
5367
5367
|
command,
|
|
5368
5368
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5369
|
+
"--json",
|
|
5369
5370
|
agentId
|
|
5370
5371
|
];
|
|
5371
5372
|
}
|
|
5373
|
+
function extractFirstJsonObject(text) {
|
|
5374
|
+
let depth = 0;
|
|
5375
|
+
let start = -1;
|
|
5376
|
+
let inString = false;
|
|
5377
|
+
let escaped = false;
|
|
5378
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5379
|
+
const ch = text[i];
|
|
5380
|
+
if (inString) {
|
|
5381
|
+
if (escaped)
|
|
5382
|
+
escaped = false;
|
|
5383
|
+
else if (ch === "\\")
|
|
5384
|
+
escaped = true;
|
|
5385
|
+
else if (ch === '"')
|
|
5386
|
+
inString = false;
|
|
5387
|
+
continue;
|
|
5388
|
+
}
|
|
5389
|
+
if (ch === '"') {
|
|
5390
|
+
inString = true;
|
|
5391
|
+
} else if (ch === "{") {
|
|
5392
|
+
if (depth === 0)
|
|
5393
|
+
start = i;
|
|
5394
|
+
depth += 1;
|
|
5395
|
+
} else if (ch === "}") {
|
|
5396
|
+
if (depth > 0) {
|
|
5397
|
+
depth -= 1;
|
|
5398
|
+
if (depth === 0 && start !== -1)
|
|
5399
|
+
return text.slice(start, i + 1);
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5372
5405
|
function parseJsonOutput(stdout, label) {
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5406
|
+
const raw = stdout || "{}";
|
|
5407
|
+
const candidates = [raw.trim()];
|
|
5408
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5409
|
+
if (scanned && scanned !== raw.trim())
|
|
5410
|
+
candidates.push(scanned);
|
|
5411
|
+
let lastError;
|
|
5412
|
+
for (const candidate of candidates) {
|
|
5413
|
+
try {
|
|
5414
|
+
const value = JSON.parse(candidate);
|
|
5415
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5416
|
+
throw new Error("not an object");
|
|
5417
|
+
return value;
|
|
5418
|
+
} catch (error) {
|
|
5419
|
+
lastError = error;
|
|
5420
|
+
}
|
|
5380
5421
|
}
|
|
5422
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5381
5423
|
}
|
|
5382
5424
|
function recordField(value, key) {
|
|
5383
5425
|
if (!value || typeof value !== "object" || Array.isArray(value))
|