@rallycry/conveyor-agent 10.13.68 → 10.13.70

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.
@@ -9,14 +9,14 @@ import {
9
9
  readAgentVersion,
10
10
  redactToken,
11
11
  reportBootMilestone
12
- } from "./chunk-KG4ORL3Y.js";
12
+ } from "./chunk-QU53HND5.js";
13
13
  import {
14
14
  workbenchPort
15
15
  } from "./chunk-KMB3BU4S.js";
16
16
  import {
17
17
  startWorkbenchServer
18
- } from "./chunk-E2SHIH6Y.js";
19
- import "./chunk-LE6ZUDZT.js";
18
+ } from "./chunk-DOB2XE2I.js";
19
+ import "./chunk-GJXAAPJ6.js";
20
20
  import {
21
21
  DEFAULT_WORKBENCH_PORT
22
22
  } from "./chunk-6Q6LQBWO.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadPtySpawn,
3
3
  terminateProcessGroup
4
- } from "./chunk-LE6ZUDZT.js";
4
+ } from "./chunk-GJXAAPJ6.js";
5
5
  import {
6
6
  FrameReader,
7
7
  timingSafeTokenEqual,
@@ -7,7 +7,7 @@ import {
7
7
  readAgentVersion,
8
8
  registerBootMilestoneSocketFallback,
9
9
  reportBootMilestone
10
- } from "./chunk-KG4ORL3Y.js";
10
+ } from "./chunk-QU53HND5.js";
11
11
  import {
12
12
  LoopLagMonitor,
13
13
  buildConveyorSocketOptions,
@@ -55,7 +55,7 @@ import {
55
55
  transcriptSize,
56
56
  turnOptionsFrom,
57
57
  writeGitCredential
58
- } from "./chunk-LE6ZUDZT.js";
58
+ } from "./chunk-GJXAAPJ6.js";
59
59
 
60
60
  // src/setup/bootstrap.ts
