@hasna/loops 0.4.7 → 0.4.9
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 +34 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +97 -353
- package/dist/daemon/index.js +25 -313
- package/dist/index.js +26 -308
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +11 -1
- package/dist/lib/storage/index.js +10 -11
- package/dist/lib/storage/sqlite.js +10 -11
- package/dist/lib/store.js +10 -11
- package/dist/mcp/index.js +12 -300
- package/dist/runner/index.js +12 -300
- package/dist/sdk/index.js +12 -300
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -581,7 +581,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
581
581
|
|
|
582
582
|
// src/lib/agent-adapter.ts
|
|
583
583
|
import { spawn } from "child_process";
|
|
584
|
-
var
|
|
584
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
585
585
|
"e",
|
|
586
586
|
"exec",
|
|
587
587
|
"agent",
|
|
@@ -630,12 +630,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
630
630
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
631
631
|
}
|
|
632
632
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
633
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
633
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
634
634
|
}
|
|
635
635
|
if (provider === "codewith") {
|
|
636
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
636
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
637
637
|
if (unsafe) {
|
|
638
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
638
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
641
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -718,7 +718,10 @@ function buildAgentInvocation(target) {
|
|
|
718
718
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
719
719
|
if (target.variant)
|
|
720
720
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
721
|
-
|
|
721
|
+
const sandbox = codewithLikeSandbox(target);
|
|
722
|
+
if (sandbox === "workspace-write")
|
|
723
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
724
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
722
725
|
if (target.cwd)
|
|
723
726
|
args.push("--cd", target.cwd);
|
|
724
727
|
for (const dir of target.addDirs ?? [])
|
|
@@ -726,11 +729,7 @@ function buildAgentInvocation(target) {
|
|
|
726
729
|
if (target.model)
|
|
727
730
|
args.push("--model", target.model);
|
|
728
731
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
|
|
730
|
-
if (target.cwd)
|
|
731
|
-
args.push("--cwd", target.cwd);
|
|
732
|
-
args.push(target.prompt);
|
|
733
|
-
return { command: "codewith", args };
|
|
732
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
734
733
|
}
|
|
735
734
|
case "codex": {
|
|
736
735
|
if (target.variant)
|
|
@@ -783,7 +782,7 @@ function adapterFor(provider, capabilities) {
|
|
|
783
782
|
var PROVIDER_ADAPTERS = {
|
|
784
783
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
784
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
785
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
786
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
788
787
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
789
788
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -4219,7 +4218,7 @@ class Store {
|
|
|
4219
4218
|
// package.json
|
|
4220
4219
|
var package_default = {
|
|
4221
4220
|
name: "@hasna/loops",
|
|
4222
|
-
version: "0.4.
|
|
4221
|
+
version: "0.4.9",
|
|
4223
4222
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4224
4223
|
type: "module",
|
|
4225
4224
|
main: "dist/index.js",
|
|
@@ -4902,11 +4901,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
4902
4901
|
"USER",
|
|
4903
4902
|
"XDG_RUNTIME_DIR"
|
|
4904
4903
|
]);
|
|
4905
|
-
function boundedText(text, maxBytes) {
|
|
4906
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
4907
|
-
buffer.append(text);
|
|
4908
|
-
return buffer.value();
|
|
4909
|
-
}
|
|
4910
4904
|
function buildResult(status, startedAt, fields = {}) {
|
|
4911
4905
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
4912
4906
|
return {
|
|
@@ -4970,21 +4964,6 @@ function metadataEnv(metadata) {
|
|
|
4970
4964
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
4971
4965
|
return env;
|
|
4972
4966
|
}
|
|
4973
|
-
function codewithAgentIdempotencyKey(metadata) {
|
|
4974
|
-
const parts = [
|
|
4975
|
-
"openloops",
|
|
4976
|
-
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
4977
|
-
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
4978
|
-
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
4979
|
-
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
4980
|
-
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
4981
|
-
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
4982
|
-
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
4983
|
-
].filter(Boolean);
|
|
4984
|
-
if (parts.length > 1)
|
|
4985
|
-
return parts.join(":");
|
|
4986
|
-
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
4987
|
-
}
|
|
4988
4967
|
function allowlistEnv(allowlist) {
|
|
4989
4968
|
const env = {};
|
|
4990
4969
|
if (allowlist?.tools?.length)
|
|
@@ -5039,8 +5018,7 @@ function commandSpec(target, opts) {
|
|
|
5039
5018
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5040
5019
|
stdin: invocation.stdin,
|
|
5041
5020
|
allowlist: agentTarget.allowlist,
|
|
5042
|
-
worktree: agentTarget.worktree
|
|
5043
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
5021
|
+
worktree: agentTarget.worktree
|
|
5044
5022
|
};
|
|
5045
5023
|
}
|
|
5046
5024
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5250,266 +5228,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5250
5228
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5251
5229
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5252
5230
|
}
|
|
5253
|
-
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
5254
|
-
const args = providerAdapter(target.provider).buildInvocation(target).args;
|
|
5255
|
-
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
5256
|
-
if (startIndex === -1)
|
|
5257
|
-
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
5258
|
-
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
5259
|
-
return args;
|
|
5260
|
-
}
|
|
5261
|
-
function codewithAgentControlArgs(target, command, agentId) {
|
|
5262
|
-
return [
|
|
5263
|
-
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
5264
|
-
"agent",
|
|
5265
|
-
command,
|
|
5266
|
-
...command === "logs" ? ["--limit", "20"] : [],
|
|
5267
|
-
"--json",
|
|
5268
|
-
agentId
|
|
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
|
-
}
|
|
5303
|
-
function parseJsonOutput(stdout, label) {
|
|
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
|
-
}
|
|
5319
|
-
}
|
|
5320
|
-
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5321
|
-
}
|
|
5322
|
-
function recordField(value, key) {
|
|
5323
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5324
|
-
return;
|
|
5325
|
-
const field = value[key];
|
|
5326
|
-
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
5327
|
-
}
|
|
5328
|
-
function stringField(value, key) {
|
|
5329
|
-
const field = value?.[key];
|
|
5330
|
-
return typeof field === "string" ? field : undefined;
|
|
5331
|
-
}
|
|
5332
|
-
function numberField(value, key) {
|
|
5333
|
-
const field = value?.[key];
|
|
5334
|
-
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
5335
|
-
}
|
|
5336
|
-
function codewithAgentStatus(readJson) {
|
|
5337
|
-
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5338
|
-
}
|
|
5339
|
-
function codewithAgentLastEventSeq(readJson) {
|
|
5340
|
-
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5341
|
-
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5342
|
-
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5343
|
-
return Math.max(agentSeq, snapshotSeq);
|
|
5344
|
-
return agentSeq ?? snapshotSeq;
|
|
5345
|
-
}
|
|
5346
|
-
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5347
|
-
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5348
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5349
|
-
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
5350
|
-
seq: numberField(event, "seq"),
|
|
5351
|
-
eventType: stringField(event, "eventType"),
|
|
5352
|
-
createdAt: numberField(event, "createdAt")
|
|
5353
|
-
})) : undefined;
|
|
5354
|
-
return JSON.stringify({
|
|
5355
|
-
codewithAgent: {
|
|
5356
|
-
agentId: stringField(agent, "agentId"),
|
|
5357
|
-
status: stringField(agent, "status"),
|
|
5358
|
-
desiredState: stringField(agent, "desiredState"),
|
|
5359
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5360
|
-
threadId: stringField(agent, "threadId"),
|
|
5361
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5362
|
-
pid: numberField(agent, "pid"),
|
|
5363
|
-
exitCode: numberField(agent, "exitCode"),
|
|
5364
|
-
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
5365
|
-
},
|
|
5366
|
-
statusSnapshot: statusSnapshot ? {
|
|
5367
|
-
seq: numberField(statusSnapshot, "seq"),
|
|
5368
|
-
status: stringField(statusSnapshot, "status"),
|
|
5369
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5370
|
-
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
5371
|
-
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
5372
|
-
} : undefined,
|
|
5373
|
-
events
|
|
5374
|
-
}, null, 2);
|
|
5375
|
-
}
|
|
5376
|
-
function codewithAgentProgress(readJson) {
|
|
5377
|
-
const agent = recordField(readJson, "agent");
|
|
5378
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5379
|
-
return {
|
|
5380
|
-
provider: "codewith",
|
|
5381
|
-
agentId: stringField(agent, "agentId"),
|
|
5382
|
-
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5383
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5384
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5385
|
-
threadId: stringField(agent, "threadId"),
|
|
5386
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5387
|
-
pid: numberField(agent, "pid"),
|
|
5388
|
-
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5389
|
-
};
|
|
5390
|
-
}
|
|
5391
|
-
function sleep(ms) {
|
|
5392
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5393
|
-
}
|
|
5394
|
-
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
5395
|
-
const target = spec.codewithDurableAgent?.target;
|
|
5396
|
-
if (!target)
|
|
5397
|
-
throw new Error("internal error: missing codewith durable target");
|
|
5398
|
-
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
5399
|
-
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
5400
|
-
const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
5401
|
-
cwd: spec.cwd,
|
|
5402
|
-
env,
|
|
5403
|
-
timeoutMs: 30000,
|
|
5404
|
-
maxOutputBytes
|
|
5405
|
-
});
|
|
5406
|
-
const stderr = new BoundedOutputBuffer(maxOutputBytes);
|
|
5407
|
-
stderr.append(start.stderr);
|
|
5408
|
-
if (start.error || (start.status ?? 1) !== 0) {
|
|
5409
|
-
return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
|
|
5410
|
-
exitCode: start.status ?? undefined,
|
|
5411
|
-
stdout: start.stdout,
|
|
5412
|
-
stderr: stderr.value()
|
|
5413
|
-
});
|
|
5414
|
-
}
|
|
5415
|
-
let startJson;
|
|
5416
|
-
try {
|
|
5417
|
-
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
5418
|
-
} catch (error) {
|
|
5419
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
|
|
5420
|
-
}
|
|
5421
|
-
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
5422
|
-
if (!agentId) {
|
|
5423
|
-
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5424
|
-
}
|
|
5425
|
-
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5426
|
-
const stopAgent = async () => {
|
|
5427
|
-
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5428
|
-
cwd: spec.cwd,
|
|
5429
|
-
env,
|
|
5430
|
-
timeoutMs: 15000,
|
|
5431
|
-
maxOutputBytes
|
|
5432
|
-
});
|
|
5433
|
-
};
|
|
5434
|
-
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
5435
|
-
let lastReadJson = startJson;
|
|
5436
|
-
let lastLogsJson;
|
|
5437
|
-
let lastFingerprint;
|
|
5438
|
-
let lastProgressAt = Date.now();
|
|
5439
|
-
const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
|
|
5440
|
-
while (true) {
|
|
5441
|
-
if (opts.signal?.aborted) {
|
|
5442
|
-
await stopAgent();
|
|
5443
|
-
return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
|
|
5444
|
-
}
|
|
5445
|
-
const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
5446
|
-
cwd: spec.cwd,
|
|
5447
|
-
env,
|
|
5448
|
-
timeoutMs: 30000,
|
|
5449
|
-
maxOutputBytes
|
|
5450
|
-
});
|
|
5451
|
-
stderr.append(read.stderr);
|
|
5452
|
-
if (read.error || (read.status ?? 1) !== 0) {
|
|
5453
|
-
return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
|
|
5454
|
-
exitCode: read.status ?? undefined,
|
|
5455
|
-
stdout: evidence(),
|
|
5456
|
-
stderr: stderr.value()
|
|
5457
|
-
});
|
|
5458
|
-
}
|
|
5459
|
-
try {
|
|
5460
|
-
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
5461
|
-
} catch (error) {
|
|
5462
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
|
|
5463
|
-
stdout: boundedText(read.stdout, maxOutputBytes),
|
|
5464
|
-
stderr: stderr.value()
|
|
5465
|
-
});
|
|
5466
|
-
}
|
|
5467
|
-
const status = codewithAgentStatus(lastReadJson);
|
|
5468
|
-
const fingerprint = JSON.stringify({
|
|
5469
|
-
status,
|
|
5470
|
-
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5471
|
-
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5472
|
-
});
|
|
5473
|
-
if (fingerprint !== lastFingerprint) {
|
|
5474
|
-
lastFingerprint = fingerprint;
|
|
5475
|
-
lastProgressAt = Date.now();
|
|
5476
|
-
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5477
|
-
}
|
|
5478
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5479
|
-
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
5480
|
-
cwd: spec.cwd,
|
|
5481
|
-
env,
|
|
5482
|
-
timeoutMs: 30000,
|
|
5483
|
-
maxOutputBytes
|
|
5484
|
-
});
|
|
5485
|
-
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
5486
|
-
try {
|
|
5487
|
-
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
5488
|
-
} catch {
|
|
5489
|
-
lastLogsJson = undefined;
|
|
5490
|
-
}
|
|
5491
|
-
} else {
|
|
5492
|
-
stderr.append(logs.stderr || logs.error || "");
|
|
5493
|
-
}
|
|
5494
|
-
if (status === "completed") {
|
|
5495
|
-
return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
|
|
5496
|
-
}
|
|
5497
|
-
return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
|
|
5498
|
-
}
|
|
5499
|
-
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
5500
|
-
await stopAgent();
|
|
5501
|
-
return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
|
|
5502
|
-
}
|
|
5503
|
-
if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
|
|
5504
|
-
await stopAgent();
|
|
5505
|
-
return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
|
|
5506
|
-
stdout: evidence(),
|
|
5507
|
-
stderr: stderr.value()
|
|
5508
|
-
});
|
|
5509
|
-
}
|
|
5510
|
-
await sleep(pollMs);
|
|
5511
|
-
}
|
|
5512
|
-
}
|
|
5513
5231
|
function resolvedDirEquals(left, right) {
|
|
5514
5232
|
try {
|
|
5515
5233
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5743,9 +5461,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5743
5461
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5744
5462
|
let spec = commandSpec(target, opts);
|
|
5745
5463
|
const machine = resolvedMachine(opts);
|
|
5746
|
-
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
5747
|
-
return failureResult(nowIso(), "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch");
|
|
5748
|
-
}
|
|
5749
5464
|
if (machine && !machine.local) {
|
|
5750
5465
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5751
5466
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5776,9 +5491,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5776
5491
|
if (worktreeEntry?.fallbackCwd) {
|
|
5777
5492
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5778
5493
|
}
|
|
5779
|
-
if (spec.codewithDurableAgent) {
|
|
5780
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5781
|
-
}
|
|
5782
5494
|
const child = spawn2(spec.command, spec.args, {
|
|
5783
5495
|
cwd: spec.cwd,
|
|
5784
5496
|
env,
|
|
@@ -7676,7 +7388,7 @@ function realSleep(ms) {
|
|
|
7676
7388
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
7677
7389
|
}
|
|
7678
7390
|
async function runLoop(opts) {
|
|
7679
|
-
const
|
|
7391
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7680
7392
|
const sliceMs = opts.sliceMs ?? 200;
|
|
7681
7393
|
while (!opts.shouldStop()) {
|
|
7682
7394
|
try {
|
|
@@ -7687,7 +7399,7 @@ async function runLoop(opts) {
|
|
|
7687
7399
|
let waited = 0;
|
|
7688
7400
|
while (waited < opts.intervalMs && !opts.shouldStop()) {
|
|
7689
7401
|
const chunk = Math.min(sliceMs, opts.intervalMs - waited);
|
|
7690
|
-
await
|
|
7402
|
+
await sleep(chunk);
|
|
7691
7403
|
waited += chunk;
|
|
7692
7404
|
}
|
|
7693
7405
|
}
|
|
@@ -7779,7 +7491,7 @@ function processExists(pid) {
|
|
|
7779
7491
|
}
|
|
7780
7492
|
}
|
|
7781
7493
|
async function reapProcessGroups(entries, opts = {}) {
|
|
7782
|
-
const
|
|
7494
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7783
7495
|
const graceMs = Math.max(100, opts.graceMs ?? 2000);
|
|
7784
7496
|
const ownPgid = ownProcessGroupId();
|
|
7785
7497
|
const targets = new Map;
|
|
@@ -7808,7 +7520,7 @@ async function reapProcessGroups(entries, opts = {}) {
|
|
|
7808
7520
|
const steps = Math.max(1, Math.ceil(graceMs / 100));
|
|
7809
7521
|
let remaining = live;
|
|
7810
7522
|
for (let i = 0;i < steps && remaining.length > 0; i++) {
|
|
7811
|
-
await
|
|
7523
|
+
await sleep(100);
|
|
7812
7524
|
remaining = remaining.filter((target) => signalReapTarget(target, 0));
|
|
7813
7525
|
}
|
|
7814
7526
|
for (const target of remaining) {
|
|
@@ -7850,7 +7562,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7850
7562
|
}
|
|
7851
7563
|
if (!state.running || !state.pid)
|
|
7852
7564
|
return { wasRunning: false, stopped: false, forced: false };
|
|
7853
|
-
const
|
|
7565
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7854
7566
|
const store = new Store(join5(dirname4(path), "loops.db"));
|
|
7855
7567
|
try {
|
|
7856
7568
|
const lease = store.getDaemonLease();
|
|
@@ -7863,7 +7575,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7863
7575
|
}
|
|
7864
7576
|
const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
|
|
7865
7577
|
for (let i = 0;i < steps; i++) {
|
|
7866
|
-
await
|
|
7578
|
+
await sleep(100);
|
|
7867
7579
|
if (!isAlive(state.pid, record?.startedAt)) {
|
|
7868
7580
|
removePid(path);
|
|
7869
7581
|
return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
|
|
@@ -7872,13 +7584,13 @@ async function stopDaemon(opts = {}) {
|
|
|
7872
7584
|
try {
|
|
7873
7585
|
process.kill(state.pid, "SIGKILL");
|
|
7874
7586
|
} catch {}
|
|
7875
|
-
await
|
|
7587
|
+
await sleep(150);
|
|
7876
7588
|
removePid(path);
|
|
7877
7589
|
let reapedPgids = [];
|
|
7878
7590
|
if (leaseVerified && lease) {
|
|
7879
7591
|
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
7880
7592
|
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
7881
|
-
sleep
|
|
7593
|
+
sleep,
|
|
7882
7594
|
graceMs: opts.reapGraceMs
|
|
7883
7595
|
});
|
|
7884
7596
|
}
|
|
@@ -7953,7 +7665,7 @@ async function runDaemon(opts = {}) {
|
|
|
7953
7665
|
const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
|
|
7954
7666
|
const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
|
|
7955
7667
|
const log = opts.log ?? defaultDaemonLog;
|
|
7956
|
-
const
|
|
7668
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7957
7669
|
const lease = store.acquireDaemonLease({
|
|
7958
7670
|
id: leaseId,
|
|
7959
7671
|
pid: process.pid,
|
|
@@ -8017,7 +7729,7 @@ async function runDaemon(opts = {}) {
|
|
|
8017
7729
|
const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
|
|
8018
7730
|
if (entries.length === 0)
|
|
8019
7731
|
return;
|
|
8020
|
-
const killed = await reapProcessGroups(entries, { sleep
|
|
7732
|
+
const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
|
|
8021
7733
|
if (killed.length > 0)
|
|
8022
7734
|
log(`reaped orphan process groups: ${killed.join(", ")}`);
|
|
8023
7735
|
};
|
|
@@ -8092,7 +7804,7 @@ async function runDaemon(opts = {}) {
|
|
|
8092
7804
|
}
|
|
8093
7805
|
await runLoop({
|
|
8094
7806
|
intervalMs,
|
|
8095
|
-
sleep
|
|
7807
|
+
sleep,
|
|
8096
7808
|
shouldStop: opts.shouldStop ?? (() => stopFlag),
|
|
8097
7809
|
onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
|
|
8098
7810
|
tickFn: async () => {
|
|
@@ -8142,10 +7854,10 @@ async function startDaemon(opts) {
|
|
|
8142
7854
|
stdio: ["ignore", out, out]
|
|
8143
7855
|
});
|
|
8144
7856
|
child.unref();
|
|
8145
|
-
const
|
|
7857
|
+
const sleep = opts.sleep ?? realSleep;
|
|
8146
7858
|
const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
|
|
8147
7859
|
for (let i = 0;i < deadline; i++) {
|
|
8148
|
-
await
|
|
7860
|
+
await sleep(100);
|
|
8149
7861
|
const current = isDaemonRunning();
|
|
8150
7862
|
if (current.running)
|
|
8151
7863
|
return { started: true, alreadyRunning: false, pid: current.pid };
|
|
@@ -10505,9 +10217,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10505
10217
|
`) : "";
|
|
10506
10218
|
const shared = [
|
|
10507
10219
|
worktreePrompt(plan),
|
|
10508
|
-
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
10509
|
-
|
|
10510
|
-
]),
|
|
10220
|
+
...todosExactCommandsFragment(todosProjectPath, input.taskId, []),
|
|
10221
|
+
"Use concrete task-specific text in lifecycle comments. Do not copy placeholder text into lifecycle comments; triage and planner comments must start with the exact stage marker when advancing or blocking the workflow.",
|
|
10511
10222
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
10512
10223
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
10513
10224
|
prReviewFollowUpFragment(input),
|
|
@@ -10518,6 +10229,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10518
10229
|
`);
|
|
10519
10230
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
10520
10231
|
const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
10232
|
+
const markerCommentCommand = (stage, state, evidencePlaceholder) => `todos --project ${todosProjectPath} comment ${input.taskId} "${gateMarker(stage, state)}
|
|
10233
|
+
<${evidencePlaceholder}>"`;
|
|
10521
10234
|
const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
|
|
10522
10235
|
const triagePrompt = [
|
|
10523
10236
|
...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
@@ -10525,9 +10238,12 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10525
10238
|
"Decide whether the task is eligible for loop execution. Check status, dependencies, duplicate tasks, no-auto/manual/approval metadata, project path, acceptance criteria, and whether the requested work should be split before implementation.",
|
|
10526
10239
|
"Do not implement repo changes in this step.",
|
|
10527
10240
|
`If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
|
|
10528
|
-
|
|
10241
|
+
`Use this copy-safe marker comment command for triage go: ${markerCommentCommand("triage", "go", "task-specific triage evidence")}`,
|
|
10242
|
+
"Do not run a separate generic evidence comment before the marker; include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same marker comment.",
|
|
10529
10243
|
`If the task should not proceed automatically, run: ${blockTaskCommand}`,
|
|
10530
10244
|
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
10245
|
+
`Use this copy-safe marker comment command for triage blocked: ${markerCommentCommand("triage", "blocked", "task-specific triage evidence")}`,
|
|
10246
|
+
"Do not run a separate generic blocker comment before the marker; include the blocker evidence in that same marker comment.",
|
|
10531
10247
|
gateStopFragment("triage", "later steps")
|
|
10532
10248
|
].join(`
|
|
10533
10249
|
`);
|
|
@@ -10536,17 +10252,19 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10536
10252
|
shared,
|
|
10537
10253
|
"Read the triage comment and current task details.",
|
|
10538
10254
|
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
10539
|
-
|
|
10255
|
+
`Use this copy-safe marker comment command for planner go: ${markerCommentCommand("planner", "go", "task-specific plan/evidence")}`,
|
|
10256
|
+
"Do not run a separate generic evidence comment before the marker; in that same marker 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.",
|
|
10540
10257
|
`Do not implement repo changes in this step. If the task is too broad or unsafe for automation, run: ${blockTaskCommand}`,
|
|
10541
10258
|
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
10542
|
-
`
|
|
10259
|
+
`Use this copy-safe marker comment command for planner blocked: ${markerCommentCommand("planner", "blocked", "task-specific plan/evidence")}`,
|
|
10260
|
+
`Do not run a separate generic blocker comment before the marker; create smaller deduped tasks and record blocker evidence in that same marker comment. ${gateStopFragment("planner", "the worker")}`
|
|
10543
10261
|
].join(`
|
|
10544
10262
|
`);
|
|
10545
10263
|
const workerPrompt = [
|
|
10546
10264
|
...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
10547
10265
|
shared,
|
|
10548
10266
|
todosStartLine(todosProjectPath, input.taskId),
|
|
10549
|
-
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits,
|
|
10267
|
+
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record concrete worker evidence in todos: changed files, commits, validation results, blockers, and residual risks.",
|
|
10550
10268
|
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,
|
|
10551
10269
|
WORKER_LEAVES_COMPLETION_FRAGMENT
|
|
10552
10270
|
].filter(Boolean).join(`
|
|
@@ -10554,7 +10272,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
10554
10272
|
const verifierPrompt = [
|
|
10555
10273
|
...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
10556
10274
|
shared,
|
|
10557
|
-
|
|
10275
|
+
"Before completion, record concrete verification evidence in todos with changed files, validation results, findings, and the task decision.",
|
|
10558
10276
|
todosDoneLine(todosProjectPath, input.taskId),
|
|
10559
10277
|
adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
|
|
10560
10278
|
verifierRuntimeGuidance(input),
|
|
@@ -11072,7 +10790,7 @@ function eventMetadata(event) {
|
|
|
11072
10790
|
return metadata;
|
|
11073
10791
|
return {};
|
|
11074
10792
|
}
|
|
11075
|
-
function
|
|
10793
|
+
function stringField(value) {
|
|
11076
10794
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
11077
10795
|
}
|
|
11078
10796
|
function slugSegment2(value, fallback = "event") {
|
|
@@ -11087,14 +10805,14 @@ function stableHash(parts) {
|
|
|
11087
10805
|
}
|
|
11088
10806
|
function taskEventField(data, keys) {
|
|
11089
10807
|
for (const key of keys) {
|
|
11090
|
-
const direct =
|
|
10808
|
+
const direct = stringField(data[key]);
|
|
11091
10809
|
if (direct)
|
|
11092
10810
|
return direct;
|
|
11093
10811
|
}
|
|
11094
10812
|
const task = data.task;
|
|
11095
10813
|
if (task && typeof task === "object" && !Array.isArray(task)) {
|
|
11096
10814
|
for (const key of keys) {
|
|
11097
|
-
const direct =
|
|
10815
|
+
const direct = stringField(task[key]);
|
|
11098
10816
|
if (direct)
|
|
11099
10817
|
return direct;
|
|
11100
10818
|
}
|
|
@@ -11102,7 +10820,7 @@ function taskEventField(data, keys) {
|
|
|
11102
10820
|
const payload = data.payload;
|
|
11103
10821
|
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
11104
10822
|
for (const key of keys) {
|
|
11105
|
-
const direct =
|
|
10823
|
+
const direct = stringField(payload[key]);
|
|
11106
10824
|
if (direct)
|
|
11107
10825
|
return direct;
|
|
11108
10826
|
}
|
|
@@ -11606,6 +11324,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
11606
11324
|
return mode;
|
|
11607
11325
|
}
|
|
11608
11326
|
// src/lib/route/pr-review.ts
|
|
11327
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
11609
11328
|
var PR_AUTHOR_FIELDS = [
|
|
11610
11329
|
"github_author",
|
|
11611
11330
|
"githubAuthor",
|
|
@@ -11720,6 +11439,21 @@ function routeTextFieldValues(record, field) {
|
|
|
11720
11439
|
}
|
|
11721
11440
|
return values;
|
|
11722
11441
|
}
|
|
11442
|
+
function prReferenceFrom(text) {
|
|
11443
|
+
const url = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)/i.exec(text);
|
|
11444
|
+
if (url)
|
|
11445
|
+
return { owner: url[1], repo: url[2], number: Number(url[3]) };
|
|
11446
|
+
const short = /github-pr:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)/i.exec(text);
|
|
11447
|
+
if (short)
|
|
11448
|
+
return { owner: short[1], repo: short[2], number: Number(short[3]) };
|
|
11449
|
+
return;
|
|
11450
|
+
}
|
|
11451
|
+
function ghAuthorResolver(ref) {
|
|
11452
|
+
const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "author", "-q", ".author.login"], { encoding: "utf8", timeout: 20000 });
|
|
11453
|
+
if (result.error || result.status !== 0)
|
|
11454
|
+
return;
|
|
11455
|
+
return githubLogin((result.stdout ?? "").trim());
|
|
11456
|
+
}
|
|
11723
11457
|
function authorFromPrText(text) {
|
|
11724
11458
|
const patterns = [
|
|
11725
11459
|
/\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
|
|
@@ -11746,7 +11480,7 @@ function reviewersFromPrText(text) {
|
|
|
11746
11480
|
}
|
|
11747
11481
|
return githubLogins(reviewers);
|
|
11748
11482
|
}
|
|
11749
|
-
function prReviewRoutingDecision(data, metadata, opts) {
|
|
11483
|
+
function prReviewRoutingDecision(data, metadata, opts, resolveAuthor = ghAuthorResolver) {
|
|
11750
11484
|
const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
|
|
11751
11485
|
const text = routeEvidenceText(records);
|
|
11752
11486
|
const signals = [];
|
|
@@ -11766,7 +11500,6 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11766
11500
|
const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
|
|
11767
11501
|
if (!required)
|
|
11768
11502
|
return { required: false, allowed: true, reviewers: [], signals };
|
|
11769
|
-
const author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
|
|
11770
11503
|
const reviewers = githubLogins([
|
|
11771
11504
|
opts.githubReviewer,
|
|
11772
11505
|
...splitList(opts.githubReviewerPool) ?? [],
|
|
@@ -11774,6 +11507,17 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11774
11507
|
...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
|
|
11775
11508
|
...reviewersFromPrText(text)
|
|
11776
11509
|
]);
|
|
11510
|
+
let author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
|
|
11511
|
+
if (!author) {
|
|
11512
|
+
const ref = prReferenceFrom(text);
|
|
11513
|
+
if (ref) {
|
|
11514
|
+
const derived = githubLogin(resolveAuthor(ref));
|
|
11515
|
+
if (derived) {
|
|
11516
|
+
author = derived;
|
|
11517
|
+
signals.push("author-derived-gh");
|
|
11518
|
+
}
|
|
11519
|
+
}
|
|
11520
|
+
}
|
|
11777
11521
|
const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
|
|
11778
11522
|
if (!author) {
|
|
11779
11523
|
return {
|
|
@@ -11815,7 +11559,7 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11815
11559
|
}
|
|
11816
11560
|
// src/lib/route/throttle.ts
|
|
11817
11561
|
import { realpathSync as realpathSync2 } from "fs";
|
|
11818
|
-
import { spawnSync as
|
|
11562
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
11819
11563
|
import { resolve as resolve6 } from "path";
|
|
11820
11564
|
function routeThrottleLimitsFromOpts(opts) {
|
|
11821
11565
|
return {
|
|
@@ -11837,7 +11581,7 @@ function normalizeRoutePath(value) {
|
|
|
11837
11581
|
} catch {
|
|
11838
11582
|
return canonical;
|
|
11839
11583
|
}
|
|
11840
|
-
const gitRoot =
|
|
11584
|
+
const gitRoot = spawnSync8("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
11841
11585
|
if (gitRoot.status === 0 && gitRoot.stdout.trim()) {
|
|
11842
11586
|
try {
|
|
11843
11587
|
return realpathSync2(gitRoot.stdout.trim());
|
|
@@ -11887,7 +11631,7 @@ function routeThrottleDryRunPreview(args) {
|
|
|
11887
11631
|
};
|
|
11888
11632
|
}
|
|
11889
11633
|
function isExistingGitProjectPath(path) {
|
|
11890
|
-
const result =
|
|
11634
|
+
const result = spawnSync8("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
|
|
11891
11635
|
return result.status === 0;
|
|
11892
11636
|
}
|
|
11893
11637
|
function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
|
|
@@ -11979,11 +11723,11 @@ async function readEventEnvelopeInput(opts = {}) {
|
|
|
11979
11723
|
const event = JSON.parse(raw);
|
|
11980
11724
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
11981
11725
|
throw new ValidationError("event JSON must be an object");
|
|
11982
|
-
if (!
|
|
11726
|
+
if (!stringField(event.id))
|
|
11983
11727
|
throw new ValidationError("event.id is required");
|
|
11984
|
-
if (!
|
|
11728
|
+
if (!stringField(event.type))
|
|
11985
11729
|
throw new ValidationError("event.type is required");
|
|
11986
|
-
if (!
|
|
11730
|
+
if (!stringField(event.source))
|
|
11987
11731
|
throw new ValidationError("event.source is required");
|
|
11988
11732
|
return event;
|
|
11989
11733
|
}
|
|
@@ -12354,8 +12098,8 @@ function routeGenericEvent(event, opts) {
|
|
|
12354
12098
|
eventId: event.id,
|
|
12355
12099
|
eventType: event.type,
|
|
12356
12100
|
eventSource: event.source,
|
|
12357
|
-
eventSubject:
|
|
12358
|
-
eventMessage:
|
|
12101
|
+
eventSubject: stringField(event.subject),
|
|
12102
|
+
eventMessage: stringField(event.message),
|
|
12359
12103
|
eventJson: JSON.stringify(event),
|
|
12360
12104
|
projectPath,
|
|
12361
12105
|
routeProjectPath,
|
|
@@ -12396,9 +12140,9 @@ function routeGenericEvent(event, opts) {
|
|
|
12396
12140
|
},
|
|
12397
12141
|
subjectRef: {
|
|
12398
12142
|
kind: "event",
|
|
12399
|
-
id:
|
|
12143
|
+
id: stringField(event.subject) ?? event.id,
|
|
12400
12144
|
path: routeProjectPath,
|
|
12401
|
-
raw: { message:
|
|
12145
|
+
raw: { message: stringField(event.message) }
|
|
12402
12146
|
},
|
|
12403
12147
|
intent: "route",
|
|
12404
12148
|
scope: {
|
|
@@ -12427,7 +12171,7 @@ function routeGenericEvent(event, opts) {
|
|
|
12427
12171
|
invocationInput,
|
|
12428
12172
|
routeProjectPath,
|
|
12429
12173
|
projectGroup,
|
|
12430
|
-
subjectRef:
|
|
12174
|
+
subjectRef: stringField(event.subject) ?? event.id,
|
|
12431
12175
|
loopName,
|
|
12432
12176
|
loopDescription: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
|
|
12433
12177
|
throttleLimits,
|
|
@@ -12449,14 +12193,14 @@ import { resolve as resolve8 } from "path";
|
|
|
12449
12193
|
|
|
12450
12194
|
// src/lib/route/todos-cli.ts
|
|
12451
12195
|
import { closeSync, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, rmSync as rmSync6 } from "fs";
|
|
12452
|
-
import { spawnSync as
|
|
12196
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
12453
12197
|
import { join as join10 } from "path";
|
|
12454
12198
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
12455
12199
|
function defaultLoopsProject() {
|
|
12456
12200
|
return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir5()}/.hasna/loops`;
|
|
12457
12201
|
}
|
|
12458
12202
|
function runLocalCommand(command, args, opts = {}) {
|
|
12459
|
-
const result =
|
|
12203
|
+
const result = spawnSync9(command, args, {
|
|
12460
12204
|
input: opts.input,
|
|
12461
12205
|
encoding: "utf8",
|
|
12462
12206
|
timeout: opts.timeoutMs ?? 30000,
|
|
@@ -12477,7 +12221,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
12477
12221
|
const stdoutFd = openSync2(stdoutPath, "w");
|
|
12478
12222
|
let result;
|
|
12479
12223
|
try {
|
|
12480
|
-
result =
|
|
12224
|
+
result = spawnSync9(command, args, {
|
|
12481
12225
|
input: opts.input,
|
|
12482
12226
|
encoding: "utf8",
|
|
12483
12227
|
timeout: opts.timeoutMs ?? 30000,
|
|
@@ -12522,7 +12266,7 @@ function todosMutationSummary(result) {
|
|
|
12522
12266
|
// src/lib/route/drain.ts
|
|
12523
12267
|
function taskField(task, keys) {
|
|
12524
12268
|
for (const key of keys) {
|
|
12525
|
-
const value =
|
|
12269
|
+
const value = stringField(task[key]);
|
|
12526
12270
|
if (value)
|
|
12527
12271
|
return value;
|
|
12528
12272
|
}
|
|
@@ -12547,9 +12291,9 @@ function taskListValues(task) {
|
|
|
12547
12291
|
const taskList = objectField2(task.task_list);
|
|
12548
12292
|
return [
|
|
12549
12293
|
taskField(task, ["task_list_id", "taskListId"]),
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12294
|
+
stringField(task.task_list?.id),
|
|
12295
|
+
stringField(task.task_list?.slug),
|
|
12296
|
+
stringField(taskList?.name),
|
|
12553
12297
|
taskField(task, ["task_list", "taskList"])
|
|
12554
12298
|
].filter((value) => Boolean(value));
|
|
12555
12299
|
}
|
|
@@ -12623,12 +12367,12 @@ function compactDrainResult(result) {
|
|
|
12623
12367
|
kind: result.kind,
|
|
12624
12368
|
taskId: event?.subject,
|
|
12625
12369
|
eventId: event?.id,
|
|
12626
|
-
idempotencyKey:
|
|
12627
|
-
reason:
|
|
12628
|
-
loopId:
|
|
12629
|
-
loopName:
|
|
12630
|
-
workflowId:
|
|
12631
|
-
workflowName:
|
|
12370
|
+
idempotencyKey: stringField(value.idempotencyKey),
|
|
12371
|
+
reason: stringField(value.reason) ?? throttle?.reason,
|
|
12372
|
+
loopId: stringField(loop?.id),
|
|
12373
|
+
loopName: stringField(loop?.name),
|
|
12374
|
+
workflowId: stringField(workflow?.id),
|
|
12375
|
+
workflowName: stringField(workflow?.name),
|
|
12632
12376
|
providerRouting,
|
|
12633
12377
|
requeue,
|
|
12634
12378
|
queuedAtSource: value.queuedAtSource,
|
|
@@ -12662,7 +12406,7 @@ function loadTodoProjectPathsFromRegistry(opts) {
|
|
|
12662
12406
|
const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
|
|
12663
12407
|
const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
|
|
12664
12408
|
const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
|
|
12665
|
-
const path =
|
|
12409
|
+
const path = stringField(project.path) ?? stringField(project.projectPath) ?? stringField(project.project_path) ?? stringField(project.dir) ?? stringField(project.root) ?? stringField(project.cwd);
|
|
12666
12410
|
if (!path)
|
|
12667
12411
|
return;
|
|
12668
12412
|
return { path };
|
|
@@ -14057,7 +13801,7 @@ routes.command("show <id>").description("show one admission work item").action(r
|
|
|
14057
13801
|
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(runAction((id, opts) => {
|
|
14058
13802
|
const store = new Store;
|
|
14059
13803
|
try {
|
|
14060
|
-
const reason =
|
|
13804
|
+
const reason = stringField(opts.reason);
|
|
14061
13805
|
if (!reason)
|
|
14062
13806
|
throw new ValidationError("routes requeue requires --reason <text>");
|
|
14063
13807
|
const item = store.requeueWorkflowWorkItem(id, { reason });
|