@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/dist/index.js CHANGED
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
579
579
 
580
580
  // src/lib/agent-adapter.ts
581
581
  import { spawn } from "child_process";
582
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
582
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
583
583
  "e",
584
584
  "exec",
585
585
  "agent",
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
628
628
  throw new Error(`${label}.agent is not supported for provider codex`);
629
629
  }
630
630
  if (provider === "codewith" && target.agent !== undefined) {
631
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
631
+ throw new Error(`${label}.agent is not supported for provider codewith`);
632
632
  }
633
633
  if (provider === "codewith") {
634
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
634
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
635
635
  if (unsafe) {
636
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
636
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
637
637
  }
638
638
  }
639
639
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
716
716
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
717
717
  if (target.variant)
718
718
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
719
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
719
+ const sandbox = codewithLikeSandbox(target);
720
+ if (sandbox === "workspace-write")
721
+ args.push("-c", "sandbox_workspace_write.network_access=true");
722
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
720
723
  if (target.cwd)
721
724
  args.push("--cd", target.cwd);
722
725
  for (const dir of target.addDirs ?? [])
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
724
727
  if (target.model)
725
728
  args.push("--model", target.model);
726
729
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start", "--json");
728
- if (target.cwd)
729
- args.push("--cwd", target.cwd);
730
- args.push(target.prompt);
731
- return { command: "codewith", args };
730
+ return { command: "codewith", args, stdin: target.prompt };
732
731
  }
