@rallycry/conveyor-agent 10.13.69 → 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.
- package/dist/{chunk-2KZV6I5Z.js → chunk-E6WWH4WJ.js} +147 -57
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -1613,6 +1613,18 @@ async function hasUnpushedCommits(cwd) {
|
|
|
1613
1613
|
return false;
|
|
1614
1614
|
}
|
|
1615
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
|
+
}
|
|
1616
1628
|
async function stageAndCommit(cwd, message) {
|
|
1617
1629
|
try {
|
|
1618
1630
|
await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
|
|
@@ -8343,7 +8355,7 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
|
|
|
8343
8355
|
|
|
8344
8356
|
// src/execution/query-executor.ts
|
|
8345
8357
|
import { createHash as createHash2 } from "crypto";
|
|
8346
|
-
import { existsSync as existsSync2, readFileSync as
|
|
8358
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, truncateSync } from "fs";
|
|
8347
8359
|
|
|
8348
8360
|
// src/execution/chat-instructions.ts
|
|
8349
8361
|
function buildChatInstructions(context, scenario, newMessages) {
|
|
@@ -8612,7 +8624,7 @@ function baseDiffCommand(baseBranch, flags) {
|
|
|
8612
8624
|
function gateFailureModes() {
|
|
8613
8625
|
return [
|
|
8614
8626
|
`Reading a gate result correctly is what keeps this to ONE pass:`,
|
|
8615
|
-
`- Capture the exit code
|
|
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.`,
|
|
8616
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\`).`,
|
|
8617
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.`,
|
|
8618
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.`,
|
|
@@ -8625,7 +8637,7 @@ function gateWaitProtocol() {
|
|
|
8625
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.`,
|
|
8626
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.`,
|
|
8627
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.`,
|
|
8628
|
-
`- The completion notification reports the WRAPPER's exit code,
|
|
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.`,
|
|
8629
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.`,
|
|
8630
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.`
|
|
8631
8643
|
];
|
|
@@ -9162,6 +9174,10 @@ function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
|
|
|
9162
9174
|
return parts;
|
|
9163
9175
|
}
|
|
9164
9176
|
|
|
9177
|
+
// src/execution/system-prompt.ts
|
|
9178
|
+
import { readFileSync } from "fs";
|
|
9179
|
+
import { join as join11 } from "path";
|
|
9180
|
+
|
|
9165
9181
|
// src/execution/mode-prompt.ts
|
|
9166
9182
|
var SP_DESC_MAX_CHARS = 80;
|
|
9167
9183
|
function truncateDescription(desc, maxChars) {
|
|
@@ -9633,6 +9649,14 @@ function buildReviewPrompt(context) {
|
|
|
9633
9649
|
}
|
|
9634
9650
|
|
|
9635
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
|
+
}
|
|
9636
9660
|
function formatProjectAgentLine(agent) {
|
|
9637
9661
|
const role = agent.role ? `role: ${agent.role}` : "role: unassigned";
|
|
9638
9662
|
const sp = agent.storyPoints === null || agent.storyPoints === void 0 ? "" : `, story points: ${agent.storyPoints}`;
|
|
@@ -9700,27 +9724,34 @@ Workflow:`,
|
|
|
9700
9724
|
`- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
|
|
9701
9725
|
].filter(Boolean);
|
|
9702
9726
|
}
|
|
9703
|
-
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
|
+
];
|
|
9704
9735
|
return [
|
|
9705
9736
|
`You are an AI agent working on a task for the "${context.title}" project.`,
|
|
9706
9737
|
`You are running inside a Claudespace pod (a Kubernetes container, NOT a GitHub Codespace) with full access to the repository.`,
|
|
9707
9738
|
`
|
|
9708
|
-
Environment \u2014 already built and running. These are the facts you would otherwise spend calls discovering:`,
|
|
9709
|
-
`- 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.`,
|
|
9710
|
-
|
|
9711
|
-
`- 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,
|
|
9712
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.`,
|
|
9713
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>\`.`,
|
|
9714
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.`,
|
|
9715
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.`,
|
|
9716
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.`,
|
|
9717
|
-
`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.`,
|
|
9718
9748
|
`
|
|
9719
9749
|
Working rules:`,
|
|
9720
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.`,
|
|
9721
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.`,
|
|
9722
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.`,
|
|
9723
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.`,
|
|
9724
9755
|
`
|
|
9725
9756
|
Git:`,
|
|
9726
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.`,
|
|
@@ -9734,7 +9765,7 @@ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
|
|
|
9734
9765
|
if (isPackRunner) {
|
|
9735
9766
|
return buildPackRunnerSystemPrompt(context, config, setupLog);
|
|
9736
9767
|
}
|
|
9737
|
-
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);
|
|
9738
9769
|
if (setupLog.length > 0) {
|
|
9739
9770
|
parts.push(
|
|
9740
9771
|
`
|
|
@@ -10926,7 +10957,7 @@ var getAttachmentContract = defineToolContract({
|
|
|
10926
10957
|
});
|
|
10927
10958
|
var attachmentTags = f.optional(
|
|
10928
10959
|
f.array(f.string(), {
|
|
10929
|
-
desc: `Glossary tag names this file is a relevant example of, e.g.
|
|
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.`
|
|
10930
10961
|
})
|
|
10931
10962
|
);
|
|
10932
10963
|
var uploadAttachmentContract = defineToolContract({
|
|
@@ -11478,6 +11509,35 @@ function buildForceUpdateTaskStatusTool(connection) {
|
|
|
11478
11509
|
}
|
|
11479
11510
|
);
|
|
11480
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
|
+
}
|
|
11481
11541
|
function buildCreatePullRequestTool(connection, config) {
|
|
11482
11542
|
return defineContractTool(
|
|
11483
11543
|
createPullRequestContract,
|
|
@@ -11506,32 +11566,8 @@ Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
|
|
|
11506
11566
|
);
|
|
11507
11567
|
}
|
|
11508
11568
|
}
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
cwd,
|
|
11512
|
-
async () => {
|
|
11513
|
-
try {
|
|
11514
|
-
const result2 = await connection.call("refreshGithubToken", {
|
|
11515
|
-
sessionId: connection.sessionId
|
|
11516
|
-
});
|
|
11517
|
-
return result2.token;
|
|
11518
|
-
} catch {
|
|
11519
|
-
return void 0;
|
|
11520
|
-
}
|
|
11521
|
-
},
|
|
11522
|
-
skipVerify ?? true
|
|
11523
|
-
);
|
|
11524
|
-
if (pushSuccess) {
|
|
11525
|
-
connection.sendEvent({
|
|
11526
|
-
type: "message",
|
|
11527
|
-
content: "Auto-pushed committed changes to origin"
|
|
11528
|
-
});
|
|
11529
|
-
} else {
|
|
11530
|
-
return textResult(
|
|
11531
|
-
"Failed to push changes to origin. Please check git status and push manually before creating PR."
|
|
11532
|
-
);
|
|
11533
|
-
}
|
|
11534
|
-
}
|
|
11569
|
+
const pushError = await ensureHeadPushed(connection, cwd, headBranch, skipVerify ?? true);
|
|
11570
|
+
if (pushError) return textResult(pushError);
|
|
11535
11571
|
const result = await connection.call("createPullRequest", {
|
|
11536
11572
|
sessionId: connection.sessionId,
|
|
11537
11573
|
title,
|
|
@@ -11556,10 +11592,10 @@ ${result.glossaryNote}` : "";
|
|
|
11556
11592
|
`Failed to create pull request: ${msg}
|
|
11557
11593
|
|
|
11558
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
|
|
11559
11596
|
- Ensure all changes are committed and pushed to the remote branch
|
|
11560
11597
|
- Check that the branch exists on the remote (run: git push -u origin HEAD)
|
|
11561
|
-
-
|
|
11562
|
-
- 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`
|
|
11563
11599
|
);
|
|
11564
11600
|
}
|
|
11565
11601
|
}
|
|
@@ -11785,7 +11821,7 @@ function buildMutationTools(connection, config) {
|
|
|
11785
11821
|
}
|
|
11786
11822
|
|
|
11787
11823
|
// src/tools/attachment-tools.ts
|
|
11788
|
-
import { basename, extname, isAbsolute, join as
|
|
11824
|
+
import { basename, extname, isAbsolute, join as join12 } from "path";
|
|
11789
11825
|
var MIME_BY_EXT = {
|
|
11790
11826
|
".png": "image/png",
|
|
11791
11827
|
".jpg": "image/jpeg",
|
|
@@ -11838,7 +11874,7 @@ ${snippet}`;
|
|
|
11838
11874
|
function buildUploadAttachmentTool(connection, config) {
|
|
11839
11875
|
return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
|
|
11840
11876
|
try {
|
|
11841
|
-
const filePath = isAbsolute(path2) ? path2 :
|
|
11877
|
+
const filePath = isAbsolute(path2) ? path2 : join12(config.workspaceDir, path2);
|
|
11842
11878
|
const mimeType = inferMimeType(filePath);
|
|
11843
11879
|
const info = await statWorkspacePath(filePath);
|
|
11844
11880
|
if (!info.isFile) {
|
|
@@ -12378,7 +12414,7 @@ import { z as z16 } from "zod";
|
|
|
12378
12414
|
|
|
12379
12415
|
// src/execution/context-path-verifier.ts
|
|
12380
12416
|
import { readFile as readFile2 } from "fs/promises";
|
|
12381
|
-
import { isAbsolute as isAbsolute2, join as
|
|
12417
|
+
import { isAbsolute as isAbsolute2, join as join13, normalize } from "path";
|
|
12382
12418
|
var PROBLEM_TEXT = {
|
|
12383
12419
|
not_found: "does not exist in the repo",
|
|
12384
12420
|
expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
|
|
@@ -12423,7 +12459,7 @@ async function verifyContextPaths(links, workspaceDir) {
|
|
|
12423
12459
|
problems.push({ type: link.type, path: link.path, reason: shape });
|
|
12424
12460
|
continue;
|
|
12425
12461
|
}
|
|
12426
|
-
const absolutePath =
|
|
12462
|
+
const absolutePath = join13(workspaceDir, toRelativePath(link.path));
|
|
12427
12463
|
const stat = await statWorkspacePath(absolutePath);
|
|
12428
12464
|
const wantsDirectory = expectsDirectory(link.type);
|
|
12429
12465
|
if (!stat.exists) {
|
|
@@ -12983,17 +13019,26 @@ var ReviewGuideToolSchema = z18.strictObject({
|
|
|
12983
13019
|
"REQUIRED top-level array (never text appended to overview) of ordered conceptual sections."
|
|
12984
13020
|
)
|
|
12985
13021
|
});
|
|
12986
|
-
var FLATTENED_SECTIONS_PATTERN = /<\/overview>\s
|
|
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
|
+
}
|
|
12987
13029
|
function recoverFlattenedGuide(overview) {
|
|
12988
13030
|
const match = FLATTENED_SECTIONS_PATTERN.exec(overview);
|
|
12989
13031
|
if (!match) return null;
|
|
12990
|
-
const head = overview.slice(0, match.index);
|
|
12991
|
-
const tail = overview.slice(match.index + match[0].length).replace(
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
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
|
+
}
|
|
12996
13040
|
}
|
|
13041
|
+
return null;
|
|
12997
13042
|
}
|
|
12998
13043
|
async function resolveGitHeadSha(cwd) {
|
|
12999
13044
|
try {
|
|
@@ -13020,7 +13065,7 @@ function buildPublishReviewGuideTool(connection, options = {}) {
|
|
|
13020
13065
|
const recovered = recoverFlattenedGuide(overview);
|
|
13021
13066
|
if (!recovered) {
|
|
13022
13067
|
throw new Error(
|
|
13023
|
-
"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."
|
|
13024
13069
|
);
|
|
13025
13070
|
}
|
|
13026
13071
|
resolvedOverview = recovered.overview;
|
|
@@ -13746,7 +13791,7 @@ function collectMissingProps(taskProps) {
|
|
|
13746
13791
|
}
|
|
13747
13792
|
|
|
13748
13793
|
// src/runner/heavy-gate.ts
|
|
13749
|
-
import { readFileSync } from "fs";
|
|
13794
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
13750
13795
|
import path from "path";
|
|
13751
13796
|
var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
|
|
13752
13797
|
function runDir() {
|
|
@@ -13763,7 +13808,7 @@ function pidAlive(pid) {
|
|
|
13763
13808
|
function isHeavyGateActive() {
|
|
13764
13809
|
for (const key of GATE_KEYS) {
|
|
13765
13810
|
try {
|
|
13766
|
-
const raw =
|
|
13811
|
+
const raw = readFileSync2(path.join(runDir(), `${key}.pid`), "utf8").trim();
|
|
13767
13812
|
const pid = Number.parseInt(raw, 10);
|
|
13768
13813
|
if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
|
|
13769
13814
|
} catch {
|
|
@@ -13776,6 +13821,18 @@ function isHeavyGateActive() {
|
|
|
13776
13821
|
var REPEAT_INTERRUPT_THRESHOLD = 4;
|
|
13777
13822
|
var REPEAT_INTERRUPT_INTERVAL = 4;
|
|
13778
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
|
+
}
|
|
13779
13836
|
var UNCOUNTED_TOOLS = /* @__PURE__ */ new Set(["ExitPlanMode", "AskUserQuestion"]);
|
|
13780
13837
|
function stableStringify(value) {
|
|
13781
13838
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
@@ -13787,7 +13844,9 @@ function fingerprintToolCall(toolName, input) {
|
|
|
13787
13844
|
if (!toolName || UNCOUNTED_TOOLS.has(toolName)) return null;
|
|
13788
13845
|
if (toolName === "Bash") {
|
|
13789
13846
|
const command = String(input.command ?? "").trim().replace(/\s+/g, " ");
|
|
13790
|
-
|
|
13847
|
+
if (!command) return null;
|
|
13848
|
+
if (isNoOpKeepAlive(command)) return NOOP_FINGERPRINT;
|
|
13849
|
+
return `Bash:${command}`;
|
|
13791
13850
|
}
|
|
13792
13851
|
return `${toolName}:${stableStringify(input)}`;
|
|
13793
13852
|
}
|
|
@@ -13796,10 +13855,16 @@ var ToolLoopTracker = class {
|
|
|
13796
13855
|
streak = 0;
|
|
13797
13856
|
/** True once this streak has been reported to chat — one message per streak. */
|
|
13798
13857
|
reported = false;
|
|
13858
|
+
/** Session-cumulative no-op keep-alive count (never reset by other calls). */
|
|
13859
|
+
noOps = 0;
|
|
13799
13860
|
/** Repeats of the current fingerprint, including the call being recorded. */
|
|
13800
13861
|
get repeatCount() {
|
|
13801
13862
|
return this.streak;
|
|
13802
13863
|
}
|
|
13864
|
+
/** Total no-op keep-alive calls recorded this session. */
|
|
13865
|
+
get noOpCount() {
|
|
13866
|
+
return this.noOps;
|
|
13867
|
+
}
|
|
13803
13868
|
/** True when the current streak has already been posted to chat. */
|
|
13804
13869
|
get alreadyReported() {
|
|
13805
13870
|
return this.reported;
|
|
@@ -13810,6 +13875,14 @@ var ToolLoopTracker = class {
|
|
|
13810
13875
|
}
|
|
13811
13876
|
/** Record one call and decide what to do about it. */
|
|
13812
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
|
+
}
|
|
13813
13886
|
if (fingerprint !== this.fingerprint) {
|
|
13814
13887
|
this.fingerprint = fingerprint;
|
|
13815
13888
|
this.streak = 1;
|
|
@@ -13829,6 +13902,9 @@ function buildRepeatLoopMessage(repeatCount, heavyGateActive) {
|
|
|
13829
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.`;
|
|
13830
13903
|
return `${head} ${advice} Call a different tool now.`;
|
|
13831
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
|
+
}
|
|
13832
13908
|
function buildRepeatLoopChatMessage(repeatCount, forceStopped) {
|
|
13833
13909
|
if (forceStopped) {
|
|
13834
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.`;
|
|
@@ -14078,6 +14154,20 @@ function checkToolLoop(host, toolName, input) {
|
|
|
14078
14154
|
const tracker = host.toolLoop ??= new ToolLoopTracker();
|
|
14079
14155
|
const verdict = tracker.record(fingerprint);
|
|
14080
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
|
+
}
|
|
14081
14171
|
const repeats = tracker.repeatCount;
|
|
14082
14172
|
if (verdict === "force_stop") {
|
|
14083
14173
|
host.connection.postChatMessage(buildRepeatLoopChatMessage(repeats, true));
|
|
@@ -14220,7 +14310,7 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
14220
14310
|
function repairTornSessionFile(path2) {
|
|
14221
14311
|
try {
|
|
14222
14312
|
if (!existsSync2(path2)) return false;
|
|
14223
|
-
const content =
|
|
14313
|
+
const content = readFileSync3(path2, "utf8");
|
|
14224
14314
|
if (content.length === 0) return false;
|
|
14225
14315
|
let keepEnd = content.length;
|
|
14226
14316
|
if (!content.endsWith("\n")) {
|
|
@@ -17084,12 +17174,12 @@ ${outcome.failures.join("\n")}
|
|
|
17084
17174
|
};
|
|
17085
17175
|
|
|
17086
17176
|
// src/setup/config.ts
|
|
17087
|
-
import { join as
|
|
17177
|
+
import { join as join14 } from "path";
|
|
17088
17178
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
17089
17179
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
17090
17180
|
async function loadForwardPorts(workspaceDir) {
|
|
17091
17181
|
try {
|
|
17092
|
-
const raw = await readWorkspaceFile(
|
|
17182
|
+
const raw = await readWorkspaceFile(join14(workspaceDir, DEVCONTAINER_PATH));
|
|
17093
17183
|
const parsed = JSON.parse(raw);
|
|
17094
17184
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
17095
17185
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-agent",
|
|
3
|
-
"version": "10.13.
|
|
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",
|