github-router 0.3.163 → 0.3.175

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/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 isAdvisorRequested, $t as cacheModels, A as liveExec, At as hasSupportedBrowserInstalled, Bt as DEFAULT_CODEX_MODEL_FALLBACKS, C as fileReviewDebounce, Ct as pickEndpoint, D as stopGateEnabledForRepo, Dt as readResponseBodyCapped, E as repoRoot, Et as MAX_RESPONSE_BODY_BYTES, Ft as ArtifactClient, G as vscodeRipgrepPath, Gt as pickClaudeDefault, H as buildToolbeltAwareness, Ht as UPSTREAM_FETCH_TIMEOUT_MS, It as collapsePathKeys, J as searchWeb, Jt as setupCopilotToken, K as TOOLBELT_TOOLS, Kt as getPackageVersion, Lt as toolbeltPathOverride, Mt as extractTarGzMember, Nt as extractZipMember, O as stopReviewStateDir, Ot as parseJsonOrDiagnose, Pt as shouldUseInsecureTls, Q as injectAdvisorTool, Qt as cacheCopilotVersion, Rt as DEFAULT_CLAUDE_MODEL_FALLBACKS, S as fileLastPromptStore, St as resolveMcpToolTimeoutMs, T as repoFingerprint, Tt as createChatCompletions, U as toolbeltEnabled, Ut as UPSTREAM_INACTIVITY_TIMEOUT_MS, V as availableToolCommands, Vt as DEFAULT_PORT, W as toolbeltSkipSet, Wt as generateRandomPort, X as ADVISOR_TOOL_INSTRUCTIONS, Xt as setupGitHubToken, Y as ADVISOR_INTERNAL_TOOL_NAME, Yt as setupGitHubAgentToken, Z as buildAdvisorStream, Zt as tryRefreshAndRetry, _ as stopGateId, _t as workerToolsEnabled, a as buildPeerAwarenessSnippet, an as sleep, at as relayAnthropicStream, b as fileBaselineStore, bt as getTokenCount, c as buildArtifactOpenHookCommand, cn as HTTPError, ct as agentToolsEnabled, d as captureLaunchBaseline, dn as copilotBaseUrl, dt as browserCompoundToolsEnabled, en as cacheVSCodeVersion, et as buildAnthropicErrorEvent, f as decideStopHook, fn as copilotHeaders, ft as browserToolsEnabled, g as stopGateDisabled, gt as standInToolEnabled, h as launchBaselineKey, ht as implementerSubagentModel, i as buildAgentPrompt, in as resolveModel, it as readIteratorWithTimeout, j as resolveSealedGate, jt as provisionAndIndexColbert, k as trustRepo, kt as provisionBrowserAssets, l as buildSessionBindHookCommand, ln as forwardError, lt as artifactToolsEnabled, m as injectStopHookIntoSettingsFile, mn as state, mt as geminiAvailable, n as MCP_GROUPS, nn as isNullish, nt as isControllerClosedError, o as buildPeerAwarenessSummary, on as getModels, ot as handleMcpDelete, p as fileBlockBudget, pn as githubHeaders, pt as fleetToolsEnabled, q as assetFor, qt as withInstallLock, r as assertMcpToolSurfaceConsistent, rn as resolveCodexModel, rt as logStreamError, s as personasFor, sn as fetchWithTransientRetry, st as handleMcpPost, t as GROUP_META, tn as filterBetaHeader, tt as buildOpenAIErrorEvent, u as buildStopHookCommand, un as GITHUB_API_BASE_URL, ut as browseAgentEnabled, v as stopGatePlanMode, vt as countTokens, w as isSubagentContext, wt as createResponses, x as fileFindingsStore, xt as assembleResponsesPayload, y as stopReviewEnabled, yt as createMessages, zt as DEFAULT_CODEX_MODEL } from "./peer-mcp-personas-BCGYWok0.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";
@@ -16,11 +16,11 @@ import * as fs$2 from "node:fs";
16
16
  import fs$1, { existsSync, mkdirSync, promises, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
17
17
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
18
18
  import { Writable } from "node:stream";
19
+ import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from "node:timers";
19
20
  import { serve } from "srvx";
20
21
  import { getProxyForUrl } from "proxy-from-env";
21
22
  import { Hono } from "hono";
22
23
  import { cors } from "hono/cors";
23
- import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from "node:timers";
24
24
  import { Marked } from "marked";
25
25
  import clipboard from "clipboardy";
26
26
 
@@ -969,6 +969,9 @@ function dispatcherDescription(mode) {
969
969
  * relay verbatim, do nothing else. */
970
970
  function dispatcherPrompt(mode, workersKey) {
971
971
  const tool = workerToolName(workersKey, mode);
972
+ const briefField = mode === "browse" ? "task" : "prompt";
973
+ const briefDescription = mode === "browse" ? "the lead's browse task, copied verbatim" : "the lead's worker brief, copied verbatim";
974
+ const modeSpecificPassThrough = mode === "implement" || mode === "test" ? "\n - `worktree` (optional): pass `true` if the lead asked for isolated-worktree execution" : mode === "browse" ? "\n - `sessionId` (optional): pass through if the lead specified one" : "";
972
975
  return [
973
976
  `# Subagent: ${dispatcherAgentName(mode)}`,
974
977
  "",
@@ -978,10 +981,10 @@ function dispatcherPrompt(mode, workersKey) {
978
981
  "## Your only job",
979
982
  "",
980
983
  `Call the \`${tool}\` tool EXACTLY ONCE, passing through the fields from the lead's brief:`,
981
- " - `prompt`: the lead's worker brief, copied verbatim",
984
+ ` - \`${briefField}\`: ${briefDescription}`,
982
985
  " - `workspace` (optional): absolute path, if the lead specified one",
983
986
  " - `model` / `thinking` (optional): only if the lead specified them",
984
- " - `maxWallClockMs` (optional): per-call wall-clock budget in ms, if the lead specified one" + (mode === "implement" || mode === "test" ? "\n - `worktree` (optional): pass `true` if the lead asked for isolated-worktree execution" : ""),
987
+ " - `maxWallClockMs` (optional): per-call wall-clock budget in ms, if the lead specified one" + modeSpecificPassThrough,
985
988
  "",
986
989
  "When the tool returns, output its result VERBATIM as your final message. That final",
987
990
  "message is what the lead receives in the completion notification — it IS the result.",
@@ -1001,7 +1004,7 @@ function dispatcherPrompt(mode, workersKey) {
1001
1004
  * tool names, so this grants exactly the workers tools and NOTHING else — no
1002
1005
  * Agent/Task (so it cannot spawn further agents → no recursion), no Read/Bash
1003
1006
  * (so it cannot do extra work). The dispatcher's prompt narrows it to the one
1004
- * mode; the guard allows any dispatcher-named caller regardless. */
1007
+ * mode; the guard allows only the exact dispatcher for that worker mode. */
1005
1008
  function dispatcherTools(_mode, workersKey) {
1006
1009
  return [`mcp__${workersKey}__*`];
1007
1010
  }
@@ -1736,6 +1739,270 @@ function listModelsForEndpoint(path) {
1736
1739
  }).map((m) => m.id);
1737
1740
  }
1738
1741
 
1742
+ //#endregion
1743
+ //#region src/lib/orchestration/hook-mcp-client.ts
1744
+ /**
1745
+ * Read the proxy URL + nonce the launcher injected into the spawned child env
1746
+ * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when
1747
+ * either is absent — the hook then skips its LLM layer and falls back to its
1748
+ * deterministic / regex behavior.
1749
+ */
1750
+ function hookMcpRuntimeFromEnv(env = process.env) {
1751
+ const serverUrl = (env.GH_ROUTER_HOOK_MCP_URL ?? "").trim();
1752
+ const nonce = (env.GH_ROUTER_HOOK_NONCE ?? "").trim();
1753
+ if (serverUrl.length === 0 || nonce.length === 0) return void 0;
1754
+ return {
1755
+ serverUrl,
1756
+ nonce
1757
+ };
1758
+ }
1759
+ /**
1760
+ * POST a JSON-RPC `tools/call` and return the tool's text + isError. Throws on
1761
+ * any transport/HTTP/parse failure (caller fails open). A JSON-RPC `error`
1762
+ * envelope is mapped to `{ text: message, isError: true }` (a well-formed
1763
+ * negative result, not a transport failure).
1764
+ */
1765
+ async function callMcpTool(opts) {
1766
+ const body = await postJson(`${opts.runtime.serverUrl.replace(/\/+$/, "")}/mcp/${opts.group}`, {
1767
+ jsonrpc: "2.0",
1768
+ id: 1,
1769
+ method: "tools/call",
1770
+ params: {
1771
+ name: opts.tool,
1772
+ arguments: opts.args
1773
+ }
1774
+ }, {
1775
+ timeoutMs: opts.timeoutMs,
1776
+ signal: opts.signal,
1777
+ headers: { Authorization: `Bearer ${opts.runtime.nonce}` }
1778
+ });
1779
+ const rpc = body && typeof body === "object" ? body : {};
1780
+ if (rpc.error) return {
1781
+ text: rpc.error.message ?? "MCP error",
1782
+ isError: true
1783
+ };
1784
+ return {
1785
+ text: (Array.isArray(rpc.result?.content) ? rpc.result.content : []).filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join(""),
1786
+ isError: rpc.result?.isError === true
1787
+ };
1788
+ }
1789
+ /**
1790
+ * One non-streaming gpt-5.5 (or any model id) inference via `/v1/responses`.
1791
+ * Returns the assistant text (possibly empty). Throws on transport/HTTP/parse
1792
+ * failure. `effort` maps to the Responses `reasoning.effort` knob.
1793
+ */
1794
+ async function callInference(opts) {
1795
+ const body = await postJson(`${opts.serverUrl.replace(/\/+$/, "")}/v1/responses`, {
1796
+ model: opts.model,
1797
+ instructions: opts.instructions,
1798
+ input: [{
1799
+ role: "user",
1800
+ content: [{
1801
+ type: "input_text",
1802
+ text: opts.input
1803
+ }]
1804
+ }],
1805
+ stream: false,
1806
+ reasoning: { effort: opts.effort }
1807
+ }, {
1808
+ timeoutMs: opts.timeoutMs,
1809
+ signal: opts.signal
1810
+ });
1811
+ const out = [];
1812
+ const items = Array.isArray(body?.output) ? body.output : [];
1813
+ for (const item of items) {
1814
+ if (item?.type !== "message" || item.role !== "assistant") continue;
1815
+ const parts = Array.isArray(item.content) ? item.content : [];
1816
+ for (const part of parts) if ((part?.type === "output_text" || part?.type === "text") && typeof part.text === "string") out.push(part.text);
1817
+ }
1818
+ return out.join("");
1819
+ }
1820
+ /**
1821
+ * POST `payload` as JSON with a hard timeout, returning the parsed JSON body.
1822
+ * Throws on non-2xx, network error, timeout (AbortController), or non-JSON body.
1823
+ * An external `signal` is honored alongside the internal timeout.
1824
+ */
1825
+ async function postJson(url, payload, opts) {
1826
+ const controller = new AbortController();
1827
+ const timer = setTimeout$1(() => controller.abort(/* @__PURE__ */ new Error("hook MCP request timed out")), opts.timeoutMs);
1828
+ const onExternalAbort = () => controller.abort(/* @__PURE__ */ new Error("hook MCP request aborted"));
1829
+ if (opts.signal) if (opts.signal.aborted) onExternalAbort();
1830
+ else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
1831
+ try {
1832
+ const res = await fetch(url, {
1833
+ method: "POST",
1834
+ headers: {
1835
+ "Content-Type": "application/json",
1836
+ Accept: "application/json",
1837
+ ...opts.headers
1838
+ },
1839
+ body: JSON.stringify(payload),
1840
+ signal: controller.signal
1841
+ });
1842
+ if (!res.ok) throw new Error(`hook MCP request failed: HTTP ${res.status}`);
1843
+ return await res.json();
1844
+ } finally {
1845
+ clearTimeout$1(timer);
1846
+ if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
1847
+ }
1848
+ }
1849
+
1850
+ //#endregion
1851
+ //#region src/lib/orchestration/plan-review-hook.ts
1852
+ /** Minimum finalized-plan size worth spending a model review on. */
1853
+ const PLAN_REVIEW_MIN_CHARS = 220;
1854
+ /** Hard wall-clock for the advisory plan critic. */
1855
+ const PLAN_REVIEW_TIMEOUT_MS = 25e3;
1856
+ /** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */
1857
+ const MAX_REVIEWED_PLAN_CHARS = 80 * 1024;
1858
+ /** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */
1859
+ function planReviewEnabled(env = process.env) {
1860
+ return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true;
1861
+ }
1862
+ /** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */
1863
+ function filePlanReviewDebounce(stateDir) {
1864
+ const fileFor = (sid) => nodePath.join(stateDir, `plan-review-hash-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
1865
+ const readLast = async (sid) => {
1866
+ try {
1867
+ return (await promises.readFile(fileFor(sid), "utf8")).trim();
1868
+ } catch {
1869
+ return "";
1870
+ }
1871
+ };
1872
+ return {
1873
+ async shouldReview(sid, planHash) {
1874
+ if (planHash.length === 0) return false;
1875
+ return await readLast(sid) !== planHash;
1876
+ },
1877
+ async markReviewed(sid, planHash) {
1878
+ await promises.mkdir(stateDir, { recursive: true });
1879
+ await promises.writeFile(fileFor(sid), planHash, { mode: 384 });
1880
+ }
1881
+ };
1882
+ }
1883
+ function parseObject(raw) {
1884
+ try {
1885
+ const parsed = JSON.parse(raw);
1886
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1887
+ } catch {}
1888
+ }
1889
+ async function resolvePlanText(toolInput, readFile$1) {
1890
+ if (!toolInput || typeof toolInput !== "object") return "";
1891
+ const ti = toolInput;
1892
+ const planFilePath = typeof ti.planFilePath === "string" && ti.planFilePath.trim().length > 0 ? ti.planFilePath.trim() : "";
1893
+ if (planFilePath.length > 0) try {
1894
+ const fromFile = await readFile$1(planFilePath);
1895
+ if (fromFile.trim().length > 0) return fromFile;
1896
+ } catch {}
1897
+ return typeof ti.plan === "string" ? ti.plan : "";
1898
+ }
1899
+ /**
1900
+ * Decide whether the ExitPlanMode PostToolUse hook should spawn a detached review.
1901
+ * Pure aside from injected file reads + debounce store. Fail-open: any malformed
1902
+ * payload, missing runtime, subagent context, trivial/absent plan, or store error
1903
+ * returns a skip decision.
1904
+ */
1905
+ async function decidePlanReviewHook(input) {
1906
+ try {
1907
+ if (!input.runtimeAvailable) return {
1908
+ kind: "skip",
1909
+ reason: "missing-runtime"
1910
+ };
1911
+ const payload = parseObject(input.stdin);
1912
+ if (!payload) return {
1913
+ kind: "skip",
1914
+ reason: "bad-payload"
1915
+ };
1916
+ if (isSubagentContext(payload)) return {
1917
+ kind: "skip",
1918
+ reason: "subagent"
1919
+ };
1920
+ const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
1921
+ if (!sessionId) return {
1922
+ kind: "skip",
1923
+ reason: "missing-session"
1924
+ };
1925
+ const cwd = typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd;
1926
+ const trimmed = (await resolvePlanText(payload.tool_input, input.readFile ?? ((filePath) => promises.readFile(filePath, "utf8")))).trim();
1927
+ if (trimmed.length < (input.minChars ?? PLAN_REVIEW_MIN_CHARS)) return {
1928
+ kind: "skip",
1929
+ reason: "trivial-plan"
1930
+ };
1931
+ const planHash = createHash("sha256").update(trimmed).digest("hex");
1932
+ if (!await input.debounce.shouldReview(sessionId, planHash)) return {
1933
+ kind: "skip",
1934
+ reason: "debounced"
1935
+ };
1936
+ await input.debounce.markReviewed(sessionId, planHash);
1937
+ return {
1938
+ kind: "spawn",
1939
+ sessionId,
1940
+ cwd,
1941
+ plan: trimmed,
1942
+ planHash
1943
+ };
1944
+ } catch {
1945
+ return {
1946
+ kind: "skip",
1947
+ reason: "error"
1948
+ };
1949
+ }
1950
+ }
1951
+ function reviewBrief(plan, cwd) {
1952
+ const embeddedPlan = plan.length > MAX_REVIEWED_PLAN_CHARS ? `${plan.slice(0, MAX_REVIEWED_PLAN_CHARS)}\n\n[plan truncated at ${MAX_REVIEWED_PLAN_CHARS} characters]` : plan;
1953
+ return `You are an independent plan reviewer. A coding agent has just finalized a plan with ExitPlanMode. Your job is to find load-bearing risks before implementation starts: unstated assumptions, missing failure modes, gaps in verification, dangerous sequencing, or places where the plan could satisfy the letter of the request while missing the user's likely intent.
1954
+
1955
+ Be concise and skeptical. Do NOT rewrite the plan, do NOT praise it, and do NOT invent new product requirements. Report only material objections that would change what the agent should do next. Include file:line anchors when the plan names concrete files or when the risk depends on repo code. If the plan is sound, say exactly: "no material objection".
1956
+
1957
+ Workspace: ${cwd}\n\nFINALIZED PLAN:
1958
+ ` + embeddedPlan;
1959
+ }
1960
+ function hasMaterialFinding(text) {
1961
+ const normalized = text.trim().toLowerCase().replace(/[.!]+$/g, "");
1962
+ return normalized.length > 0 && normalized !== "no material objection" && normalized !== "no material objections";
1963
+ }
1964
+ function framePlanFindings(text) {
1965
+ return "PLAN REVIEW: an independent cross-lab critic reviewed the finalized plan and found the following material concern(s).\n" + text.trim();
1966
+ }
1967
+ /** Run the bounded critic call and write material findings to the shared findings store. */
1968
+ async function runPlanReview(input) {
1969
+ const timeoutMs = input.timeoutMs ?? PLAN_REVIEW_TIMEOUT_MS;
1970
+ let timer;
1971
+ const controller = new AbortController();
1972
+ try {
1973
+ const brief = reviewBrief(input.plan, input.cwd);
1974
+ const review = (input.callReview ?? ((prompt, signal) => callMcpTool({
1975
+ runtime: input.runtime,
1976
+ group: "peers",
1977
+ tool: "codex_critic",
1978
+ args: {
1979
+ prompt,
1980
+ effort: "high"
1981
+ },
1982
+ timeoutMs,
1983
+ signal
1984
+ })))(brief, controller.signal);
1985
+ review.catch(() => {});
1986
+ const raced = await Promise.race([review, new Promise((resolve) => {
1987
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
1988
+ })]);
1989
+ if (raced === "timeout" || raced.isError || !hasMaterialFinding(raced.text)) return;
1990
+ const framed = framePlanFindings(raced.text);
1991
+ const existing = await input.findingsStore.read(input.sessionId).catch(() => null);
1992
+ const next = existing && existing.trim().length > 0 ? `${existing.trim()}\n\n${framed}` : framed;
1993
+ await input.findingsStore.write(input.sessionId, next);
1994
+ } catch {} finally {
1995
+ if (timer) clearTimeout(timer);
1996
+ controller.abort();
1997
+ }
1998
+ }
1999
+ /** Build the command registered for PostToolUse(ExitPlanMode). */
2000
+ function buildPlanReviewHookCommand(execPath, scriptPath) {
2001
+ const q = (s) => `"${s}"`;
2002
+ if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-plan-review`;
2003
+ return `${q(execPath)} internal-plan-review`;
2004
+ }
2005
+
1739
2006
  //#endregion
1740
2007
  //#region src/lib/orchestration/harness-parse.ts
1741
2008
  /** The fast static checks that are always-on; `test` is opt-in (it runs project
@@ -2289,7 +2556,7 @@ async function discoverGateCommands(cwd, opts) {
2289
2556
  if (files.length === 0) return null;
2290
2557
  let result;
2291
2558
  try {
2292
- const { runWorkerAgent } = await import("./engine-DHFa2_hK.js");
2559
+ const { runWorkerAgent } = await import("./engine-cUomEg92.js");
2293
2560
  result = await runWorkerAgent({
2294
2561
  mode: "explore",
2295
2562
  workspace: root,
@@ -2350,9 +2617,9 @@ function decidePromptSubmit(input) {
2350
2617
  return decision;
2351
2618
  }
2352
2619
  /**
2353
- * Static encouragement injected for a TRIVIAL prompt (no model call, no latency
2354
- * tax): nudge parallel lexical+semantic search before concluding. Mirrors the v1
2355
- * advisory tone additive, never blocking.
2620
+ * Static encouragement injected only for a non-trivial prompt (no extra model
2621
+ * call, no latency tax): nudge parallel lexical+semantic search before
2622
+ * concluding. Mirrors the v1 advisory tone: additive, never blocking.
2356
2623
  */
2357
2624
  const PROMPT_SEARCH_TIP = "TIP (advisory): when this task needs code context, search lexical + semantic in parallel — one `mcp__search__code` call with mode:\"lexical\" and one with mode:\"semantic\", issued in the same turn — before concluding.";
2358
2625
  /** System prompt for the single gpt-5.5 scope/goal inference. Steers a SHORT,
@@ -2375,8 +2642,8 @@ function joinSections(sections) {
2375
2642
  *
2376
2643
  * - subagent/teammate -> empty (top-level only, like v1).
2377
2644
  * - findings -> always surfaced (+ cleared) regardless of triviality.
2378
- * - trivial prompt -> static search tip only (no model call).
2379
- * - substantive prompt -> parallel lexical+semantic search -> ONE gpt-5.5 call
2645
+ * - trivial prompt -> findings only (no search tip, no model call).
2646
+ * - substantive prompt -> static search tip + parallel lexical+semantic search -> ONE gpt-5.5 call
2380
2647
  * -> grounded scope/goal note. Fail-open to PROMPT_STEER_GOAL.
2381
2648
  * - steerEnabled=false -> findings only (no goal/tip).
2382
2649
  */
@@ -2407,7 +2674,7 @@ async function decidePromptSubmitV2(input) {
2407
2674
  return decision;
2408
2675
  }
2409
2676
  if (!isNonTrivialPrompt(prompt)) {
2410
- decision.inject = joinSections([PROMPT_SEARCH_TIP, findingsBlock]);
2677
+ decision.inject = findingsBlock;
2411
2678
  return decision;
2412
2679
  }
2413
2680
  const timeoutMs = input.io.timeoutMs ?? 22e3;
@@ -2429,7 +2696,11 @@ async function decidePromptSubmitV2(input) {
2429
2696
  if (timer) clearTimeout(timer);
2430
2697
  controller.abort();
2431
2698
  }
2432
- decision.inject = joinSections([goal, findingsBlock]);
2699
+ decision.inject = joinSections([
2700
+ PROMPT_SEARCH_TIP,
2701
+ goal,
2702
+ findingsBlock
2703
+ ]);
2433
2704
  return decision;
2434
2705
  }
2435
2706
  /**
@@ -2443,13 +2714,117 @@ function buildPromptSubmitHookCommand(execPath, scriptPath) {
2443
2714
  return `${q(execPath)} internal-prompt-submit`;
2444
2715
  }
2445
2716
 
2717
+ //#endregion
2718
+ //#region src/lib/first-mate/operator-shaping.ts
2719
+ /** MCP prefixes denied to the main operator; these remain subagent-only. */
2720
+ const OPERATOR_DENIED_MCP_PREFIXES = ["mcp__workers__", "mcp__orchestrate__"];
2721
+ /**
2722
+ * Decide whether a tool call must be blocked for the main operator. Pure — used
2723
+ * both by the PreToolUse hook handler and by config-assertion tests. When
2724
+ * operator mode is off, nothing is blocked (normal sessions unaffected).
2725
+ */
2726
+ function parseWorkerMode(toolName) {
2727
+ if (!toolName.startsWith("mcp__workers__")) return null;
2728
+ const mode = toolName.slice(14);
2729
+ return ALL_WORKER_DISPATCH_MODES.includes(mode) ? mode : null;
2730
+ }
2731
+ function isMatchingWorkerDispatcher(toolName, input) {
2732
+ const mode = parseWorkerMode(toolName);
2733
+ if (mode === null) return false;
2734
+ return input?.agent_type === dispatcherAgentName(mode);
2735
+ }
2736
+ function shouldDenyOperatorTool(toolName, operatorMode, input) {
2737
+ if (!operatorMode) return false;
2738
+ if (isMatchingWorkerDispatcher(toolName, input)) return false;
2739
+ return OPERATOR_DENIED_MCP_PREFIXES.some((prefix) => toolName.startsWith(prefix));
2740
+ }
2741
+ /**
2742
+ * M4 — fail-CLOSED. If operator/`--agents` mode is active but the reduced
2743
+ * worker/orchestrate guard could NOT be installed (e.g. settings.json unwritable),
2744
+ * the operator session must NOT start with direct main-thread worker/orchestrate
2745
+ * MCP calls available. Non-agents sessions are never affected.
2746
+ */
2747
+ function assertShapingInstalled(agentsMode, injectionSucceeded) {
2748
+ 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.");
2749
+ }
2750
+ /**
2751
+ * The PreToolUse hook decision for a given tool name in operator mode.
2752
+ *
2753
+ * Bash and file-authoring tools are always allowed. Only direct main-operator
2754
+ * calls to local worker/orchestrate MCP tools are blocked so those backends stay
2755
+ * reachable through the worker-* Agent subagents rather than the lead context.
2756
+ */
2757
+ function operatorPreToolUse(toolName, operatorMode, input) {
2758
+ if (!operatorMode) return { block: false };
2759
+ if (shouldDenyOperatorTool(toolName, operatorMode, input)) return {
2760
+ block: true,
2761
+ 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.`
2762
+ };
2763
+ return { block: false };
2764
+ }
2765
+
2766
+ //#endregion
2767
+ //#region src/internal-first-mate-guard.ts
2768
+ function readStdinSync() {
2769
+ try {
2770
+ return readFileSync(0, "utf8");
2771
+ } catch {
2772
+ return "";
2773
+ }
2774
+ }
2775
+ const internalFirstMateGuard = defineCommand({
2776
+ meta: {
2777
+ name: "internal-first-mate-guard",
2778
+ description: "PreToolUse guard enforcing cloud-agent operator capability shaping."
2779
+ },
2780
+ run() {
2781
+ let toolName = "";
2782
+ let hookInput;
2783
+ let parsed = false;
2784
+ try {
2785
+ const payload = JSON.parse(readStdinSync());
2786
+ if (typeof payload.tool_name === "string") toolName = payload.tool_name;
2787
+ if (typeof payload.agent_type === "string") hookInput = { agent_type: payload.agent_type };
2788
+ parsed = true;
2789
+ } catch {
2790
+ parsed = false;
2791
+ }
2792
+ if (toolName.length === 0) {
2793
+ if (!parsed) {
2794
+ process.stderr.write("operator guard: unparseable PreToolUse payload — blocking (fail-closed)");
2795
+ process.exit(2);
2796
+ }
2797
+ process.exit(0);
2798
+ }
2799
+ const decision = operatorPreToolUse(toolName, true, hookInput);
2800
+ if (decision.block) {
2801
+ process.stderr.write(decision.reason ?? `${toolName} is disabled in operator mode`);
2802
+ process.exit(2);
2803
+ }
2804
+ process.exit(0);
2805
+ }
2806
+ });
2807
+ /** The regex matcher scoping the guard hook to exactly the denied tools. */
2808
+ const FIRST_MATE_GUARD_MATCHER = "mcp__workers__.*|mcp__orchestrate__.*";
2809
+ /** Build the hook command string that runs this subcommand. */
2810
+ function buildFirstMateGuardHookCommand(execPath, entry) {
2811
+ return `${JSON.stringify(execPath)} ${JSON.stringify(entry)} internal-first-mate-guard`;
2812
+ }
2813
+
2814
+ //#endregion
2815
+ //#region src/lib/injected-skills/first-mate-setup-skill.ts
2816
+ const FIRST_MATE_SETUP_SKILL = {
2817
+ name: "gh-first-mate-scaffold",
2818
+ md: "---\nname: gh-first-mate-scaffold\ndescription: Scaffolds a repo-geared agentic-dev foundation through first-mate: seeds guidance files, role agents, ADRs, changelog, learnings, PR template, test instructions, Copilot setup, and CI through a scaffold branch and PR. Use before the first build wave on an owned repository.\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"
2819
+ };
2820
+
2446
2821
  //#endregion
2447
2822
  //#region src/lib/injected-skills/first-mate-skill.ts
2448
2823
  const FIRST_MATE_SKILL = {
2449
2824
  name: "gh-first-mate",
2450
2825
  md: `---
2451
2826
  name: gh-first-mate
2452
- description: Thin operating protocol for the first-mate GitHub cloud-agent controller: start missions, wake the durable controller loop, answer model and human requests, keep context compact, and report from the board/ledger rather than rereading full diffs or logs.
2827
+ description: Thin operating protocol for the first-mate GitHub cloud-agent controller: starts missions, wakes the durable loop, answers model and human requests, keeps context compact, and reports from the board/ledger. Use when the user wants first-mate to drive GitHub cloud coding agents across one or more repositories with foundation-first scaffolding and scoped, testable work.
2453
2828
  user-invocable: true
2454
2829
  ---
2455
2830
 
@@ -2459,6 +2834,18 @@ Use this skill when the user wants first-mate to drive GitHub cloud coding agent
2459
2834
  The first-mate controller is the durable system of record: missions, units, decisions, handles, and controller state live in its registry and ledger.
2460
2835
  Your job is to run the thin protocol, not to hold the mission in context.
2461
2836
 
2837
+ ## Foundation-first mandate
2838
+
2839
+ 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.
2840
+
2841
+ 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.
2842
+
2843
+ ## Scoped-work discipline
2844
+
2845
+ 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.
2846
+
2847
+ 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.
2848
+
2462
2849
  ## Start a mission
2463
2850
 
2464
2851
  For a new goal, call mcp__first-mate__start_mission with:
@@ -2469,7 +2856,7 @@ For a new goal, call mcp__first-mate__start_mission with:
2469
2856
  - priority and house_rules only when the user supplied them or they are necessary constraints.
2470
2857
 
2471
2858
  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.
2859
+ 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
2860
 
2474
2861
  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
2862
 
@@ -2496,6 +2883,7 @@ Keep verdicts small and typed to the request kind.
2496
2883
 
2497
2884
  Use the request's kind and payload as the contract:
2498
2885
 
2886
+ - 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
2887
  - 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
2888
  - 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
2889
  - 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 +2895,8 @@ Delegate heavy reading to workers:
2507
2895
  - Use the worker-review subagent (Agent tool) for compact review of a specific plan, PR summary, or suspicious change.
2508
2896
  - Use the worker-test subagent (Agent tool) when a missing executable check is the blocker.
2509
2897
 
2898
+ 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.
2899
+
2510
2900
  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
2901
 
2512
2902
  ## Human requests
@@ -2540,6 +2930,8 @@ Responsiveness (optional): the buckets above already tighten cadence to ~2 min f
2540
2930
 
2541
2931
  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
2932
 
2933
+ 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.
2934
+
2543
2935
  ## Board reports
2544
2936
 
2545
2937
  Use mcp__first-mate__board or mcp__first-mate__mission_status for read-only status checks.
@@ -2549,6 +2941,9 @@ Report compactly:
2549
2941
  - repositories
2550
2942
  - phase counts
2551
2943
  - blocked count and why, when available
2944
+ - per-unit handles for non-terminal units: unitId, issue/PR, phase, provider, validation, model, and blockedReason when present
2945
+ - resolved cloud-agent model per unit (surfaced on the board — verify model choice before approving plans)
2946
+ - terminal work only as summary counts; use include_all only when the user asks for completed/abandoned history
2552
2947
  - next wake time or the next requested action
2553
2948
 
2554
2949
  Never reconstruct status by rereading raw logs when the controller board already has the handles.
@@ -2580,7 +2975,7 @@ const FLOOR_KEEPER_SKILL = {
2580
2975
  name: "gh-floor-keeper",
2581
2976
  md: `---
2582
2977
  name: gh-floor-keeper
2583
- description: Done-checkpoint verification for non-trivial changes: run the executable gate, send the diff to OpenAI and Google reviewers, consult the advisor, reconcile findings by severity, author missing tests through a different lab when bounded and appropriate, and return an honest go/no-go before declaring work complete.
2978
+ description: Done-checkpoint verification for non-trivial changes: runs the executable gate, sends the diff to OpenAI and Google reviewers, consults the advisor, reconciles findings by severity, authors missing tests through a different lab when bounded and appropriate, and returns an honest go/no-go. Use before declaring non-trivial work complete.
2584
2979
  user-invocable: true
2585
2980
  ---
2586
2981
 
@@ -2731,7 +3126,7 @@ const ORCHESTRATE_SKILL = {
2731
3126
  name: "gh-orchestrate",
2732
3127
  md: `---
2733
3128
  name: gh-orchestrate
2734
- description: Right-sized blind-spot-elimination for non-trivial implementation asks: capture user-blessed acceptance criteria, delegate bounded research, decompose and plan, compose a native Workflow with explicit deterministic/advisory annotations, verify the workflow, checkpoint residual risks and cost, then run only when the pipeline actually raises the floor.
3129
+ description: Right-sized blind-spot-elimination for non-trivial implementation asks: captures user-blessed acceptance criteria, delegates bounded research, decomposes and plans, composes a native Workflow with explicit deterministic/advisory annotations, verifies the workflow, checkpoints residual risks and cost, then runs only when the pipeline actually raises the floor. Use when a non-trivial implementation ask benefits from workflow-level blind-spot reduction.
2735
3130
  user-invocable: true
2736
3131
  ---
2737
3132
 
@@ -2870,7 +3265,7 @@ const RESEARCH_SKILL = {
2870
3265
  name: "gh-research",
2871
3266
  md: `---
2872
3267
  name: gh-research
2873
- description: Bounded saturation research for non-trivial GitHub Router asks: enumerate unknowns, gather in parallel through code search, web search, and explore workers, adversarially verify load-bearing claims, persist a freshness-stamped brief, and return a compact confidence-tagged root-cause summary when you need grounded context before planning or changing code.
3268
+ description: Bounded saturation research for non-trivial GitHub Router asks: enumerates unknowns, gathers in parallel through code search, web search, and explore workers, adversarially verifies load-bearing claims, persists a freshness-stamped brief, and returns a compact confidence-tagged root-cause summary. Use when grounded context is needed before planning or changing code.
2874
3269
  user-invocable: true
2875
3270
  ---
2876
3271
 
@@ -2989,7 +3384,7 @@ const WORKER_SKILL = {
2989
3384
  name: "gh-worker",
2990
3385
  md: `---
2991
3386
  name: gh-worker
2992
- description: How to run github-router workers without blocking your turn. Workers (explore/implement/review/plan/test) can run up to 6 hours; dispatch the matching worker-* background subagent so you get a completion notification instead of waiting. Use whenever you would reach for a worker.
3387
+ description: Runs github-router workers without blocking the turn: dispatches the matching worker-* background subagent (explore/implement/review/plan/test, up to 6 hours each) for a completion notification instead of a blocking wait. Use when a task would benefit from a background worker for investigation, planning, review, implementation, or test authoring.
2993
3388
  user-invocable: true
2994
3389
  ---
2995
3390
 
@@ -3043,7 +3438,7 @@ const ARTIFACT_REVIEW_SKILL = {
3043
3438
  name: "gh-artifact-review",
3044
3439
  md: `---
3045
3440
  name: gh-artifact-review
3046
- description: Review plans and artifacts in the ai-or-die panel. Default to authoring a self-contained HTML artifact (rich, annotatable, optionally interactive) and opening THAT for the human, then drain feedback with artifact_await, revise, and end the loop. Use when running inside an ai-or-die tab and you have a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
3441
+ description: Reviews plans and artifacts in the ai-or-die panel. Defaults to authoring a self-contained HTML artifact (rich, annotatable, optionally interactive) and opening THAT for the human, then drains feedback with artifact_await, revises, and ends the loop. Use when running inside an ai-or-die tab and there is a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
3047
3442
  user-invocable: true
3048
3443
  ---
3049
3444
 
@@ -3134,7 +3529,12 @@ const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injecte
3134
3529
  const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
3135
3530
  const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
3136
3531
  const ARTIFACT_MARKER_CLOSE = "<!-- /gh-router artifact-panel directive -->";
3137
- const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by default)\n\nYou are running inside an ai-or-die tab, so the `mcp__peers__artifact_*` tools drive a live human-review panel. Default to an HTML artifact for anything the user should review before you proceed, not just plans but also design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Author a self-contained `.html` (inline CSS, no external deps, readable typography) and open it with `mcp__peers__artifact_open` (pass `mode:\"interactive\"` if it carries `data-aod-*` action controls); tell the user to review, then drain their feedback with `mcp__peers__artifact_await` (pass back the returned `cursor` each call), revise, `mcp__peers__artifact_reply`, and `mcp__peers__artifact_end` when done. `artifact_await` returns typed events: `comment` (free-text anchored by selector/text/sourceLine) and `action` (the human clicked a control you emitted). Use `mcp__peers__artifact_update`/`artifact_refresh` to change the shown content and `mcp__peers__artifact_dismiss` to hide the panel while keeping the review alive. `mcp__peers__artifact_poll` is a frozen legacy alias (comments only). Plan-mode plans are auto-rendered to HTML and auto-opened for you; raw markdown is only a fallback. Skip the panel only for trivial one-line answers.\n\nMake the HTML good: match the subject project's design system (its Tailwind / theme / tokens) when the artifact represents a specific app, otherwise clean readable defaults. Per-type cheatsheet. plan: goal, current state, proposed approach, risks / open questions. comparison: options as columns with trade-off rows and a recommendation. table: scannable rows with a sticky header. diagram: boxes + arrows (SVG/CSS or Mermaid). code / diff: `<pre>` with before/after. To let the user act (not just comment), emit declarative controls (no JS): `data-aod-action` (verb) + `data-aod-id` (stable) [+ `data-aod-value`] — a choose-one option fires on click, a multi-select is checkboxes sharing `data-aod-group` plus a submit button with that group. The `gh-artifact-review` skill carries the fuller playbook.";
3532
+ function ARTIFACT_PANEL_DIRECTIVE(peersKey = "peers") {
3533
+ const toolPrefix = `mcp__${peersKey}__artifact_`;
3534
+ return `## Review in the artifact panel (HTML by default)
3535
+
3536
+ You are running inside an ai-or-die tab, so the \`${toolPrefix}*\` tools drive a live human-review panel. Default to opening a self-contained HTML artifact for anything the user should review before you proceed: plans, design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Plan-mode plans are auto-rendered to HTML and opened for you; skip the panel only for trivial one-line answers. Run the \`gh-artifact-review\` skill for the full playbook: HTML + design conventions, the \`${toolPrefix}open\` / \`${toolPrefix}await\` (pass back the \`cursor\`) / \`${toolPrefix}reply\` / \`${toolPrefix}end\` loop, and the \`data-aod-*\` interactive controls.`;
3537
+ }
3138
3538
  /**
3139
3539
  * Writing / communication style directive injected at the TOP of the
3140
3540
  * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
@@ -3145,27 +3545,45 @@ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by defau
3145
3545
  */
3146
3546
  const STYLE_DIRECTIVE = "Write concisely without losing detail. Use a natural human voice. Avoid em dashes. Do not attribute work to Claude, AI, LLM, or Anthropic anywhere (commits, PRs, issues, code, comments, docs).";
3147
3547
  /**
3148
- * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md
3149
- * AND into the main agent's system prompt (`--append-system-prompt`), so it is
3150
- * the highest-salience behavioral default. Two defaults, both explicitly
3151
- * overridden by the user's own direction and the domain's standards:
3548
+ * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md.
3549
+ * The main agent's system prompt (`--append-system-prompt`) gets
3550
+ * OPERATING_DEFAULTS_DIGEST instead, with this full statement available through
3551
+ * CLAUDE.md. Three defaults, each explicitly overridden by the user's own
3552
+ * direction and the domain's standards:
3152
3553
  *
3153
3554
  * 1. Orchestrate (strong default): delegate the heavy / parallel /
3154
3555
  * context-heavy work to the right subagent / worker / model, keeping the
3155
3556
  * main context free to reason and collaborate with the user, while still
3156
3557
  * doing trivial / surgical / last-mile work directly (delegating that
3157
3558
  * would only add relay-fidelity loss + latency).
3158
- * 2. Excellence lens (HYBRID per the peer review): the reliable control
3159
- * signal is the PRINCIPLE stated plainly; the well-known names appear only
3160
- * as a "bar to clear" calibration, NOT as "channel X", with an explicit
3161
- * no-impersonation / no-theatrics guardrail (cross-lab critics: a named
3162
- * entity is a dense, high-variance vector that pulls in noise + persona
3163
- * mannerisms at top salience, so the principle leads and the name calibrates).
3559
+ * 2. Excellence lens: the principles stated plainly and concretely (radical
3560
+ * simplicity + real-user focus; whole-system first-principles thinking that
3561
+ * anticipates scale; work back from the customer outcome). Named exemplars
3562
+ * were dropped per the injected-surface review: a named entity is a dense,
3563
+ * high-variance vector that pulls in persona mannerisms at top salience, and
3564
+ * the guidance favors specific functional framing over comparison, so
3565
+ * specificity carries the vividness instead.
3566
+ * 3. Engineering excellence: quality / robustness / maintainability over
3567
+ * development cost; reproduce a bug end-to-end (as a real user hits it)
3568
+ * before fixing so the fix targets the real cause; a pixel-perfect UI bar;
3569
+ * and fix any lint error / test failure / flake on sight, whoever caused it,
3570
+ * folded into the current work rather than derailing the user's task (the
3571
+ * scope guardrail keeps proactive quality from becoming yak-shaving). The
3572
+ * digest carries a one-line form; the full statement lives here so it does
3573
+ * not cost the context window every turn.
3164
3574
  *
3165
3575
  * Self-referentially compliant with the style directive: no em dashes, no
3166
3576
  * Claude / Anthropic attribution.
3167
3577
  */
3168
- const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (worker-* agents for background non-blocking runs, Task subagents for parallel work, peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context.\n\nAim high. Default to radical simplicity and relentless focus on the user's real experience (the Jobs and Ive bar for design), whole-systems first-principles thinking that anticipates scale and the long arc (the Gates bar for architecture and tech), and customer obsession that works backwards from the outcome (the Bezos bar for product and business). Question every assumption and prefer what you can derive, reproduce, or test. Adopt the principles, not a persona: no impersonation, name-dropping, or theatrics.";
3578
+ const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (worker-* agents for background non-blocking runs, Task subagents for parallel work, peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context.\n\nAim high. Default to radical simplicity and a relentless focus on the user's real experience: design for the person and the job to be done, not the demo. Reason about the whole system from first principles, anticipating scale and the long arc rather than patching the surface. Work backwards from the outcome the user actually needs. Question every assumption and prefer what you can derive, reproduce, or test.\n\nEngineering excellence. When making technical decisions, give little weight to development cost; prefer quality, simplicity, robustness, scalability, and long-term maintainability. Fix a bug by first reproducing it end to end, as close to how a real user hits it as you can, so you solve the real problem and not a symptom. When testing a product end to end, be picky about the UI and obsessed with pixel perfection: if something clearly looks off, even when it is unrelated to your task, get it fixed along the way. Hold that same bar for the codebase itself: a lint error, a failing test, or a flaky test is worth fixing the moment you see it, whoever introduced it. Fold it into your current work rather than letting it derail the task the user actually asked for.";
3579
+ /**
3580
+ * Condensed digest of OPERATING_DEFAULTS_DIRECTIVE for the spawned session's
3581
+ * system prompt (--append-system-prompt). The FULL directive is prepended to
3582
+ * the mirrored CLAUDE.md (read by the main agent and descendants); this digest
3583
+ * keeps both behavioral directives at top salience without duplicating the full
3584
+ * ~310-token block in the context window every turn. Points to the full copy.
3585
+ */
3586
+ const OPERATING_DEFAULTS_DIGEST = "## Operating defaults (the user's explicit direction and the domain's standards always override)\n\nOrchestrate: delegate research, implementation, review, and large reads to the right subagent, worker, or model, preferring parallel delegation for independent work, so your own context stays free to reason and collaborate with the user; do trivial, surgical, and last-mile work directly. Aim high: default to radical simplicity and the user's real experience, whole-system first-principles thinking that anticipates scale and the long arc, and working backwards from the outcome the user actually needs; question every assumption and prefer what you can derive, reproduce, or test. Engineering excellence: prefer quality and long-term maintainability over dev cost; reproduce bugs end to end before fixing; keep a pixel-perfect UI bar; fix any lint, test failure, or flake on sight without letting it derail the task at hand. The full statement of these defaults is in your CLAUDE.md project instructions.";
3169
3587
  /**
3170
3588
  * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
3171
3589
  * the would-be post-write file) has grown past this size.
@@ -3506,10 +3924,11 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
3506
3924
  /**
3507
3925
  * Prepend the operating-defaults directive (orchestrator posture + hybrid
3508
3926
  * excellence lens; `OPERATING_DEFAULTS_DIRECTIVE` above) to the TOP of the
3509
- * mirrored CLAUDE.md so descendant agents (Agent subagents, agent-teams
3510
- * teammates) inherit it. The main agent gets the same text at higher salience
3511
- * via `--append-system-prompt`. Separate marker fence from the style / peer
3512
- * blocks so all coexist; best-effort (warn-and-continue) like its siblings.
3927
+ * mirrored CLAUDE.md so the main agent and descendant agents (Agent subagents,
3928
+ * agent-teams teammates) inherit the full statement. The main agent also gets
3929
+ * OPERATING_DEFAULTS_DIGEST at higher salience via `--append-system-prompt`.
3930
+ * Separate marker fence from the style / peer blocks so all coexist;
3931
+ * best-effort (warn-and-continue) like its siblings.
3513
3932
  */
3514
3933
  async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_DEFAULTS_DIRECTIVE) {
3515
3934
  await injectMarkerBlock({
@@ -3523,8 +3942,9 @@ async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_
3523
3942
  /**
3524
3943
  * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
3525
3944
  * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
3526
- * subagents, agent-teams teammates) learn about the provisioned tools.
3527
- * The main agent gets the same line via `--append-system-prompt`.
3945
+ * subagents, agent-teams teammates) and the main agent learn about the
3946
+ * provisioned tools via the mirrored CLAUDE.md. This line is not sent via
3947
+ * `--append-system-prompt`.
3528
3948
  * Separate marker fence from the peer-awareness / style blocks.
3529
3949
  */
3530
3950
  async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
@@ -3542,9 +3962,9 @@ async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
3542
3962
  * Gated by the caller on AIORDIE_SESSION_ID (only useful inside a tab). Separate
3543
3963
  * marker fence; best-effort like the style/peer blocks.
3544
3964
  */
3545
- async function prependArtifactPanelDirectiveToMirroredClaudeMd(directive = ARTIFACT_PANEL_DIRECTIVE) {
3965
+ async function prependArtifactPanelDirectiveToMirroredClaudeMd(peersKey = "peers") {
3546
3966
  await injectMarkerBlock({
3547
- snippet: directive,
3967
+ snippet: ARTIFACT_PANEL_DIRECTIVE(peersKey),
3548
3968
  markerOpen: ARTIFACT_MARKER_OPEN,
3549
3969
  markerClose: ARTIFACT_MARKER_CLOSE,
3550
3970
  position: "top",
@@ -3613,9 +4033,75 @@ const INJECTED_SKILLS = [
3613
4033
  ORCHESTRATE_SKILL,
3614
4034
  FLOOR_KEEPER_SKILL,
3615
4035
  WORKER_SKILL,
3616
- FIRST_MATE_SKILL
4036
+ FIRST_MATE_SKILL,
4037
+ FIRST_MATE_SETUP_SKILL
3617
4038
  ];
3618
4039
 
4040
+ //#endregion
4041
+ //#region src/lib/attribution-settings.ts
4042
+ /**
4043
+ * Deterministic backstop for the injected "no attribution" style directive.
4044
+ *
4045
+ * github-router injects a CLAUDE.md style directive telling the agent not to
4046
+ * attribute work to Claude / AI / Anthropic. That directive is ADVISORY (prose
4047
+ * the model may or may not follow). Claude Code also exposes a HARNESS-ENFORCED
4048
+ * control that suppresses attribution at the source: the `attribution`
4049
+ * settings.json key (Claude Code v2.0.62+; string `commit` / `pr` sub-fields),
4050
+ * which supersedes the deprecated boolean `includeCoAuthoredBy`. Setting both
4051
+ * sub-fields to the empty string removes the "Generated with Claude Code"
4052
+ * commit footer and the "Co-Authored-By: Claude" byline from commits and PRs.
4053
+ * Because the harness reads and applies this itself, it holds even when the
4054
+ * model ignores the prose directive — a deterministic backstop, not a second
4055
+ * suggestion.
4056
+ *
4057
+ * PRESENCE-GUARDED. If the mirrored settings already carry `attribution` OR
4058
+ * `includeCoAuthoredBy`, the user expressed a deliberate preference in their
4059
+ * real config (which the one-way mirror snapshotted); we DO NOT override it.
4060
+ * Only a user who has expressed no preference receives github-router's
4061
+ * no-attribution default. This keeps faith with "the user's explicit direction
4062
+ * always overrides".
4063
+ *
4064
+ * The mirror is a per-launch snapshot of the user's real `~/.claude` config, so
4065
+ * this write never propagates back to the user's own settings.json. A
4066
+ * project-scope `<repo>/.claude/settings.json` still overrides the mirror
4067
+ * (global scope) at runtime, so the default remains overridable per-repo.
4068
+ *
4069
+ * Failure model matches the sibling settings writers: a transient read error
4070
+ * or a non-object settings.json throws (never clobber a file we don't
4071
+ * understand); the caller wraps this in warn-and-continue so a settings-write
4072
+ * hiccup never blocks launch.
4073
+ */
4074
+ async function injectAttributionSuppressionIntoSettingsFile(settingsPath) {
4075
+ let existing = {};
4076
+ let raw;
4077
+ try {
4078
+ raw = await fs.readFile(settingsPath, "utf8");
4079
+ } catch (err) {
4080
+ if (err.code !== "ENOENT") throw err;
4081
+ raw = void 0;
4082
+ }
4083
+ if (raw !== void 0) {
4084
+ const parsed = JSON.parse(raw);
4085
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
4086
+ else throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`);
4087
+ }
4088
+ if ("attribution" in existing || "includeCoAuthoredBy" in existing) return {
4089
+ written: false,
4090
+ reason: "user-set"
4091
+ };
4092
+ const merged = {
4093
+ ...existing,
4094
+ attribution: {
4095
+ commit: "",
4096
+ pr: ""
4097
+ }
4098
+ };
4099
+ const tmp = `${settingsPath}.${process.pid}.attr.tmp`;
4100
+ await fs.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
4101
+ await fs.rename(tmp, settingsPath);
4102
+ return { written: true };
4103
+ }
4104
+
3619
4105
  //#endregion
3620
4106
  //#region src/lib/toolbelt/provision.ts
3621
4107
  /** Per-download cap (bytes) — these binaries are a few MB at most. */
@@ -4046,6 +4532,141 @@ async function stopKeepAwake() {
4046
4532
  releaseSync();
4047
4533
  }
4048
4534
 
4535
+ //#endregion
4536
+ //#region src/lib/first-mate/scheduler/autospawn.ts
4537
+ /**
4538
+ * Opt-in auto-spawn of the first-mate scheduler daemon as a SEPARATE process
4539
+ * (a hung tick must not starve the proxy event loop).
4540
+ *
4541
+ * DEFAULT OFF — gated by GH_ROUTER_FM_DAEMON === "1" (explicit opt-in). The
4542
+ * durable [fm-heartbeat] cron is the DEFAULT driver and the proven path; the
4543
+ * daemon is experimental and NOT yet safe as the default because two hardening
4544
+ * items remain (a verifier-stall wall-clock escalation, and an atomic-claim
4545
+ * lock in durable-store) — flip the default only once those land.
4546
+ *
4547
+ * Requirements + honest boundaries:
4548
+ * - The daemon entry is `scripts/first-mate-daemon.ts` (a TS source file run via
4549
+ * `bun`); it is NOT in the published dist tarball, so auto-spawn only fires
4550
+ * from a source checkout with `bun` available. If the script is absent we
4551
+ * no-op (never a false "spawned" log).
4552
+ * - Spawn failures NEVER crash the proxy: an async ENOENT (`bun` not on PATH)
4553
+ * arrives as an `'error'` EVENT, not a sync throw, so we attach an `'error'`
4554
+ * listener (without it the emitter re-throws → uncaughtException → exit(1)).
4555
+ * - The returned handle MUST be killed by the caller on shutdown (no `detached`);
4556
+ * an orphaned drive-primary daemon would keep merging PRs after the proxy is
4557
+ * gone. The parent ALSO holds the child's stdin write end (`stdio[0]==="pipe"`)
4558
+ * so it can EOF the child for a GRACEFUL stop (lease + pidfile released
4559
+ * immediately, not by expiry) — the cross-platform teardown trigger, since an
4560
+ * external SIGTERM on Windows is a hard kill the child can't observe. `kill()`
4561
+ * stays the hard backstop so a wedged child is never orphaned (see
4562
+ * `wireDaemonTeardown`).
4563
+ * - It owns the deterministic drive loop only; live judgments still wake the
4564
+ * lead via the heartbeat (no server->lead push).
4565
+ */
4566
+ function shouldAutoSpawnDaemon(env, agentsEnabled) {
4567
+ if (!agentsEnabled) return false;
4568
+ return env.GH_ROUTER_FM_DAEMON === "1";
4569
+ }
4570
+ /**
4571
+ * The real (non-injected) spawner: node:child_process (not the Bun runtime's spawn) so the
4572
+ * bundled dist/main.js stays node-loadable. No `detached` — the caller kills
4573
+ * this on shutdown. stdin is "pipe" (parent holds the write end for a graceful
4574
+ * EOF stop); stdout/stderr stay "ignore".
4575
+ */
4576
+ function nodeDaemonSpawn(cmd, spawnOpts, spawnImpl = spawn) {
4577
+ const proc = spawnImpl(cmd[0], cmd.slice(1), {
4578
+ env: spawnOpts.env,
4579
+ stdio: spawnOpts.stdio
4580
+ });
4581
+ proc.on("error", (err) => consola.debug("first-mate daemon spawn error (ignored):", err));
4582
+ return {
4583
+ pid: proc.pid,
4584
+ kill: () => void proc.kill("SIGKILL"),
4585
+ endStdin: () => {
4586
+ try {
4587
+ proc.stdin?.end();
4588
+ } catch {}
4589
+ }
4590
+ };
4591
+ }
4592
+ /**
4593
+ * Spawn the daemon child if the opt-in gate passes AND the daemon script is
4594
+ * present; else return undefined. Never throws, and never lets an async spawn
4595
+ * error crash bootstrap.
4596
+ */
4597
+ function maybeSpawnDaemon(opts) {
4598
+ const env = opts.env ?? process.env;
4599
+ if (!shouldAutoSpawnDaemon(env, opts.agentsEnabled)) return void 0;
4600
+ const script = nodePath.join(opts.repoRoot ?? process.cwd(), "scripts/first-mate-daemon.ts");
4601
+ if (opts.spawn === void 0 && !existsSync(script)) {
4602
+ consola.debug(`first-mate daemon entry not found (${script}); auto-spawn skipped.`);
4603
+ return;
4604
+ }
4605
+ try {
4606
+ const child = (opts.spawn ?? ((cmd, spawnOpts) => nodeDaemonSpawn(cmd, spawnOpts)))(["bun", script], {
4607
+ env,
4608
+ stdio: [
4609
+ "pipe",
4610
+ "ignore",
4611
+ "ignore"
4612
+ ]
4613
+ });
4614
+ return {
4615
+ pid: child.pid,
4616
+ kill: child.kill,
4617
+ endStdin: child.endStdin ?? (() => {})
4618
+ };
4619
+ } catch {
4620
+ return;
4621
+ }
4622
+ }
4623
+ /**
4624
+ * Wire the proxy's teardown to shut the daemon child down GRACEFULLY, then hard
4625
+ * kill as a backstop. Ordering (per teardown path):
4626
+ * 1. `endStdin()` — EOF the child so it releases its lease/pidfile cleanly (the
4627
+ * cross-platform trigger; on Windows an external SIGTERM would never run the
4628
+ * child's handler, so this EOF is the ONLY graceful path there).
4629
+ * 2. `kill()` — hard backstop so a wedged child is never orphaned (a prior
4630
+ * review blocker: an orphaned drive-primary daemon keeps merging PRs).
4631
+ *
4632
+ * On SIGINT/SIGTERM the kill is deferred by a short, NON-BLOCKING, unref'd grace
4633
+ * window (proxy exit is never delayed for it). On `'exit'` — where timers cannot
4634
+ * run — we EOF then kill synchronously as a last resort. All steps are
4635
+ * once-guarded and non-throwing.
4636
+ */
4637
+ function wireDaemonTeardown(handle, opts = {}) {
4638
+ const proc = opts.proc ?? process;
4639
+ const graceMs = opts.graceMs ?? 300;
4640
+ const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
4641
+ let killed = false;
4642
+ const kill = () => {
4643
+ if (killed) return;
4644
+ killed = true;
4645
+ try {
4646
+ handle.kill();
4647
+ } catch {}
4648
+ };
4649
+ let ended = false;
4650
+ const endStdin = () => {
4651
+ if (ended) return;
4652
+ ended = true;
4653
+ try {
4654
+ handle.endStdin();
4655
+ } catch {}
4656
+ };
4657
+ const onSignal = () => {
4658
+ endStdin();
4659
+ const t = setTimer(kill, graceMs);
4660
+ if (t && typeof t.unref === "function") t.unref();
4661
+ };
4662
+ proc.once("SIGINT", onSignal);
4663
+ proc.once("SIGTERM", onSignal);
4664
+ proc.once("exit", () => {
4665
+ endStdin();
4666
+ kill();
4667
+ });
4668
+ }
4669
+
4049
4670
  //#endregion
4050
4671
  //#region src/lib/proxy.ts
4051
4672
  function initProxyFromEnv() {
@@ -4095,7 +4716,7 @@ function initProxyFromEnv() {
4095
4716
  //#endregion
4096
4717
  //#region package.json
4097
4718
  var name = "github-router";
4098
- var version$1 = "0.3.163";
4719
+ var version$1 = "0.3.175";
4099
4720
 
4100
4721
  //#endregion
4101
4722
  //#region src/lib/approval.ts
@@ -7283,6 +7904,15 @@ async function setupAndServe(options) {
7283
7904
  const url = srvxServer.url;
7284
7905
  if (!url) throw new Error("Server started but URL is not available");
7285
7906
  const serverUrl = url.replace(/\/$/, "");
7907
+ try {
7908
+ const handle = maybeSpawnDaemon({ agentsEnabled: agentToolsEnabled() });
7909
+ if (handle) {
7910
+ wireDaemonTeardown(handle);
7911
+ consola.debug(`first-mate daemon spawn attempted (pid ${handle.pid ?? "?"}).`);
7912
+ }
7913
+ } catch (err) {
7914
+ consola.debug("first-mate daemon auto-spawn skipped:", err);
7915
+ }
7286
7916
  return {
7287
7917
  server: srvxServer,
7288
7918
  serverUrl
@@ -7639,6 +8269,9 @@ function getCodexEnvVars(serverUrl) {
7639
8269
 
7640
8270
  //#endregion
7641
8271
  //#region src/claude.ts
8272
+ function isFirstMateSkillName(name$1) {
8273
+ return name$1 === "gh-first-mate" || name$1 === "gh-first-mate-scaffold";
8274
+ }
7642
8275
  const claudeArgs = {
7643
8276
  ...sharedServerArgs,
7644
8277
  model: {
@@ -7836,14 +8469,15 @@ const claude = defineCommand({
7836
8469
  };
7837
8470
  let onShutdown = baseShutdown;
7838
8471
  let peerAwarenessSnippet;
8472
+ let peerAwarenessSummary;
7839
8473
  if (args["codex-mcp"] !== false) try {
7840
8474
  const requestedCli = args["codex-cli"] ?? false;
7841
8475
  const backend = resolveCodexCliBackend({
7842
8476
  requested: requestedCli,
7843
8477
  codexInfo: requestedCli ? getCodexVersion() : null
7844
8478
  });
7845
- const geminiAvailable = state.models?.data.some((m) => /^gemini-3\..*pro/i.test(m.id)) ?? false;
7846
- if (!geminiAvailable) consola.info("gemini-3.1-pro-preview not found in your Copilot model catalog; gemini-critic persona will not be registered.");
8479
+ const geminiModelsAvailable = geminiAvailable();
8480
+ if (!geminiModelsAvailable) consola.info("gemini-3.1-pro-preview not found in your Copilot model catalog; gemini-critic persona will not be registered.");
7847
8481
  const enabledGroups = [
7848
8482
  "peers",
7849
8483
  "search",
@@ -7857,7 +8491,7 @@ const claude = defineCommand({
7857
8491
  const { keys: groupKeys, skipped: skippedGroups } = await resolveGroupKeysFromMirror(enabledGroups);
7858
8492
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
7859
8493
  codexCli: backend === "cli",
7860
- geminiAvailable,
8494
+ geminiAvailable: geminiModelsAvailable,
7861
8495
  groupKeys,
7862
8496
  workerToolsAvailable: workerToolsEnabled(),
7863
8497
  browseAvailable: browseAgentEnabled(),
@@ -7872,7 +8506,7 @@ const claude = defineCommand({
7872
8506
  };
7873
8507
  const injected = await injectPeerMcpIntoMirror(serverUrl, {
7874
8508
  codexCli: backend === "cli",
7875
- geminiAvailable,
8509
+ geminiAvailable: geminiModelsAvailable,
7876
8510
  groupKeys,
7877
8511
  nonce: runtime.nonce
7878
8512
  });
@@ -7886,7 +8520,7 @@ const claude = defineCommand({
7886
8520
  process$1.stderr.write(`Peer MCP wired (backend=${backend}, personas=[${personaNames}], subagent .md files=${runtime.agentMdPaths.length}, ${subagentVisibility}).${skippedNote}\n`);
7887
8521
  const sessionCwd = process$1.cwd();
7888
8522
  if (workerToolsEnabled()) {
7889
- const skillsToWrite = INJECTED_SKILLS.filter((s) => s.name !== "gh-first-mate" || agentToolsEnabled());
8523
+ const skillsToWrite = INJECTED_SKILLS.filter((s) => !isFirstMateSkillName(s.name) || agentToolsEnabled());
7890
8524
  let skillsWritten = 0;
7891
8525
  for (const s of skillsToWrite) if ((await writeInjectedSkill(s.name, s.md).catch(() => ({ written: false }))).written) skillsWritten++;
7892
8526
  try {
@@ -7909,6 +8543,16 @@ const claude = defineCommand({
7909
8543
  process$1.stderr.write(`Injected skills (${skillsWritten}/${skillsToWrite.length}): ${skillNames}.\n`);
7910
8544
  }
7911
8545
  }
8546
+ if (agentToolsEnabled()) {
8547
+ let shapingInstalled = false;
8548
+ try {
8549
+ 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);
8550
+ shapingInstalled = true;
8551
+ } catch (err) {
8552
+ consola.error(`Could not register the operator worker/orchestrate guard hook: ${String(err)}`);
8553
+ }
8554
+ assertShapingInstalled(true, shapingInstalled);
8555
+ }
7912
8556
  const aiordieSidecar = (process$1.env.AIORDIE_CLAUDE_BIND ?? "").trim();
7913
8557
  if (aiordieSidecar.length > 0 && !aiordieSidecar.includes("\"")) try {
7914
8558
  const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
@@ -7918,10 +8562,10 @@ const claude = defineCommand({
7918
8562
  } catch (err) {
7919
8563
  consola.warn(`Could not register the ai-or-die session-bind hook: ${String(err)}`);
7920
8564
  }
7921
- if ((process$1.env.AIORDIE_SESSION_ID ?? "").trim().length > 0) {
8565
+ if (artifactToolsEnabled()) {
7922
8566
  await writeInjectedSkill(ARTIFACT_REVIEW_SKILL.name, ARTIFACT_REVIEW_SKILL.md).catch(() => ({ written: false }));
7923
8567
  try {
7924
- await prependArtifactPanelDirectiveToMirroredClaudeMd();
8568
+ await prependArtifactPanelDirectiveToMirroredClaudeMd(groupKeys.peers);
7925
8569
  } catch (err) {
7926
8570
  consola.warn(`Artifact-panel directive prepend failed: ${String(err)}`);
7927
8571
  }
@@ -7932,6 +8576,11 @@ const claude = defineCommand({
7932
8576
  consola.warn(`Could not register the artifact auto-open hook: ${String(err)}`);
7933
8577
  }
7934
8578
  }
8579
+ if (hookMcpRuntimeFromEnv(envVars) && planReviewEnabled()) try {
8580
+ await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildPlanReviewHookCommand(process$1.execPath, process$1.argv[1]), "PostToolUse", void 0, "ExitPlanMode");
8581
+ } catch (err) {
8582
+ consola.warn(`Could not register the advisory plan-review hook: ${String(err)}`);
8583
+ }
7935
8584
  if (args["trust-gate"] === true) try {
7936
8585
  const root = await trustRepo(sessionCwd);
7937
8586
  process$1.stderr.write(`Structural gate trusted for this repo (${root}); it will run on launch here from now on.\n`);
@@ -8035,19 +8684,30 @@ const claude = defineCommand({
8035
8684
  } else if (!gateDisabled && !descriptor && !discovering) process$1.stderr.write("Structural Stop-gate not enabled: no checks found in this repo (looked at package.json scripts / CI workflows / Makefile / Cargo.toml / go.mod / pyproject). Force a sealed gate with GH_ROUTER_ENABLE_STOP_GATE=1" + (workerToolsEnabled() ? ", or let a model discover them with GH_ROUTER_ENABLE_GATE_DISCOVERY=1" : "") + ".\n");
8036
8685
  const peerSnippet = buildPeerAwarenessSnippet({
8037
8686
  codexCli: backend === "cli",
8038
- geminiAvailable,
8687
+ geminiAvailable: geminiModelsAvailable,
8039
8688
  workerToolsAvailable: workerToolsEnabled(),
8040
8689
  standInAvailable: standInToolEnabled(),
8041
- browseAvailable: state.browseEnabled,
8690
+ browseAvailable: browserToolsEnabled(),
8691
+ compoundBrowseAvailable: browserCompoundToolsEnabled(),
8042
8692
  powerBrowseAvailable: state.powerBrowseEnabled,
8693
+ fleetAvailable: fleetToolsEnabled(),
8043
8694
  agentToolsAvailable: agentToolsEnabled(),
8695
+ implementerAvailable: implementerSubagentModel() != null,
8044
8696
  groupKeys
8045
8697
  });
8046
8698
  peerAwarenessSnippet = peerSnippet;
8699
+ peerAwarenessSummary = buildPeerAwarenessSummary({
8700
+ workerToolsAvailable: workerToolsEnabled(),
8701
+ standInAvailable: standInToolEnabled(),
8702
+ browseAvailable: browserToolsEnabled(),
8703
+ fleetAvailable: fleetToolsEnabled(),
8704
+ agentToolsAvailable: agentToolsEnabled(),
8705
+ groupKeys
8706
+ });
8047
8707
  try {
8048
8708
  await appendPeerAwarenessToMirroredClaudeMd(peerSnippet);
8049
8709
  } catch (err) {
8050
- consola.warn(`Peer-awareness CLAUDE.md append failed (main agent still covered via --append-system-prompt): ${err instanceof Error ? err.message : String(err)}`);
8710
+ consola.warn(`Peer-awareness CLAUDE.md append failed (agent keeps tool descriptions via tools/list, loses the inventory overview): ${err instanceof Error ? err.message : String(err)}`);
8051
8711
  }
8052
8712
  try {
8053
8713
  await prependStyleDirectiveToMirroredClaudeMd();
@@ -8057,12 +8717,17 @@ const claude = defineCommand({
8057
8717
  } catch (err) {
8058
8718
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
8059
8719
  }
8060
- extraArgs.push("--append-system-prompt", peerAwarenessSnippet ? `${OPERATING_DEFAULTS_DIRECTIVE}\n\n${peerAwarenessSnippet}` : OPERATING_DEFAULTS_DIRECTIVE);
8720
+ extraArgs.push("--append-system-prompt", peerAwarenessSnippet && peerAwarenessSummary ? `${OPERATING_DEFAULTS_DIGEST}\n\n${peerAwarenessSummary}` : OPERATING_DEFAULTS_DIGEST);
8061
8721
  try {
8062
8722
  await prependOperatingDefaultsToMirroredClaudeMd();
8063
8723
  } catch (err) {
8064
8724
  consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
8065
8725
  }
8726
+ if (process$1.env.GH_ROUTER_DISABLE_NO_ATTRIBUTION !== "1") try {
8727
+ await injectAttributionSuppressionIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"));
8728
+ } catch (err) {
8729
+ consola.warn(`No-attribution settings backstop skipped: ${err instanceof Error ? err.message : String(err)}`);
8730
+ }
8066
8731
  launchChild({
8067
8732
  kind: "claude-code",
8068
8733
  envVars,
@@ -8221,114 +8886,6 @@ const debug = defineCommand({
8221
8886
  }
8222
8887
  });
8223
8888
 
8224
- //#endregion
8225
- //#region src/lib/orchestration/hook-mcp-client.ts
8226
- /**
8227
- * Read the proxy URL + nonce the launcher injected into the spawned child env
8228
- * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when
8229
- * either is absent — the hook then skips its LLM layer and falls back to its
8230
- * deterministic / regex behavior.
8231
- */
8232
- function hookMcpRuntimeFromEnv(env = process.env) {
8233
- const serverUrl = (env.GH_ROUTER_HOOK_MCP_URL ?? "").trim();
8234
- const nonce = (env.GH_ROUTER_HOOK_NONCE ?? "").trim();
8235
- if (serverUrl.length === 0 || nonce.length === 0) return void 0;
8236
- return {
8237
- serverUrl,
8238
- nonce
8239
- };
8240
- }
8241
- /**
8242
- * POST a JSON-RPC `tools/call` and return the tool's text + isError. Throws on
8243
- * any transport/HTTP/parse failure (caller fails open). A JSON-RPC `error`
8244
- * envelope is mapped to `{ text: message, isError: true }` (a well-formed
8245
- * negative result, not a transport failure).
8246
- */
8247
- async function callMcpTool(opts) {
8248
- const body = await postJson(`${opts.runtime.serverUrl.replace(/\/+$/, "")}/mcp/${opts.group}`, {
8249
- jsonrpc: "2.0",
8250
- id: 1,
8251
- method: "tools/call",
8252
- params: {
8253
- name: opts.tool,
8254
- arguments: opts.args
8255
- }
8256
- }, {
8257
- timeoutMs: opts.timeoutMs,
8258
- signal: opts.signal,
8259
- headers: { Authorization: `Bearer ${opts.runtime.nonce}` }
8260
- });
8261
- const rpc = body && typeof body === "object" ? body : {};
8262
- if (rpc.error) return {
8263
- text: rpc.error.message ?? "MCP error",
8264
- isError: true
8265
- };
8266
- return {
8267
- text: (Array.isArray(rpc.result?.content) ? rpc.result.content : []).filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join(""),
8268
- isError: rpc.result?.isError === true
8269
- };
8270
- }
8271
- /**
8272
- * One non-streaming gpt-5.5 (or any model id) inference via `/v1/responses`.
8273
- * Returns the assistant text (possibly empty). Throws on transport/HTTP/parse
8274
- * failure. `effort` maps to the Responses `reasoning.effort` knob.
8275
- */
8276
- async function callInference(opts) {
8277
- const body = await postJson(`${opts.serverUrl.replace(/\/+$/, "")}/v1/responses`, {
8278
- model: opts.model,
8279
- instructions: opts.instructions,
8280
- input: [{
8281
- role: "user",
8282
- content: [{
8283
- type: "input_text",
8284
- text: opts.input
8285
- }]
8286
- }],
8287
- stream: false,
8288
- reasoning: { effort: opts.effort }
8289
- }, {
8290
- timeoutMs: opts.timeoutMs,
8291
- signal: opts.signal
8292
- });
8293
- const out = [];
8294
- const items = Array.isArray(body?.output) ? body.output : [];
8295
- for (const item of items) {
8296
- if (item?.type !== "message" || item.role !== "assistant") continue;
8297
- const parts = Array.isArray(item.content) ? item.content : [];
8298
- for (const part of parts) if ((part?.type === "output_text" || part?.type === "text") && typeof part.text === "string") out.push(part.text);
8299
- }
8300
- return out.join("");
8301
- }
8302
- /**
8303
- * POST `payload` as JSON with a hard timeout, returning the parsed JSON body.
8304
- * Throws on non-2xx, network error, timeout (AbortController), or non-JSON body.
8305
- * An external `signal` is honored alongside the internal timeout.
8306
- */
8307
- async function postJson(url, payload, opts) {
8308
- const controller = new AbortController();
8309
- const timer = setTimeout$1(() => controller.abort(/* @__PURE__ */ new Error("hook MCP request timed out")), opts.timeoutMs);
8310
- const onExternalAbort = () => controller.abort(/* @__PURE__ */ new Error("hook MCP request aborted"));
8311
- if (opts.signal) if (opts.signal.aborted) onExternalAbort();
8312
- else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
8313
- try {
8314
- const res = await fetch(url, {
8315
- method: "POST",
8316
- headers: {
8317
- "Content-Type": "application/json",
8318
- Accept: "application/json",
8319
- ...opts.headers
8320
- },
8321
- body: JSON.stringify(payload),
8322
- signal: controller.signal
8323
- });
8324
- if (!res.ok) throw new Error(`hook MCP request failed: HTTP ${res.status}`);
8325
- return await res.json();
8326
- } finally {
8327
- clearTimeout$1(timer);
8328
- if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
8329
- }
8330
- }
8331
-
8332
8889
  //#endregion
8333
8890
  //#region src/internal-prompt-submit.ts
8334
8891
  /**
@@ -8338,7 +8895,7 @@ async function postJson(url, payload, opts) {
8338
8895
  * no such handle. Hooks always receive piped/redirected stdin, so this never
8339
8896
  * blocks (guarded against an interactive TTY, and any error -> "").
8340
8897
  */
8341
- function readStdin$4() {
8898
+ function readStdin$5() {
8342
8899
  try {
8343
8900
  if (process.stdin.isTTY) return "";
8344
8901
  return readFileSync(0, "utf8");
@@ -8369,7 +8926,7 @@ const internalPromptSubmit = defineCommand({
8369
8926
  },
8370
8927
  async run() {
8371
8928
  try {
8372
- const stdin = readStdin$4();
8929
+ const stdin = readStdin$5();
8373
8930
  const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
8374
8931
  const runtime = hookMcpRuntimeFromEnv();
8375
8932
  let decision;
@@ -8421,6 +8978,99 @@ const internalPromptSubmit = defineCommand({
8421
8978
  }
8422
8979
  });
8423
8980
 
8981
+ //#endregion
8982
+ //#region src/internal-plan-review.ts
8983
+ function readStdin$4() {
8984
+ try {
8985
+ if (process.stdin.isTTY) return "";
8986
+ return readFileSync(0, "utf8");
8987
+ } catch {
8988
+ return "";
8989
+ }
8990
+ }
8991
+ async function readDetachedPayload() {
8992
+ const payloadPath = (process.env.GH_ROUTER_PLAN_REVIEW_PAYLOAD ?? "").trim();
8993
+ if (payloadPath.length === 0) return void 0;
8994
+ try {
8995
+ const raw = await promises.readFile(payloadPath, "utf8");
8996
+ await promises.unlink(payloadPath).catch(() => {});
8997
+ const parsed = JSON.parse(raw);
8998
+ if (!parsed || typeof parsed !== "object") return void 0;
8999
+ const p = parsed;
9000
+ if (!p.sessionId || !p.cwd || !p.plan || !p.planHash) return void 0;
9001
+ return {
9002
+ sessionId: p.sessionId,
9003
+ cwd: p.cwd,
9004
+ plan: p.plan,
9005
+ planHash: p.planHash
9006
+ };
9007
+ } catch {
9008
+ await promises.unlink(payloadPath).catch(() => {});
9009
+ return;
9010
+ }
9011
+ }
9012
+ function spawnDetachedPlanReview(ctx) {
9013
+ let payloadPath;
9014
+ try {
9015
+ const dir = stopReviewStateDir();
9016
+ mkdirSync(dir, { recursive: true });
9017
+ payloadPath = nodePath.join(dir, `plan-review-${process.pid}-${randomBytes(4).toString("hex")}.json`);
9018
+ writeFileSync(payloadPath, JSON.stringify(ctx), { mode: 384 });
9019
+ const scriptArgs = process.argv[1] && process.argv[1] !== process.execPath ? [process.argv[1]] : [];
9020
+ const child = spawn(process.execPath, [...scriptArgs, "internal-plan-review"], {
9021
+ detached: true,
9022
+ windowsHide: true,
9023
+ stdio: "ignore",
9024
+ env: {
9025
+ ...process.env,
9026
+ GH_ROUTER_PLAN_REVIEW_PAYLOAD: payloadPath
9027
+ }
9028
+ });
9029
+ const orphan = payloadPath;
9030
+ child.on("error", () => {
9031
+ if (orphan) try {
9032
+ unlinkSync(orphan);
9033
+ } catch {}
9034
+ });
9035
+ child.unref();
9036
+ } catch {
9037
+ if (payloadPath) try {
9038
+ unlinkSync(payloadPath);
9039
+ } catch {}
9040
+ }
9041
+ }
9042
+ const internalPlanReview = defineCommand({
9043
+ meta: {
9044
+ name: "internal-plan-review",
9045
+ description: "Internal: advisory ExitPlanMode plan reviewer. Spawns a bounded detached critic review for substantive plans and writes findings for the next prompt. Always exit 0."
9046
+ },
9047
+ async run() {
9048
+ try {
9049
+ const runtime = hookMcpRuntimeFromEnv();
9050
+ if (!runtime) return;
9051
+ const detached = await readDetachedPayload();
9052
+ if (detached) {
9053
+ await runPlanReview({
9054
+ runtime,
9055
+ sessionId: detached.sessionId,
9056
+ cwd: detached.cwd,
9057
+ plan: detached.plan,
9058
+ findingsStore: fileFindingsStore(stopReviewStateDir())
9059
+ });
9060
+ return;
9061
+ }
9062
+ const decision = await decidePlanReviewHook({
9063
+ stdin: readStdin$4(),
9064
+ runtimeAvailable: true,
9065
+ debounce: filePlanReviewDebounce(stopReviewStateDir()),
9066
+ fallbackCwd: process.cwd()
9067
+ });
9068
+ if (decision.kind === "spawn") spawnDetachedPlanReview(decision);
9069
+ } catch {}
9070
+ process.exitCode = 0;
9071
+ }
9072
+ });
9073
+
8424
9074
  //#endregion
8425
9075
  //#region src/internal-session-bind.ts
8426
9076
  /**
@@ -8823,17 +9473,20 @@ function readStdin$1() {
8823
9473
  const MAX_DIFF_BYTES = 2 * 1024 * 1024;
8824
9474
  /** Capture the working-tree diff WITHOUT mutating the user's index (no
8825
9475
  * `git add -N`): `git diff HEAD` covers modified tracked files, which is where
8826
- * gate-weakening edits live. Best-effort: any git failure yields an empty diff
8827
- * (the weakening scan is then a no-op; the executable gate still runs). Capped. */
9476
+ * gate-weakening edits live. A git failure rejects, so the decision layer can run
9477
+ * checks with an empty weakening scan instead of treating an unknown diff as a
9478
+ * genuine no-diff turn. Capped. */
8828
9479
  async function captureDiff(cwd) {
8829
- const out = (await runCommandCapture([
9480
+ const r = await runCommandCapture([
8830
9481
  "git",
8831
9482
  "diff",
8832
9483
  "HEAD"
8833
9484
  ], {
8834
9485
  cwd,
8835
9486
  timeoutMs: 5e3
8836
- }).catch(() => void 0))?.stdout ?? "";
9487
+ });
9488
+ if (r.code !== 0) throw new Error(`git diff failed with exit ${r.code}`);
9489
+ const out = r.stdout;
8837
9490
  return out.length > MAX_DIFF_BYTES ? out.slice(0, MAX_DIFF_BYTES) : out;
8838
9491
  }
8839
9492
  /** Flush a message to stderr before exiting (process.exit can drop an unflushed
@@ -8965,6 +9618,7 @@ const internalStopHook = defineCommand({
8965
9618
  isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),
8966
9619
  resolveChecks: mode ? buildResolveChecks(mode) : void 0,
8967
9620
  timeoutMs: Number.isFinite(timeoutEnv) && timeoutEnv > 0 ? timeoutEnv : void 0,
9621
+ planMode: stopGatePlanMode(),
8968
9622
  reviewDebounce: reviewEnabled ? fileReviewDebounce(stopReviewStateDir()) : void 0,
8969
9623
  spawnReview: reviewEnabled ? (ctx) => spawnStopReview(ctx, {
8970
9624
  prompt: userPrompt,
@@ -9410,7 +10064,7 @@ process.on("uncaughtException", (error) => {
9410
10064
  const version = getPackageVersion();
9411
10065
  const argv = process.argv.slice(2);
9412
10066
  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";
10067
+ const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-plan-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-artifact-open" || argv[0] === "internal-first-mate-guard" || argv[0] === "internal-worker-guard";
9414
10068
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
9415
10069
  await runMain(defineCommand({
9416
10070
  meta: {
@@ -9429,8 +10083,10 @@ await runMain(defineCommand({
9429
10083
  "internal-stop-hook": internalStopHook,
9430
10084
  "internal-prompt-submit": internalPromptSubmit,
9431
10085
  "internal-stop-review": internalStopReview,
10086
+ "internal-plan-review": internalPlanReview,
9432
10087
  "internal-session-bind": internalSessionBind,
9433
10088
  "internal-artifact-open": internalArtifactOpen,
10089
+ "internal-first-mate-guard": internalFirstMateGuard,
9434
10090
  "internal-worker-guard": internalWorkerGuard
9435
10091
  }
9436
10092
  }));