github-router 0.3.168 → 0.3.176

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-BKkdfOyK.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
  }
@@ -1134,16 +1137,16 @@ function buildCoordinatorAgent(opts) {
1134
1137
  "",
1135
1138
  "The lead's brief will include an artifact (plan, design, diff, or code) and a goal (e.g. 'review before exit-plan', 'review the commit I just made', 'cross-check codex-critic's verdict'). Pick the right peers for the artifact type:",
1136
1139
  "",
1137
- "- **Plan / design / architecture choice** → fan out to `codex-critic` (gpt-5.5, strongest reasoning, cross-lab)" + (opts.geminiAvailable ? " AND `gemini-critic` (third-lab triangulation, strong on formal reasoning) in parallel" : "") + ". codex-reviewer is the wrong tool for plans (it's a code-specialist, not an architecture critic).",
1140
+ "- **Plan / design / architecture choice** → fan out to `codex-critic` (gpt-5.6-sol, strongest reasoning, cross-lab)" + (opts.geminiAvailable ? " AND `gemini-critic` (third-lab triangulation, strong on formal reasoning) in parallel" : "") + ". codex-reviewer is the wrong tool for plans (it's a code-specialist, not an architecture critic).",
1138
1141
  "- **Concrete diff or single file** → fan out to `codex-reviewer` (gpt-5.3-codex, line-level code specialist, fastest at ~16s)" + (opts.geminiAvailable ? " AND `gemini-reviewer` (gemini-3.1-pro, second-lab line-level review)" : "") + (opts.geminiAvailable ? " AND `gemini-critic` for cross-lab triangulation" : "") + ". For very small changes (<20 lines), one `codex-reviewer` call is enough.",
1139
- "- **Large artifact** → the only peers that take a large artifact WHOLE are `codex-critic` (gpt-5.5, ≈922K-token input window) and `opus-critic` (Opus-4.7-1M, ≈936K-token input on enterprise catalogs; ≈168K otherwise). Route the full artifact to those for cross-lab coverage. `codex-reviewer` (≈272K) and `gemini-critic` (≈136K) have small windows — see Decomposition below: never summarize or downsize the request to squeeze a large artifact into a small-window peer.",
1142
+ "- **Large artifact** → the only peers that take a large artifact WHOLE are `codex-critic` (gpt-5.6-sol, ≈1M-token input window) and `opus-critic` (Opus-4.7-1M, ≈936K-token input on enterprise catalogs; ≈168K otherwise). Route the full artifact to those for cross-lab coverage. `codex-reviewer` (≈272K) and `gemini-critic` (≈136K) have small windows — see Decomposition below: never summarize or downsize the request to squeeze a large artifact into a small-window peer.",
1140
1143
  "- **Formal reasoning, proofs, or invariants** → prefer `gemini-critic`" + (opts.geminiAvailable ? " (gemini-3.1-pro, strong on math and formally-stated properties)" : " (NOT REGISTERED in this session — gemini-3.x not in catalog)") + ".",
1141
1144
  "- **Tie-breaker after codex-critic has weighed in** → call `gemini-critic`" + (opts.geminiAvailable ? "" : " (NOT REGISTERED in this session)") + " or `opus-critic` with the artifact AND codex-critic's verdict for cross-check.",
1142
1145
  "- **Fast sanity check** → `opus-critic` (~22s, same lab as lead but fresh context — catches confabulation and motivated reasoning).",
1143
1146
  "",
1144
1147
  "## Decomposition for large artifacts",
1145
1148
  "",
1146
- "Route by the peer's real PROMPT WINDOW (input tokens): `codex-critic` gpt-5.5922K · `opus-critic` Opus-4.7-1M ≈936K (enterprise catalogs; ≈168K otherwise) · `codex-reviewer` gpt-5.3-codex ≈272K · `gemini-critic` gemini-3.1-pro ≈136K. The proxy REJECTS (with an actionable message) any single call whose brief exceeds the target peer's window — it will NOT silently truncate, because dropping lines from a review artifact is worse than a clear error. So: send the full artifact only to peers whose window fits it (large artifacts → `codex-critic` and/or `opus-critic`). When a peer's window is too small (commonly `gemini-critic` at ≈136K, or `codex-reviewer` at ≈272K), do NOT summarize or downsize the request to include it — either skip that peer, or split the artifact into 2-4 logical batches BY CONCERN (not by raw size — semantic batches give better per-batch reviews) that each fit, and call in parallel. Use the big-window peers for the whole and reserve a small-window peer like gemini for the concerns it can actually hold. The proxy's MCP cap allows up to 8 in-flight calls. Aggregate findings yourself before reporting back. (Separately, on the JSON transport a per-effort `predictedTooLong` byte cap still guards the ~60s tools/call timeout for non-SSE clients; Claude Code uses SSE, which streams with heartbeats and isn't subject to that cap.)",
1149
+ "Route by the peer's real PROMPT WINDOW (input tokens): `codex-critic` gpt-5.6-sol1M · `opus-critic` Opus-4.7-1M ≈936K (enterprise catalogs; ≈168K otherwise) · `codex-reviewer` gpt-5.3-codex ≈272K · `gemini-critic` gemini-3.1-pro ≈136K. The proxy REJECTS (with an actionable message) any single call whose brief exceeds the target peer's window — it will NOT silently truncate, because dropping lines from a review artifact is worse than a clear error. So: send the full artifact only to peers whose window fits it (large artifacts → `codex-critic` and/or `opus-critic`). When a peer's window is too small (commonly `gemini-critic` at ≈136K, or `codex-reviewer` at ≈272K), do NOT summarize or downsize the request to include it — either skip that peer, or split the artifact into 2-4 logical batches BY CONCERN (not by raw size — semantic batches give better per-batch reviews) that each fit, and call in parallel. Use the big-window peers for the whole and reserve a small-window peer like gemini for the concerns it can actually hold. The proxy's MCP cap allows up to 8 in-flight calls. Aggregate findings yourself before reporting back. (Separately, on the JSON transport a per-effort `predictedTooLong` byte cap still guards the ~60s tools/call timeout for non-SSE clients; Claude Code uses SSE, which streams with heartbeats and isn't subject to that cap.)",
1147
1150
  "",
1148
1151
  "## Aggregation contract",
1149
1152
  "",
@@ -1203,7 +1206,7 @@ function buildPeerAgentDefinitions(opts) {
1203
1206
  geminiAvailable: opts.geminiAvailable
1204
1207
  });
