@hasna/loops 0.4.8 → 0.4.10
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 +24 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +226 -368
- package/dist/daemon/index.js +76 -327
- package/dist/index.js +63 -314
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +18 -1
- package/dist/lib/storage/index.js +40 -22
- package/dist/lib/storage/sqlite.js +40 -22
- package/dist/lib/store.js +40 -22
- package/dist/mcp/index.js +63 -314
- package/dist/runner/index.js +33 -303
- package/dist/sdk/index.js +63 -314
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -56,8 +56,12 @@ function nowIso() {
|
|
|
56
56
|
import { mkdirSync } from "fs";
|
|
57
57
|
import { homedir } from "os";
|
|
58
58
|
import { join } from "path";
|
|
59
|
+
function homeDir() {
|
|
60
|
+
const home = process.env.HOME?.trim();
|
|
61
|
+
return home ? home : homedir();
|
|
62
|
+
}
|
|
59
63
|
function dataDir() {
|
|
60
|
-
return process.env.LOOPS_DATA_DIR || join(
|
|
64
|
+
return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
|
|
61
65
|
}
|
|
62
66
|
function ensureDataDir() {
|
|
63
67
|
const dir = dataDir();
|
|
@@ -74,10 +78,10 @@ function daemonLogPath() {
|
|
|
74
78
|
return join(dataDir(), "daemon.log");
|
|
75
79
|
}
|
|
76
80
|
function systemdServicePath() {
|
|
77
|
-
return join(
|
|
81
|
+
return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
|
|
78
82
|
}
|
|
79
83
|
function launchdPlistPath() {
|
|
80
|
-
return join(
|
|
84
|
+
return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
// src/lib/process-identity.ts
|
|
@@ -581,7 +585,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
581
585
|
|
|
582
586
|
// src/lib/agent-adapter.ts
|
|
583
587
|
import { spawn } from "child_process";
|
|
584
|
-
var
|
|
588
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
585
589
|
"e",
|
|
586
590
|
"exec",
|
|
587
591
|
"agent",
|
|
@@ -630,12 +634,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
630
634
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
631
635
|
}
|
|
632
636
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
633
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
637
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
634
638
|
}
|
|
635
639
|
if (provider === "codewith") {
|
|
636
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
640
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
637
641
|
if (unsafe) {
|
|
638
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
642
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
639
643
|
}
|
|
640
644
|
}
|
|
641
645
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -718,7 +722,10 @@ function buildAgentInvocation(target) {
|
|
|
718
722
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
719
723
|
if (target.variant)
|
|
720
724
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
721
|
-
|
|
725
|
+
const sandbox = codewithLikeSandbox(target);
|
|
726
|
+
if (sandbox === "workspace-write")
|
|
727
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
728
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
722
729
|
if (target.cwd)
|
|
723
730
|
args.push("--cd", target.cwd);
|
|
724
731
|
for (const dir of target.addDirs ?? [])
|
|
@@ -726,11 +733,7 @@ function buildAgentInvocation(target) {
|
|
|
726
733
|
if (target.model)
|
|
727
734
|
args.push("--model", target.model);
|
|
728
735
|
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 };
|
|
736
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
734
737
|
}
|
|
735
738
|
case "codex": {
|
|
736
739
|
if (target.variant)
|
|
@@ -783,7 +786,7 @@ function adapterFor(provider, capabilities) {
|
|
|
783
786
|
var PROVIDER_ADAPTERS = {
|
|
784
787
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
788
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
789
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
790
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
788
791
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
789
792
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -1475,6 +1478,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
|
|
|
1475
1478
|
function scrubbedOrNull(value) {
|
|
1476
1479
|
return value == null ? null : scrubSecrets(value);
|
|
1477
1480
|
}
|
|
1481
|
+
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1482
|
+
function clampPersistedRunOutput(value) {
|
|
1483
|
+
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1484
|
+
return value;
|
|
1485
|
+
const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
|
|
1486
|
+
const head = value.slice(0, half);
|
|
1487
|
+
const tail = value.slice(value.length - half);
|
|
1488
|
+
const omitted = value.length - head.length - tail.length;
|
|
1489
|
+
return `${head}
|
|
1490
|
+
\u2026[${omitted} chars truncated by loops run-output retention]\u2026
|
|
1491
|
+
${tail}`;
|
|
1492
|
+
}
|
|
1493
|
+
function persistedRunOutput(value) {
|
|
1494
|
+
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1495
|
+
}
|
|
1478
1496
|
function chmodIfExists(path, mode) {
|
|
1479
1497
|
try {
|
|
1480
1498
|
if (existsSync(path))
|
|
@@ -3221,8 +3239,8 @@ class Store {
|
|
|
3221
3239
|
))`).run({
|
|
3222
3240
|
$workflowRunId: workflowRunId,
|
|
3223
3241
|
$stepId: stepId,
|
|
3224
|
-
$stdout: progress.stdout === undefined ? null :
|
|
3225
|
-
$stderr: progress.stderr === undefined ? null :
|
|
3242
|
+
$stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
|
|
3243
|
+
$stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
|
|
3226
3244
|
$updated: now,
|
|
3227
3245
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3228
3246
|
$now: now
|
|
@@ -3283,8 +3301,8 @@ class Store {
|
|
|
3283
3301
|
$finished: finishedAt,
|
|
3284
3302
|
$exitCode: patch.exitCode ?? null,
|
|
3285
3303
|
$durationMs: patch.durationMs ?? null,
|
|
3286
|
-
$stdout:
|
|
3287
|
-
$stderr:
|
|
3304
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3305
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3288
3306
|
$error: error ?? null,
|
|
3289
3307
|
$updated: finishedAt,
|
|
3290
3308
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
@@ -3662,8 +3680,8 @@ class Store {
|
|
|
3662
3680
|
$pid: patch.pid ?? null,
|
|
3663
3681
|
$exitCode: patch.exitCode ?? null,
|
|
3664
3682
|
$durationMs: patch.durationMs ?? null,
|
|
3665
|
-
$stdout:
|
|
3666
|
-
$stderr:
|
|
3683
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3684
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3667
3685
|
$error: error ?? null,
|
|
3668
3686
|
$updated: finishedAt,
|
|
3669
3687
|
$claimedBy: opts.claimedBy ?? null,
|
|
@@ -4060,8 +4078,8 @@ class Store {
|
|
|
4060
4078
|
$processStartedAt: run.processStartedAt ?? null,
|
|
4061
4079
|
$exitCode: run.exitCode ?? null,
|
|
4062
4080
|
$durationMs: run.durationMs ?? null,
|
|
4063
|
-
$stdout:
|
|
4064
|
-
$stderr:
|
|
4081
|
+
$stdout: persistedRunOutput(run.stdout),
|
|
4082
|
+
$stderr: persistedRunOutput(run.stderr),
|
|
4065
4083
|
$error: scrubbedOrNull(run.error),
|
|
4066
4084
|
$goalRunId: run.goalRunId ?? null,
|
|
4067
4085
|
$created: run.createdAt,
|
|
@@ -4219,7 +4237,7 @@ class Store {
|
|
|
4219
4237
|
// package.json
|
|
4220
4238
|
var package_default = {
|
|
4221
4239
|
name: "@hasna/loops",
|
|
4222
|
-
version: "0.4.
|
|
4240
|
+
version: "0.4.10",
|
|
4223
4241
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4224
4242
|
type: "module",
|
|
4225
4243
|
main: "dist/index.js",
|
|
@@ -4902,11 +4920,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
4902
4920
|
"USER",
|
|
4903
4921
|
"XDG_RUNTIME_DIR"
|
|
4904
4922
|
]);
|
|
4905
|
-
function boundedText(text, maxBytes) {
|
|
4906
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
4907
|
-
buffer.append(text);
|
|
4908
|
-
return buffer.value();
|
|
4909
|
-
}
|
|
4910
4923
|
function buildResult(status, startedAt, fields = {}) {
|
|
4911
4924
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
4912
4925
|
return {
|
|
@@ -4944,6 +4957,20 @@ function notifySpawn(pid, opts) {
|
|
|
4944
4957
|
function shellQuote(value) {
|
|
4945
4958
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4946
4959
|
}
|
|
4960
|
+
function codewithProfileCandidateFromLine(line) {
|
|
4961
|
+
const cols = line.trim().split(/\s+/);
|
|
4962
|
+
if (cols[0] === "*")
|
|
4963
|
+
return cols[1];
|
|
4964
|
+
return cols[0];
|
|
4965
|
+
}
|
|
4966
|
+
function codewithProfileForError(profile) {
|
|
4967
|
+
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
4968
|
+
}
|
|
4969
|
+
function assertCodewithAuthProfileSupported(profile) {
|
|
4970
|
+
if (profile.includes("\x00")) {
|
|
4971
|
+
throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4947
4974
|
function metadataEnv(metadata) {
|
|
4948
4975
|
const env = {};
|
|
4949
4976
|
if (metadata.loopId)
|
|
@@ -4970,21 +4997,6 @@ function metadataEnv(metadata) {
|
|
|
4970
4997
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
4971
4998
|
return env;
|
|
4972
4999
|
}
|
|
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
5000
|
function allowlistEnv(allowlist) {
|
|
4989
5001
|
const env = {};
|
|
4990
5002
|
if (allowlist?.tools?.length)
|
|
@@ -5025,6 +5037,9 @@ function commandSpec(target, opts) {
|
|
|
5025
5037
|
};
|
|
5026
5038
|
}
|
|
5027
5039
|
const agentTarget = target;
|
|
5040
|
+
if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
|
|
5041
|
+
assertCodewithAuthProfileSupported(agentTarget.authProfile);
|
|
5042
|
+
}
|
|
5028
5043
|
const adapter = providerAdapter(agentTarget.provider);
|
|
5029
5044
|
const invocation = adapter.buildInvocation(agentTarget);
|
|
5030
5045
|
return {
|
|
@@ -5039,8 +5054,7 @@ function commandSpec(target, opts) {
|
|
|
5039
5054
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5040
5055
|
stdin: invocation.stdin,
|
|
5041
5056
|
allowlist: agentTarget.allowlist,
|
|
5042
|
-
worktree: agentTarget.worktree
|
|
5043
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
5057
|
+
worktree: agentTarget.worktree
|
|
5044
5058
|
};
|
|
5045
5059
|
}
|
|
5046
5060
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5198,7 +5212,8 @@ function remotePreflightScript(spec, metadata) {
|
|
|
5198
5212
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
5199
5213
|
}
|
|
5200
5214
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
5201
|
-
|
|
5215
|
+
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
5216
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
|
|
5202
5217
|
}
|
|
5203
5218
|
return lines.join(`
|
|
5204
5219
|
`);
|
|
@@ -5223,9 +5238,9 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
5223
5238
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5224
5239
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
5225
5240
|
}
|
|
5226
|
-
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(
|
|
5241
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
|
|
5227
5242
|
if (!profiles.has(profile)) {
|
|
5228
|
-
throw new Error(`codewith auth profile not found: ${profile}`);
|
|
5243
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
5229
5244
|
}
|
|
5230
5245
|
}
|
|
5231
5246
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
@@ -5250,266 +5265,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5250
5265
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5251
5266
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5252
5267
|
}
|
|
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
5268
|
function resolvedDirEquals(left, right) {
|
|
5514
5269
|
try {
|
|
5515
5270
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5743,9 +5498,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5743
5498
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5744
5499
|
let spec = commandSpec(target, opts);
|
|
5745
5500
|
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
5501
|
if (machine && !machine.local) {
|
|
5750
5502
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5751
5503
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5776,9 +5528,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5776
5528
|
if (worktreeEntry?.fallbackCwd) {
|
|
5777
5529
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5778
5530
|
}
|
|
5779
|
-
if (spec.codewithDurableAgent) {
|
|
5780
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5781
|
-
}
|
|
5782
5531
|
const child = spawn2(spec.command, spec.args, {
|
|
5783
5532
|
cwd: spec.cwd,
|
|
5784
5533
|
env,
|
|
@@ -7676,7 +7425,7 @@ function realSleep(ms) {
|
|
|
7676
7425
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
7677
7426
|
}
|
|
7678
7427
|
async function runLoop(opts) {
|
|
7679
|
-
const
|
|
7428
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7680
7429
|
const sliceMs = opts.sliceMs ?? 200;
|
|
7681
7430
|
while (!opts.shouldStop()) {
|
|
7682
7431
|
try {
|
|
@@ -7687,7 +7436,7 @@ async function runLoop(opts) {
|
|
|
7687
7436
|
let waited = 0;
|
|
7688
7437
|
while (waited < opts.intervalMs && !opts.shouldStop()) {
|
|
7689
7438
|
const chunk = Math.min(sliceMs, opts.intervalMs - waited);
|
|
7690
|
-
await
|
|
7439
|
+
await sleep(chunk);
|
|
7691
7440
|
waited += chunk;
|
|
7692
7441
|
}
|
|
7693
7442
|
}
|
|
@@ -7779,7 +7528,7 @@ function processExists(pid) {
|
|
|
7779
7528
|
}
|
|
7780
7529
|
}
|
|
7781
7530
|
async function reapProcessGroups(entries, opts = {}) {
|
|
7782
|
-
const
|
|
7531
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7783
7532
|
const graceMs = Math.max(100, opts.graceMs ?? 2000);
|
|
7784
7533
|
const ownPgid = ownProcessGroupId();
|
|
7785
7534
|
const targets = new Map;
|
|
@@ -7808,7 +7557,7 @@ async function reapProcessGroups(entries, opts = {}) {
|
|
|
7808
7557
|
const steps = Math.max(1, Math.ceil(graceMs / 100));
|
|
7809
7558
|
let remaining = live;
|
|
7810
7559
|
for (let i = 0;i < steps && remaining.length > 0; i++) {
|
|
7811
|
-
await
|
|
7560
|
+
await sleep(100);
|
|
7812
7561
|
remaining = remaining.filter((target) => signalReapTarget(target, 0));
|
|
7813
7562
|
}
|
|
7814
7563
|
for (const target of remaining) {
|
|
@@ -7850,7 +7599,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7850
7599
|
}
|
|
7851
7600
|
if (!state.running || !state.pid)
|
|
7852
7601
|
return { wasRunning: false, stopped: false, forced: false };
|
|
7853
|
-
const
|
|
7602
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7854
7603
|
const store = new Store(join5(dirname4(path), "loops.db"));
|
|
7855
7604
|
try {
|
|
7856
7605
|
const lease = store.getDaemonLease();
|
|
@@ -7863,7 +7612,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7863
7612
|
}
|
|
7864
7613
|
const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
|
|
7865
7614
|
for (let i = 0;i < steps; i++) {
|
|
7866
|
-
await
|
|
7615
|
+
await sleep(100);
|
|
7867
7616
|
if (!isAlive(state.pid, record?.startedAt)) {
|
|
7868
7617
|
removePid(path);
|
|
7869
7618
|
return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
|
|
@@ -7872,13 +7621,13 @@ async function stopDaemon(opts = {}) {
|
|
|
7872
7621
|
try {
|
|
7873
7622
|
process.kill(state.pid, "SIGKILL");
|
|
7874
7623
|
} catch {}
|
|
7875
|
-
await
|
|
7624
|
+
await sleep(150);
|
|
7876
7625
|
removePid(path);
|
|
7877
7626
|
let reapedPgids = [];
|
|
7878
7627
|
if (leaseVerified && lease) {
|
|
7879
7628
|
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
7880
7629
|
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
7881
|
-
sleep
|
|
7630
|
+
sleep,
|
|
7882
7631
|
graceMs: opts.reapGraceMs
|
|
7883
7632
|
});
|
|
7884
7633
|
}
|
|
@@ -7953,7 +7702,7 @@ async function runDaemon(opts = {}) {
|
|
|
7953
7702
|
const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
|
|
7954
7703
|
const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
|
|
7955
7704
|
const log = opts.log ?? defaultDaemonLog;
|
|
7956
|
-
const
|
|
7705
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7957
7706
|
const lease = store.acquireDaemonLease({
|
|
7958
7707
|
id: leaseId,
|
|
7959
7708
|
pid: process.pid,
|
|
@@ -8017,7 +7766,7 @@ async function runDaemon(opts = {}) {
|
|
|
8017
7766
|
const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
|
|
8018
7767
|
if (entries.length === 0)
|
|
8019
7768
|
return;
|
|
8020
|
-
const killed = await reapProcessGroups(entries, { sleep
|
|
7769
|
+
const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
|
|
8021
7770
|
if (killed.length > 0)
|
|
8022
7771
|
log(`reaped orphan process groups: ${killed.join(", ")}`);
|
|
8023
7772
|
};
|
|
@@ -8092,7 +7841,7 @@ async function runDaemon(opts = {}) {
|
|
|
8092
7841
|
}
|
|
8093
7842
|
await runLoop({
|
|
8094
7843
|
intervalMs,
|
|
8095
|
-
sleep
|
|
7844
|
+
sleep,
|
|
8096
7845
|
shouldStop: opts.shouldStop ?? (() => stopFlag),
|
|
8097
7846
|
onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
|
|
8098
7847
|
tickFn: async () => {
|
|
@@ -8142,10 +7891,10 @@ async function startDaemon(opts) {
|
|
|
8142
7891
|
stdio: ["ignore", out, out]
|
|
8143
7892
|
});
|
|
8144
7893
|
child.unref();
|
|
8145
|
-
const
|
|
7894
|
+
const sleep = opts.sleep ?? realSleep;
|
|
8146
7895
|
const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
|
|
8147
7896
|
for (let i = 0;i < deadline; i++) {
|
|
8148
|
-
await
|
|
7897
|
+
await sleep(100);
|
|
8149
7898
|
const current = isDaemonRunning();
|
|
8150
7899
|
if (current.running)
|
|
8151
7900
|
return { started: true, alreadyRunning: false, pid: current.pid };
|
|
@@ -11078,7 +10827,7 @@ function eventMetadata(event) {
|
|
|
11078
10827
|
return metadata;
|
|
11079
10828
|
return {};
|
|
11080
10829
|
}
|
|
11081
|
-
function
|
|
10830
|
+
function stringField(value) {
|
|
11082
10831
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
11083
10832
|
}
|
|
11084
10833
|
function slugSegment2(value, fallback = "event") {
|
|
@@ -11093,14 +10842,14 @@ function stableHash(parts) {
|
|
|
11093
10842
|
}
|
|
11094
10843
|
function taskEventField(data, keys) {
|
|
11095
10844
|
for (const key of keys) {
|
|
11096
|
-
const direct =
|
|
10845
|
+
const direct = stringField(data[key]);
|
|
11097
10846
|
if (direct)
|
|
11098
10847
|
return direct;
|
|
11099
10848
|
}
|
|
11100
10849
|
const task = data.task;
|
|
11101
10850
|
if (task && typeof task === "object" && !Array.isArray(task)) {
|
|
11102
10851
|
for (const key of keys) {
|
|
11103
|
-
const direct =
|
|
10852
|
+
const direct = stringField(task[key]);
|
|
11104
10853
|
if (direct)
|
|
11105
10854
|
return direct;
|
|
11106
10855
|
}
|
|
@@ -11108,7 +10857,7 @@ function taskEventField(data, keys) {
|
|
|
11108
10857
|
const payload = data.payload;
|
|
11109
10858
|
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
11110
10859
|
for (const key of keys) {
|
|
11111
|
-
const direct =
|
|
10860
|
+
const direct = stringField(payload[key]);
|
|
11112
10861
|
if (direct)
|
|
11113
10862
|
return direct;
|
|
11114
10863
|
}
|
|
@@ -11612,6 +11361,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
11612
11361
|
return mode;
|
|
11613
11362
|
}
|
|
11614
11363
|
// src/lib/route/pr-review.ts
|
|
11364
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
11615
11365
|
var PR_AUTHOR_FIELDS = [
|
|
11616
11366
|
"github_author",
|
|
11617
11367
|
"githubAuthor",
|
|
@@ -11663,11 +11413,18 @@ var PR_REVIEW_REQUIRED_FIELDS = [
|
|
|
11663
11413
|
"branch_protection_review_required",
|
|
11664
11414
|
"branchProtectionReviewRequired"
|
|
11665
11415
|
];
|
|
11416
|
+
var GITHUB_USER_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
11666
11417
|
function githubLogin(value) {
|
|
11667
11418
|
if (!value?.trim())
|
|
11668
11419
|
return;
|
|
11669
|
-
|
|
11670
|
-
|
|
11420
|
+
let login = value.trim().replace(/^@/, "");
|
|
11421
|
+
const appActor = /^app\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))$/.exec(login);
|
|
11422
|
+
if (appActor)
|
|
11423
|
+
login = `${appActor[1]}[bot]`;
|
|
11424
|
+
const bot = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\[bot\]$/.exec(login);
|
|
11425
|
+
if (bot)
|
|
11426
|
+
return `${bot[1]}[bot]`;
|
|
11427
|
+
return GITHUB_USER_LOGIN.test(login) ? login : undefined;
|
|
11671
11428
|
}
|
|
11672
11429
|
function githubLogins(values) {
|
|
11673
11430
|
const seen = new Set;
|
|
@@ -11726,6 +11483,53 @@ function routeTextFieldValues(record, field) {
|
|
|
11726
11483
|
}
|
|
11727
11484
|
return values;
|
|
11728
11485
|
}
|
|
11486
|
+
var PR_STATE_FIELDS = [
|
|
11487
|
+
"pr_state",
|
|
11488
|
+
"prState",
|
|
11489
|
+
"pull_request_state",
|
|
11490
|
+
"pullRequestState",
|
|
11491
|
+
"state",
|
|
11492
|
+
"pr_status",
|
|
11493
|
+
"prStatus"
|
|
11494
|
+
];
|
|
11495
|
+
var CLOSED_PR_STATES = new Set(["MERGED", "CLOSED"]);
|
|
11496
|
+
function normalizePrState(value) {
|
|
11497
|
+
const trimmed = value?.trim().toUpperCase();
|
|
11498
|
+
return trimmed ? trimmed : undefined;
|
|
11499
|
+
}
|
|
11500
|
+
function prStateFromEvidence(records, text) {
|
|
11501
|
+
const field = normalizePrState(firstRouteField(records, PR_STATE_FIELDS));
|
|
11502
|
+
if (field)
|
|
11503
|
+
return field;
|
|
11504
|
+
const match = /\b(?:pr[_\s-]?state|pull[_\s-]?request[_\s-]?state|state)\s*[:=]\s*(MERGED|CLOSED|OPEN)\b/i.exec(text);
|
|
11505
|
+
return match ? match[1].toUpperCase() : undefined;
|
|
11506
|
+
}
|
|
11507
|
+
function prReferenceFrom(text) {
|
|
11508
|
+
const url = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)/i.exec(text);
|
|
11509
|
+
if (url)
|
|
11510
|
+
return { owner: url[1], repo: url[2], number: Number(url[3]) };
|
|
11511
|
+
const short = /github-pr:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)/i.exec(text);
|
|
11512
|
+
if (short)
|
|
11513
|
+
return { owner: short[1], repo: short[2], number: Number(short[3]) };
|
|
11514
|
+
return;
|
|
11515
|
+
}
|
|
11516
|
+
function ghAuthorResolver(ref) {
|
|
11517
|
+
const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "author", "-q", ".author.login"], { encoding: "utf8", timeout: 20000 });
|
|
11518
|
+
if (result.error || result.status !== 0)
|
|
11519
|
+
return;
|
|
11520
|
+
return githubLogin((result.stdout ?? "").trim());
|
|
11521
|
+
}
|
|
11522
|
+
function ghStateResolver(ref) {
|
|
11523
|
+
const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergeStateStatus"], { encoding: "utf8", timeout: 20000 });
|
|
11524
|
+
if (result.error || result.status !== 0)
|
|
11525
|
+
return;
|
|
11526
|
+
try {
|
|
11527
|
+
const parsed = JSON.parse(result.stdout ?? "{}");
|
|
11528
|
+
return { state: parsed.state, mergeStateStatus: parsed.mergeStateStatus };
|
|
11529
|
+
} catch {
|
|
11530
|
+
return;
|
|
11531
|
+
}
|
|
11532
|
+
}
|
|
11729
11533
|
function authorFromPrText(text) {
|
|
11730
11534
|
const patterns = [
|
|
11731
11535
|
/\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
|
|
@@ -11752,7 +11556,7 @@ function reviewersFromPrText(text) {
|
|
|
11752
11556
|
}
|
|
11753
11557
|
return githubLogins(reviewers);
|
|
11754
11558
|
}
|
|
11755
|
-
function prReviewRoutingDecision(data, metadata, opts) {
|
|
11559
|
+
function prReviewRoutingDecision(data, metadata, opts, resolveAuthor = ghAuthorResolver, resolveState = ghStateResolver) {
|
|
11756
11560
|
const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
|
|
11757
11561
|
const text = routeEvidenceText(records);
|
|
11758
11562
|
const signals = [];
|
|
@@ -11772,7 +11576,21 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11772
11576
|
const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
|
|
11773
11577
|
if (!required)
|
|
11774
11578
|
return { required: false, allowed: true, reviewers: [], signals };
|
|
11775
|
-
const
|
|
11579
|
+
const prRef = prReferenceFrom(text);
|
|
11580
|
+
let prState = prStateFromEvidence(records, text);
|
|
11581
|
+
if (!prState && prRef) {
|
|
11582
|
+
const live = resolveState(prRef);
|
|
11583
|
+
prState = normalizePrState(live?.state);
|
|
11584
|
+
}
|
|
11585
|
+
if (prState && CLOSED_PR_STATES.has(prState)) {
|
|
11586
|
+
return {
|
|
11587
|
+
required: true,
|
|
11588
|
+
allowed: false,
|
|
11589
|
+
reason: `PR is already ${prState.toLowerCase()}; skipping merge/review route (freshness gate)`,
|
|
11590
|
+
reviewers: [],
|
|
11591
|
+
signals: [...signals, "pr-not-open"]
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
11776
11594
|
const reviewers = githubLogins([
|
|
11777
11595
|
opts.githubReviewer,
|
|
11778
11596
|
...splitList(opts.githubReviewerPool) ?? [],
|
|
@@ -11780,6 +11598,14 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11780
11598
|
...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
|
|
11781
11599
|
...reviewersFromPrText(text)
|
|
11782
11600
|
]);
|
|
11601
|
+
let author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
|
|
11602
|
+
if (!author && prRef) {
|
|
11603
|
+
const derived = githubLogin(resolveAuthor(prRef));
|
|
11604
|
+
if (derived) {
|
|
11605
|
+
author = derived;
|
|
11606
|
+
signals.push("author-derived-gh");
|
|
11607
|
+
}
|
|
11608
|
+
}
|
|
11783
11609
|
const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
|
|
11784
11610
|
if (!author) {
|
|
11785
11611
|
return {
|
|
@@ -11821,7 +11647,7 @@ function prReviewRoutingDecision(data, metadata, opts) {
|
|
|
11821
11647
|
}
|
|
11822
11648
|
// src/lib/route/throttle.ts
|
|
11823
11649
|
import { realpathSync as realpathSync2 } from "fs";
|
|
11824
|
-
import { spawnSync as
|
|
11650
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
11825
11651
|
import { resolve as resolve6 } from "path";
|
|
11826
11652
|
function routeThrottleLimitsFromOpts(opts) {
|
|
11827
11653
|
return {
|
|
@@ -11843,7 +11669,7 @@ function normalizeRoutePath(value) {
|
|
|
11843
11669
|
} catch {
|
|
11844
11670
|
return canonical;
|
|
11845
11671
|
}
|
|
11846
|
-
const gitRoot =
|
|
11672
|
+
const gitRoot = spawnSync8("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
11847
11673
|
if (gitRoot.status === 0 && gitRoot.stdout.trim()) {
|
|
11848
11674
|
try {
|
|
11849
11675
|
return realpathSync2(gitRoot.stdout.trim());
|
|
@@ -11893,7 +11719,7 @@ function routeThrottleDryRunPreview(args) {
|
|
|
11893
11719
|
};
|
|
11894
11720
|
}
|
|
11895
11721
|
function isExistingGitProjectPath(path) {
|
|
11896
|
-
const result =
|
|
11722
|
+
const result = spawnSync8("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
|
|
11897
11723
|
return result.status === 0;
|
|
11898
11724
|
}
|
|
11899
11725
|
function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
|
|
@@ -11973,6 +11799,34 @@ var UNCLEARED_ROUTE_WORK_ITEM_STATUSES = new Set([
|
|
|
11973
11799
|
function isUnclearedRouteWorkItem(item) {
|
|
11974
11800
|
return UNCLEARED_ROUTE_WORK_ITEM_STATUSES.has(item.status);
|
|
11975
11801
|
}
|
|
11802
|
+
var REACTIVATABLE_TERMINAL_STATUSES = new Set([
|
|
11803
|
+
"succeeded",
|
|
11804
|
+
"failed",
|
|
11805
|
+
"dead_letter",
|
|
11806
|
+
"cancelled"
|
|
11807
|
+
]);
|
|
11808
|
+
var MAX_TODOS_TASK_ROUTE_REDISPATCHES = 8;
|
|
11809
|
+
function todosTaskRouteRedispatchBackoffMs(attempts) {
|
|
11810
|
+
const base = 2 * 60000;
|
|
11811
|
+
const cap = 30 * 60000;
|
|
11812
|
+
const exp = Math.max(0, Math.min(attempts - 1, 10));
|
|
11813
|
+
return Math.min(cap, base * 2 ** exp);
|
|
11814
|
+
}
|
|
11815
|
+
function reactivateStaleTodosTaskWorkItem(store, routeKey, item, now = Date.now()) {
|
|
11816
|
+
if (routeKey !== "todos-task")
|
|
11817
|
+
return;
|
|
11818
|
+
if (!REACTIVATABLE_TERMINAL_STATUSES.has(item.status))
|
|
11819
|
+
return;
|
|
11820
|
+
if (item.attempts >= MAX_TODOS_TASK_ROUTE_REDISPATCHES)
|
|
11821
|
+
return;
|
|
11822
|
+
const finishedAt = Date.parse(item.updatedAt);
|
|
11823
|
+
if (Number.isFinite(finishedAt) && now - finishedAt < todosTaskRouteRedispatchBackoffMs(item.attempts)) {
|
|
11824
|
+
return;
|
|
11825
|
+
}
|
|
11826
|
+
return store.requeueWorkflowWorkItem(item.id, {
|
|
11827
|
+
reason: `re-admitted from ${item.status}: todos task still actionable after prior run (attempt ${item.attempts + 1}/${MAX_TODOS_TASK_ROUTE_REDISPATCHES})`
|
|
11828
|
+
});
|
|
11829
|
+
}
|
|
11976
11830
|
function todosTaskRouteTemplateId(opts) {
|
|
11977
11831
|
const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
|
|
11978
11832
|
if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
|
|
@@ -11985,11 +11839,11 @@ async function readEventEnvelopeInput(opts = {}) {
|
|
|
11985
11839
|
const event = JSON.parse(raw);
|
|
11986
11840
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
11987
11841
|
throw new ValidationError("event JSON must be an object");
|
|
11988
|
-
if (!
|
|
11842
|
+
if (!stringField(event.id))
|
|
11989
11843
|
throw new ValidationError("event.id is required");
|
|
11990
|
-
if (!
|
|
11844
|
+
if (!stringField(event.type))
|
|
11991
11845
|
throw new ValidationError("event.type is required");
|
|
11992
|
-
if (!
|
|
11846
|
+
if (!stringField(event.source))
|
|
11993
11847
|
throw new ValidationError("event.source is required");
|
|
11994
11848
|
return event;
|
|
11995
11849
|
}
|
|
@@ -12065,9 +11919,11 @@ function routeEvent(plan) {
|
|
|
12065
11919
|
const invocation = store.createWorkflowInvocation(plan.invocationInput);
|
|
12066
11920
|
const existingItem = store.findWorkflowWorkItem(plan.routeKey, idempotencyKey);
|
|
12067
11921
|
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
11922
|
+
if (!reactivateStaleTodosTaskWorkItem(store, plan.routeKey, existingItem)) {
|
|
11923
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
11924
|
+
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
11925
|
+
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
11926
|
+
}
|
|
12071
11927
|
}
|
|
12072
11928
|
const throttle = hasThrottleLimits(plan.throttleLimits) ? routeThrottleDecision(store, { projectPath: plan.routeProjectPath, projectGroup: plan.projectGroup, limits: plan.throttleLimits }) : undefined;
|
|
12073
11929
|
const workItem = store.upsertWorkflowWorkItem({
|
|
@@ -12199,10 +12055,12 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
12199
12055
|
try {
|
|
12200
12056
|
const existingItem = store.findWorkflowWorkItem("todos-task", idempotencyKey);
|
|
12201
12057
|
if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12058
|
+
if (!reactivateStaleTodosTaskWorkItem(store, "todos-task", existingItem)) {
|
|
12059
|
+
const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
|
|
12060
|
+
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
12061
|
+
const existingInvocation = store.getWorkflowInvocation(existingItem.invocationId);
|
|
12062
|
+
return dedupedRoutePrint({ event, idempotencyKey, dedupeValueExtras: {} }, { existingItem, existingLoop, existingWorkflow, invocation: existingInvocation });
|
|
12063
|
+
}
|
|
12206
12064
|
}
|
|
12207
12065
|
} finally {
|
|
12208
12066
|
store.close();
|
|
@@ -12360,8 +12218,8 @@ function routeGenericEvent(event, opts) {
|
|
|
12360
12218
|
eventId: event.id,
|
|
12361
12219
|
eventType: event.type,
|
|
12362
12220
|
eventSource: event.source,
|
|
12363
|
-
eventSubject:
|
|
12364
|
-
eventMessage:
|
|
12221
|
+
eventSubject: stringField(event.subject),
|
|
12222
|
+
eventMessage: stringField(event.message),
|
|
12365
12223
|
eventJson: JSON.stringify(event),
|
|
12366
12224
|
projectPath,
|
|
12367
12225
|
routeProjectPath,
|
|
@@ -12402,9 +12260,9 @@ function routeGenericEvent(event, opts) {
|
|
|
12402
12260
|
},
|
|
12403
12261
|
subjectRef: {
|
|
12404
12262
|
kind: "event",
|
|
12405
|
-
id:
|
|
12263
|
+
id: stringField(event.subject) ?? event.id,
|
|
12406
12264
|
path: routeProjectPath,
|
|
12407
|
-
raw: { message:
|
|
12265
|
+
raw: { message: stringField(event.message) }
|
|
12408
12266
|
},
|
|
12409
12267
|
intent: "route",
|
|
12410
12268
|
scope: {
|
|
@@ -12433,7 +12291,7 @@ function routeGenericEvent(event, opts) {
|
|
|
12433
12291
|
invocationInput,
|
|
12434
12292
|
routeProjectPath,
|
|
12435
12293
|
projectGroup,
|
|
12436
|
-
subjectRef:
|
|
12294
|
+
subjectRef: stringField(event.subject) ?? event.id,
|
|
12437
12295
|
loopName,
|
|
12438
12296
|
loopDescription: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
|
|
12439
12297
|
throttleLimits,
|
|
@@ -12455,14 +12313,14 @@ import { resolve as resolve8 } from "path";
|
|
|
12455
12313
|
|
|
12456
12314
|
// src/lib/route/todos-cli.ts
|
|
12457
12315
|
import { closeSync, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, rmSync as rmSync6 } from "fs";
|
|
12458
|
-
import { spawnSync as
|
|
12316
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
12459
12317
|
import { join as join10 } from "path";
|
|
12460
12318
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
12461
12319
|
function defaultLoopsProject() {
|
|
12462
12320
|
return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir5()}/.hasna/loops`;
|
|
12463
12321
|
}
|
|
12464
12322
|
function runLocalCommand(command, args, opts = {}) {
|
|
12465
|
-
const result =
|
|
12323
|
+
const result = spawnSync9(command, args, {
|
|
12466
12324
|
input: opts.input,
|
|
12467
12325
|
encoding: "utf8",
|
|
12468
12326
|
timeout: opts.timeoutMs ?? 30000,
|
|
@@ -12483,7 +12341,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
12483
12341
|
const stdoutFd = openSync2(stdoutPath, "w");
|
|
12484
12342
|
let result;
|
|
12485
12343
|
try {
|
|
12486
|
-
result =
|
|
12344
|
+
result = spawnSync9(command, args, {
|
|
12487
12345
|
input: opts.input,
|
|
12488
12346
|
encoding: "utf8",
|
|
12489
12347
|
timeout: opts.timeoutMs ?? 30000,
|
|
@@ -12528,7 +12386,7 @@ function todosMutationSummary(result) {
|
|
|
12528
12386
|
// src/lib/route/drain.ts
|
|
12529
12387
|
function taskField(task, keys) {
|
|
12530
12388
|
for (const key of keys) {
|
|
12531
|
-
const value =
|
|
12389
|
+
const value = stringField(task[key]);
|
|
12532
12390
|
if (value)
|
|
12533
12391
|
return value;
|
|
12534
12392
|
}
|
|
@@ -12553,9 +12411,9 @@ function taskListValues(task) {
|
|
|
12553
12411
|
const taskList = objectField2(task.task_list);
|
|
12554
12412
|
return [
|
|
12555
12413
|
taskField(task, ["task_list_id", "taskListId"]),
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12414
|
+
stringField(task.task_list?.id),
|
|
12415
|
+
stringField(task.task_list?.slug),
|
|
12416
|
+
stringField(taskList?.name),
|
|
12559
12417
|
taskField(task, ["task_list", "taskList"])
|
|
12560
12418
|
].filter((value) => Boolean(value));
|
|
12561
12419
|
}
|
|
@@ -12629,12 +12487,12 @@ function compactDrainResult(result) {
|
|
|
12629
12487
|
kind: result.kind,
|
|
12630
12488
|
taskId: event?.subject,
|
|
12631
12489
|
eventId: event?.id,
|
|
12632
|
-
idempotencyKey:
|
|
12633
|
-
reason:
|
|
12634
|
-
loopId:
|
|
12635
|
-
loopName:
|
|
12636
|
-
workflowId:
|
|
12637
|
-
workflowName:
|
|
12490
|
+
idempotencyKey: stringField(value.idempotencyKey),
|
|
12491
|
+
reason: stringField(value.reason) ?? throttle?.reason,
|
|
12492
|
+
loopId: stringField(loop?.id),
|
|
12493
|
+
loopName: stringField(loop?.name),
|
|
12494
|
+
workflowId: stringField(workflow?.id),
|
|
12495
|
+
workflowName: stringField(workflow?.name),
|
|
12638
12496
|
providerRouting,
|
|
12639
12497
|
requeue,
|
|
12640
12498
|
queuedAtSource: value.queuedAtSource,
|
|
@@ -12668,7 +12526,7 @@ function loadTodoProjectPathsFromRegistry(opts) {
|
|
|
12668
12526
|
const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
|
|
12669
12527
|
const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
|
|
12670
12528
|
const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
|
|
12671
|
-
const path =
|
|
12529
|
+
const path = stringField(project.path) ?? stringField(project.projectPath) ?? stringField(project.project_path) ?? stringField(project.dir) ?? stringField(project.root) ?? stringField(project.cwd);
|
|
12672
12530
|
if (!path)
|
|
12673
12531
|
return;
|
|
12674
12532
|
return { path };
|
|
@@ -14063,7 +13921,7 @@ routes.command("show <id>").description("show one admission work item").action(r
|
|
|
14063
13921
|
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) => {
|
|
14064
13922
|
const store = new Store;
|
|
14065
13923
|
try {
|
|
14066
|
-
const reason =
|
|
13924
|
+
const reason = stringField(opts.reason);
|
|
14067
13925
|
if (!reason)
|
|
14068
13926
|
throw new ValidationError("routes requeue requires --reason <text>");
|
|
14069
13927
|
const item = store.requeueWorkflowWorkItem(id, { reason });
|