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.
@@ -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.162",
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 { A as BROWSE_DEFAULT_MODEL, F as REVIEW_DEFAULT_MODEL, I as appendPlanReminder, L as runWorkerAgent, M as EXPLORE_DEFAULT_MODEL, N as IMPLEMENT_DEFAULT_MODEL, P as PLAN_DEFAULT_MODEL, R as withNoOutputRetry, j as DEFAULT_MODEL } from "./peer-mcp-personas-Bfq3FPKc.js";
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 buildOpenAIErrorEvent, $t as resolveModel, At as ArtifactClient, B as buildToolbeltAwareness, Bt as pickClaudeDefault, C as repoFingerprint, Ct as parseJsonOrDiagnose, D as trustRepo, Dt as extractTarGzMember, E as stopReviewStateDir, Et as provisionAndIndexColbert, Ft as DEFAULT_CODEX_MODEL_FALLBACKS, G as assetFor, Gt as setupGitHubToken, H as toolbeltSkipSet, Ht as withInstallLock, It as DEFAULT_PORT, J as ADVISOR_TOOL_INSTRUCTIONS, Jt as cacheModels, K as searchWeb, Kt as tryRefreshAndRetry, Lt as UPSTREAM_FETCH_TIMEOUT_MS, Mt as toolbeltPathOverride, Nt as DEFAULT_CLAUDE_MODEL_FALLBACKS, O as resolveSealedGate, Ot as extractZipMember, Pt as DEFAULT_CODEX_MODEL, Q as buildAnthropicErrorEvent, Qt as resolveCodexModel, Rt as UPSTREAM_INACTIVITY_TIMEOUT_MS, S as isSubagentContext, St as readResponseBodyCapped, T as stopGateEnabledForRepo, Tt as hasSupportedBrowserInstalled, U as vscodeRipgrepPath, Ut as setupCopilotToken, V as toolbeltEnabled, Vt as getPackageVersion, W as TOOLBELT_TOOLS, Wt as setupGitHubAgentToken, X as injectAdvisorTool, Xt as filterBetaHeader, Y as buildAdvisorStream, Yt as cacheVSCodeVersion, Z as isAdvisorRequested, Zt as isNullish, _ as stopReviewEnabled, _t as resolveMcpToolTimeoutMs, a as buildPeerAwarenessSnippet, an as GITHUB_API_BASE_URL, at as handleMcpPost, b as fileLastPromptStore, bt as createChatCompletions, c as buildSessionBindHookCommand, cn as githubHeaders, ct as browserToolsEnabled, d as decideStopHook, dt as standInToolEnabled, en as sleep, et as isControllerClosedError, f as fileBlockBudget, ft as workerToolsEnabled, g as stopGateId, gt as assembleResponsesPayload, h as stopGateDisabled, ht as getTokenCount, i as buildAgentPrompt, in as forwardError, it as handleMcpDelete, jt as collapsePathKeys, k as liveExec, kt as shouldUseInsecureTls, l as buildStopHookCommand, ln as state, lt as fleetToolsEnabled, m as launchBaselineKey, mt as createMessages, n as MCP_GROUPS, nn as fetchWithTransientRetry, nt as readIteratorWithTimeout, o as personasFor, on as copilotBaseUrl, ot as agentToolsEnabled, p as injectStopHookIntoSettingsFile, pt as countTokens, q as ADVISOR_INTERNAL_TOOL_NAME, qt as cacheCopilotVersion, r as assertMcpToolSurfaceConsistent, rn as HTTPError, rt as relayAnthropicStream, s as buildArtifactOpenHookCommand, sn as copilotHeaders, st as browseAgentEnabled, t as GROUP_META, tn as getModels, tt as logStreamError, u as captureLaunchBaseline, ut as implementerSubagentModel, v as fileBaselineStore, vt as pickEndpoint, w as repoRoot, wt as provisionBrowserAssets, x as fileReviewDebounce, xt as MAX_RESPONSE_BODY_BYTES, y as fileFindingsStore, yt as createResponses, z as availableToolCommands, zt as generateRandomPort } from "./peer-mcp-personas-Bfq3FPKc.js";
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-DHFa2_hK.js");
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; v1 mission registration is intentionally simple and later controller wakes/model requests drive decomposition and steering.
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.162";
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: parseTools(body.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 !== "gh-first-mate" || agentToolsEnabled());
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
  }));