github-router 0.3.168 → 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 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";
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-RA1Orodr.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
  /**
@@ -2452,8 +2723,19 @@ const OPERATOR_DENIED_MCP_PREFIXES = ["mcp__workers__", "mcp__orchestrate__"];
2452
2723
  * both by the PreToolUse hook handler and by config-assertion tests. When
2453
2724
  * operator mode is off, nothing is blocked (normal sessions unaffected).
2454
2725
  */
2455
- function shouldDenyOperatorTool(toolName, operatorMode) {
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) {
2456
2737
  if (!operatorMode) return false;
2738
+ if (isMatchingWorkerDispatcher(toolName, input)) return false;
2457
2739
  return OPERATOR_DENIED_MCP_PREFIXES.some((prefix) => toolName.startsWith(prefix));
2458
2740
  }
2459
2741
  /**
@@ -2472,9 +2754,9 @@ function assertShapingInstalled(agentsMode, injectionSucceeded) {
2472
2754
  * calls to local worker/orchestrate MCP tools are blocked so those backends stay
2473
2755
  * reachable through the worker-* Agent subagents rather than the lead context.
2474
2756
  */
2475
- function operatorPreToolUse(toolName, operatorMode, _input) {
2757
+ function operatorPreToolUse(toolName, operatorMode, input) {
2476
2758
  if (!operatorMode) return { block: false };
2477
- if (shouldDenyOperatorTool(toolName, operatorMode)) return {
2759
+ if (shouldDenyOperatorTool(toolName, operatorMode, input)) return {
2478
2760
  block: true,
2479
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.`
2480
2762
  };
@@ -2497,12 +2779,12 @@ const internalFirstMateGuard = defineCommand({
2497
2779
  },
2498
2780
  run() {
2499
2781
  let toolName = "";
2500
- let toolInput;
2782
+ let hookInput;
2501
2783
  let parsed = false;
2502
2784
  try {
2503
2785
  const payload = JSON.parse(readStdinSync());
2504
2786
  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;
2787
+ if (typeof payload.agent_type === "string") hookInput = { agent_type: payload.agent_type };
2506
2788
  parsed = true;
2507
2789
  } catch {
2508
2790
  parsed = false;
@@ -2514,7 +2796,7 @@ const internalFirstMateGuard = defineCommand({
2514
2796
  }
2515
2797
  process.exit(0);
2516
2798
  }
2517
- const decision = operatorPreToolUse(toolName, true, toolInput);
2799
+ const decision = operatorPreToolUse(toolName, true, hookInput);
2518
2800
  if (decision.block) {
2519
2801
  process.stderr.write(decision.reason ?? `${toolName} is disabled in operator mode`);
2520
2802
  process.exit(2);
@@ -2533,7 +2815,7 @@ function buildFirstMateGuardHookCommand(execPath, entry) {
2533
2815
  //#region src/lib/injected-skills/first-mate-setup-skill.ts
2534
2816
  const FIRST_MATE_SETUP_SKILL = {
2535
2817
  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"
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"
2537
2819
  };
2538
2820
 
2539
2821
  //#endregion
@@ -2542,7 +2824,7 @@ const FIRST_MATE_SKILL = {
2542
2824
  name: "gh-first-mate",
2543
2825
  md: `---
2544
2826
  name: gh-first-mate
2545
- 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.
2546
2828
  user-invocable: true
2547
2829
  ---
2548
2830
 
@@ -2693,7 +2975,7 @@ const FLOOR_KEEPER_SKILL = {
2693
2975
  name: "gh-floor-keeper",
2694
2976
  md: `---
2695
2977
  name: gh-floor-keeper
2696
- 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.
2697
2979
  user-invocable: true
2698
2980
  ---
2699
2981
 
@@ -2844,7 +3126,7 @@ const ORCHESTRATE_SKILL = {
2844
3126
  name: "gh-orchestrate",
2845
3127
  md: `---
2846
3128
  name: gh-orchestrate
2847
- 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.
2848
3130
  user-invocable: true
2849
3131
  ---
2850
3132
 
@@ -2983,7 +3265,7 @@ const RESEARCH_SKILL = {
2983
3265
  name: "gh-research",
2984
3266
  md: `---
2985
3267
  name: gh-research
2986
- 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.
2987
3269
  user-invocable: true
2988
3270
  ---
2989
3271
 
@@ -3102,7 +3384,7 @@ const WORKER_SKILL = {
3102
3384
  name: "gh-worker",
3103
3385
  md: `---
3104
3386
  name: gh-worker
3105
- 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.
3106
3388
  user-invocable: true
3107
3389
  ---
3108
3390
 
@@ -3156,7 +3438,7 @@ const ARTIFACT_REVIEW_SKILL = {
3156
3438
  name: "gh-artifact-review",
3157
3439
  md: `---
3158
3440
  name: gh-artifact-review
3159
- 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.
3160
3442
  user-invocable: true
3161
3443
  ---
3162
3444
 
@@ -3247,7 +3529,12 @@ const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injecte
3247
3529
  const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
3248
3530
  const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
3249
3531
  const ARTIFACT_MARKER_CLOSE = "<!-- /gh-router artifact-panel directive -->";
3250
- 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
+ }
3251
3538
  /**
3252
3539
  * Writing / communication style directive injected at the TOP of the
3253
3540
  * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
@@ -3258,27 +3545,45 @@ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by defau
3258
3545
  */
3259
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).";
3260
3547
  /**
3261
- * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md
3262
- * AND into the main agent's system prompt (`--append-system-prompt`), so it is
3263
- * the highest-salience behavioral default. Two defaults, both explicitly
3264
- * 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:
3265
3553
  *
3266
3554
  * 1. Orchestrate (strong default): delegate the heavy / parallel /
3267
3555
  * context-heavy work to the right subagent / worker / model, keeping the
3268
3556
  * main context free to reason and collaborate with the user, while still
3269
3557
  * doing trivial / surgical / last-mile work directly (delegating that
3270
3558
  * would only add relay-fidelity loss + latency).
3271
- * 2. Excellence lens (HYBRID per the peer review): the reliable control
3272
- * signal is the PRINCIPLE stated plainly; the well-known names appear only
3273
- * as a "bar to clear" calibration, NOT as "channel X", with an explicit
3274
- * no-impersonation / no-theatrics guardrail (cross-lab critics: a named
3275
- * entity is a dense, high-variance vector that pulls in noise + persona
3276
- * 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.
3277
3574
  *
3278
3575
  * Self-referentially compliant with the style directive: no em dashes, no
3279
3576
  * Claude / Anthropic attribution.
3280
3577
  */
3281
- 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.";
3282
3587
  /**
3283
3588
  * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
3284
3589
  * the would-be post-write file) has grown past this size.
@@ -3619,10 +3924,11 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
3619
3924
  /**
3620
3925
  * Prepend the operating-defaults directive (orchestrator posture + hybrid
3621
3926
  * excellence lens; `OPERATING_DEFAULTS_DIRECTIVE` above) to the TOP of the
3622
- * mirrored CLAUDE.md so descendant agents (Agent subagents, agent-teams
3623
- * teammates) inherit it. The main agent gets the same text at higher salience
3624
- * via `--append-system-prompt`. Separate marker fence from the style / peer
3625
- * 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.
3626
3932
  */
3627
3933
  async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_DEFAULTS_DIRECTIVE) {
3628
3934
  await injectMarkerBlock({
@@ -3636,8 +3942,9 @@ async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_
3636
3942
  /**
3637
3943
  * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
3638
3944
  * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
3639
- * subagents, agent-teams teammates) learn about the provisioned tools.
3640
- * 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`.
3641
3948
  * Separate marker fence from the peer-awareness / style blocks.
3642
3949
  */
3643
3950
  async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
@@ -3655,9 +3962,9 @@ async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
3655
3962
  * Gated by the caller on AIORDIE_SESSION_ID (only useful inside a tab). Separate
3656
3963
  * marker fence; best-effort like the style/peer blocks.
3657
3964
  */
3658
- async function prependArtifactPanelDirectiveToMirroredClaudeMd(directive = ARTIFACT_PANEL_DIRECTIVE) {
3965
+ async function prependArtifactPanelDirectiveToMirroredClaudeMd(peersKey = "peers") {
3659
3966
  await injectMarkerBlock({
3660
- snippet: directive,
3967
+ snippet: ARTIFACT_PANEL_DIRECTIVE(peersKey),
3661
3968
  markerOpen: ARTIFACT_MARKER_OPEN,
3662
3969
  markerClose: ARTIFACT_MARKER_CLOSE,
3663
3970
  position: "top",
@@ -3730,6 +4037,71 @@ const INJECTED_SKILLS = [
3730
4037
  FIRST_MATE_SETUP_SKILL
3731
4038
  ];
3732
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
+
3733
4105
  //#endregion
3734
4106
  //#region src/lib/toolbelt/provision.ts
3735
4107
  /** Per-download cap (bytes) — these binaries are a few MB at most. */
@@ -4344,7 +4716,7 @@ function initProxyFromEnv() {
4344
4716
  //#endregion
4345
4717
  //#region package.json
4346
4718
  var name = "github-router";
4347
- var version$1 = "0.3.168";
4719
+ var version$1 = "0.3.175";
4348
4720
 
4349
4721
  //#endregion
4350
4722
  //#region src/lib/approval.ts
@@ -8097,14 +8469,15 @@ const claude = defineCommand({
8097
8469
  };
8098
8470
  let onShutdown = baseShutdown;
8099
8471
  let peerAwarenessSnippet;
8472
+ let peerAwarenessSummary;
8100
8473
  if (args["codex-mcp"] !== false) try {
8101
8474
  const requestedCli = args["codex-cli"] ?? false;
8102
8475
  const backend = resolveCodexCliBackend({
8103
8476
  requested: requestedCli,
8104
8477
  codexInfo: requestedCli ? getCodexVersion() : null
8105
8478
  });
8106
- const geminiAvailable = state.models?.data.some((m) => /^gemini-3\..*pro/i.test(m.id)) ?? false;
8107
- 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.");
8108
8481
  const enabledGroups = [
8109
8482
  "peers",
8110
8483
  "search",
@@ -8118,7 +8491,7 @@ const claude = defineCommand({
8118
8491
  const { keys: groupKeys, skipped: skippedGroups } = await resolveGroupKeysFromMirror(enabledGroups);
8119
8492
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
8120
8493
  codexCli: backend === "cli",
8121
- geminiAvailable,
8494
+ geminiAvailable: geminiModelsAvailable,
8122
8495
  groupKeys,
8123
8496
  workerToolsAvailable: workerToolsEnabled(),
8124
8497
  browseAvailable: browseAgentEnabled(),
@@ -8133,7 +8506,7 @@ const claude = defineCommand({
8133
8506
  };
8134
8507
  const injected = await injectPeerMcpIntoMirror(serverUrl, {
8135
8508
  codexCli: backend === "cli",
8136
- geminiAvailable,
8509
+ geminiAvailable: geminiModelsAvailable,
8137
8510
  groupKeys,
8138
8511
  nonce: runtime.nonce
8139
8512
  });
@@ -8189,10 +8562,10 @@ const claude = defineCommand({
8189
8562
  } catch (err) {
8190
8563
  consola.warn(`Could not register the ai-or-die session-bind hook: ${String(err)}`);
8191
8564
  }
8192
- if ((process$1.env.AIORDIE_SESSION_ID ?? "").trim().length > 0) {
8565
+ if (artifactToolsEnabled()) {
8193
8566
  await writeInjectedSkill(ARTIFACT_REVIEW_SKILL.name, ARTIFACT_REVIEW_SKILL.md).catch(() => ({ written: false }));
8194
8567
  try {
8195
- await prependArtifactPanelDirectiveToMirroredClaudeMd();
8568
+ await prependArtifactPanelDirectiveToMirroredClaudeMd(groupKeys.peers);
8196
8569
  } catch (err) {
8197
8570
  consola.warn(`Artifact-panel directive prepend failed: ${String(err)}`);
8198
8571
  }
@@ -8203,6 +8576,11 @@ const claude = defineCommand({
8203
8576
  consola.warn(`Could not register the artifact auto-open hook: ${String(err)}`);
8204
8577
  }
8205
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
+ }
8206
8584
  if (args["trust-gate"] === true) try {
8207
8585
  const root = await trustRepo(sessionCwd);
8208
8586
  process$1.stderr.write(`Structural gate trusted for this repo (${root}); it will run on launch here from now on.\n`);
@@ -8306,19 +8684,30 @@ const claude = defineCommand({
8306
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");
8307
8685
  const peerSnippet = buildPeerAwarenessSnippet({
8308
8686
  codexCli: backend === "cli",
8309
- geminiAvailable,
8687
+ geminiAvailable: geminiModelsAvailable,
8310
8688
  workerToolsAvailable: workerToolsEnabled(),
8311
8689
  standInAvailable: standInToolEnabled(),
8312
- browseAvailable: state.browseEnabled,
8690
+ browseAvailable: browserToolsEnabled(),
8691
+ compoundBrowseAvailable: browserCompoundToolsEnabled(),
8313
8692
  powerBrowseAvailable: state.powerBrowseEnabled,
8693
+ fleetAvailable: fleetToolsEnabled(),
8314
8694
  agentToolsAvailable: agentToolsEnabled(),
8695
+ implementerAvailable: implementerSubagentModel() != null,
8315
8696
  groupKeys
8316
8697
  });
8317
8698
  peerAwarenessSnippet = peerSnippet;
8699
+ peerAwarenessSummary = buildPeerAwarenessSummary({
8700
+ workerToolsAvailable: workerToolsEnabled(),
8701
+ standInAvailable: standInToolEnabled(),
8702
+ browseAvailable: browserToolsEnabled(),
8703
+ fleetAvailable: fleetToolsEnabled(),
8704
+ agentToolsAvailable: agentToolsEnabled(),
8705
+ groupKeys
8706
+ });
8318
8707
  try {
8319
8708
  await appendPeerAwarenessToMirroredClaudeMd(peerSnippet);
8320
8709
  } catch (err) {
8321
- 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)}`);
8322
8711
  }
8323
8712
  try {
8324
8713
  await prependStyleDirectiveToMirroredClaudeMd();
@@ -8328,12 +8717,17 @@ const claude = defineCommand({
8328
8717
  } catch (err) {
8329
8718
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
8330
8719
  }
8331
- 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);
8332
8721
  try {
8333
8722
  await prependOperatingDefaultsToMirroredClaudeMd();
8334
8723
  } catch (err) {
8335
8724
  consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
8336
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
+ }
8337
8731
  launchChild({
8338
8732
  kind: "claude-code",
8339
8733
  envVars,
@@ -8492,114 +8886,6 @@ const debug = defineCommand({
8492
8886
  }
8493
8887
  });
8494
8888
 
8495
- //#endregion
8496
- //#region src/lib/orchestration/hook-mcp-client.ts
8497
- /**
8498
- * Read the proxy URL + nonce the launcher injected into the spawned child env
8499
- * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when
8500
- * either is absent — the hook then skips its LLM layer and falls back to its
8501
- * deterministic / regex behavior.
8502
- */
8503
- function hookMcpRuntimeFromEnv(env = process.env) {
8504
- const serverUrl = (env.GH_ROUTER_HOOK_MCP_URL ?? "").trim();
8505
- const nonce = (env.GH_ROUTER_HOOK_NONCE ?? "").trim();
8506
- if (serverUrl.length === 0 || nonce.length === 0) return void 0;
8507
- return {
8508
- serverUrl,
8509
- nonce
8510
- };
8511
- }
8512
- /**
8513
- * POST a JSON-RPC `tools/call` and return the tool's text + isError. Throws on
8514
- * any transport/HTTP/parse failure (caller fails open). A JSON-RPC `error`
8515
- * envelope is mapped to `{ text: message, isError: true }` (a well-formed
8516
- * negative result, not a transport failure).
8517
- */
8518
- async function callMcpTool(opts) {
8519
- const body = await postJson(`${opts.runtime.serverUrl.replace(/\/+$/, "")}/mcp/${opts.group}`, {
8520
- jsonrpc: "2.0",
8521
- id: 1,
8522
- method: "tools/call",
8523
- params: {
8524
- name: opts.tool,
8525
- arguments: opts.args
8526
- }
8527
- }, {
8528
- timeoutMs: opts.timeoutMs,
8529
- signal: opts.signal,
8530
- headers: { Authorization: `Bearer ${opts.runtime.nonce}` }
8531
- });
8532
- const rpc = body && typeof body === "object" ? body : {};
8533
- if (rpc.error) return {
8534
- text: rpc.error.message ?? "MCP error",
8535
- isError: true
8536
- };
8537
- return {
8538
- text: (Array.isArray(rpc.result?.content) ? rpc.result.content : []).filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join(""),
8539
- isError: rpc.result?.isError === true
8540
- };
8541
- }
8542
- /**
8543
- * One non-streaming gpt-5.5 (or any model id) inference via `/v1/responses`.
8544
- * Returns the assistant text (possibly empty). Throws on transport/HTTP/parse
8545
- * failure. `effort` maps to the Responses `reasoning.effort` knob.
8546
- */
8547
- async function callInference(opts) {
8548
- const body = await postJson(`${opts.serverUrl.replace(/\/+$/, "")}/v1/responses`, {
8549
- model: opts.model,
8550
- instructions: opts.instructions,
8551
- input: [{
8552
- role: "user",
8553
- content: [{
8554
- type: "input_text",
8555
- text: opts.input
8556
- }]
8557
- }],
8558
- stream: false,
8559
- reasoning: { effort: opts.effort }
8560
- }, {
8561
- timeoutMs: opts.timeoutMs,
8562
- signal: opts.signal
8563
- });
8564
- const out = [];
8565
- const items = Array.isArray(body?.output) ? body.output : [];
8566
- for (const item of items) {
8567
- if (item?.type !== "message" || item.role !== "assistant") continue;
8568
- const parts = Array.isArray(item.content) ? item.content : [];
8569
- for (const part of parts) if ((part?.type === "output_text" || part?.type === "text") && typeof part.text === "string") out.push(part.text);
8570
- }
8571
- return out.join("");
8572
- }
8573
- /**
8574
- * POST `payload` as JSON with a hard timeout, returning the parsed JSON body.
8575
- * Throws on non-2xx, network error, timeout (AbortController), or non-JSON body.
8576
- * An external `signal` is honored alongside the internal timeout.
8577
- */
8578
- async function postJson(url, payload, opts) {
8579
- const controller = new AbortController();
8580
- const timer = setTimeout$1(() => controller.abort(/* @__PURE__ */ new Error("hook MCP request timed out")), opts.timeoutMs);
8581
- const onExternalAbort = () => controller.abort(/* @__PURE__ */ new Error("hook MCP request aborted"));
8582
- if (opts.signal) if (opts.signal.aborted) onExternalAbort();
8583
- else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
8584
- try {
8585
- const res = await fetch(url, {
8586
- method: "POST",
8587
- headers: {
8588
- "Content-Type": "application/json",
8589
- Accept: "application/json",
8590
- ...opts.headers
8591
- },
8592
- body: JSON.stringify(payload),
8593
- signal: controller.signal
8594
- });
8595
- if (!res.ok) throw new Error(`hook MCP request failed: HTTP ${res.status}`);
8596
- return await res.json();
8597
- } finally {
8598
- clearTimeout$1(timer);
8599
- if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
8600
- }
8601
- }
8602
-
8603
8889
  //#endregion
8604
8890
  //#region src/internal-prompt-submit.ts
8605
8891
  /**
@@ -8609,7 +8895,7 @@ async function postJson(url, payload, opts) {
8609
8895
  * no such handle. Hooks always receive piped/redirected stdin, so this never
8610
8896
  * blocks (guarded against an interactive TTY, and any error -> "").
8611
8897
  */
8612
- function readStdin$4() {
8898
+ function readStdin$5() {
8613
8899
  try {
8614
8900
  if (process.stdin.isTTY) return "";
8615
8901
  return readFileSync(0, "utf8");
@@ -8640,7 +8926,7 @@ const internalPromptSubmit = defineCommand({
8640
8926
  },
8641
8927
  async run() {
8642
8928
  try {
8643
- const stdin = readStdin$4();
8929
+ const stdin = readStdin$5();
8644
8930
  const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
8645
8931
  const runtime = hookMcpRuntimeFromEnv();
8646
8932
  let decision;
@@ -8692,6 +8978,99 @@ const internalPromptSubmit = defineCommand({
8692
8978
  }
8693
8979
  });
8694
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
+
8695
9074
  //#endregion
8696
9075
  //#region src/internal-session-bind.ts
8697
9076
  /**
@@ -9094,17 +9473,20 @@ function readStdin$1() {
9094
9473
  const MAX_DIFF_BYTES = 2 * 1024 * 1024;
9095
9474
  /** Capture the working-tree diff WITHOUT mutating the user's index (no
9096
9475
  * `git add -N`): `git diff HEAD` covers modified tracked files, which is where
9097
- * gate-weakening edits live. Best-effort: any git failure yields an empty diff
9098
- * (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. */
9099
9479
  async function captureDiff(cwd) {
9100
- const out = (await runCommandCapture([
9480
+ const r = await runCommandCapture([
9101
9481
  "git",
9102
9482
  "diff",
9103
9483
  "HEAD"
9104
9484
  ], {
9105
9485
  cwd,
9106
9486
  timeoutMs: 5e3
9107
- }).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;
9108
9490
  return out.length > MAX_DIFF_BYTES ? out.slice(0, MAX_DIFF_BYTES) : out;
9109
9491
  }
9110
9492
  /** Flush a message to stderr before exiting (process.exit can drop an unflushed
@@ -9682,7 +10064,7 @@ process.on("uncaughtException", (error) => {
9682
10064
  const version = getPackageVersion();
9683
10065
  const argv = process.argv.slice(2);
9684
10066
  const isVersionFlag = argv.includes("--version");
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";
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";
9686
10068
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
9687
10069
  await runMain(defineCommand({
9688
10070
  meta: {
@@ -9701,6 +10083,7 @@ await runMain(defineCommand({
9701
10083
  "internal-stop-hook": internalStopHook,
9702
10084
  "internal-prompt-submit": internalPromptSubmit,
9703
10085
  "internal-stop-review": internalStopReview,
10086
+ "internal-plan-review": internalPlanReview,
9704
10087
  "internal-session-bind": internalSessionBind,
9705
10088
  "internal-artifact-open": internalArtifactOpen,
9706
10089
  "internal-first-mate-guard": internalFirstMateGuard,