github-router 0.3.163 → 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.163",
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";
@@ -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.163";
4347
+ var version$1 = "0.3.168";
4099
4348
 
4100
4349
  //#endregion
4101
4350
  //#region src/lib/approval.ts
@@ -7283,6 +7532,15 @@ async function setupAndServe(options) {
7283
7532
  const url = srvxServer.url;
7284
7533
  if (!url) throw new Error("Server started but URL is not available");
7285
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
+ }
7286
7544
  return {
7287
7545
  server: srvxServer,
7288
7546
  serverUrl
@@ -7639,6 +7897,9 @@ function getCodexEnvVars(serverUrl) {
7639
7897
 
7640
7898
  //#endregion
7641
7899
  //#region src/claude.ts
7900
+ function isFirstMateSkillName(name$1) {
7901
+ return name$1 === "gh-first-mate" || name$1 === "gh-first-mate-scaffold";
7902
+ }
7642
7903
  const claudeArgs = {
7643
7904
  ...sharedServerArgs,
7644
7905
  model: {
@@ -7886,7 +8147,7 @@ const claude = defineCommand({
7886
8147
  process$1.stderr.write(`Peer MCP wired (backend=${backend}, personas=[${personaNames}], subagent .md files=${runtime.agentMdPaths.length}, ${subagentVisibility}).${skippedNote}\n`);
7887
8148
  const sessionCwd = process$1.cwd();
7888
8149
  if (workerToolsEnabled()) {
7889
- const skillsToWrite = INJECTED_SKILLS.filter((s) => s.name !== "gh-first-mate" || agentToolsEnabled());
8150
+ const skillsToWrite = INJECTED_SKILLS.filter((s) => !isFirstMateSkillName(s.name) || agentToolsEnabled());
7890
8151
  let skillsWritten = 0;
7891
8152
  for (const s of skillsToWrite) if ((await writeInjectedSkill(s.name, s.md).catch(() => ({ written: false }))).written) skillsWritten++;
7892
8153
  try {
@@ -7909,6 +8170,16 @@ const claude = defineCommand({
7909
8170
  process$1.stderr.write(`Injected skills (${skillsWritten}/${skillsToWrite.length}): ${skillNames}.\n`);
7910
8171
  }
7911
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
+ }
7912
8183
  const aiordieSidecar = (process$1.env.AIORDIE_CLAUDE_BIND ?? "").trim();
7913
8184
  if (aiordieSidecar.length > 0 && !aiordieSidecar.includes("\"")) try {
7914
8185
  const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
@@ -8965,6 +9236,7 @@ const internalStopHook = defineCommand({
8965
9236
  isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),
8966
9237
  resolveChecks: mode ? buildResolveChecks(mode) : void 0,
8967
9238
  timeoutMs: Number.isFinite(timeoutEnv) && timeoutEnv > 0 ? timeoutEnv : void 0,
9239
+ planMode: stopGatePlanMode(),
8968
9240
  reviewDebounce: reviewEnabled ? fileReviewDebounce(stopReviewStateDir()) : void 0,
8969
9241
  spawnReview: reviewEnabled ? (ctx) => spawnStopReview(ctx, {
8970
9242
  prompt: userPrompt,
@@ -9410,7 +9682,7 @@ process.on("uncaughtException", (error) => {
9410
9682
  const version = getPackageVersion();
9411
9683
  const argv = process.argv.slice(2);
9412
9684
  const isVersionFlag = argv.includes("--version");
9413
- 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";
9414
9686
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
9415
9687
  await runMain(defineCommand({
9416
9688
  meta: {
@@ -9431,6 +9703,7 @@ await runMain(defineCommand({
9431
9703
  "internal-stop-review": internalStopReview,
9432
9704
  "internal-session-bind": internalSessionBind,
9433
9705
  "internal-artifact-open": internalArtifactOpen,
9706
+ "internal-first-mate-guard": internalFirstMateGuard,
9434
9707
  "internal-worker-guard": internalWorkerGuard
9435
9708
  }
9436
9709
  }));