1205
1208
  if (opts.implementerModel && opts.implementerModel.length > 0) out.implementer = {
1206
- description: "Bounded implementation subagent running gpt-5.5 (strong non-Claude coder, high reasoning). Use for well-scoped coding tasks — edits, small features, fixes — you want implemented in an integrated subagent. Model is overridable at spawn.",
1209
+ description: `Bounded implementation subagent running ${opts.implementerModel} (strong non-Claude coder, high reasoning). Use for well-scoped coding tasks — edits, small features, fixes — you want implemented in an integrated subagent. Model is overridable at spawn.`,
1207
1210
  prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running builds, tests, and git — do not shell out (sed/awk/python/here-docs) to read or edit files. Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
1208
1211
  model: opts.implementerModel
1209
1212
  };
@@ -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.6-sol (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-BWoDQ-3C.js");
2293
2560
  result = await runWorkerAgent({
2294
2561
  mode: "explore",
2295
2562
  workspace: root,
@@ -2350,19 +2617,19 @@ 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
- /** System prompt for the single gpt-5.5 scope/goal inference. Steers a SHORT,
2625
+ /** System prompt for the single gpt-5.6-sol scope/goal inference. Steers a SHORT,
2359
2626
  * user-derived (not invented) advisory note grounded in the search results. */
2360
2627
  const PROMPT_SCOPE_SYSTEM = "You are a scoping assistant for a coding agent about to act on a user's request. You are given the user's request and the results of a lexical + semantic code search over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\n1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in what the search surfaced (reference the most relevant file(s) by name).\n2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT invent new requirements or acceptance criteria beyond what they asked.\n3. Only if the task is large/cross-cutting, add a final line: \"Consider /gh-research first to saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\" Omit it for a focused or trivial task.\nThis is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.";
2361
2628
  /** Max chars of each search-result blob fed into the scope inference. */
2362
2629
  const SEARCH_CONTEXT_CAP = 6 * 1024;
2363
2630
  /** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */
2364
2631
  function framePendingFindings(findings) {
2365
- return "ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent gpt-5.5 reviewer flagged the following. Evaluate each on its merits — fix the real ones, and ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\n" + findings.trim();
2632
+ return "ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\n" + findings.trim();
2366
2633
  }
2367
2634
  function joinSections(sections) {
2368
2635
  return sections.map((s) => s.trim()).filter((s) => s.length > 0).join("\n\n");
@@ -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.6-sol 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.176";
4348
4720
 
4349
4721
  //#endregion
4350
4722
  //#region src/lib/approval.ts
@@ -7690,6 +8062,10 @@ function parseSharedArgs(args) {
7690
8062
  * 1M/400k window, never overflows). See `seedGatewayModelCache`.
7691
8063
  */
7692
8064
  const NATIVE_NON_CLAUDE_MODELS = [
8065
+ {
8066
+ id: "gpt-5.6-sol",
8067
+ displayName: "GPT-5.6 Sol"
8068
+ },
7693
8069
  {
7694
8070
  id: "gpt-5.5",
7695
8071
  displayName: "GPT-5.5"
@@ -8097,14 +8473,15 @@ const claude = defineCommand({
8097
8473
  };
8098
8474
  let onShutdown = baseShutdown;
8099
8475
  let peerAwarenessSnippet;
8476
+ let peerAwarenessSummary;
8100
8477
  if (args["codex-mcp"] !== false) try {
8101
8478
  const requestedCli = args["codex-cli"] ?? false;
8102
8479
  const backend = resolveCodexCliBackend({
8103
8480
  requested: requestedCli,
8104
8481
  codexInfo: requestedCli ? getCodexVersion() : null
8105
8482
  });
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.");
8483
+ const geminiModelsAvailable = geminiAvailable();
8484
+ if (!geminiModelsAvailable) consola.info("gemini-3.1-pro-preview not found in your Copilot model catalog; gemini-critic persona will not be registered.");
8108
8485
  const enabledGroups = [
8109
8486
  "peers",
8110
8487
  "search",
@@ -8118,7 +8495,7 @@ const claude = defineCommand({
8118
8495
  const { keys: groupKeys, skipped: skippedGroups } = await resolveGroupKeysFromMirror(enabledGroups);
8119
8496
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
8120
8497
  codexCli: backend === "cli",
8121
- geminiAvailable,
8498
+ geminiAvailable: geminiModelsAvailable,
8122
8499
  groupKeys,
8123
8500
  workerToolsAvailable: workerToolsEnabled(),
8124
8501
  browseAvailable: browseAgentEnabled(),
@@ -8133,7 +8510,7 @@ const claude = defineCommand({
8133
8510
  };
8134
8511
  const injected = await injectPeerMcpIntoMirror(serverUrl, {
8135
8512
  codexCli: backend === "cli",
8136
- geminiAvailable,
8513
+ geminiAvailable: geminiModelsAvailable,
8137
8514
  groupKeys,
8138
8515
  nonce: runtime.nonce
8139
8516
  });
@@ -8189,10 +8566,10 @@ const claude = defineCommand({
8189
8566
  } catch (err) {
8190
8567
  consola.warn(`Could not register the ai-or-die session-bind hook: ${String(err)}`);
8191
8568
  }
8192
- if ((process$1.env.AIORDIE_SESSION_ID ?? "").trim().length > 0) {
8569
+ if (artifactToolsEnabled()) {
8193
8570
  await writeInjectedSkill(ARTIFACT_REVIEW_SKILL.name, ARTIFACT_REVIEW_SKILL.md).catch(() => ({ written: false }));
8194
8571
  try {
8195
- await prependArtifactPanelDirectiveToMirroredClaudeMd();
8572
+ await prependArtifactPanelDirectiveToMirroredClaudeMd(groupKeys.peers);
8196
8573
  } catch (err) {
8197
8574
  consola.warn(`Artifact-panel directive prepend failed: ${String(err)}`);
8198
8575
  }
@@ -8203,6 +8580,11 @@ const claude = defineCommand({
8203
8580
  consola.warn(`Could not register the artifact auto-open hook: ${String(err)}`);
8204
8581
  }
8205
8582
  }
8583
+ if (hookMcpRuntimeFromEnv(envVars) && planReviewEnabled()) try {
8584
+ await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildPlanReviewHookCommand(process$1.execPath, process$1.argv[1]), "PostToolUse", void 0, "ExitPlanMode");
8585
+ } catch (err) {
8586
+ consola.warn(`Could not register the advisory plan-review hook: ${String(err)}`);
8587
+ }
8206
8588
  if (args["trust-gate"] === true) try {
8207
8589
  const root = await trustRepo(sessionCwd);
8208
8590
  process$1.stderr.write(`Structural gate trusted for this repo (${root}); it will run on launch here from now on.\n`);
@@ -8306,19 +8688,30 @@ const claude = defineCommand({
8306
8688
  } 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
8689
  const peerSnippet = buildPeerAwarenessSnippet({
8308
8690
  codexCli: backend === "cli",
8309
- geminiAvailable,
8691
+ geminiAvailable: geminiModelsAvailable,
8310
8692
  workerToolsAvailable: workerToolsEnabled(),
8311
8693
  standInAvailable: standInToolEnabled(),
8312
- browseAvailable: state.browseEnabled,
8694
+ browseAvailable: browserToolsEnabled(),
8695
+ compoundBrowseAvailable: browserCompoundToolsEnabled(),
8313
8696
  powerBrowseAvailable: state.powerBrowseEnabled,
8697
+ fleetAvailable: fleetToolsEnabled(),
8314
8698
  agentToolsAvailable: agentToolsEnabled(),
8699
+ implementerAvailable: implementerSubagentModel() != null,
8315
8700
  groupKeys
8316
8701
  });
8317
8702
  peerAwarenessSnippet = peerSnippet;
8703
+ peerAwarenessSummary = buildPeerAwarenessSummary({
8704
+ workerToolsAvailable: workerToolsEnabled(),
8705
+ standInAvailable: standInToolEnabled(),
8706
+ browseAvailable: browserToolsEnabled(),
8707
+ fleetAvailable: fleetToolsEnabled(),
8708
+ agentToolsAvailable: agentToolsEnabled(),
8709
+ groupKeys
8710
+ });
8318
8711
  try {
8319
8712
  await appendPeerAwarenessToMirroredClaudeMd(peerSnippet);
8320
8713
  } 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)}`);
8714
+ 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
8715
  }
8323
8716
  try {
8324
8717
  await prependStyleDirectiveToMirroredClaudeMd();
@@ -8328,12 +8721,17 @@ const claude = defineCommand({
8328
8721
  } catch (err) {
8329
8722
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
8330
8723
  }
8331
- extraArgs.push("--append-system-prompt", peerAwarenessSnippet ? `${OPERATING_DEFAULTS_DIRECTIVE}\n\n${peerAwarenessSnippet}` : OPERATING_DEFAULTS_DIRECTIVE);
8724
+ extraArgs.push("--append-system-prompt", peerAwarenessSnippet && peerAwarenessSummary ? `${OPERATING_DEFAULTS_DIGEST}\n\n${peerAwarenessSummary}` : OPERATING_DEFAULTS_DIGEST);
8332
8725
  try {
8333
8726
  await prependOperatingDefaultsToMirroredClaudeMd();
8334
8727
  } catch (err) {
8335
8728
  consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
8336
8729
  }
8730
+ if (process$1.env.GH_ROUTER_DISABLE_NO_ATTRIBUTION !== "1") try {
8731
+ await injectAttributionSuppressionIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"));
8732
+ } catch (err) {
8733
+ consola.warn(`No-attribution settings backstop skipped: ${err instanceof Error ? err.message : String(err)}`);
8734
+ }
8337
8735
  launchChild({
8338
8736
  kind: "claude-code",
8339
8737
  envVars,
@@ -8390,13 +8788,10 @@ const codex = defineCommand({
8390
8788
  if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
8391
8789
  if (usingDefault && state.models) {
8392
8790
  const inCache = (id) => state.models?.data.some((m) => m.id === id) ?? false;
8393
- if (!inCache(codexModel)) for (const fallback of DEFAULT_CODEX_MODEL_FALLBACKS) {
8394
- const resolved = resolveCodexModel(fallback);
8395
- if (inCache(resolved)) {
8396
- consola.info(`Default model "${codexModel}" not in your Copilot model list; falling back to "${resolved}".`);
8397
- codexModel = resolved;
8398
- break;
8399
- }
8791
+ const firstPresent = [DEFAULT_CODEX_MODEL, ...DEFAULT_CODEX_MODEL_FALLBACKS].map((id) => resolveModel(id)).find((id) => inCache(id));
8792
+ if (firstPresent) {
8793
+ if (firstPresent !== codexModel) consola.info(`Default model "${DEFAULT_CODEX_MODEL}" not in your Copilot model list; falling back to "${firstPresent}".`);
8794
+ codexModel = firstPresent;
8400
8795
  }
8401
8796
  }
8402
8797
  const modelEntry = state.models?.data.find((m) => m.id === codexModel);
@@ -8492,114 +8887,6 @@ const debug = defineCommand({
8492
8887
  }
8493
8888
  });
8494
8889
 
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
8890
  //#endregion
8604
8891
  //#region src/internal-prompt-submit.ts
8605
8892
  /**
@@ -8609,7 +8896,7 @@ async function postJson(url, payload, opts) {
8609
8896
  * no such handle. Hooks always receive piped/redirected stdin, so this never
8610
8897
  * blocks (guarded against an interactive TTY, and any error -> "").
8611
8898
  */
8612
- function readStdin$4() {
8899
+ function readStdin$5() {
8613
8900
  try {
8614
8901
  if (process.stdin.isTTY) return "";
8615
8902
  return readFileSync(0, "utf8");
@@ -8640,7 +8927,7 @@ const internalPromptSubmit = defineCommand({
8640
8927
  },
8641
8928
  async run() {
8642
8929
  try {
8643
- const stdin = readStdin$4();
8930
+ const stdin = readStdin$5();
8644
8931
  const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
8645
8932
  const runtime = hookMcpRuntimeFromEnv();
8646
8933
  let decision;
@@ -8669,7 +8956,7 @@ const internalPromptSubmit = defineCommand({
8669
8956
  },
8670
8957
  infer: (system, user, signal) => callInference({
8671
8958
  serverUrl: runtime.serverUrl,
8672
- model: "gpt-5.5",
8959
+ model: "gpt-5.6-sol",
8673
8960
  instructions: system,
8674
8961
  input: user,
8675
8962
  effort: "low",
@@ -8692,6 +8979,99 @@ const internalPromptSubmit = defineCommand({
8692
8979
  }
8693
8980
  });
8694
8981
 
8982
+ //#endregion
8983
+ //#region src/internal-plan-review.ts
8984
+ function readStdin$4() {
8985
+ try {
8986
+ if (process.stdin.isTTY) return "";
8987
+ return readFileSync(0, "utf8");
8988
+ } catch {
8989
+ return "";
8990
+ }
8991
+ }
8992
+ async function readDetachedPayload() {
8993
+ const payloadPath = (process.env.GH_ROUTER_PLAN_REVIEW_PAYLOAD ?? "").trim();
8994
+ if (payloadPath.length === 0) return void 0;
8995
+ try {
8996
+ const raw = await promises.readFile(payloadPath, "utf8");
8997
+ await promises.unlink(payloadPath).catch(() => {});
8998
+ const parsed = JSON.parse(raw);
8999
+ if (!parsed || typeof parsed !== "object") return void 0;
9000
+ const p = parsed;
9001
+ if (!p.sessionId || !p.cwd || !p.plan || !p.planHash) return void 0;
9002
+ return {
9003
+ sessionId: p.sessionId,
9004
+ cwd: p.cwd,
9005
+ plan: p.plan,
9006
+ planHash: p.planHash
9007
+ };
9008
+ } catch {
9009
+ await promises.unlink(payloadPath).catch(() => {});
9010
+ return;
9011
+ }
9012
+ }
9013
+ function spawnDetachedPlanReview(ctx) {
9014
+ let payloadPath;
9015
+ try {
9016
+ const dir = stopReviewStateDir();
9017
+ mkdirSync(dir, { recursive: true });
9018
+ payloadPath = nodePath.join(dir, `plan-review-${process.pid}-${randomBytes(4).toString("hex")}.json`);
9019
+ writeFileSync(payloadPath, JSON.stringify(ctx), { mode: 384 });
9020
+ const scriptArgs = process.argv[1] && process.argv[1] !== process.execPath ? [process.argv[1]] : [];
9021
+ const child = spawn(process.execPath, [...scriptArgs, "internal-plan-review"], {
9022
+ detached: true,
9023
+ windowsHide: true,
9024
+ stdio: "ignore",
9025
+ env: {
9026
+ ...process.env,
9027
+ GH_ROUTER_PLAN_REVIEW_PAYLOAD: payloadPath
9028
+ }
9029
+ });
9030
+ const orphan = payloadPath;
9031
+ child.on("error", () => {
9032
+ if (orphan) try {
9033
+ unlinkSync(orphan);
9034
+ } catch {}
9035
+ });
9036
+ child.unref();
9037
+ } catch {
9038
+ if (payloadPath) try {
9039
+ unlinkSync(payloadPath);
9040
+ } catch {}
9041
+ }
9042
+ }
9043
+ const internalPlanReview = defineCommand({
9044
+ meta: {
9045
+ name: "internal-plan-review",
9046
+ 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."
9047
+ },
9048
+ async run() {
9049
+ try {
9050
+ const runtime = hookMcpRuntimeFromEnv();
9051
+ if (!runtime) return;
9052
+ const detached = await readDetachedPayload();
9053
+ if (detached) {
9054
+ await runPlanReview({
9055
+ runtime,
9056
+ sessionId: detached.sessionId,
9057
+ cwd: detached.cwd,
9058
+ plan: detached.plan,
9059
+ findingsStore: fileFindingsStore(stopReviewStateDir())
9060
+ });
9061
+ return;
9062
+ }
9063
+ const decision = await decidePlanReviewHook({
9064
+ stdin: readStdin$4(),
9065
+ runtimeAvailable: true,
9066
+ debounce: filePlanReviewDebounce(stopReviewStateDir()),
9067
+ fallbackCwd: process.cwd()
9068
+ });
9069
+ if (decision.kind === "spawn") spawnDetachedPlanReview(decision);
9070
+ } catch {}
9071
+ process.exitCode = 0;
9072
+ }
9073
+ });
9074
+
8695
9075
  //#endregion
8696
9076
  //#region src/internal-session-bind.ts
8697
9077
  /**
@@ -9094,17 +9474,20 @@ function readStdin$1() {
9094
9474
  const MAX_DIFF_BYTES = 2 * 1024 * 1024;
9095
9475
  /** Capture the working-tree diff WITHOUT mutating the user's index (no
9096
9476
  * `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. */
9477
+ * gate-weakening edits live. A git failure rejects, so the decision layer can run
9478
+ * checks with an empty weakening scan instead of treating an unknown diff as a
9479
+ * genuine no-diff turn. Capped. */
9099
9480
  async function captureDiff(cwd) {
9100
- const out = (await runCommandCapture([
9481
+ const r = await runCommandCapture([
9101
9482
  "git",
9102
9483
  "diff",
9103
9484
  "HEAD"
9104
9485
  ], {
9105
9486
  cwd,
9106
9487
  timeoutMs: 5e3
9107
- }).catch(() => void 0))?.stdout ?? "";
9488
+ });
9489
+ if (r.code !== 0) throw new Error(`git diff failed with exit ${r.code}`);
9490
+ const out = r.stdout;
9108
9491
  return out.length > MAX_DIFF_BYTES ? out.slice(0, MAX_DIFF_BYTES) : out;
9109
9492
  }
9110
9493
  /** Flush a message to stderr before exiting (process.exit can drop an unflushed
@@ -9306,7 +9689,7 @@ THE DIFF:
9306
9689
  const internalStopReview = defineCommand({
9307
9690
  meta: {
9308
9691
  name: "internal-stop-review",
9309
- description: "Internal: the detached, advisory background reviewer. Reads a JSON payload on stdin, runs a read-only gpt-5.5 review of the working tree against the user's ask, and writes advisory findings for the next prompt to surface. Never blocks anything."
9692
+ description: "Internal: the detached, advisory background reviewer. Reads a JSON payload on stdin, runs a read-only gpt-5.6-sol review of the working tree against the user's ask, and writes advisory findings for the next prompt to surface. Never blocks anything."
9310
9693
  },
9311
9694
  async run() {
9312
9695
  try {
@@ -9335,7 +9718,7 @@ const internalStopReview = defineCommand({
9335
9718
  transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : ""
9336
9719
  }),
9337
9720
  workspace: cwd,
9338
- model: "gpt-5.5",
9721
+ model: "gpt-5.6-sol",
9339
9722
  thinking: "high"
9340
9723
  },
9341
9724
  timeoutMs: REVIEW_TIMEOUT_MS
@@ -9682,7 +10065,7 @@ process.on("uncaughtException", (error) => {
9682
10065
  const version = getPackageVersion();
9683
10066
  const argv = process.argv.slice(2);
9684
10067
  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";
10068
+ 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
10069
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
9687
10070
  await runMain(defineCommand({
9688
10071
  meta: {
@@ -9701,6 +10084,7 @@ await runMain(defineCommand({
9701
10084
  "internal-stop-hook": internalStopHook,
9702
10085
  "internal-prompt-submit": internalPromptSubmit,
9703
10086
  "internal-stop-review": internalStopReview,
10087
+ "internal-plan-review": internalPlanReview,
9704
10088
  "internal-session-bind": internalSessionBind,
9705
10089
  "internal-artifact-open": internalArtifactOpen,
9706
10090
  "internal-first-mate-guard": internalFirstMateGuard,