61
61
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -1507,6 +1507,28 @@ async function verifyGitCredential(cwd) {
1507
1507
 
1508
1508
  // src/runner/git-utils.ts
1509
1509
  import { realpathSync } from "fs";
1510
+
1511
+ // src/runner/force-fresh-cooldown.ts
1512
+ var FORCE_FRESH_COOLDOWN_MS = 30 * 60 * 1e3;
1513
+ var blockedUntil = 0;
1514
+ function forceFreshCooldownRemainingMs() {
1515
+ return Math.max(0, blockedUntil - Date.now());
1516
+ }
1517
+ function forceFreshMintBlocked() {
1518
+ return forceFreshCooldownRemainingMs() > 0;
1519
+ }
1520
+ function recordForceFreshFailure() {
1521
+ blockedUntil = Date.now() + FORCE_FRESH_COOLDOWN_MS;
1522
+ }
1523
+ function clearForceFreshCooldown() {
1524
+ blockedUntil = 0;
1525
+ }
1526
+ function forceFreshCooldownNotice() {
1527
+ const minutes = Math.ceil(forceFreshCooldownRemainingMs() / 6e4);
1528
+ return `- the force-fresh retry was SKIPPED: one already failed against this pod, so it is on a ${Math.round(FORCE_FRESH_COOLDOWN_MS / 6e4)}-minute cooldown (${minutes} min left). Re-minting cannot fix a credential the pod cannot serve.`;
1529
+ }
1530
+
1531
+ // src/runner/git-utils.ts
1510
1532
  async function ensureOnTaskBranch(cwd, taskBranch, baseBranch) {
1511
1533
  if (!taskBranch) return true;
1512
1534
  try {
@@ -1591,6 +1613,18 @@ async function hasUnpushedCommits(cwd) {
1591
1613
  return false;
1592
1614
  }
1593
1615
  }
1616
+ async function remoteMatchesLocalHead(cwd, branch) {
1617
+ try {
1618
+ const [remote, local] = await Promise.all([
1619
+ git(cwd, ["ls-remote", "origin", `refs/heads/${branch}`], GIT_SLOW_TIMEOUT_MS),
1620
+ git(cwd, ["rev-parse", "HEAD"])
1621
+ ]);
1622
+ const remoteSha = remote.split(/\s+/)[0] ?? "";
1623
+ return /^[0-9a-f]{40}$/i.test(remoteSha) && remoteSha === local.trim();
1624
+ } catch {
1625
+ return false;
1626
+ }
1627
+ }
1594
1628
  async function stageAndCommit(cwd, message) {
1595
1629
  try {
1596
1630
  await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
@@ -1776,14 +1810,20 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
1776
1810
  } catch {
1777
1811
  }
1778
1812
  }
1779
- if (await tryPush(cwd, currentBranch, skipVerify)) return true;
1780
- if (refreshToken && await isAuthError(cwd)) {
1813
+ if (await tryPush(cwd, currentBranch, skipVerify)) {
1814
+ clearForceFreshCooldown();
1815
+ return true;
1816
+ }
1817
+ if (refreshToken && !forceFreshMintBlocked() && await isAuthError(cwd)) {
1781
1818
  const token = await refreshToken({ forceFresh: true });
1782
1819
  if (token) {
1783
1820
  await updateRemoteToken(cwd, token);
1784
1821
  process.env.GITHUB_TOKEN = token;
1785
1822
  process.env.GH_TOKEN = token;
1786
- return await tryPush(cwd, currentBranch, skipVerify);
1823
+ const pushed = await tryPush(cwd, currentBranch, skipVerify);
1824
+ if (pushed) clearForceFreshCooldown();
1825
+ else recordForceFreshFailure();
1826
+ return pushed;
1787
1827
  }
1788
1828
  }
1789
1829
  return false;
@@ -8315,7 +8355,7 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
8315
8355
 
8316
8356
  // src/execution/query-executor.ts
8317
8357
  import { createHash as createHash2 } from "crypto";
8318
- import { existsSync as existsSync2, readFileSync as readFileSync2, truncateSync } from "fs";
8358
+ import { existsSync as existsSync2, readFileSync as readFileSync3, truncateSync } from "fs";
8319
8359
 
8320
8360
  // src/execution/chat-instructions.ts
8321
8361
  function buildChatInstructions(context, scenario, newMessages) {
@@ -8584,7 +8624,7 @@ function baseDiffCommand(baseBranch, flags) {
8584
8624
  function gateFailureModes() {
8585
8625
  return [
8586
8626
  `Reading a gate result correctly is what keeps this to ONE pass:`,
8587
- `- Capture the exit code explicitly (\`<gate> > <log> 2>&1; echo "EXIT:$?" >> <log>\`) \u2014 piping a gate through \`tail\` masks it. That \`EXIT:\` line, plus turbo's final \`Tasks:\` count, is the authority on pass/fail.`,
8627
+ `- Capture AND propagate the exit code (\`<gate> > <log> 2>&1; ec=$?; echo "EXIT:$ec" >> <log>; (exit $ec)\`) \u2014 piping a gate through \`tail\` masks it, and a bare trailing \`echo "EXIT:$?"\` makes the wrapper exit 0 over a failed gate. That \`EXIT:\` line, plus turbo's final \`Tasks:\` count, is the authority on pass/fail.`,
8588
8628
  `- A green gate that printed no per-suite summary (no \`Test Files\`/\`Tests\` line) is still green: \`turbo.json\` sets \`outputLogs: "errors-only"\`, so a PASSING run prints nothing per suite. Do NOT re-run a clean-exit gate to "see the counts" \u2014 if you genuinely need them, run one package's script directly (\`bun run --cwd <pkg> test\`).`,
8589
8629
  `- Exit 143, or \`singleton: stopping running '<label>'\` on stderr, means a second gate you started evicted this one (on a pod the heavy gates share one lock). 143 is never a pass \u2014 but an evicted run never finished, so re-running it alone is still your one pass, not a repeat. Run one gate at a time; \`bun run check\` is not lock-wrapped, so it is safe alongside a test run.`,
8590
8630
  `- Confirm every package your diff touches actually appears in the run. \`--affected\` can select nothing for a package you changed; when that happens run just that package's suite directly, e.g. \`bun run --cwd apps/api test:unit <changed test files>\` \u2014 not the whole gate again.`,
@@ -8597,7 +8637,7 @@ function gateWaitProtocol() {
8597
8637
  `- Launch it ONCE with \`run_in_background: true\`, then END YOUR TURN. The completion notification re-invokes you. Do not read the output file, \`tail\`/\`wc\`/\`pgrep\` it, or start a second watcher on a log another run already owns \u2014 a quiet gate is still running.`,
8598
8638
  `- Never wrap a gate in a foreground \`timeout\`: killing your own run at 590s produces exit 124/143 and ZERO information, and you then have to run it again unbounded. \`sleep N; <cmd>\` is hard-blocked for the same reason.`,
8599
8639
  `- Never relaunch a command you just killed without changing something. State cwd explicitly in the command (\`cd /workspaces/repo && \u2026\`, \`--root\`, \`git -C\`) rather than trusting the shell's working directory.`,
8600
- `- The completion notification reports the WRAPPER's exit code, not the gate's. A notification saying "exit code 0" over a log containing \`EXIT:1\` is a failed gate. The \`EXIT:\` line in the log is the authority.`,
8640
+ `- The completion notification reports the WRAPPER's exit code. With the propagating idiom above, \`(exit $ec)\` makes that the gate's own code; a wrapper that ends on a bare \`echo\` reports 0 no matter what failed. Whenever the notification and the log disagree, the \`EXIT:\` line in the log is the authority.`,
8601
8641
  `- Scope heavy gates to what you changed (\`--filter=<pkg>\`). An unscoped \`check:affected\` can pull a large web typecheck into scope for a diff touching no web files and get OOM-killed (exit 137) after minutes, where the scoped run takes seconds.`,
8602
8642
  `- Merge the base BEFORE the gate pass, never after (see the Pre-PR Protocol). If that merge touched \`package.json\`/\`bun.lock\`, run \`bun install\` before starting the gate \u2014 a changed lockfile makes gates fail for reasons unrelated to your diff.`
8603
8643
  ];
@@ -9134,6 +9174,10 @@ function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
9134
9174
  return parts;
9135
9175
  }
9136
9176
 
9177
+ // src/execution/system-prompt.ts
9178
+ import { readFileSync } from "fs";
9179
+ import { join as join11 } from "path";
9180
+
9137
9181
  // src/execution/mode-prompt.ts
9138
9182
  var SP_DESC_MAX_CHARS = 80;
9139
9183
  function truncateDescription(desc, maxChars) {
@@ -9605,6 +9649,14 @@ function buildReviewPrompt(context) {
9605
9649
  }
9606
9650
 
9607
9651
  // src/execution/system-prompt.ts
9652
+ function repoHasScript(workspaceDir, script) {
9653
+ try {
9654
+ const pkg = JSON.parse(readFileSync(join11(workspaceDir, "package.json"), "utf8"));
9655
+ return typeof pkg.scripts?.[script] === "string";
9656
+ } catch {
9657
+ return false;
9658
+ }
9659
+ }
9608
9660
  function formatProjectAgentLine(agent) {
9609
9661
  const role = agent.role ? `role: ${agent.role}` : "role: unassigned";
9610
9662
  const sp = agent.storyPoints === null || agent.storyPoints === void 0 ? "" : `, story points: ${agent.storyPoints}`;
@@ -9672,27 +9724,34 @@ Workflow:`,
9672
9724
  `- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
9673
9725
  ].filter(Boolean);
9674
9726
  }
9675
- function buildTaskAgentPreamble(context) {
9727
+ function buildTaskAgentPreamble(context, workspaceDir) {
9728
+ const managedStack = repoHasScript(workspaceDir, "web:rebuild");
9729
+ const stackLines = managedStack ? [
9730
+ `- The web app is served on port 3050, the API on port 7090.`,
9731
+ `- Web is served from a production build, not \`next dev\` \u2014 your edits do NOT hot-reload. Run \`bun run web:rebuild\` (rebuild + restart lands in ~2s) before re-testing a UI change. The API hot-reloads on its own.`
9732
+ ] : [
9733
+ `- Ports, running services, and rebuild commands differ per repo, so this brief asserts none of them. Whether an app is already running depends on the Start command this project configured. Read this repo's CLAUDE.md and its rules files for those facts, check what is actually listening before you use a port, and start or rebuild the app with the repo's own scripts.`
9734
+ ];
9676
9735
  return [
9677
9736
  `You are an AI agent working on a task for the "${context.title}" project.`,
9678
9737
  `You are running inside a Claudespace pod (a Kubernetes container, NOT a GitHub Codespace) with full access to the repository.`,
9679
9738
  `
9680
- Environment \u2014 already built and running. These are the facts you would otherwise spend calls discovering:`,
9681
- `- The repo is cloned at your working directory with \`${context.githubBranch}\` checked out, dependencies installed, database migrated, git configured, and the dev stack up. Commit and push directly to this branch.`,
9682
- `- The web app is served on port 3050, the API on port 7090.`,
9683
- `- Web is served from a production build, not \`next dev\` \u2014 your edits do NOT hot-reload. Run \`bun run web:rebuild\` (rebuild + restart lands in ~2s) before re-testing a UI change. The API hot-reloads on its own.`,
9739
+ Environment \u2014 already built${managedStack ? " and running" : ""}. These are the facts you would otherwise spend calls discovering:`,
9740
+ `- The repo is cloned at your working directory with \`${context.githubBranch}\` checked out, dependencies installed, database migrated, git configured${managedStack ? ", and the dev stack up" : ""}. Commit and push directly to this branch.`,
9741
+ ...stackLines,
9684
9742
  `- Browser automation is the Playwright CLI (\`playwright\`, pinned 1.62.1), NOT an MCP server \u2014 there are no \`mcp__playwright__*\` tools here. Only the headless shell is baked, so a launch must name it AND disable the sandbox: \`chromium.launch({ channel: "chromium-headless-shell", args: ["--no-sandbox"] })\`. A bare \`chromium.launch()\` FAILS: since playwright 1.49 that resolves to the full browser, which is deliberately not installed (pods have no display and cannot run Chromium's sandbox). Screenshots land wherever you write them \u2014 move or delete them before committing.`,
9685
9743
  `- The clone is \`--single-branch\`, so a bare \`git fetch origin <branch>\` does NOT create \`origin/<branch>\`. To reference any other branch, use the explicit refspec: \`git fetch origin <branch>:refs/remotes/origin/<branch>\`.`,
9686
9744
  `- The shell cwd resets between Bash calls, and so does every shell variable. A var you export in one call is EMPTY in the next, which silently redirects output to \`/\` and loses it. Write literal absolute paths (\`git -C\`, \`bun run --cwd\`), and \`mkdir -p\` a directory in the SAME call as the redirect that writes to it.`,
9687
9745
  `- The \`gh\` CLI is available for READ-ONLY PR and CI state (\`gh pr view\`, \`gh pr checks\`, \`gh pr diff\`). Use the mcp__conveyor__* tools for anything that mutates a PR or card.`,
9688
9746
  `- The core mcp__conveyor__* tools are preloaded \u2014 call them directly; do NOT spend a ToolSearch call on them. For any tool that IS still deferred (schema not loaded), load ALL the schemas you expect to need in ONE ToolSearch call using fully-qualified names (query "select:Monitor,mcp__conveyor__<name>" \u2014 bare MCP tool names without the mcp__<server>__ prefix do not match); never guess a deferred tool's parameters.`,
9689
- `Because the environment is already up, do not run installs, builds, database setup, dev-server starts, or exploratory \`pwd\`/\`ls\` probes to confirm any of the above. Run them only when a specific error demands it.`,
9747
+ managedStack ? `Because the environment is already up, do not run installs, builds, database setup, dev-server starts, or exploratory \`pwd\`/\`ls\` probes to confirm any of the above. Run them only when a specific error demands it.` : `Do not run installs, database setup, or exploratory \`pwd\`/\`ls\` probes to confirm any of the above \u2014 they are done. If the task needs a running app, check whether one is already up before you start it, and use the repo's own scripts.`,
9690
9748
  `
9691
9749
  Working rules:`,
9692
9750
  `- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
9693
9751
  `- To learn what calls a symbol or where it lives, query the prebuilt code graph before grepping: \`graphify query "<SymbolName>"\` from the repo root. Query a SYMBOL, never a sentence \u2014 \`graphify query "resolveTaskBaseBranch"\` returns the definition plus every call site, while "how does a task get its base branch" seeds unrelated start nodes and returns test files and loggers. Don't know the symbol yet? Grep for the name first, then query it: grep finds names, the graph finds relationships. \`No matching nodes found\` means "not in this graph" (it is prebuilt, so very recent code is absent), NOT "not in the codebase" \u2014 fall back to \`git grep\`. Skip all of this if \`graphify-out/graph.json\` is not present.`,
9694
9752
  `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
9695
9753
  `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and the workspace stays awake for as long as background work is outstanding, so a backgrounded gate will not be killed by an idle sleep. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth \u2014 it survives a pod resume, which a background job does not. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
9754
+ `- Ending your turn with NO tool call is the correct way to wait, and it is safe: the pod stays alive and the next notification re-invokes you. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn. Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 an external CI run, a deploy, a remote queue \u2014 never as insurance against a background job's own notification, which does fire.`,
9696
9755
  `
9697
9756
  Git:`,
9698
9757
  `- Stay on \`${context.githubBranch}\` for the whole task: do not check out another branch and do not create one. It was cut from \`${context.baseBranch}\`, and PRs target that automatically.`,
@@ -9706,7 +9765,7 @@ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
9706
9765
  if (isPackRunner) {
9707
9766
  return buildPackRunnerSystemPrompt(context, config, setupLog);
9708
9767
  }
9709
- const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context);
9768
+ const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context, config.workspaceDir);
9710
9769
  if (setupLog.length > 0) {
9711
9770
  parts.push(
9712
9771
  `
@@ -10898,7 +10957,7 @@ var getAttachmentContract = defineToolContract({
10898
10957
  });
10899
10958
  var attachmentTags = f.optional(
10900
10959
  f.array(f.string(), {
10901
- desc: `Glossary tag names this file is a relevant example of, e.g. a screenshot of the tag page tagged "tag". Use it when the file shows a tagged entity in a particular state: the tag's page lists its recent tagged attachments, so a reader can see what the entity looks like across the app and spot visual changes over time. Names are matched case-insensitively within the project; a name that matches no tag is reported back and never fails the upload. Max 5.`
10960
+ desc: `Glossary tag names this file is a relevant example of. MUST be a JSON array of quoted strings, e.g. ["ops-hub", "platform-support"] \u2014 bare unquoted words are invalid JSON and fail the whole call before it is parsed (21 fleet calls in one week died this way). Use it when the file shows a tagged entity in a particular state: the tag's page lists its recent tagged attachments, so a reader can see what the entity looks like across the app and spot visual changes over time. Names are matched case-insensitively within the project; a name that matches no tag is reported back and never fails the upload. Max 5.`
10902
10961
  })
10903
10962
  );
10904
10963
  var uploadAttachmentContract = defineToolContract({
@@ -11287,10 +11346,18 @@ import { z as z13 } from "zod";
11287
11346
  // src/runner/refresh-verify-heal.ts
11288
11347
  async function refreshAndVerifyGithubCredential(cwd, mint) {
11289
11348
  const first = await mintWriteProbe(cwd, mint, false);
11290
- if (first.probe.ok) return first;
11349
+ if (first.probe.ok) {
11350
+ clearForceFreshCooldown();
11351
+ return first;
11352
+ }
11291
11353
  if (first.probe.outcome !== "denied") return first;
11292
11354
  if (!first.written) return first;
11355
+ if (forceFreshMintBlocked()) {
11356
+ return { ...first, failures: [...first.failures, forceFreshCooldownNotice()] };
11357
+ }
11293
11358
  const healed = await mintWriteProbe(cwd, mint, true);
11359
+ if (healed.probe.ok) clearForceFreshCooldown();
11360
+ else if (healed.probe.outcome === "denied") recordForceFreshFailure();
11294
11361
  return { ...healed, healAttempted: true };
11295
11362
  }
11296
11363
  async function mintWriteProbe(cwd, mint, forceFresh) {
@@ -11442,6 +11509,35 @@ function buildForceUpdateTaskStatusTool(connection) {
11442
11509
  }
11443
11510
  );
11444
11511
  }
11512
+ async function ensureHeadPushed(connection, cwd, headBranch, skipVerify) {
11513
+ if (!await hasUnpushedCommits(cwd)) return null;
11514
+ const pushSuccess = await pushToOrigin(
11515
+ cwd,
11516
+ async () => {
11517
+ try {
11518
+ const result = await connection.call("refreshGithubToken", {
11519
+ sessionId: connection.sessionId
11520
+ });
11521
+ return result.token;
11522
+ } catch {
11523
+ return void 0;
11524
+ }
11525
+ },
11526
+ skipVerify
11527
+ );
11528
+ if (pushSuccess) {
11529
+ connection.sendEvent({ type: "message", content: "Auto-pushed committed changes to origin" });
11530
+ return null;
11531
+ }
11532
+ if (await remoteMatchesLocalHead(cwd, headBranch)) {
11533
+ connection.sendEvent({
11534
+ type: "message",
11535
+ content: "Push reported an error but origin already has this HEAD \u2014 continuing"
11536
+ });
11537
+ return null;
11538
+ }
11539
+ return `Failed to push changes to origin, and origin does not have this HEAD. Verify before treating this as fatal: \`git ls-remote origin ${headBranch}\` \u2014 if the tip equals your HEAD the push actually landed and you can retry this tool. Otherwise refresh the credential (refresh_github_token) or push manually, then retry.`;
11540
+ }
11445
11541
  function buildCreatePullRequestTool(connection, config) {
11446
11542
  return defineContractTool(
11447
11543
  createPullRequestContract,
@@ -11470,32 +11566,8 @@ Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
11470
11566
  );
11471
11567
  }
11472
11568
  }
11473
- if (await hasUnpushedCommits(cwd)) {
11474
- const pushSuccess = await pushToOrigin(
11475
- cwd,
11476
- async () => {
11477
- try {
11478
- const result2 = await connection.call("refreshGithubToken", {
11479
- sessionId: connection.sessionId
11480
- });
11481
- return result2.token;
11482
- } catch {
11483
- return void 0;
11484
- }
11485
- },
11486
- skipVerify ?? true
11487
- );
11488
- if (pushSuccess) {
11489
- connection.sendEvent({
11490
- type: "message",
11491
- content: "Auto-pushed committed changes to origin"
11492
- });
11493
- } else {
11494
- return textResult(
11495
- "Failed to push changes to origin. Please check git status and push manually before creating PR."
11496
- );
11497
- }
11498
- }
11569
+ const pushError = await ensureHeadPushed(connection, cwd, headBranch, skipVerify ?? true);
11570
+ if (pushError) return textResult(pushError);
11499
11571
  const result = await connection.call("createPullRequest", {
11500
11572
  sessionId: connection.sessionId,
11501
11573
  title,
@@ -11520,10 +11592,10 @@ ${result.glossaryNote}` : "";
11520
11592
  `Failed to create pull request: ${msg}
11521
11593
 
11522
11594
  Troubleshooting:
11595
+ - FIRST verify the failure is real: \`gh pr list --head <branch>\` \u2014 if a PR exists, it opened and this error is stale; do not re-create, bundle, or post a blocked escalation
11523
11596
  - Ensure all changes are committed and pushed to the remote branch
11524
11597
  - Check that the branch exists on the remote (run: git push -u origin HEAD)
11525
- - Verify there isn't already an open PR for this branch
11526
- - If git auth fails, the token may have expired \u2014 retry the operation`
11598
+ - If git auth fails, the token may have expired \u2014 refresh_github_token once, then retry`
11527
11599
  );
11528
11600
  }
11529
11601
  }
@@ -11749,7 +11821,7 @@ function buildMutationTools(connection, config) {
11749
11821
  }
11750
11822
 
11751
11823
  // src/tools/attachment-tools.ts
11752
- import { basename, extname, isAbsolute, join as join11 } from "path";
11824
+ import { basename, extname, isAbsolute, join as join12 } from "path";
11753
11825
  var MIME_BY_EXT = {
11754
11826
  ".png": "image/png",
11755
11827
  ".jpg": "image/jpeg",
@@ -11802,7 +11874,7 @@ ${snippet}`;
11802
11874
  function buildUploadAttachmentTool(connection, config) {
11803
11875
  return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
11804
11876
  try {
11805
- const filePath = isAbsolute(path2) ? path2 : join11(config.workspaceDir, path2);
11877
+ const filePath = isAbsolute(path2) ? path2 : join12(config.workspaceDir, path2);
11806
11878
  const mimeType = inferMimeType(filePath);
11807
11879
  const info = await statWorkspacePath(filePath);
11808
11880
  if (!info.isFile) {
@@ -12342,7 +12414,7 @@ import { z as z16 } from "zod";
12342
12414
 
12343
12415
  // src/execution/context-path-verifier.ts
12344
12416
  import { readFile as readFile2 } from "fs/promises";
12345
- import { isAbsolute as isAbsolute2, join as join12, normalize } from "path";
12417
+ import { isAbsolute as isAbsolute2, join as join13, normalize } from "path";
12346
12418
  var PROBLEM_TEXT = {
12347
12419
  not_found: "does not exist in the repo",
12348
12420
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -12387,7 +12459,7 @@ async function verifyContextPaths(links, workspaceDir) {
12387
12459
  problems.push({ type: link.type, path: link.path, reason: shape });
12388
12460
  continue;
12389
12461
  }
12390
- const absolutePath = join12(workspaceDir, toRelativePath(link.path));
12462
+ const absolutePath = join13(workspaceDir, toRelativePath(link.path));
12391
12463
  const stat = await statWorkspacePath(absolutePath);
12392
12464
  const wantsDirectory = expectsDirectory(link.type);
12393
12465
  if (!stat.exists) {
@@ -12947,17 +13019,26 @@ var ReviewGuideToolSchema = z18.strictObject({
12947
13019
  "REQUIRED top-level array (never text appended to overview) of ordered conceptual sections."
12948
13020
  )
12949
13021
  });
12950
- var FLATTENED_SECTIONS_PATTERN = /<\/overview>\s*<parameter name="sections">/;
13022
+ var FLATTENED_SECTIONS_PATTERN = /<\/overview>\s*(?:<parameter name="sections">|<sections>)/;
13023
+ var TRAILING_TAGS_PATTERN = /(?:\s*<\/(?:parameter|sections|invoke)>)+\s*$/;
13024
+ function sliceOutermostArray(tail) {
13025
+ const start = tail.indexOf("[");
13026
+ const end = tail.lastIndexOf("]");
13027
+ return start >= 0 && end > start ? tail.slice(start, end + 1) : null;
13028
+ }
12951
13029
  function recoverFlattenedGuide(overview) {
12952
13030
  const match = FLATTENED_SECTIONS_PATTERN.exec(overview);
12953
13031
  if (!match) return null;
12954
- const head = overview.slice(0, match.index);
12955
- const tail = overview.slice(match.index + match[0].length).replace(/<\/parameter>\s*$/, "").trim();
12956
- try {
12957
- return { overview: head.trim(), sections: JSON.parse(tail) };
12958
- } catch {
12959
- return null;
13032
+ const head = overview.slice(0, match.index).trim();
13033
+ const tail = overview.slice(match.index + match[0].length).replace(TRAILING_TAGS_PATTERN, "").trim();
13034
+ for (const candidate of [tail, sliceOutermostArray(tail)]) {
13035
+ if (!candidate) continue;
13036
+ try {
13037
+ return { overview: head, sections: JSON.parse(candidate) };
13038
+ } catch {
13039
+ }
12960
13040
  }
13041
+ return null;
12961
13042
  }
12962
13043
  async function resolveGitHeadSha(cwd) {
12963
13044
  try {
@@ -12984,7 +13065,7 @@ function buildPublishReviewGuideTool(connection, options = {}) {
12984
13065
  const recovered = recoverFlattenedGuide(overview);
12985
13066
  if (!recovered) {
12986
13067
  throw new Error(
12987
- "publish_review_guide requires a top-level `sections` array \u2014 an ordered list of conceptual sections, each with title, explanation, and files[]. Do not append the sections to `overview` as text. Retry with sections as a real array argument."
13068
+ "publish_review_guide requires a top-level `sections` array \u2014 an ordered list of conceptual sections, each with title, explanation, and files[]. Your call arrived with only `reviewedSha` and `overview`; the `sections` parameter never reached the server, which usually means the call encoding flattened it into `overview`. Do not append the sections to `overview` as text. Retry with sections as a real array argument, and shorten `overview` if the failure repeats."
12988
13069
  );
12989
13070
  }
12990
13071
  resolvedOverview = recovered.overview;
@@ -13710,7 +13791,7 @@ function collectMissingProps(taskProps) {
13710
13791
  }
13711
13792
 
13712
13793
  // src/runner/heavy-gate.ts
13713
- import { readFileSync } from "fs";
13794
+ import { readFileSync as readFileSync2 } from "fs";
13714
13795
  import path from "path";
13715
13796
  var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
13716
13797
  function runDir() {
@@ -13727,7 +13808,7 @@ function pidAlive(pid) {
13727
13808
  function isHeavyGateActive() {
13728
13809
  for (const key of GATE_KEYS) {
13729
13810
  try {
13730
- const raw = readFileSync(path.join(runDir(), `${key}.pid`), "utf8").trim();
13811
+ const raw = readFileSync2(path.join(runDir(), `${key}.pid`), "utf8").trim();
13731
13812
  const pid = Number.parseInt(raw, 10);
13732
13813
  if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
13733
13814
  } catch {
@@ -13740,6 +13821,18 @@ function isHeavyGateActive() {
13740
13821
  var REPEAT_INTERRUPT_THRESHOLD = 4;
13741
13822
  var REPEAT_INTERRUPT_INTERVAL = 4;
13742
13823
  var REPEAT_FORCE_STOP_THRESHOLD = 12;
13824
+ var NOOP_ALLOWANCE = 2;
13825
+ var NOOP_FORCE_STOP_THRESHOLD = 30;
13826
+ var NOOP_FINGERPRINT = "Bash:NOOP_KEEPALIVE";
13827
+ function isNoOpKeepAlive(command) {
13828
+ const cmd = command.trim().replace(/\s+/g, " ");
13829
+ if (/^(true|:)$/.test(cmd)) return true;
13830
+ const constantEcho = String.raw`echo( -[neE]+)?( ["']?[A-Za-z0-9 ._,:-]{0,60}["']?)?`;
13831
+ if (new RegExp(`^${constantEcho}$`).test(cmd)) return true;
13832
+ if (new RegExp(`^sleep \\d+ ?(?:(?:;|&&) ?${constantEcho})?$`).test(cmd)) return true;
13833
+ if (/^date( \+\S+)?$/.test(cmd)) return true;
13834
+ return false;
13835
+ }
13743
13836
  var UNCOUNTED_TOOLS = /* @__PURE__ */ new Set(["ExitPlanMode", "AskUserQuestion"]);
13744
13837
  function stableStringify(value) {
13745
13838
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
@@ -13751,7 +13844,9 @@ function fingerprintToolCall(toolName, input) {
13751
13844
  if (!toolName || UNCOUNTED_TOOLS.has(toolName)) return null;
13752
13845
  if (toolName === "Bash") {
13753
13846
  const command = String(input.command ?? "").trim().replace(/\s+/g, " ");
13754
- return command ? `Bash:${command}` : null;
13847
+ if (!command) return null;
13848
+ if (isNoOpKeepAlive(command)) return NOOP_FINGERPRINT;
13849
+ return `Bash:${command}`;
13755
13850
  }
13756
13851
  return `${toolName}:${stableStringify(input)}`;
13757
13852
  }
@@ -13760,10 +13855,16 @@ var ToolLoopTracker = class {
13760
13855
  streak = 0;
13761
13856
  /** True once this streak has been reported to chat — one message per streak. */
13762
13857
  reported = false;
13858
+ /** Session-cumulative no-op keep-alive count (never reset by other calls). */
13859
+ noOps = 0;
13763
13860
  /** Repeats of the current fingerprint, including the call being recorded. */
13764
13861
  get repeatCount() {
13765
13862
  return this.streak;
13766
13863
  }
13864
+ /** Total no-op keep-alive calls recorded this session. */
13865
+ get noOpCount() {
13866
+ return this.noOps;
13867
+ }
13767
13868
  /** True when the current streak has already been posted to chat. */
13768
13869
  get alreadyReported() {
13769
13870
  return this.reported;
@@ -13774,6 +13875,14 @@ var ToolLoopTracker = class {
13774
13875
  }
13775
13876
  /** Record one call and decide what to do about it. */
13776
13877
  record(fingerprint) {
13878
+ if (fingerprint === NOOP_FINGERPRINT) {
13879
+ this.noOps++;
13880
+ this.fingerprint = fingerprint;
13881
+ this.streak = 1;
13882
+ if (this.noOps >= NOOP_FORCE_STOP_THRESHOLD) return "force_stop";
13883
+ if (this.noOps > NOOP_ALLOWANCE) return "interrupt";
13884
+ return "ok";
13885
+ }
13777
13886
  if (fingerprint !== this.fingerprint) {
13778
13887
  this.fingerprint = fingerprint;
13779
13888
  this.streak = 1;
@@ -13793,6 +13902,9 @@ function buildRepeatLoopMessage(repeatCount, heavyGateActive) {
13793
13902
  const advice = heavyGateActive ? `A build gate (test/typecheck/build) is running on this pod right now. Do NOT poll its log. End your turn \u2014 the completion notification re-invokes you when the gate finishes.` : `If you are waiting on a background job, end your turn instead of polling; the completion notification re-invokes you. If you are stuck, change your approach: read a different file, run a different command, or post to chat and ask the team.`;
13794
13903
  return `${head} ${advice} Call a different tool now.`;
13795
13904
  }
13905
+ function buildNoOpKeepAliveMessage(noOpCount) {
13906
+ return `Conveyor blocked this call: it is a no-op keep-alive (${noOpCount} this session). You do not need to emit tool calls to stay alive. Ending your turn with NO tool call is safe and expected while waiting: the pod stays up and the completion notification re-invokes you. If nothing is running, do real work or end the turn. For a long wait with no notification source, use Monitor or ScheduleWakeup instead of filler commands.`;
13907
+ }
13796
13908
  function buildRepeatLoopChatMessage(repeatCount, forceStopped) {
13797
13909
  if (forceStopped) {
13798
13910
  return `Agent force-stopped after repeating the same tool call ${repeatCount} times in a row. It appears stuck \u2014 send a message to resume.`;
@@ -14042,6 +14154,20 @@ function checkToolLoop(host, toolName, input) {
14042
14154
  const tracker = host.toolLoop ??= new ToolLoopTracker();
14043
14155
  const verdict = tracker.record(fingerprint);
14044
14156
  if (verdict === "ok") return null;
14157
+ if (fingerprint === NOOP_FINGERPRINT) {
14158
+ const noOps = tracker.noOpCount;
14159
+ if (verdict === "force_stop") {
14160
+ host.connection.postChatMessage(
14161
+ `Agent force-stopped after ${noOps} no-op keep-alive commands. It appears to be spinning instead of waiting \u2014 send a message to resume.`
14162
+ );
14163
+ host.requestStop();
14164
+ return {
14165
+ behavior: "deny",
14166
+ message: `Stopped after ${noOps} no-op keep-alive calls this session.`
14167
+ };
14168
+ }
14169
+ return { behavior: "deny", message: buildNoOpKeepAliveMessage(noOps) };
14170
+ }
14045
14171
  const repeats = tracker.repeatCount;
14046
14172
  if (verdict === "force_stop") {
14047
14173
  host.connection.postChatMessage(buildRepeatLoopChatMessage(repeats, true));
@@ -14184,7 +14310,7 @@ function resolveSessionStart(lineageKey, cwd) {
14184
14310
  function repairTornSessionFile(path2) {
14185
14311
  try {
14186
14312
  if (!existsSync2(path2)) return false;
14187
- const content = readFileSync2(path2, "utf8");
14313
+ const content = readFileSync3(path2, "utf8");
14188
14314
  if (content.length === 0) return false;
14189
14315
  let keepEnd = content.length;
14190
14316
  if (!content.endsWith("\n")) {
@@ -17048,12 +17174,12 @@ ${outcome.failures.join("\n")}
17048
17174
  };
17049
17175
 
17050
17176
  // src/setup/config.ts
17051
- import { join as join13 } from "path";
17177
+ import { join as join14 } from "path";
17052
17178
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
17053
17179
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
17054
17180
  async function loadForwardPorts(workspaceDir) {
17055
17181
  try {
17056
- const raw = await readWorkspaceFile(join13(workspaceDir, DEVCONTAINER_PATH));
17182
+ const raw = await readWorkspaceFile(join14(workspaceDir, DEVCONTAINER_PATH));
17057
17183
  const parsed = JSON.parse(raw);
17058
17184
  const ports = (parsed.forwardPorts ?? []).filter(
17059
17185
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -86,7 +86,7 @@ import {
86
86
  import { homedir } from "os";
87
87
  import { dirname, join } from "path";
88
88
  function credentialDir(cwd) {
89
- return join(dirname(cwd), ".conveyor-git-credentials");
89
+ return process.env.CONVEYOR_GIT_CREDENTIAL_DIR || join(dirname(cwd), ".conveyor-git-credentials");
90
90
  }
91
91
  function credentialKey(cwd) {
92
92
  return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
@@ -2,7 +2,7 @@ import {
2
2
  gitCredentialHelper,
3
3
  syncGithubTokenFiles,
4
4
  writeGitCredential
5
- } from "./chunk-LE6ZUDZT.js";
5
+ } from "./chunk-GJXAAPJ6.js";
6
6
 
7
7
  // src/runner/session-runner-helpers.ts
8
8
  import { readFileSync } from "fs";
package/dist/cli.js CHANGED
@@ -35,10 +35,10 @@ import {
35
35
  sampleKeyUsage,
36
36
  statWorkspacePath,
37
37
  workspacePathExists
38
- } from "./chunk-E3QHGYZ2.js";
38
+ } from "./chunk-E6WWH4WJ.js";
39
39
  import {
40
40
  reportBootMilestone
41
- } from "./chunk-KG4ORL3Y.js";
41
+ } from "./chunk-QU53HND5.js";
42
42
  import "./chunk-IA45XHOA.js";
43
43
  import {
44
44
  getWorkbenchClient
@@ -53,7 +53,7 @@ import {
53
53
  runStartCommand,
54
54
  sessionTempBase,
55
55
  terminateProcessGroup
56
- } from "./chunk-LE6ZUDZT.js";
56
+ } from "./chunk-GJXAAPJ6.js";
57
57
  import "./chunk-6Q6LQBWO.js";
58
58
 
59
59
  // src/cli.ts
@@ -1715,7 +1715,7 @@ function hostsSpawnedChildren(mode) {
1715
1715
 
1716
1716
  // src/cli.ts
1717
1717
  if (process.argv[2] === "boot") {
1718
- const { runBoot } = await import("./boot-UUPTBQ6R.js");
1718
+ const { runBoot } = await import("./boot-7UWVK777.js");
1719
1719
  process.exit(await runBoot(process.argv.slice(3)));
1720
1720
  }
1721
1721
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1817,7 +1817,7 @@ process.on("unhandledRejection", (reason) => {
1817
1817
  process.exit(1);
1818
1818
  });
1819
1819
  if (process.env.CONVEYOR_MODE === "workbench") {
1820
- const { startWorkbenchServer } = await import("./server-US2DDQSW.js");
1820
+ const { startWorkbenchServer } = await import("./server-CC7KUJOK.js");
1821
1821
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1822
1822
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1823
1823
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
package/dist/index.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  unshallowRepo,
14
14
  updateRemoteToken,
15
15
  workspacePathExists
16
- } from "./chunk-E3QHGYZ2.js";
17
- import "./chunk-KG4ORL3Y.js";
16
+ } from "./chunk-E6WWH4WJ.js";
17
+ import "./chunk-QU53HND5.js";
18
18
  import "./chunk-IA45XHOA.js";
19
19
  import {
20
20
  getWorkbenchClient
@@ -26,7 +26,7 @@ import {
26
26
  runAuthTokenCommand,
27
27
  runSetupCommand,
28
28
  runStartCommand
29
- } from "./chunk-LE6ZUDZT.js";
29
+ } from "./chunk-GJXAAPJ6.js";
30
30
  import "./chunk-6Q6LQBWO.js";
31
31
 
32
32
  // src/runner/worktree.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-E2SHIH6Y.js";
4
- import "./chunk-LE6ZUDZT.js";
3
+ } from "./chunk-DOB2XE2I.js";
4
+ import "./chunk-GJXAAPJ6.js";
5
5
  import "./chunk-6Q6LQBWO.js";
6
6
  import "./chunk-6W6UZ4SJ.js";
7
7
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "10.13.68",
3
+ "version": "10.13.70",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",