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