github-router 0.3.162 → 0.3.168
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/browser-ext/manifest.json +1 -1
- package/dist/{engine-DHFa2_hK.js → engine-RA1Orodr.js} +1 -1
- package/dist/main.js +312 -10
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-Bfq3FPKc.js → peer-mcp-personas-D826LsJJ.js} +4431 -405
- package/dist/peer-mcp-personas-D826LsJJ.js.map +1 -0
- package/package.json +1 -1
- package/dist/peer-mcp-personas-Bfq3FPKc.js.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "github-router browser bridge",
|
|
4
4
|
"short_name": "gh-router-browser",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.168",
|
|
6
6
|
"description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
|
|
7
7
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
|
|
8
8
|
"background": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as PLAN_DEFAULT_MODEL, I as REVIEW_DEFAULT_MODEL, L as appendPlanReminder, M as DEFAULT_MODEL, N as EXPLORE_DEFAULT_MODEL, P as IMPLEMENT_DEFAULT_MODEL, R as runWorkerAgent, j as BROWSE_DEFAULT_MODEL, z as withNoOutputRetry } from "./peer-mcp-personas-D826LsJJ.js";
|
|
2
2
|
import "./paths-CTlT1nTo.js";
|
|
3
3
|
import "./lifecycle-ChPBRt6K.js";
|
|
4
4
|
import "./lifecycle-BUXxiltc.js";
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as buildAnthropicErrorEvent, $t as resolveCodexModel, A as liveExec, At as shouldUseInsecureTls, B as availableToolCommands, Bt as generateRandomPort, C as isSubagentContext, Ct as readResponseBodyCapped, D as stopReviewStateDir, Dt as provisionAndIndexColbert, E as stopGateEnabledForRepo, Et as hasSupportedBrowserInstalled, Ft as DEFAULT_CODEX_MODEL, G as TOOLBELT_TOOLS, Gt as setupGitHubAgentToken, H as toolbeltEnabled, Ht as getPackageVersion, It as DEFAULT_CODEX_MODEL_FALLBACKS, J as ADVISOR_INTERNAL_TOOL_NAME, Jt as cacheCopilotVersion, K as assetFor, Kt as setupGitHubToken, Lt as DEFAULT_PORT, Mt as collapsePathKeys, Nt as toolbeltPathOverride, O as trustRepo, Ot as extractTarGzMember, Pt as DEFAULT_CLAUDE_MODEL_FALLBACKS, Q as isAdvisorRequested, Qt as isNullish, Rt as UPSTREAM_FETCH_TIMEOUT_MS, S as fileReviewDebounce, St as MAX_RESPONSE_BODY_BYTES, T as repoRoot, Tt as provisionBrowserAssets, U as toolbeltSkipSet, Ut as withInstallLock, V as buildToolbeltAwareness, Vt as pickClaudeDefault, W as vscodeRipgrepPath, Wt as setupCopilotToken, X as buildAdvisorStream, Xt as cacheVSCodeVersion, Y as ADVISOR_TOOL_INSTRUCTIONS, Yt as cacheModels, Z as injectAdvisorTool, Zt as filterBetaHeader, _ as stopGatePlanMode, _t as assembleResponsesPayload, a as buildPeerAwarenessSnippet, an as forwardError, at as handleMcpDelete, b as fileFindingsStore, bt as createResponses, c as buildSessionBindHookCommand, cn as copilotHeaders, ct as browseAgentEnabled, d as decideStopHook, dt as implementerSubagentModel, en as resolveModel, et as buildOpenAIErrorEvent, f as fileBlockBudget, ft as standInToolEnabled, g as stopGateId, gt as getTokenCount, h as stopGateDisabled, ht as createMessages, i as buildAgentPrompt, in as HTTPError, it as relayAnthropicStream, jt as ArtifactClient, k as resolveSealedGate, kt as extractZipMember, l as buildStopHookCommand, ln as githubHeaders, lt as browserToolsEnabled, m as launchBaselineKey, mt as countTokens, n as MCP_GROUPS, nn as getModels, nt as logStreamError, o as personasFor, on as GITHUB_API_BASE_URL, ot as handleMcpPost, p as injectStopHookIntoSettingsFile, pt as workerToolsEnabled, q as searchWeb, qt as tryRefreshAndRetry, r as assertMcpToolSurfaceConsistent, rn as fetchWithTransientRetry, rt as readIteratorWithTimeout, s as buildArtifactOpenHookCommand, sn as copilotBaseUrl, st as agentToolsEnabled, t as GROUP_META, tn as sleep, tt as isControllerClosedError, u as captureLaunchBaseline, un as state, ut as fleetToolsEnabled, v as stopReviewEnabled, vt as resolveMcpToolTimeoutMs, w as repoFingerprint, wt as parseJsonOrDiagnose, x as fileLastPromptStore, xt as createChatCompletions, y as fileBaselineStore, yt as pickEndpoint, zt as UPSTREAM_INACTIVITY_TIMEOUT_MS } from "./peer-mcp-personas-D826LsJJ.js";
|
|
3
3
|
import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeArtifactCredsToMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS, u as writeRuntimeFileSecure } from "./paths-CTlT1nTo.js";
|
|
4
4
|
import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-ChPBRt6K.js";
|
|
5
5
|
import { a as sweepRegistry } from "./lifecycle-BUXxiltc.js";
|
|
@@ -1204,7 +1204,7 @@ function buildPeerAgentDefinitions(opts) {
|
|
|
1204
1204
|
});
|
|
1205
1205
|
if (opts.implementerModel && opts.implementerModel.length > 0) out.implementer = {
|
|
1206
1206
|
description: "Bounded implementation subagent running gpt-5.5 (strong non-Claude coder, high reasoning). Use for well-scoped coding tasks — edits, small features, fixes — you want implemented in an integrated subagent. Model is overridable at spawn.",
|
|
1207
|
-
prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
|
|
1207
|
+
prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running builds, tests, and git — do not shell out (sed/awk/python/here-docs) to read or edit files. Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
|
|
1208
1208
|
model: opts.implementerModel
|
|
1209
1209
|
};
|
|
1210
1210
|
if (opts.workerToolsAvailable) {
|
|
@@ -2289,7 +2289,7 @@ async function discoverGateCommands(cwd, opts) {
|
|
|
2289
2289
|
if (files.length === 0) return null;
|
|
2290
2290
|
let result;
|
|
2291
2291
|
try {
|
|
2292
|
-
const { runWorkerAgent } = await import("./engine-
|
|
2292
|
+
const { runWorkerAgent } = await import("./engine-RA1Orodr.js");
|
|
2293
2293
|
result = await runWorkerAgent({
|
|
2294
2294
|
mode: "explore",
|
|
2295
2295
|
workspace: root,
|
|
@@ -2443,6 +2443,99 @@ function buildPromptSubmitHookCommand(execPath, scriptPath) {
|
|
|
2443
2443
|
return `${q(execPath)} internal-prompt-submit`;
|
|
2444
2444
|
}
|
|
2445
2445
|
|
|
2446
|
+
//#endregion
|
|
2447
|
+
//#region src/lib/first-mate/operator-shaping.ts
|
|
2448
|
+
/** MCP prefixes denied to the main operator; these remain subagent-only. */
|
|
2449
|
+
const OPERATOR_DENIED_MCP_PREFIXES = ["mcp__workers__", "mcp__orchestrate__"];
|
|
2450
|
+
/**
|
|
2451
|
+
* Decide whether a tool call must be blocked for the main operator. Pure — used
|
|
2452
|
+
* both by the PreToolUse hook handler and by config-assertion tests. When
|
|
2453
|
+
* operator mode is off, nothing is blocked (normal sessions unaffected).
|
|
2454
|
+
*/
|
|
2455
|
+
function shouldDenyOperatorTool(toolName, operatorMode) {
|
|
2456
|
+
if (!operatorMode) return false;
|
|
2457
|
+
return OPERATOR_DENIED_MCP_PREFIXES.some((prefix) => toolName.startsWith(prefix));
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* M4 — fail-CLOSED. If operator/`--agents` mode is active but the reduced
|
|
2461
|
+
* worker/orchestrate guard could NOT be installed (e.g. settings.json unwritable),
|
|
2462
|
+
* the operator session must NOT start with direct main-thread worker/orchestrate
|
|
2463
|
+
* MCP calls available. Non-agents sessions are never affected.
|
|
2464
|
+
*/
|
|
2465
|
+
function assertShapingInstalled(agentsMode, injectionSucceeded) {
|
|
2466
|
+
if (agentsMode && !injectionSucceeded) throw new Error("cloud-agent operator mode requires the worker/orchestrate PreToolUse hook, but it could not be installed — refusing to start an unguarded operator session.");
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* The PreToolUse hook decision for a given tool name in operator mode.
|
|
2470
|
+
*
|
|
2471
|
+
* Bash and file-authoring tools are always allowed. Only direct main-operator
|
|
2472
|
+
* calls to local worker/orchestrate MCP tools are blocked so those backends stay
|
|
2473
|
+
* reachable through the worker-* Agent subagents rather than the lead context.
|
|
2474
|
+
*/
|
|
2475
|
+
function operatorPreToolUse(toolName, operatorMode, _input) {
|
|
2476
|
+
if (!operatorMode) return { block: false };
|
|
2477
|
+
if (shouldDenyOperatorTool(toolName, operatorMode)) return {
|
|
2478
|
+
block: true,
|
|
2479
|
+
reason: `${toolName} is subagent-only in cloud-agent operator mode — use the worker-* Agent subagents or delegate implementation to a GitHub cloud agent via the first-mate MCP instead of calling local worker/orchestrate MCP tools from the main operator.`
|
|
2480
|
+
};
|
|
2481
|
+
return { block: false };
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/internal-first-mate-guard.ts
|
|
2486
|
+
function readStdinSync() {
|
|
2487
|
+
try {
|
|
2488
|
+
return readFileSync(0, "utf8");
|
|
2489
|
+
} catch {
|
|
2490
|
+
return "";
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
const internalFirstMateGuard = defineCommand({
|
|
2494
|
+
meta: {
|
|
2495
|
+
name: "internal-first-mate-guard",
|
|
2496
|
+
description: "PreToolUse guard enforcing cloud-agent operator capability shaping."
|
|
2497
|
+
},
|
|
2498
|
+
run() {
|
|
2499
|
+
let toolName = "";
|
|
2500
|
+
let toolInput;
|
|
2501
|
+
let parsed = false;
|
|
2502
|
+
try {
|
|
2503
|
+
const payload = JSON.parse(readStdinSync());
|
|
2504
|
+
if (typeof payload.tool_name === "string") toolName = payload.tool_name;
|
|
2505
|
+
if (typeof payload.tool_input === "object" && payload.tool_input !== null) toolInput = payload.tool_input;
|
|
2506
|
+
parsed = true;
|
|
2507
|
+
} catch {
|
|
2508
|
+
parsed = false;
|
|
2509
|
+
}
|
|
2510
|
+
if (toolName.length === 0) {
|
|
2511
|
+
if (!parsed) {
|
|
2512
|
+
process.stderr.write("operator guard: unparseable PreToolUse payload — blocking (fail-closed)");
|
|
2513
|
+
process.exit(2);
|
|
2514
|
+
}
|
|
2515
|
+
process.exit(0);
|
|
2516
|
+
}
|
|
2517
|
+
const decision = operatorPreToolUse(toolName, true, toolInput);
|
|
2518
|
+
if (decision.block) {
|
|
2519
|
+
process.stderr.write(decision.reason ?? `${toolName} is disabled in operator mode`);
|
|
2520
|
+
process.exit(2);
|
|
2521
|
+
}
|
|
2522
|
+
process.exit(0);
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
/** The regex matcher scoping the guard hook to exactly the denied tools. */
|
|
2526
|
+
const FIRST_MATE_GUARD_MATCHER = "mcp__workers__.*|mcp__orchestrate__.*";
|
|
2527
|
+
/** Build the hook command string that runs this subcommand. */
|
|
2528
|
+
function buildFirstMateGuardHookCommand(execPath, entry) {
|
|
2529
|
+
return `${JSON.stringify(execPath)} ${JSON.stringify(entry)} internal-first-mate-guard`;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
//#endregion
|
|
2533
|
+
//#region src/lib/injected-skills/first-mate-setup-skill.ts
|
|
2534
|
+
const FIRST_MATE_SETUP_SKILL = {
|
|
2535
|
+
name: "gh-first-mate-scaffold",
|
|
2536
|
+
md: "---\nname: gh-first-mate-scaffold\ndescription: Seed a world-class repo-geared agentic-dev foundation through first-mate.\nuser-invocable: true\n---\n\n# gh-first-mate-scaffold\n\nInvoke the `scaffold_repo` MCP tool (`mcp__first-mate__scaffold_repo`) before the first build wave on an owned repository. The goal is not generic TODO stubs; it is a repo-geared foundation that GitHub agents, local agents, reviewers, and CI can read.\n\n## What it seeds\n\n- `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` / `.github/copilot-instructions.md` — identical guidance with overview, detected stack, commands, hard DoD gate, primary OS, conventions, structure, decisions/memory, handoff, testing, and gotchas.\n- `.github/agents/{planner,implementer,reviewer,researcher,tester}.md` mirrored into `.claude/agents/` — role agents with frontmatter, cold-start contract, method, quality bar, output contract, and self-reminder.\n- `docs/adrs/0000-template.md` plus `docs/adr/0001-record-architecture-decisions.md` — Nygard-style decision record foundation.\n- `LEARNINGS.md`, `CHANGELOG.md`, `docs/history/0000-template.md`, `docs/plans/README.md`, and `docs/research/README.md` — durable memory, history, plans, and research conventions.\n- `.github/pull_request_template.md` — summary, type, failure-modes-considered-and-tested, and DoD checklist.\n- `.github/instructions/tests.instructions.md` — path-scoped test guidance filled from detected framework/dir/glob where possible.\n- `.github/workflows/copilot-setup-steps.yml` and starter `.github/workflows/ci.yml` — detected toolchain setup with stable quality-gate job names.\n\nIt does not seed factory-protocol or `docs/factory/` files. Orchestration remains outside the product repo in first-mate.\n\n## Usage\n\n```\nmcp__first-mate__scaffold_repo({ repo: \"owner/repo\" })\nmcp__first-mate__scaffold_repo({ repo: \"owner/repo\", mode: \"enhance\" })\nmcp__first-mate__scaffold_repo({\n repo: \"owner/repo\",\n mode: \"add-missing-only\",\n detection_overrides: { primary_os: \"windows-latest\", test_command: \"npm test\" }\n})\n```\n\nModes:\n\n- `add-missing-only` (default): seed absent files and skip present files.\n- `enhance`: for guidance files, ADR index, changelog, and learnings, append only missing `##` sections; never rewrite existing prose. Other present files are skipped.\n- `overwrite-approved`: replace existing files only when explicitly approved.\n\nAlways inspect the returned per-file report and PR. A no-op result means the repo already has the foundation or has no missing enhanceable sections.\n"
|
|
2537
|
+
};
|
|
2538
|
+
|
|
2446
2539
|
//#endregion
|
|
2447
2540
|
//#region src/lib/injected-skills/first-mate-skill.ts
|
|
2448
2541
|
const FIRST_MATE_SKILL = {
|
|
@@ -2459,6 +2552,18 @@ Use this skill when the user wants first-mate to drive GitHub cloud coding agent
|
|
|
2459
2552
|
The first-mate controller is the durable system of record: missions, units, decisions, handles, and controller state live in its registry and ledger.
|
|
2460
2553
|
Your job is to run the thin protocol, not to hold the mission in context.
|
|
2461
2554
|
|
|
2555
|
+
## Foundation-first mandate
|
|
2556
|
+
|
|
2557
|
+
Before the first build wave on an owned repository, run \`mcp__first-mate__scaffold_repo\` and verify the PR landed or is already present. The scaffold must seed a repo-geared foundation that GitHub agents and CI can read: guidance, role agents, ADRs, changelog, learnings, PR template, test instructions, Copilot setup, and CI. Do not seed factory-protocol files into product repos; first-mate is the external orchestrator.
|
|
2558
|
+
|
|
2559
|
+
Use \`mode: "add-missing-only"\` for new repos, \`mode: "enhance"\` when a repo has existing guidance that should keep its prose while appending missing \`##\` sections, and \`mode: "overwrite-approved"\` only with explicit approval.
|
|
2560
|
+
|
|
2561
|
+
## Scoped-work discipline
|
|
2562
|
+
|
|
2563
|
+
Well-scoped, testable work items succeed; vague meta-work fails. Discovery/decompose must emit concrete units with acceptance criteria, expected evidence, and dependencies. Keep one active build unit per concern. Parallelism is for read-only producers (research, review, planning) and independent units only, not for racing broad implementation waves.
|
|
2564
|
+
|
|
2565
|
+
Judgment and merge policy: merge remains human-gated, evidence-gated, and head/base-bound. Use the best available model tier for plan review, judgment, and merge decisions; never cheap out on plan/judge/merge calls.
|
|
2566
|
+
|
|
2462
2567
|
## Start a mission
|
|
2463
2568
|
|
|
2464
2569
|
For a new goal, call mcp__first-mate__start_mission with:
|
|
@@ -2469,7 +2574,7 @@ For a new goal, call mcp__first-mate__start_mission with:
|
|
|
2469
2574
|
- priority and house_rules only when the user supplied them or they are necessary constraints.
|
|
2470
2575
|
|
|
2471
2576
|
If acceptance criteria are missing or ambiguous, ask the user before starting.
|
|
2472
|
-
Do not decompose the mission yourself at start time;
|
|
2577
|
+
Do not decompose the mission yourself at start time; mission registration is intentionally simple and later controller wakes/model requests drive decomposition and steering. If the user explicitly asks to append scoped work to an existing active mission, use mcp__first-mate__add_units with concrete unit titles.
|
|
2473
2578
|
|
|
2474
2579
|
Invariant (closes the stranding hole): work only ever becomes active inside a turn — start_mission is a tool call, nothing activates a mission server-side. So immediately after start_mission, run one loop turn (advance, then arm the heartbeat) in the SAME turn, before you yield. Never register a mission and stop without arming; otherwise nothing will wake to drive it.
|
|
2475
2580
|
|
|
@@ -2496,6 +2601,7 @@ Keep verdicts small and typed to the request kind.
|
|
|
2496
2601
|
|
|
2497
2602
|
Use the request's kind and payload as the contract:
|
|
2498
2603
|
|
|
2604
|
+
- decompose: split a unit-less active mission into dispatchable units. Return { units: [{ title, repo?, agent?, dependsOn?, model? }] }. \`dependsOn\` entries are 0-based indices into the same units list. Emit once per unit-less active mission; the controller creates durable unit ids and will not ask again after units exist.
|
|
2499
2605
|
- review_plan: review the plan against the mission goal, acceptance criteria, and house rules. Return { decision: "approve" } when the plan is good enough to implement, or { decision: "refine", instruction: "..." } with a short actionable refinement.
|
|
2500
2606
|
- answer_agent_question: answer only from the acceptance criteria and supplied context. Return { answer: "..." }. If the answer is not derivable, do not invent policy; escalate by leaving a short answer that says what the human must decide.
|
|
2501
2607
|
- author_fix: author a concise fix instruction for the cloud agent. Return { instruction: "..." } with the failure, expected behavior, and any bounded check to run.
|
|
@@ -2507,6 +2613,8 @@ Delegate heavy reading to workers:
|
|
|
2507
2613
|
- Use the worker-review subagent (Agent tool) for compact review of a specific plan, PR summary, or suspicious change.
|
|
2508
2614
|
- Use the worker-test subagent (Agent tool) when a missing executable check is the blocker.
|
|
2509
2615
|
|
|
2616
|
+
Operator / --agents mode constraint: delegate product implementation to GitHub cloud agents. Direct \`mcp__workers__*\` / \`mcp__orchestrate__*\` calls are subagent-only for the main operator; use the worker-* Agent subagents when local worker help is genuinely needed. Local tools (Edit/Write/Bash, \`gh\`, \`git\`) remain available, but prefer authoring fix instructions for the cloud agent rather than pulling large diffs or CI logs into the lead context.
|
|
2617
|
+
|
|
2510
2618
|
Do not read a full diff, full CI log, or full transcript in the lead context. Ask workers for narrow facts and compact excerpts. The ledger is durable memory; context is not.
|
|
2511
2619
|
|
|
2512
2620
|
## Human requests
|
|
@@ -2540,6 +2648,8 @@ Responsiveness (optional): the buckets above already tighten cadence to ~2 min f
|
|
|
2540
2648
|
|
|
2541
2649
|
Guardrails: never leave more than one [fm-heartbeat] job; never busy-loop advance; if no scheduler tool is available, tell the user the next wake is in nextWakeSeconds seconds and stop.
|
|
2542
2650
|
|
|
2651
|
+
MCP unavailable: if the first-mate MCP server is not present (this is not a \`--agents\` session and \`mcp__first-mate__advance\` would return a tool-not-found error), do NOT attempt to advance. CronList then CronDelete every [fm-heartbeat] job (same as Disarm above), and report "first-mate paused — re-run under \`github-router claude --agents\` to resume." Stop without further action.
|
|
2652
|
+
|
|
2543
2653
|
## Board reports
|
|
2544
2654
|
|
|
2545
2655
|
Use mcp__first-mate__board or mcp__first-mate__mission_status for read-only status checks.
|
|
@@ -2549,6 +2659,9 @@ Report compactly:
|
|
|
2549
2659
|
- repositories
|
|
2550
2660
|
- phase counts
|
|
2551
2661
|
- blocked count and why, when available
|
|
2662
|
+
- per-unit handles for non-terminal units: unitId, issue/PR, phase, provider, validation, model, and blockedReason when present
|
|
2663
|
+
- resolved cloud-agent model per unit (surfaced on the board — verify model choice before approving plans)
|
|
2664
|
+
- terminal work only as summary counts; use include_all only when the user asks for completed/abandoned history
|
|
2552
2665
|
- next wake time or the next requested action
|
|
2553
2666
|
|
|
2554
2667
|
Never reconstruct status by rereading raw logs when the controller board already has the handles.
|
|
@@ -3613,7 +3726,8 @@ const INJECTED_SKILLS = [
|
|
|
3613
3726
|
ORCHESTRATE_SKILL,
|
|
3614
3727
|
FLOOR_KEEPER_SKILL,
|
|
3615
3728
|
WORKER_SKILL,
|
|
3616
|
-
FIRST_MATE_SKILL
|
|
3729
|
+
FIRST_MATE_SKILL,
|
|
3730
|
+
FIRST_MATE_SETUP_SKILL
|
|
3617
3731
|
];
|
|
3618
3732
|
|
|
3619
3733
|
//#endregion
|
|
@@ -4046,6 +4160,141 @@ async function stopKeepAwake() {
|
|
|
4046
4160
|
releaseSync();
|
|
4047
4161
|
}
|
|
4048
4162
|
|
|
4163
|
+
//#endregion
|
|
4164
|
+
//#region src/lib/first-mate/scheduler/autospawn.ts
|
|
4165
|
+
/**
|
|
4166
|
+
* Opt-in auto-spawn of the first-mate scheduler daemon as a SEPARATE process
|
|
4167
|
+
* (a hung tick must not starve the proxy event loop).
|
|
4168
|
+
*
|
|
4169
|
+
* DEFAULT OFF — gated by GH_ROUTER_FM_DAEMON === "1" (explicit opt-in). The
|
|
4170
|
+
* durable [fm-heartbeat] cron is the DEFAULT driver and the proven path; the
|
|
4171
|
+
* daemon is experimental and NOT yet safe as the default because two hardening
|
|
4172
|
+
* items remain (a verifier-stall wall-clock escalation, and an atomic-claim
|
|
4173
|
+
* lock in durable-store) — flip the default only once those land.
|
|
4174
|
+
*
|
|
4175
|
+
* Requirements + honest boundaries:
|
|
4176
|
+
* - The daemon entry is `scripts/first-mate-daemon.ts` (a TS source file run via
|
|
4177
|
+
* `bun`); it is NOT in the published dist tarball, so auto-spawn only fires
|
|
4178
|
+
* from a source checkout with `bun` available. If the script is absent we
|
|
4179
|
+
* no-op (never a false "spawned" log).
|
|
4180
|
+
* - Spawn failures NEVER crash the proxy: an async ENOENT (`bun` not on PATH)
|
|
4181
|
+
* arrives as an `'error'` EVENT, not a sync throw, so we attach an `'error'`
|
|
4182
|
+
* listener (without it the emitter re-throws → uncaughtException → exit(1)).
|
|
4183
|
+
* - The returned handle MUST be killed by the caller on shutdown (no `detached`);
|
|
4184
|
+
* an orphaned drive-primary daemon would keep merging PRs after the proxy is
|
|
4185
|
+
* gone. The parent ALSO holds the child's stdin write end (`stdio[0]==="pipe"`)
|
|
4186
|
+
* so it can EOF the child for a GRACEFUL stop (lease + pidfile released
|
|
4187
|
+
* immediately, not by expiry) — the cross-platform teardown trigger, since an
|
|
4188
|
+
* external SIGTERM on Windows is a hard kill the child can't observe. `kill()`
|
|
4189
|
+
* stays the hard backstop so a wedged child is never orphaned (see
|
|
4190
|
+
* `wireDaemonTeardown`).
|
|
4191
|
+
* - It owns the deterministic drive loop only; live judgments still wake the
|
|
4192
|
+
* lead via the heartbeat (no server->lead push).
|
|
4193
|
+
*/
|
|
4194
|
+
function shouldAutoSpawnDaemon(env, agentsEnabled) {
|
|
4195
|
+
if (!agentsEnabled) return false;
|
|
4196
|
+
return env.GH_ROUTER_FM_DAEMON === "1";
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
4199
|
+
* The real (non-injected) spawner: node:child_process (not the Bun runtime's spawn) so the
|
|
4200
|
+
* bundled dist/main.js stays node-loadable. No `detached` — the caller kills
|
|
4201
|
+
* this on shutdown. stdin is "pipe" (parent holds the write end for a graceful
|
|
4202
|
+
* EOF stop); stdout/stderr stay "ignore".
|
|
4203
|
+
*/
|
|
4204
|
+
function nodeDaemonSpawn(cmd, spawnOpts, spawnImpl = spawn) {
|
|
4205
|
+
const proc = spawnImpl(cmd[0], cmd.slice(1), {
|
|
4206
|
+
env: spawnOpts.env,
|
|
4207
|
+
stdio: spawnOpts.stdio
|
|
4208
|
+
});
|
|
4209
|
+
proc.on("error", (err) => consola.debug("first-mate daemon spawn error (ignored):", err));
|
|
4210
|
+
return {
|
|
4211
|
+
pid: proc.pid,
|
|
4212
|
+
kill: () => void proc.kill("SIGKILL"),
|
|
4213
|
+
endStdin: () => {
|
|
4214
|
+
try {
|
|
4215
|
+
proc.stdin?.end();
|
|
4216
|
+
} catch {}
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Spawn the daemon child if the opt-in gate passes AND the daemon script is
|
|
4222
|
+
* present; else return undefined. Never throws, and never lets an async spawn
|
|
4223
|
+
* error crash bootstrap.
|
|
4224
|
+
*/
|
|
4225
|
+
function maybeSpawnDaemon(opts) {
|
|
4226
|
+
const env = opts.env ?? process.env;
|
|
4227
|
+
if (!shouldAutoSpawnDaemon(env, opts.agentsEnabled)) return void 0;
|
|
4228
|
+
const script = nodePath.join(opts.repoRoot ?? process.cwd(), "scripts/first-mate-daemon.ts");
|
|
4229
|
+
if (opts.spawn === void 0 && !existsSync(script)) {
|
|
4230
|
+
consola.debug(`first-mate daemon entry not found (${script}); auto-spawn skipped.`);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
try {
|
|
4234
|
+
const child = (opts.spawn ?? ((cmd, spawnOpts) => nodeDaemonSpawn(cmd, spawnOpts)))(["bun", script], {
|
|
4235
|
+
env,
|
|
4236
|
+
stdio: [
|
|
4237
|
+
"pipe",
|
|
4238
|
+
"ignore",
|
|
4239
|
+
"ignore"
|
|
4240
|
+
]
|
|
4241
|
+
});
|
|
4242
|
+
return {
|
|
4243
|
+
pid: child.pid,
|
|
4244
|
+
kill: child.kill,
|
|
4245
|
+
endStdin: child.endStdin ?? (() => {})
|
|
4246
|
+
};
|
|
4247
|
+
} catch {
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* Wire the proxy's teardown to shut the daemon child down GRACEFULLY, then hard
|
|
4253
|
+
* kill as a backstop. Ordering (per teardown path):
|
|
4254
|
+
* 1. `endStdin()` — EOF the child so it releases its lease/pidfile cleanly (the
|
|
4255
|
+
* cross-platform trigger; on Windows an external SIGTERM would never run the
|
|
4256
|
+
* child's handler, so this EOF is the ONLY graceful path there).
|
|
4257
|
+
* 2. `kill()` — hard backstop so a wedged child is never orphaned (a prior
|
|
4258
|
+
* review blocker: an orphaned drive-primary daemon keeps merging PRs).
|
|
4259
|
+
*
|
|
4260
|
+
* On SIGINT/SIGTERM the kill is deferred by a short, NON-BLOCKING, unref'd grace
|
|
4261
|
+
* window (proxy exit is never delayed for it). On `'exit'` — where timers cannot
|
|
4262
|
+
* run — we EOF then kill synchronously as a last resort. All steps are
|
|
4263
|
+
* once-guarded and non-throwing.
|
|
4264
|
+
*/
|
|
4265
|
+
function wireDaemonTeardown(handle, opts = {}) {
|
|
4266
|
+
const proc = opts.proc ?? process;
|
|
4267
|
+
const graceMs = opts.graceMs ?? 300;
|
|
4268
|
+
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
4269
|
+
let killed = false;
|
|
4270
|
+
const kill = () => {
|
|
4271
|
+
if (killed) return;
|
|
4272
|
+
killed = true;
|
|
4273
|
+
try {
|
|
4274
|
+
handle.kill();
|
|
4275
|
+
} catch {}
|
|
4276
|
+
};
|
|
4277
|
+
let ended = false;
|
|
4278
|
+
const endStdin = () => {
|
|
4279
|
+
if (ended) return;
|
|
4280
|
+
ended = true;
|
|
4281
|
+
try {
|
|
4282
|
+
handle.endStdin();
|
|
4283
|
+
} catch {}
|
|
4284
|
+
};
|
|
4285
|
+
const onSignal = () => {
|
|
4286
|
+
endStdin();
|
|
4287
|
+
const t = setTimer(kill, graceMs);
|
|
4288
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
4289
|
+
};
|
|
4290
|
+
proc.once("SIGINT", onSignal);
|
|
4291
|
+
proc.once("SIGTERM", onSignal);
|
|
4292
|
+
proc.once("exit", () => {
|
|
4293
|
+
endStdin();
|
|
4294
|
+
kill();
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4049
4298
|
//#endregion
|
|
4050
4299
|
//#region src/lib/proxy.ts
|
|
4051
4300
|
function initProxyFromEnv() {
|
|
@@ -4095,7 +4344,7 @@ function initProxyFromEnv() {
|
|
|
4095
4344
|
//#endregion
|
|
4096
4345
|
//#region package.json
|
|
4097
4346
|
var name = "github-router";
|
|
4098
|
-
var version$1 = "0.3.
|
|
4347
|
+
var version$1 = "0.3.168";
|
|
4099
4348
|
|
|
4100
4349
|
//#endregion
|
|
4101
4350
|
//#region src/lib/approval.ts
|
|
@@ -4918,6 +5167,34 @@ function clampEffort(bucketed, supported) {
|
|
|
4918
5167
|
|
|
4919
5168
|
//#endregion
|
|
4920
5169
|
//#region src/lib/anthropic-translate/anthropic-request.ts
|
|
5170
|
+
/**
|
|
5171
|
+
* Steering appended to `instructions` for shim-routed (non-Claude) models when
|
|
5172
|
+
* the request carries Claude Code's native file tools. gpt-5.5 and other
|
|
5173
|
+
* OpenAI/Gemini-lineage models receive the Edit/Write tool definitions verbatim
|
|
5174
|
+
* (the shim never mangles them), but their base prior is to script file ops in
|
|
5175
|
+
* Python/Bash rather than call the dedicated tools. Claude models never reach
|
|
5176
|
+
* this code path (they fall through to the /v1/messages passthrough), so this is
|
|
5177
|
+
* automatically scoped to the models that need the nudge. Strong PREFERENCE, not
|
|
5178
|
+
* a Bash ban — running builds/tests/git still belongs in Bash.
|
|
5179
|
+
*/
|
|
5180
|
+
const FILE_TOOL_GUIDANCE = `<file_tools>
|
|
5181
|
+
You have dedicated tools for files: use Read to read a file, Edit to modify an existing file, and Write to create one. Prefer them over shell for reading or editing. Do NOT shell out (cat, sed, awk, echo >, here-docs, or python/one-off scripts) to read, search, or rewrite file contents when a dedicated tool exists — the dedicated tools are safer and produce reviewable diffs. Use Grep/Glob to search rather than shell grep/find. Reserve Bash for commands that have no dedicated tool: builds, tests, git, package managers, and running programs.
|
|
5182
|
+
</file_tools>`;
|
|
5183
|
+
/**
|
|
5184
|
+
* Append `FILE_TOOL_GUIDANCE` to the flattened system `instructions` iff the
|
|
5185
|
+
* request carries Claude Code's canonical `Edit` or `Write` tool. The exact
|
|
5186
|
+
* capitalized-name match is deliberately precise: it fires for a Claude Code
|
|
5187
|
+
* editing session but not for arbitrary MCP tools like `write_file`, and not for
|
|
5188
|
+
* non-editing chats (so a plain gpt-5.5 conversation is not polluted). The block
|
|
5189
|
+
* is appended AFTER the existing instructions (end-of-prompt recency) and the
|
|
5190
|
+
* original system text is preserved, never replaced. Opt out with
|
|
5191
|
+
* `GH_ROUTER_DISABLE_SHIM_TOOL_STEERING=1`.
|
|
5192
|
+
*/
|
|
5193
|
+
function appendFileToolGuidance(instructions, tools) {
|
|
5194
|
+
if (parseBoolEnv(process.env.GH_ROUTER_DISABLE_SHIM_TOOL_STEERING) === true) return instructions;
|
|
5195
|
+
if (!tools?.some((t) => t.name === "Edit" || t.name === "Write")) return instructions;
|
|
5196
|
+
return instructions && instructions.length > 0 ? `${instructions}\n\n${FILE_TOOL_GUIDANCE}` : FILE_TOOL_GUIDANCE;
|
|
5197
|
+
}
|
|
4921
5198
|
/** Flatten Anthropic `system` (string | array of text blocks) into a string. */
|
|
4922
5199
|
function flattenSystem(system) {
|
|
4923
5200
|
if (typeof system === "string") return system.length > 0 ? system : void 0;
|
|
@@ -5183,11 +5460,12 @@ function parseAnthropicRequest(body, resolvedModel, model) {
|
|
|
5183
5460
|
}
|
|
5184
5461
|
const maxTokens = typeof body.max_tokens === "number" && body.max_tokens > 0 ? body.max_tokens : void 0;
|
|
5185
5462
|
const stopSequences = Array.isArray(body.stop_sequences) ? body.stop_sequences.filter((s) => typeof s === "string") : void 0;
|
|
5463
|
+
const tools = parseTools(body.tools);
|
|
5186
5464
|
return {
|
|
5187
5465
|
model: resolvedModel,
|
|
5188
|
-
instructions: flattenSystem(body.system),
|
|
5466
|
+
instructions: appendFileToolGuidance(flattenSystem(body.system), tools),
|
|
5189
5467
|
messages,
|
|
5190
|
-
tools
|
|
5468
|
+
tools,
|
|
5191
5469
|
toolChoice: parseToolChoice(body.tool_choice),
|
|
5192
5470
|
parallelToolCalls: parseDisableParallelToolUse(body.tool_choice),
|
|
5193
5471
|
reasoningEffort: body.thinking === void 0 ? defaultReasoningEffort(model) : parseReasoningEffort(body.thinking, model),
|
|
@@ -7254,6 +7532,15 @@ async function setupAndServe(options) {
|
|
|
7254
7532
|
const url = srvxServer.url;
|
|
7255
7533
|
if (!url) throw new Error("Server started but URL is not available");
|
|
7256
7534
|
const serverUrl = url.replace(/\/$/, "");
|
|
7535
|
+
try {
|
|
7536
|
+
const handle = maybeSpawnDaemon({ agentsEnabled: agentToolsEnabled() });
|
|
7537
|
+
if (handle) {
|
|
7538
|
+
wireDaemonTeardown(handle);
|
|
7539
|
+
consola.debug(`first-mate daemon spawn attempted (pid ${handle.pid ?? "?"}).`);
|
|
7540
|
+
}
|
|
7541
|
+
} catch (err) {
|
|
7542
|
+
consola.debug("first-mate daemon auto-spawn skipped:", err);
|
|
7543
|
+
}
|
|
7257
7544
|
return {
|
|
7258
7545
|
server: srvxServer,
|
|
7259
7546
|
serverUrl
|
|
@@ -7610,6 +7897,9 @@ function getCodexEnvVars(serverUrl) {
|
|
|
7610
7897
|
|
|
7611
7898
|
//#endregion
|
|
7612
7899
|
//#region src/claude.ts
|
|
7900
|
+
function isFirstMateSkillName(name$1) {
|
|
7901
|
+
return name$1 === "gh-first-mate" || name$1 === "gh-first-mate-scaffold";
|
|
7902
|
+
}
|
|
7613
7903
|
const claudeArgs = {
|
|
7614
7904
|
...sharedServerArgs,
|
|
7615
7905
|
model: {
|
|
@@ -7857,7 +8147,7 @@ const claude = defineCommand({
|
|
|
7857
8147
|
process$1.stderr.write(`Peer MCP wired (backend=${backend}, personas=[${personaNames}], subagent .md files=${runtime.agentMdPaths.length}, ${subagentVisibility}).${skippedNote}\n`);
|
|
7858
8148
|
const sessionCwd = process$1.cwd();
|
|
7859
8149
|
if (workerToolsEnabled()) {
|
|
7860
|
-
const skillsToWrite = INJECTED_SKILLS.filter((s) => s.name
|
|
8150
|
+
const skillsToWrite = INJECTED_SKILLS.filter((s) => !isFirstMateSkillName(s.name) || agentToolsEnabled());
|
|
7861
8151
|
let skillsWritten = 0;
|
|
7862
8152
|
for (const s of skillsToWrite) if ((await writeInjectedSkill(s.name, s.md).catch(() => ({ written: false }))).written) skillsWritten++;
|
|
7863
8153
|
try {
|
|
@@ -7880,6 +8170,16 @@ const claude = defineCommand({
|
|
|
7880
8170
|
process$1.stderr.write(`Injected skills (${skillsWritten}/${skillsToWrite.length}): ${skillNames}.\n`);
|
|
7881
8171
|
}
|
|
7882
8172
|
}
|
|
8173
|
+
if (agentToolsEnabled()) {
|
|
8174
|
+
let shapingInstalled = false;
|
|
8175
|
+
try {
|
|
8176
|
+
await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildFirstMateGuardHookCommand(process$1.execPath, process$1.argv[1]), "PreToolUse", void 0, FIRST_MATE_GUARD_MATCHER);
|
|
8177
|
+
shapingInstalled = true;
|
|
8178
|
+
} catch (err) {
|
|
8179
|
+
consola.error(`Could not register the operator worker/orchestrate guard hook: ${String(err)}`);
|
|
8180
|
+
}
|
|
8181
|
+
assertShapingInstalled(true, shapingInstalled);
|
|
8182
|
+
}
|
|
7883
8183
|
const aiordieSidecar = (process$1.env.AIORDIE_CLAUDE_BIND ?? "").trim();
|
|
7884
8184
|
if (aiordieSidecar.length > 0 && !aiordieSidecar.includes("\"")) try {
|
|
7885
8185
|
const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
|
|
@@ -8936,6 +9236,7 @@ const internalStopHook = defineCommand({
|
|
|
8936
9236
|
isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),
|
|
8937
9237
|
resolveChecks: mode ? buildResolveChecks(mode) : void 0,
|
|
8938
9238
|
timeoutMs: Number.isFinite(timeoutEnv) && timeoutEnv > 0 ? timeoutEnv : void 0,
|
|
9239
|
+
planMode: stopGatePlanMode(),
|
|
8939
9240
|
reviewDebounce: reviewEnabled ? fileReviewDebounce(stopReviewStateDir()) : void 0,
|
|
8940
9241
|
spawnReview: reviewEnabled ? (ctx) => spawnStopReview(ctx, {
|
|
8941
9242
|
prompt: userPrompt,
|
|
@@ -9381,7 +9682,7 @@ process.on("uncaughtException", (error) => {
|
|
|
9381
9682
|
const version = getPackageVersion();
|
|
9382
9683
|
const argv = process.argv.slice(2);
|
|
9383
9684
|
const isVersionFlag = argv.includes("--version");
|
|
9384
|
-
const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-artifact-open" || argv[0] === "internal-worker-guard";
|
|
9685
|
+
const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-artifact-open" || argv[0] === "internal-first-mate-guard" || argv[0] === "internal-worker-guard";
|
|
9385
9686
|
if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
|
|
9386
9687
|
await runMain(defineCommand({
|
|
9387
9688
|
meta: {
|
|
@@ -9402,6 +9703,7 @@ await runMain(defineCommand({
|
|
|
9402
9703
|
"internal-stop-review": internalStopReview,
|
|
9403
9704
|
"internal-session-bind": internalSessionBind,
|
|
9404
9705
|
"internal-artifact-open": internalArtifactOpen,
|
|
9706
|
+
"internal-first-mate-guard": internalFirstMateGuard,
|
|
9405
9707
|
"internal-worker-guard": internalWorkerGuard
|
|
9406
9708
|
}
|
|
9407
9709
|
}));
|