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/README.md +5 -5
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{engine-RA1Orodr.js → engine-BWoDQ-3C.js} +1 -1
- package/dist/main.js +572 -188
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-D826LsJJ.js → peer-mcp-personas-BKkdfOyK.js} +591 -427
- package/dist/peer-mcp-personas-BKkdfOyK.js.map +1 -0
- package/package.json +1 -1
- package/dist/peer-mcp-personas-D826LsJJ.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
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
|
-
|
|
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" +
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
1149
|
+
"Route by the peer's real PROMPT WINDOW (input tokens): `codex-critic` gpt-5.6-sol ≈1M · `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:
|
|
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-
|
|
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
|
|
2354
|
-
* tax): nudge parallel lexical+semantic search before
|
|
2355
|
-
* advisory tone
|
|
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.
|
|
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.
|
|
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 ->
|
|
2379
|
-
* - substantive prompt -> parallel lexical+semantic search -> ONE gpt-5.
|
|
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 =
|
|
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([
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
3263
|
-
*
|
|
3264
|
-
*
|
|
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
|
|
3272
|
-
*
|
|
3273
|
-
*
|
|
3274
|
-
*
|
|
3275
|
-
*
|
|
3276
|
-
*
|
|
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
|
|
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,
|
|
3623
|
-
* teammates) inherit
|
|
3624
|
-
* via `--append-system-prompt`.
|
|
3625
|
-
* blocks so all coexist;
|
|
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
|
|
3640
|
-
*
|
|
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(
|
|
3965
|
+
async function prependArtifactPanelDirectiveToMirroredClaudeMd(peersKey = "peers") {
|
|
3659
3966
|
await injectMarkerBlock({
|
|
3660
|
-
snippet:
|
|
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.
|
|
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
|
|
8107
|
-
if (!
|
|
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 ((
|
|
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:
|
|
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 (
|
|
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 ? `${
|
|
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
|
-
|
|
8394
|
-
|
|
8395
|
-
if (
|
|
8396
|
-
|
|
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$
|
|
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$
|
|
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.
|
|
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.
|
|
9098
|
-
*
|
|
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
|
|
9481
|
+
const r = await runCommandCapture([
|
|
9101
9482
|
"git",
|
|
9102
9483
|
"diff",
|
|
9103
9484
|
"HEAD"
|
|
9104
9485
|
], {
|
|
9105
9486
|
cwd,
|
|
9106
9487
|
timeoutMs: 5e3
|
|
9107
|
-
})
|
|
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.
|
|
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.
|
|
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,
|