733
732
  case "codex": {
734
733
  if (target.variant)
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
781
780
  var PROVIDER_ADAPTERS = {
782
781
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
783
782
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
784
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
783
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
785
784
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
785
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
787
786
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -4217,7 +4216,7 @@ class Store {
4217
4216
  // package.json
4218
4217
  var package_default = {
4219
4218
  name: "@hasna/loops",
4220
- version: "0.4.7",
4219
+ version: "0.4.9",
4221
4220
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4222
4221
  type: "module",
4223
4222
  main: "dist/index.js",
@@ -5009,11 +5008,6 @@ var TRANSPORT_ENV_KEYS = new Set([
5009
5008
  "USER",
5010
5009
  "XDG_RUNTIME_DIR"
5011
5010
  ]);
5012
- function boundedText(text, maxBytes) {
5013
- const buffer = new BoundedOutputBuffer(maxBytes);
5014
- buffer.append(text);
5015
- return buffer.value();
5016
- }
5017
5011
  function buildResult(status, startedAt, fields = {}) {
5018
5012
  const finishedAt = fields.finishedAt ?? nowIso();
5019
5013
  return {
@@ -5077,21 +5071,6 @@ function metadataEnv(metadata) {
5077
5071
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
5078
5072
  return env;
5079
5073
  }
5080
- function codewithAgentIdempotencyKey(metadata) {
5081
- const parts = [
5082
- "openloops",
5083
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
5084
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
5085
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
5086
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
5087
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
5088
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
5089
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
5090
- ].filter(Boolean);
5091
- if (parts.length > 1)
5092
- return parts.join(":");
5093
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
5094
- }
5095
5074
  function allowlistEnv(allowlist) {
5096
5075
  const env = {};
5097
5076
  if (allowlist?.tools?.length)
@@ -5146,8 +5125,7 @@ function commandSpec(target, opts) {
5146
5125
  preflightAnyOf: invocation.preflightAnyOf,
5147
5126
  stdin: invocation.stdin,
5148
5127
  allowlist: agentTarget.allowlist,
5149
- worktree: agentTarget.worktree,
5150
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
5128
+ worktree: agentTarget.worktree
5151
5129
  };
5152
5130
  }
5153
5131
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -5357,266 +5335,6 @@ async function preflightNativeAuthProfile(spec, env) {
5357
5335
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
5358
5336
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
5359
5337
  }
5360
- function codewithAgentStartArgs(target, idempotencyKey) {
5361
- const args = providerAdapter(target.provider).buildInvocation(target).args;
5362
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
5363
- if (startIndex === -1)
5364
- throw new Error("internal error: codewith durable agent args missing agent start");
5365
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
5366
- return args;
5367
- }
5368
- function codewithAgentControlArgs(target, command, agentId) {
5369
- return [
5370
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
5371
- "agent",
5372
- command,
5373
- ...command === "logs" ? ["--limit", "20"] : [],
5374
- "--json",
5375
- agentId
5376
- ];
5377
- }
5378
- function extractFirstJsonObject(text) {
5379
- let depth = 0;
5380
- let start = -1;
5381
- let inString = false;
5382
- let escaped = false;
5383
- for (let i = 0;i < text.length; i += 1) {
5384
- const ch = text[i];
5385
- if (inString) {
5386
- if (escaped)
5387
- escaped = false;
5388
- else if (ch === "\\")
5389
- escaped = true;
5390
- else if (ch === '"')
5391
- inString = false;
5392
- continue;
5393
- }
5394
- if (ch === '"') {
5395
- inString = true;
5396
- } else if (ch === "{") {
5397
- if (depth === 0)
5398
- start = i;
5399
- depth += 1;
5400
- } else if (ch === "}") {
5401
- if (depth > 0) {
5402
- depth -= 1;
5403
- if (depth === 0 && start !== -1)
5404
- return text.slice(start, i + 1);
5405
- }
5406
- }
5407
- }
5408
- return;
5409
- }
5410
- function parseJsonOutput(stdout, label) {
5411
- const raw = stdout || "{}";
5412
- const candidates = [raw.trim()];
5413
- const scanned = extractFirstJsonObject(raw);
5414
- if (scanned && scanned !== raw.trim())
5415
- candidates.push(scanned);
5416
- let lastError;
5417
- for (const candidate of candidates) {
5418
- try {
5419
- const value = JSON.parse(candidate);
5420
- if (!value || typeof value !== "object" || Array.isArray(value))
5421
- throw new Error("not an object");
5422
- return value;
5423
- } catch (error) {
5424
- lastError = error;
5425
- }
5426
- }
5427
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
5428
- }
5429
- function recordField(value, key) {
5430
- if (!value || typeof value !== "object" || Array.isArray(value))
5431
- return;
5432
- const field = value[key];
5433
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
5434
- }
5435
- function stringField(value, key) {
5436
- const field = value?.[key];
5437
- return typeof field === "string" ? field : undefined;
5438
- }
5439
- function numberField(value, key) {
5440
- const field = value?.[key];
5441
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
5442
- }
5443
- function codewithAgentStatus(readJson) {
5444
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
5445
- }
5446
- function codewithAgentLastEventSeq(readJson) {
5447
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
5448
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
5449
- if (agentSeq !== undefined && snapshotSeq !== undefined)
5450
- return Math.max(agentSeq, snapshotSeq);
5451
- return agentSeq ?? snapshotSeq;
5452
- }
5453
- function codewithAgentEvidence(startJson, readJson, logsJson) {
5454
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
5455
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5456
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
5457
- seq: numberField(event, "seq"),
5458
- eventType: stringField(event, "eventType"),
5459
- createdAt: numberField(event, "createdAt")
5460
- })) : undefined;
5461
- return JSON.stringify({
5462
- codewithAgent: {
5463
- agentId: stringField(agent, "agentId"),
5464
- status: stringField(agent, "status"),
5465
- desiredState: stringField(agent, "desiredState"),
5466
- statusReason: stringField(agent, "statusReason"),
5467
- threadId: stringField(agent, "threadId"),
5468
- rolloutPath: stringField(agent, "rolloutPath"),
5469
- pid: numberField(agent, "pid"),
5470
- exitCode: numberField(agent, "exitCode"),
5471
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
5472
- },
5473
- statusSnapshot: statusSnapshot ? {
5474
- seq: numberField(statusSnapshot, "seq"),
5475
- status: stringField(statusSnapshot, "status"),
5476
- summary: stringField(statusSnapshot, "summary"),
5477
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
5478
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
5479
- } : undefined,
5480
- events
5481
- }, null, 2);
5482
- }
5483
- function codewithAgentProgress(readJson) {
5484
- const agent = recordField(readJson, "agent");
5485
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5486
- return {
5487
- provider: "codewith",
5488
- agentId: stringField(agent, "agentId"),
5489
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
5490
- summary: stringField(statusSnapshot, "summary"),
5491
- statusReason: stringField(agent, "statusReason"),
5492
- threadId: stringField(agent, "threadId"),
5493
- rolloutPath: stringField(agent, "rolloutPath"),
5494
- pid: numberField(agent, "pid"),
5495
- lastEventSeq: codewithAgentLastEventSeq(readJson)
5496
- };
5497
- }
5498
- function sleep(ms) {
5499
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5500
- }
5501
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
5502
- const target = spec.codewithDurableAgent?.target;
5503
- if (!target)
5504
- throw new Error("internal error: missing codewith durable target");
5505
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
5506
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
5507
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
5508
- cwd: spec.cwd,
5509
- env,
5510
- timeoutMs: 30000,
5511
- maxOutputBytes
5512
- });
5513
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
5514
- stderr.append(start.stderr);
5515
- if (start.error || (start.status ?? 1) !== 0) {
5516
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
5517
- exitCode: start.status ?? undefined,
5518
- stdout: start.stdout,
5519
- stderr: stderr.value()
5520
- });
5521
- }
5522
- let startJson;
5523
- try {
5524
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
5525
- } catch (error) {
5526
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
5527
- }
5528
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
5529
- if (!agentId) {
5530
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
5531
- }
5532
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
5533
- const stopAgent = async () => {
5534
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
5535
- cwd: spec.cwd,
5536
- env,
5537
- timeoutMs: 15000,
5538
- maxOutputBytes
5539
- });
5540
- };
5541
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
5542
- let lastReadJson = startJson;
5543
- let lastLogsJson;
5544
- let lastFingerprint;
5545
- let lastProgressAt = Date.now();
5546
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
5547
- while (true) {
5548
- if (opts.signal?.aborted) {
5549
- await stopAgent();
5550
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
5551
- }
5552
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
5553
- cwd: spec.cwd,
5554
- env,
5555
- timeoutMs: 30000,
5556
- maxOutputBytes
5557
- });
5558
- stderr.append(read.stderr);
5559
- if (read.error || (read.status ?? 1) !== 0) {
5560
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
5561
- exitCode: read.status ?? undefined,
5562
- stdout: evidence(),
5563
- stderr: stderr.value()
5564
- });
5565
- }
5566
- try {
5567
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
5568
- } catch (error) {
5569
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
5570
- stdout: boundedText(read.stdout, maxOutputBytes),
5571
- stderr: stderr.value()
5572
- });
5573
- }
5574
- const status = codewithAgentStatus(lastReadJson);
5575
- const fingerprint = JSON.stringify({
5576
- status,
5577
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
5578
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
5579
- });
5580
- if (fingerprint !== lastFingerprint) {
5581
- lastFingerprint = fingerprint;
5582
- lastProgressAt = Date.now();
5583
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
5584
- }
5585
- if (status === "completed" || status === "failed" || status === "cancelled") {
5586
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
5587
- cwd: spec.cwd,
5588
- env,
5589
- timeoutMs: 30000,
5590
- maxOutputBytes
5591
- });
5592
- if (!logs.error && (logs.status ?? 1) === 0) {
5593
- try {
5594
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
5595
- } catch {
5596
- lastLogsJson = undefined;
5597
- }
5598
- } else {
5599
- stderr.append(logs.stderr || logs.error || "");
5600
- }
5601
- if (status === "completed") {
5602
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
5603
- }
5604
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
5605
- }
5606
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
5607
- await stopAgent();
5608
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
5609
- }
5610
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
5611
- await stopAgent();
5612
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
5613
- stdout: evidence(),
5614
- stderr: stderr.value()
5615
- });
5616
- }
5617
- await sleep(pollMs);
5618
- }
5619
- }
5620
5338
  function resolvedDirEquals(left, right) {
5621
5339
  try {
5622
5340
  return realpathSync(left) === realpathSync(right);
@@ -5850,9 +5568,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
5850
5568
  async function executeTarget(target, metadata = {}, opts = {}) {
5851
5569
  let spec = commandSpec(target, opts);
5852
5570
  const machine = resolvedMachine(opts);
5853
- if (machine && !machine.local && spec.codewithDurableAgent) {
5854
- 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");
5855
- }
5856
5571
  if (machine && !machine.local) {
5857
5572
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
5858
5573
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -5883,9 +5598,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
5883
5598
  if (worktreeEntry?.fallbackCwd) {
5884
5599
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
5885
5600
  }
5886
- if (spec.codewithDurableAgent) {
5887
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
5888
- }
5889
5601
  const child = spawn2(spec.command, spec.args, {
5890
5602
  cwd: spec.cwd,
5891
5603
  env,
@@ -11261,9 +10973,8 @@ function renderTaskLifecycleWorkflow(input) {
11261
10973
  `) : "";
11262
10974
  const shared = [
11263
10975
  worktreePrompt(plan),
11264
- ...todosExactCommandsFragment(todosProjectPath, input.taskId, [
11265
- todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker")
11266
- ]),
10976
+ ...todosExactCommandsFragment(todosProjectPath, input.taskId, []),
10977
+ "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.",
11267
10978
  NO_TMUX_DISPATCH_FRAGMENT,
11268
10979
  "Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
11269
10980
  prReviewFollowUpFragment(input),
@@ -11274,6 +10985,8 @@ function renderTaskLifecycleWorkflow(input) {
11274
10985
  `);
11275
10986
  const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
11276
10987
  const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
10988
+ const markerCommentCommand = (stage, state, evidencePlaceholder) => `todos --project ${todosProjectPath} comment ${input.taskId} "${gateMarker(stage, state)}
10989
+ <${evidencePlaceholder}>"`;
11277
10990
  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.`;
11278
10991
  const triagePrompt = [
11279
10992
  ...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
@@ -11281,9 +10994,12 @@ function renderTaskLifecycleWorkflow(input) {
11281
10994
  "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.",
11282
10995
  "Do not implement repo changes in this step.",
11283
10996
  `If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
11284
- "Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
10997
+ `Use this copy-safe marker comment command for triage go: ${markerCommentCommand("triage", "go", "task-specific triage evidence")}`,
10998
+ "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.",
11285
10999
  `If the task should not proceed automatically, run: ${blockTaskCommand}`,
11286
11000
  `Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
11001
+ `Use this copy-safe marker comment command for triage blocked: ${markerCommentCommand("triage", "blocked", "task-specific triage evidence")}`,
11002
+ "Do not run a separate generic blocker comment before the marker; include the blocker evidence in that same marker comment.",
11287
11003
  gateStopFragment("triage", "later steps")
11288
11004
  ].join(`
11289
11005
  `);
@@ -11292,17 +11008,19 @@ function renderTaskLifecycleWorkflow(input) {
11292
11008
  shared,
11293
11009
  "Read the triage comment and current task details.",
11294
11010
  `If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
11295
- "In that same comment, include a concise implementation plan: files/areas to inspect, validation commands, risk checks, expected commit/PR behavior, and any cross-repo tasks that should be created separately.",
11011
+ `Use this copy-safe marker comment command for planner go: ${markerCommentCommand("planner", "go", "task-specific plan/evidence")}`,
11012
+ "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.",
11296
11013
  `Do not implement repo changes in this step. If the task is too broad or unsafe for automation, run: ${blockTaskCommand}`,
11297
11014
  `Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
11298
- `Create smaller deduped tasks and record evidence. ${gateStopFragment("planner", "the worker")}`
11015
+ `Use this copy-safe marker comment command for planner blocked: ${markerCommentCommand("planner", "blocked", "task-specific plan/evidence")}`,
11016
+ `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")}`
11299
11017
  ].join(`
11300
11018
  `);
11301
11019
  const workerPrompt = [
11302
11020
  ...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
11303
11021
  shared,
11304
11022
  todosStartLine(todosProjectPath, input.taskId),
11305
- "Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits, evidence, blockers, and residual risks.",
11023
+ "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.",
11306
11024
  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,
11307
11025
  WORKER_LEAVES_COMPLETION_FRAGMENT
11308
11026
  ].filter(Boolean).join(`
@@ -11310,7 +11028,7 @@ function renderTaskLifecycleWorkflow(input) {
11310
11028
  const verifierPrompt = [
11311
11029
  ...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
11312
11030
  shared,
11313
- todosVerificationLine(todosProjectPath, input.taskId),
11031
+ "Before completion, record concrete verification evidence in todos with changed files, validation results, findings, and the task decision.",
11314
11032
  todosDoneLine(todosProjectPath, input.taskId),
11315
11033
  adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
11316
11034
  verifierRuntimeGuidance(input),
@@ -24,7 +24,7 @@ export interface ProviderAdapter {
24
24
  validate(target: AgentTarget, label?: string): void;
25
25
  buildInvocation(target: AgentTarget): AgentInvocation;
26
26
  }
27
- export declare const UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS: Set<string>;
27
+ export declare const UNSAFE_CODEWITH_EXEC_EXTRA_ARGS: Set<string>;
28
28
  export declare const PROVIDER_ADAPTERS: Record<AgentProvider, ProviderAdapter>;
29
29
  export declare const AGENT_PROVIDERS: AgentProvider[];
30
30
  export declare function providerAdapter(provider: AgentProvider): ProviderAdapter;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.7",
5
+ version: "0.4.9",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -8,4 +8,14 @@ export interface PrReviewRoutingDecision {
8
8
  selectedReviewer?: string;
9
9
  signals: string[];
10
10
  }
11
- export declare function prReviewRoutingDecision(data: Record<string, unknown>, metadata: Record<string, unknown>, opts: TodosTaskRouteOptions): PrReviewRoutingDecision;
11
+ /** A GitHub PR reference resolvable to `owner/repo#number`. */
12
+ export interface PrReference {
13
+ owner: string;
14
+ repo: string;
15
+ number: number;
16
+ }
17
+ /** Resolves a PR author login from a concrete `owner/repo#number` reference. */
18
+ export type PrAuthorResolver = (ref: PrReference) => string | undefined;
19
+ /** Extracts a concrete owner/repo/number PR reference from route evidence text. */
20
+ export declare function prReferenceFrom(text: string): PrReference | undefined;
21
+ export declare function prReviewRoutingDecision(data: Record<string, unknown>, metadata: Record<string, unknown>, opts: TodosTaskRouteOptions, resolveAuthor?: PrAuthorResolver): PrReviewRoutingDecision;
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
579
579
 
580
580
  // src/lib/agent-adapter.ts
581
581
  import { spawn } from "child_process";
582
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
582
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
583
583
  "e",
584
584
  "exec",
585
585
  "agent",
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
628
628
  throw new Error(`${label}.agent is not supported for provider codex`);
629
629
  }
630
630
  if (provider === "codewith" && target.agent !== undefined) {
631
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
631
+ throw new Error(`${label}.agent is not supported for provider codewith`);
632
632
  }
633
633
  if (provider === "codewith") {
634
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
634
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
635
635
  if (unsafe) {
636
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
636
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
637
637
  }
638
638
  }
639
639
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
716
716
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
717
717
  if (target.variant)
718
718
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
719
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
719
+ const sandbox = codewithLikeSandbox(target);
720
+ if (sandbox === "workspace-write")
721
+ args.push("-c", "sandbox_workspace_write.network_access=true");
722
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
720
723
  if (target.cwd)
721
724
  args.push("--cd", target.cwd);
722
725
  for (const dir of target.addDirs ?? [])
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
724
727
  if (target.model)
725
728
  args.push("--model", target.model);
726
729
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start", "--json");
728
- if (target.cwd)
729
- args.push("--cwd", target.cwd);
730
- args.push(target.prompt);
731
- return { command: "codewith", args };
730
+ return { command: "codewith", args, stdin: target.prompt };
732
731
  }
733
732
  case "codex": {
734
733
  if (target.variant)
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
781
780
  var PROVIDER_ADAPTERS = {
782
781
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
783
782
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
784
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
783
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
785
784
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
785
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
787
786
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
579
579
 
580
580
  // src/lib/agent-adapter.ts
581
581
  import { spawn } from "child_process";
582
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
582
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
583
583
  "e",
584
584
  "exec",
585
585
  "agent",
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
628
628
  throw new Error(`${label}.agent is not supported for provider codex`);
629
629
  }
630
630
  if (provider === "codewith" && target.agent !== undefined) {
631
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
631
+ throw new Error(`${label}.agent is not supported for provider codewith`);
632
632
  }
633
633
  if (provider === "codewith") {
634
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
634
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
635
635
  if (unsafe) {
636
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
636
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
637
637
  }
638
638
  }
639
639
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
716
716
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
717
717
  if (target.variant)
718
718
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
719
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
719
+ const sandbox = codewithLikeSandbox(target);
720
+ if (sandbox === "workspace-write")
721
+ args.push("-c", "sandbox_workspace_write.network_access=true");
722
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
720
723
  if (target.cwd)
721
724
  args.push("--cd", target.cwd);
722
725
  for (const dir of target.addDirs ?? [])
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
724
727
  if (target.model)
725
728
  args.push("--model", target.model);
726
729
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start", "--json");
728
- if (target.cwd)
729
- args.push("--cwd", target.cwd);
730
- args.push(target.prompt);
731
- return { command: "codewith", args };
730
+ return { command: "codewith", args, stdin: target.prompt };
732
731
  }
733
732
  case "codex": {
734
733
  if (target.variant)
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
781
780
  var PROVIDER_ADAPTERS = {
782
781
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
783
782
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
784
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
783
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
785
784
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
785
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
787
786
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })