github-router 0.3.151 → 0.3.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as logStreamError, $t as copilotBaseUrl, At as UPSTREAM_FETCH_TIMEOUT_MS, B as toolbeltEnabled, Bt as cacheCopilotVersion, C as repoFingerprint, Ct as ArtifactClient, D as trustRepo, Dt as DEFAULT_CODEX_MODEL, E as stopReviewStateDir, Et as DEFAULT_CLAUDE_MODEL_FALLBACKS, Ft as withInstallLock, G as searchWeb, Gt as resolveCodexModel, H as vscodeRipgrepPath, Ht as cacheVSCodeVersion, It as setupCopilotToken, J as buildAdvisorStream, Jt as getModels, K as ADVISOR_INTERNAL_TOOL_NAME, Kt as resolveModel, Lt as setupGitHubAgentToken, Mt as generateRandomPort, Nt as pickClaudeDefault, O as resolveSealedGate, Ot as DEFAULT_CODEX_MODEL_FALLBACKS, Pt as getPackageVersion, Q as isControllerClosedError, Qt as GITHUB_API_BASE_URL, R as availableToolCommands, Rt as setupGitHubToken, S as isSubagentContext, St as shouldUseInsecureTls, T as stopGateEnabledForRepo, Tt as toolbeltPathOverride, U as TOOLBELT_TOOLS, Ut as filterBetaHeader, V as toolbeltSkipSet, Vt as cacheModels, W as assetFor, Wt as isNullish, X as isAdvisorRequested, Xt as HTTPError, Y as injectAdvisorTool, Yt as fetchWithTransientRetry, Z as buildOpenAIErrorEvent, Zt as forwardError, _ as stopReviewEnabled, _t as provisionBrowserAssets, a as buildPeerAwarenessSnippet, at as browserToolsEnabled, b as fileLastPromptStore, bt as extractTarGzMember, c as buildSessionBindHookCommand, ct as workerToolsEnabled, d as decideStopHook, dt as getTokenCount, en as copilotHeaders, et as readIteratorWithTimeout, f as fileBlockBudget, ft as createResponses, g as stopGateId, gt as parseJsonOrDiagnose, h as stopGateDisabled, ht as readResponseBodyCapped, i as buildAgentPrompt, it as agentToolsEnabled, jt as UPSTREAM_INACTIVITY_TIMEOUT_MS, k as liveExec, kt as DEFAULT_PORT, l as buildStopHookCommand, lt as countTokens, m as launchBaselineKey, mt as MAX_RESPONSE_BODY_BYTES, n as MCP_GROUPS, nn as state, nt as handleMcpDelete, o as personasFor, ot as fleetToolsEnabled, p as injectStopHookIntoSettingsFile, pt as createChatCompletions, q as ADVISOR_TOOL_INSTRUCTIONS, qt as sleep, r as assertMcpToolSurfaceConsistent, rt as handleMcpPost, s as buildArtifactOpenHookCommand, st as standInToolEnabled, t as GROUP_META, tn as githubHeaders, tt as relayAnthropicStream, u as captureLaunchBaseline, ut as createMessages, v as fileBaselineStore, vt as hasSupportedBrowserInstalled, w as repoRoot, wt as collapsePathKeys, x as fileReviewDebounce, xt as extractZipMember, y as fileFindingsStore, yt as provisionAndIndexColbert, z as buildToolbeltAwareness, zt as tryRefreshAndRetry } from "./peer-mcp-personas-Dm3UCpXz.js";
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-D0tJ_tms.js";
4
- import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-DyEXZu2z.js";
5
- import { a as sweepRegistry } from "./lifecycle-DGvk4z63.js";
2
+ import { $ as logStreamError, $t as GITHUB_API_BASE_URL, At as DEFAULT_PORT, B as toolbeltEnabled, Bt as tryRefreshAndRetry, C as repoFingerprint, Ct as shouldUseInsecureTls, D as trustRepo, Dt as DEFAULT_CLAUDE_MODEL_FALLBACKS, E as stopReviewStateDir, Et as toolbeltPathOverride, Ft as getPackageVersion, G as searchWeb, Gt as isNullish, H as vscodeRipgrepPath, Ht as cacheModels, It as withInstallLock, J as buildAdvisorStream, Jt as sleep, K as ADVISOR_INTERNAL_TOOL_NAME, Kt as resolveCodexModel, Lt as setupCopilotToken, Mt as UPSTREAM_INACTIVITY_TIMEOUT_MS, Nt as generateRandomPort, O as resolveSealedGate, Ot as DEFAULT_CODEX_MODEL, Pt as pickClaudeDefault, Q as isControllerClosedError, Qt as forwardError, R as availableToolCommands, Rt as setupGitHubAgentToken, S as isSubagentContext, St as extractZipMember, T as stopGateEnabledForRepo, Tt as collapsePathKeys, U as TOOLBELT_TOOLS, Ut as cacheVSCodeVersion, V as toolbeltSkipSet, Vt as cacheCopilotVersion, W as assetFor, Wt as filterBetaHeader, X as isAdvisorRequested, Xt as fetchWithTransientRetry, Y as injectAdvisorTool, Yt as getModels, Z as buildOpenAIErrorEvent, Zt as HTTPError, _ as stopReviewEnabled, _t as parseJsonOrDiagnose, a as buildPeerAwarenessSnippet, at as browseAgentEnabled, b as fileLastPromptStore, bt as provisionAndIndexColbert, c as buildSessionBindHookCommand, ct as standInToolEnabled, d as decideStopHook, dt as createMessages, en as copilotBaseUrl, et as readIteratorWithTimeout, f as fileBlockBudget, ft as getTokenCount, g as stopGateId, gt as readResponseBodyCapped, h as stopGateDisabled, ht as MAX_RESPONSE_BODY_BYTES, i as buildAgentPrompt, it as agentToolsEnabled, jt as UPSTREAM_FETCH_TIMEOUT_MS, k as liveExec, kt as DEFAULT_CODEX_MODEL_FALLBACKS, l as buildStopHookCommand, lt as workerToolsEnabled, m as launchBaselineKey, mt as createChatCompletions, n as MCP_GROUPS, nn as githubHeaders, nt as handleMcpDelete, o as personasFor, ot as browserToolsEnabled, p as injectStopHookIntoSettingsFile, pt as createResponses, q as ADVISOR_TOOL_INSTRUCTIONS, qt as resolveModel, r as assertMcpToolSurfaceConsistent, rn as state, rt as handleMcpPost, s as buildArtifactOpenHookCommand, st as fleetToolsEnabled, t as GROUP_META, tn as copilotHeaders, tt as relayAnthropicStream, u as captureLaunchBaseline, ut as countTokens, v as fileBaselineStore, vt as provisionBrowserAssets, w as repoRoot, wt as ArtifactClient, x as fileReviewDebounce, xt as extractTarGzMember, y as fileFindingsStore, yt as hasSupportedBrowserInstalled, z as buildToolbeltAwareness, zt as setupGitHubToken } from "./peer-mcp-personas-DMM1akDa.js";
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-Bt7sqiVr.js";
4
+ import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-VTQI28wT.js";
5
+ import { a as sweepRegistry } from "./lifecycle-C4k0pEvn.js";
6
6
  import { defineCommand, runMain } from "citty";
7
7
  import consola from "consola";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -809,6 +809,223 @@ function launchChild(target, server$1, options = {}) {
809
809
  });
810
810
  }
811
811
 
812
+ //#endregion
813
+ //#region src/lib/worker-dispatch.ts
814
+ /**
815
+ * Frozen contract for the NON-BLOCKING workers surface.
816
+ *
817
+ * The `workers` MCP tools (`explore`/`implement`/`review`/`plan`/`test`, and
818
+ * `browse` when the browse agent is enabled) BLOCK the caller for up to 30 min
819
+ * (`runWorkerAgent`). The MAIN Claude Code agent must never block on one, so a
820
+ * per-mode `worker-*` DISPATCHER SUBAGENT — which Claude Code runs in the
821
+ * background and reports on via a completion notification — is the only
822
+ * sanctioned way to run a worker. This module is the single source of truth for
823
+ * three things that must never drift:
824
+ *
825
+ * 1. the tool → dispatcher map (`mcp__<workersKey>__<mode>` ↔ `worker-<mode>`),
826
+ * 2. the PreToolUse GUARD decision that denies a raw worker call from the main
827
+ * agent (or any non-dispatcher subagent) and redirects it to the matching
828
+ * `worker-*` agent, allowing it only from the dispatcher itself, and
829
+ * 3. the dispatcher subagent bodies (description / system prompt / `tools:`
830
+ * allowlist).
831
+ *
832
+ * Imported by `codex-mcp-config.ts` (dispatcher `.md` generation + the hook
833
+ * command it bakes into settings.json) AND `internal-worker-guard.ts` (the
834
+ * runtime PreToolUse hook). Keeping the map here means the settings matcher, the
835
+ * redirect target, the dispatcher names, and the `tools:` allowlist are all
836
+ * derived from one place.
837
+ *
838
+ * Discrimination mechanism: a Claude Code PreToolUse payload carries
839
+ * `agent_type` (the invoking subagent's name) ONLY inside a subagent context —
840
+ * absent for the top-level/main agent. So `agent_type === "worker-<mode>"` is
841
+ * the reliable "this call came from the dispatcher, allow it" signal, and its
842
+ * absence (or any other value) means "main agent or an unrelated subagent →
843
+ * deny". This is the SAME field this repo's Stop / prompt-submit hooks already
844
+ * key off (`isSubagentContext` in `orchestration/stop-gate-policy.ts`); the
845
+ * guard here is the inverse (allow-only-dispatcher rather than skip-any-sub).
846
+ */
847
+ /** The five always-available worker modes (gated by `workerToolsEnabled()`). */
848
+ const CORE_WORKER_MODES = [
849
+ "explore",
850
+ "implement",
851
+ "review",
852
+ "plan",
853
+ "test"
854
+ ];
855
+ /** The browse worker mode, gated separately by `browseAgentEnabled()`. */
856
+ const BROWSE_WORKER_MODE = "browse";
857
+ /** Every mode the surface can expose — used to build the sweep-regex allowlist
858
+ * and to validate `--modes`. Order is stable (core first, browse last). */
859
+ const ALL_WORKER_DISPATCH_MODES = [...CORE_WORKER_MODES, BROWSE_WORKER_MODE];
860
+ /** The dispatcher subagent name for a mode, e.g. `implement` → `worker-implement`. */
861
+ function dispatcherAgentName(mode) {
862
+ return `worker-${mode}`;
863
+ }
864
+ /** Every possible dispatcher subagent name (used by the stale-`.md` sweep). */
865
+ const ALL_DISPATCHER_AGENT_NAMES = ALL_WORKER_DISPATCH_MODES.map(dispatcherAgentName);
866
+ /** The MCP tool name the workers server exposes for a mode under the resolved key. */
867
+ function workerToolName(workersKey, mode) {
868
+ return `mcp__${workersKey}__${mode}`;
869
+ }
870
+ /** The active dispatch modes for a launch: the five core modes plus `browse`
871
+ * only when the browse agent is enabled. */
872
+ function activeDispatchModes(opts) {
873
+ return opts.browse ? [...CORE_WORKER_MODES, BROWSE_WORKER_MODE] : [...CORE_WORKER_MODES];
874
+ }
875
+ function escapeRegex(s) {
876
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
877
+ }
878
+ /**
879
+ * The Claude Code PreToolUse `matcher` (a regex over the tool name) that scopes
880
+ * the guard hook to exactly the active worker tools — nothing else invokes the
881
+ * hook. Anchored + exact-alternation so an unrelated `mcp__<key>__status` (a
882
+ * future non-blocking tool with no dispatcher) is never matched, hence never
883
+ * denied.
884
+ */
885
+ function guardToolMatcher(workersKey, modes) {
886
+ const alt = modes.map((m) => escapeRegex(m)).join("|");
887
+ return `^mcp__${escapeRegex(workersKey)}__(${alt})$`;
888
+ }
889
+ /** Parse the worker mode out of a tool name for the resolved key, or null if it
890
+ * isn't one of the recognized worker tools. */
891
+ function parseWorkerToolCall(toolName, workersKey, modes) {
892
+ const prefix = `mcp__${workersKey}__`;
893
+ if (!toolName.startsWith(prefix)) return null;
894
+ const rest = toolName.slice(prefix.length);
895
+ return modes.includes(rest) ? rest : null;
896
+ }
897
+ /** The `permissionDecisionReason` shown to the model on a deny, steering it to
898
+ * the matching background dispatcher. `mode` is null when the payload was
899
+ * unparseable (fail-closed generic message). */
900
+ function guardDenyReason(mode) {
901
+ return `Workers run as background subagents in this session so your turn never blocks. Re-issue this as ${mode ? `Agent(subagent_type: "${dispatcherAgentName(mode)}", prompt: <your worker brief>)` : `the matching background \`worker-*\` agent via the Agent tool`}. It returns immediately and delivers the worker's result as a completion notification — do not call the raw \`mcp__…__\` worker tool from the main thread.`;
902
+ }
903
+ /** The stdout JSON a PreToolUse hook prints to DENY a tool call. */
904
+ function guardDenyOutput(mode) {
905
+ return JSON.stringify({ hookSpecificOutput: {
906
+ hookEventName: "PreToolUse",
907
+ permissionDecision: "deny",
908
+ permissionDecisionReason: guardDenyReason(mode)
909
+ } });
910
+ }
911
+ /**
912
+ * Pure PreToolUse guard decision. Given the raw stdin payload, the resolved
913
+ * workers key, and the active modes, decide whether to DENY the tool call.
914
+ *
915
+ * Rules (fail toward protecting the "main never blocks" invariant):
916
+ * - Payload unparseable / no string `tool_name`: the matcher only fires this
917
+ * hook for worker tools, so a payload we can't read is still a worker call
918
+ * → DENY (fail closed, generic redirect).
919
+ * - `tool_name` is not a recognized worker tool for this key: ALLOW (not a
920
+ * tool we guard — never deny a non-worker tool).
921
+ * - `agent_type` equals one of the active dispatcher names: ALLOW (the call
922
+ * came from the dispatcher subagent that is meant to run the worker).
923
+ * - Otherwise (main agent: `agent_type` absent; or a non-dispatcher subagent):
924
+ * DENY with a redirect to the matching `worker-<mode>` agent.
925
+ */
926
+ function decideWorkerGuard(input) {
927
+ let payload = null;
928
+ try {
929
+ const parsed = JSON.parse(input.stdin);
930
+ if (parsed && typeof parsed === "object") payload = parsed;
931
+ } catch {
932
+ payload = null;
933
+ }
934
+ const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : null;
935
+ if (!toolName) return {
936
+ output: guardDenyOutput(null),
937
+ verdict: "deny-malformed"
938
+ };
939
+ const mode = parseWorkerToolCall(toolName, input.workersKey, input.modes);
940
+ if (mode === null) return {
941
+ output: null,
942
+ verdict: "allow-non-worker"
943
+ };
944
+ const agentType = payload?.agent_type;
945
+ if (typeof agentType === "string" && agentType === dispatcherAgentName(mode)) return {
946
+ output: null,
947
+ verdict: "allow-dispatcher"
948
+ };
949
+ return {
950
+ output: guardDenyOutput(mode),
951
+ verdict: "deny-main"
952
+ };
953
+ }
954
+ /** One-line human description shown to the lead when picking a subagent. Uses
955
+ * the documented "Use proactively" auto-delegation idiom. */
956
+ function dispatcherDescription(mode) {
957
+ return `${{
958
+ explore: "Non-blocking `explore` worker: dispatches a read-only autonomous worker (its own context) in the background and delivers its summary as a completion notification.",
959
+ implement: "Non-blocking `implement` worker: dispatches an autonomous coding worker (read/write/bash, optional git worktree) in the background and delivers its result as a completion notification.",
960
+ review: "Non-blocking `review` worker: dispatches a read-only reviewer that reads the code itself to verify a change or claim, in the background, and delivers findings as a completion notification.",
961
+ plan: "Non-blocking `plan` worker: dispatches a read-only planner that returns an ordered implementation plan, in the background, and delivers it as a completion notification.",
962
+ test: "Non-blocking `test` worker: dispatches an independent test author that writes tests trying to break the implementation, in the background, and delivers pass/fail as a completion notification.",
963
+ browse: "Non-blocking `browse` worker: dispatches an autonomous browser agent in the background and delivers its result as a completion notification."
964
+ }[mode]} Use proactively for any ${mode}-mode worker task so a long run never blocks your turn: it returns immediately and notifies you when done.`;
965
+ }
966
+ /** The dispatcher subagent's full system prompt: call the one worker tool once,
967
+ * relay verbatim, do nothing else. */
968
+ function dispatcherPrompt(mode, workersKey) {
969
+ const tool = workerToolName(workersKey, mode);
970
+ return [
971
+ `# Subagent: ${dispatcherAgentName(mode)}`,
972
+ "",
973
+ `You are a thin DISPATCHER for the \`${mode}\` worker. You run in the background so the`,
974
+ "lead agent's turn is never blocked while the (up-to-30-minute) worker runs.",
975
+ "",
976
+ "## Your only job",
977
+ "",
978
+ `Call the \`${tool}\` tool EXACTLY ONCE, passing through the fields from the lead's brief:`,
979
+ " - `prompt`: the lead's worker brief, copied verbatim",
980
+ " - `workspace` (optional): absolute path, if the lead specified one",
981
+ " - `model` / `thinking` (optional): only if the lead specified them" + (mode === "implement" || mode === "test" ? "\n - `worktree` (optional): pass `true` if the lead asked for isolated-worktree execution" : ""),
982
+ "",
983
+ "When the tool returns, output its result VERBATIM as your final message. That final",
984
+ "message is what the lead receives in the completion notification — it IS the result.",
985
+ "",
986
+ "## Hard rules",
987
+ "",
988
+ "- Call the worker tool exactly once. Do not retry on a normal (non-error) return.",
989
+ "- Do NOT attempt the task yourself, do NOT read/edit files, do NOT run other tools.",
990
+ "- Do NOT spawn other agents (you have no Agent tool and must not try to gain one).",
991
+ "- Do NOT summarize, paraphrase, or add commentary — relay the worker output verbatim.",
992
+ "- If the worker returns an error, relay that error verbatim (do not mask it)."
993
+ ].join("\n");
994
+ }
995
+ /** The `tools:` frontmatter allowlist for a dispatcher: the workers MCP server
996
+ * wildcard (`mcp__<workersKey>__*`). Claude Code's `tools:` field supports
997
+ * MCP patterns only at SERVER granularity (`mcp__<server>__*`), not individual
998
+ * tool names, so this grants exactly the workers tools and NOTHING else — no
999
+ * Agent/Task (so it cannot spawn further agents → no recursion), no Read/Bash
1000
+ * (so it cannot do extra work). The dispatcher's prompt narrows it to the one
1001
+ * mode; the guard allows any dispatcher-named caller regardless. */
1002
+ function dispatcherTools(_mode, workersKey) {
1003
+ return [`mcp__${workersKey}__*`];
1004
+ }
1005
+ /**
1006
+ * Build the shell command Claude Code runs for the workers `PreToolUse` guard
1007
+ * hook — the running github-router via its node/bun binary so it works
1008
+ * regardless of PATH. Mirrors `buildPromptSubmitHookCommand`.
1009
+ *
1010
+ * The resolved `workersKey` and the active `modes` are baked into the command
1011
+ * ARGS (not env): `mergeStopHookIntoSettings` dedups hooks by the command
1012
+ * string only, so baking the key/modes makes a changed resolution produce a
1013
+ * DISTINCT command — no stale-matcher entry can survive from a prior launch.
1014
+ */
1015
+ function buildWorkerGuardHookCommand(execPath, scriptPath, workersKey, modes) {
1016
+ const q = (s) => `"${s}"`;
1017
+ const args = `internal-worker-guard --workers-key ${q(workersKey)} --modes ${q(modes.join(","))}`;
1018
+ if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} ${args}`;
1019
+ return `${q(execPath)} ${args}`;
1020
+ }
1021
+ /** Parse a `--modes` CSV back into validated modes (drops unknown tokens). */
1022
+ function parseModesCsv(csv) {
1023
+ if (!csv) return [...CORE_WORKER_MODES];
1024
+ const known = new Set(ALL_WORKER_DISPATCH_MODES);
1025
+ const out = csv.split(",").map((s) => s.trim()).filter((s) => known.has(s));
1026
+ return out.length > 0 ? out : [...CORE_WORKER_MODES];
1027
+ }
1028
+
812
1029
  //#endregion
813
1030
  //#region src/lib/codex-mcp-config.ts
814
1031
  /** The `peers` server is always enabled, so its resolved key always exists;
@@ -816,6 +1033,13 @@ function launchChild(target, server$1, options = {}) {
816
1033
  function peersKeyOf(groupKeys) {
817
1034
  return groupKeys.peers ?? GROUP_META.peers.preferredKey;
818
1035
  }
1036
+ /** The resolved `workers` server key (bare `workers`, or the `gh-router-workers`
1037
+ * fallback on collision). Used to name the dispatcher tools and the guard
1038
+ * matcher. Falls back to the preferred bare key when the group is absent (the
1039
+ * caller only builds worker dispatchers when the group is enabled anyway). */
1040
+ function workersKeyOf(groupKeys) {
1041
+ return groupKeys.workers ?? GROUP_META.workers.preferredKey;
1042
+ }
819
1043
  /**
820
1044
  * Decide which MCP backend serves the codex personas.
821
1045
  *
@@ -975,6 +1199,14 @@ function buildPeerAgentDefinitions(opts) {
975
1199
  codexCli: opts.codexCli,
976
1200
  geminiAvailable: opts.geminiAvailable
977
1201
  });
1202
+ if (opts.workerToolsAvailable) {
1203
+ const workersKey = workersKeyOf(opts.groupKeys);
1204
+ for (const mode of activeDispatchModes({ browse: opts.browseAvailable === true })) out[dispatcherAgentName(mode)] = {
1205
+ description: dispatcherDescription(mode),
1206
+ prompt: dispatcherPrompt(mode, workersKey),
1207
+ tools: dispatcherTools(mode, workersKey)
1208
+ };
1209
+ }
978
1210
  return out;
979
1211
  }
980
1212
  /**
@@ -1018,17 +1250,24 @@ function escapeYamlString(s) {
1018
1250
  * component.
1019
1251
  */
1020
1252
  const VALID_AGENT_NAME = /^[a-z][a-z0-9-]*$/;
1021
- /** Build a single subagent .md file body (frontmatter + system prompt). */
1253
+ /** Build a single subagent .md file body (frontmatter + system prompt).
1254
+ *
1255
+ * `tools` (optional) becomes a `tools:` frontmatter allowlist RESTRICTING the
1256
+ * subagent to exactly those tools (omission inherits the parent's full toolset,
1257
+ * per Claude Code semantics). Used by the `worker-*` dispatchers to pin each to
1258
+ * its single `mcp__<workersKey>__<mode>` tool — which physically prevents them
1259
+ * from spawning other agents or doing extra work. Names are validated by the
1260
+ * caller (`writePeerAgentMdFiles`) / are proxy-generated, so no escaping needed
1261
+ * beyond the comma-join Claude Code's frontmatter parser expects. */
1022
1262
  function buildAgentMd(spec) {
1023
- return [
1263
+ const lines = [
1024
1264
  "---",
1025
1265
  `name: ${spec.name}`,
1026
- `description: ${escapeYamlString(spec.description)}`,
1027
- "---",
1028
- "",
1029
- spec.prompt,
1030
- ""
1031
- ].join("\n");
1266
+ `description: ${escapeYamlString(spec.description)}`
1267
+ ];
1268
+ if (spec.tools && spec.tools.length > 0) lines.push(`tools: [${spec.tools.map((t) => JSON.stringify(t)).join(", ")}]`);
1269
+ lines.push("---", "", spec.prompt, "");
1270
+ return lines.join("\n");
1032
1271
  }
1033
1272
  /**
1034
1273
  * Write per-launch subagent .md files into the user's `~/.claude/agents/`
@@ -1059,7 +1298,8 @@ async function writePeerAgentMdFiles(agents, opts) {
1059
1298
  await writeRuntimeFileSecure(filePath, buildAgentMd({
1060
1299
  name: name$1,
1061
1300
  description: def.description,
1062
- prompt: def.prompt
1301
+ prompt: def.prompt,
1302
+ tools: def.tools
1063
1303
  }));
1064
1304
  paths.push(filePath);
1065
1305
  }
@@ -1251,6 +1491,8 @@ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
1251
1491
  codexCli: opts.codexCli,
1252
1492
  geminiAvailable: opts.geminiAvailable,
1253
1493
  groupKeys: opts.groupKeys,
1494
+ workerToolsAvailable: opts.workerToolsAvailable,
1495
+ browseAvailable: opts.browseAvailable,
1254
1496
  nonce,
1255
1497
  codexHome
1256
1498
  });
@@ -2036,7 +2278,7 @@ async function discoverGateCommands(cwd, opts) {
2036
2278
  if (files.length === 0) return null;
2037
2279
  let result;
2038
2280
  try {
2039
- const { runWorkerAgent } = await import("./engine-DBIfCSAl.js");
2281
+ const { runWorkerAgent } = await import("./engine-BP5EnZ-n.js");
2040
2282
  result = await runWorkerAgent({
2041
2283
  mode: "explore",
2042
2284
  workspace: root,
@@ -2250,9 +2492,9 @@ Use the request's kind and payload as the contract:
2250
2492
 
2251
2493
  Delegate heavy reading to workers:
2252
2494
 
2253
- - Use mcp__workers__explore for focused source or history gathering.
2254
- - Use mcp__workers__review for compact review of a specific plan, PR summary, or suspicious change.
2255
- - Use mcp__workers__test when a missing executable check is the blocker.
2495
+ - Use the worker-explore subagent (Agent tool) for focused source or history gathering.
2496
+ - Use the worker-review subagent (Agent tool) for compact review of a specific plan, PR summary, or suspicious change.
2497
+ - Use the worker-test subagent (Agent tool) when a missing executable check is the blocker.
2256
2498
 
2257
2499
  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.
2258
2500
 
@@ -2385,7 +2627,7 @@ Binding rule:
2385
2627
 
2386
2628
  Ask whether changed behavior has executable coverage.
2387
2629
 
2388
- - If behavior changed and no relevant test exists, use mcp__workers__test to author a focused test through a DIFFERENT lab than the implementer when possible.
2630
+ - If behavior changed and no relevant test exists, dispatch the worker-test subagent (Agent tool, subagent_type worker-test) to author a focused test through a DIFFERENT lab than the implementer when possible.
2389
2631
  - Cap missing-test attempts; default to a small number of focused tries.
2390
2632
  - Run the new test and then the relevant existing gate.
2391
2633
  - If creating a large new harness, broad fixture system, or slow integration environment is required, ask the user before proceeding.
@@ -2534,10 +2776,10 @@ Tag every blind spot as executable-checkable or judgment-only.
2534
2776
 
2535
2777
  ## Phase 3 and 4: decompose and plan (run in parallel)
2536
2778
 
2537
- These two are INDEPENDENT: mcp__orchestrate__decompose consumes { ask, context: research brief plus blind-spots }, and mcp__workers__plan consumes the ask, acceptance criteria, research pointer, and blind-spot table. Neither needs the other's output. So issue BOTH calls in a SINGLE parallel batch (same turn) — do not wait for decompose before calling plan.
2779
+ These two are INDEPENDENT: mcp__orchestrate__decompose consumes { ask, context: research brief plus blind-spots }, and the worker-plan subagent consumes the ask, acceptance criteria, research pointer, and blind-spot table. Neither needs the other's output. So issue BOTH in a SINGLE parallel batch (same turn) — call mcp__orchestrate__decompose and dispatch the worker-plan subagent together; do not wait for decompose first.
2538
2780
 
2539
2781
  - decompose: mcp__orchestrate__decompose({ ask, context: research brief plus blind-spots }). Treat the output as a proposal, not gospel; reject or revise nodes that do not map to a real blind spot.
2540
- - plan: mcp__workers__plan with the ask, acceptance criteria, research pointer, and blind-spot table. Ask for files, tests, rollback concerns, and minimal safe increments; keep it bounded and suited to the change size.
2782
+ - plan: dispatch the worker-plan subagent (Agent tool, subagent_type worker-plan) with the ask, acceptance criteria, research pointer, and blind-spot table. Ask for files, tests, rollback concerns, and minimal safe increments; keep it bounded and suited to the change size.
2541
2783
 
2542
2784
  ## Phase 5: compose a native Workflow
2543
2785
 
@@ -2559,10 +2801,10 @@ Parallelism (the Workflow tool's core optimization rule):
2559
2801
 
2560
2802
  Role to tool mapping:
2561
2803
 
2562
- - research: mcp__workers__explore and mcp__search__code for focused follow-ups.
2563
- - plan: mcp__workers__plan.
2564
- - implement: mcp__workers__implement, with worktree:true for parallel writers.
2565
- - test: mcp__workers__test, authored by a DIFFERENT LAB than the implementer when possible. This is an advisory practice, not enforced provenance.
2804
+ - research: worker-explore (Agent subagent) and mcp__search__code for focused follow-ups.
2805
+ - plan: worker-plan (Agent subagent).
2806
+ - implement: worker-implement (Agent subagent), with worktree:true for parallel writers.
2807
+ - test: worker-test (Agent subagent), authored by a DIFFERENT LAB than the implementer when possible. This is an advisory practice, not enforced provenance.
2566
2808
  - review: mcp__peers__codex_reviewer plus mcp__peers__gemini_reviewer. Advisory unless findings are converted into executable checks or code changes.
2567
2809
  - baseline and selector: OPT-IN only because it doubles cost. Choose max(orchestrated, baseline) by EXECUTABLE gate result, not model judgment. If no executable oracle exists, say the selector is advisory.
2568
2810
  - verify: cross-lab checker plus mcp__orchestrate__attest_step with producer not equal to checker lab.
@@ -2672,7 +2914,7 @@ Default caps unless the user explicitly gives a smaller or larger budget:
2672
2914
  - Then use mcp__search__code lexically for exact symbols, filenames, errors, routes, flags, and config keys.
2673
2915
  - Use git blame or history when authorship, regression timing, or intent matters.
2674
2916
  - Use mcp__search__web for upstream APIs, package behavior, protocol docs, or public issues.
2675
- - Launch parallel mcp__workers__explore workers for heavy gathering, each with a narrow question and expected artifact.
2917
+ - Launch parallel worker-explore background subagents (via the Agent tool, subagent_type worker-explore) for heavy gathering, each with a narrow question and expected artifact. They run non-blocking and report back on completion.
2676
2918
  - Keep worker results summarized; do not paste every detail into the main context.
2677
2919
 
2678
2920
  4. Form a root-cause hypothesis.
@@ -2684,7 +2926,7 @@ Default caps unless the user explicitly gives a smaller or larger budget:
2684
2926
  5. Verify load-bearing claims adversarially.
2685
2927
  - First preference: reproduce the bug, run the failing test, or run the direct check. Tag verified-executable.
2686
2928
  - If executable proof is not available, read the actual source or primary artifact and cite the lines. Tag verified-source.
2687
- - Ask mcp__workers__review to confirm the source-reading for important claims.
2929
+ - Dispatch the worker-review subagent (via the Agent tool) to confirm the source-reading for important claims.
2688
2930
  - Ask a different-lab refuter through mcp__peers__codex_critic or mcp__peers__gemini_critic to try to refute the hypothesis.
2689
2931
  - Give the refuter the symptom, observed facts, and acceptance criteria, but not your proposed root cause. Avoid anchoring them.
2690
2932
  - If the refuter finds a plausible alternative, add it to the worklist and spend at most one bounded round resolving it.
@@ -2723,19 +2965,80 @@ Return a compact brief, not the whole research dump:
2723
2965
  `
2724
2966
  };
2725
2967
 
2968
+ //#endregion
2969
+ //#region src/lib/injected-skills/worker-skill.ts
2970
+ /**
2971
+ * The `/gh-worker` skill: the operating model for the NON-BLOCKING workers
2972
+ * surface. Materialized into the per-launch mirror whenever `workerToolsEnabled()`
2973
+ * (same gate as the other floor-raising skills). Discoverability + playbook; the
2974
+ * load-bearing enforcement is the PreToolUse guard + the `worker-*` dispatcher
2975
+ * subagents (see src/lib/worker-dispatch.ts).
2976
+ */
2977
+ const WORKER_SKILL = {
2978
+ name: "gh-worker",
2979
+ md: `---
2980
+ name: gh-worker
2981
+ description: How to run github-router workers without blocking your turn. Workers (explore/implement/review/plan/test) can run up to 30 minutes; dispatch the matching worker-* background subagent so you get a completion notification instead of waiting. Use whenever you would reach for a worker.
2982
+ user-invocable: true
2983
+ ---
2984
+
2985
+ # gh-worker: non-blocking workers
2986
+
2987
+ Worker tasks (explore, implement, review, plan, test) can run for up to 30
2988
+ minutes. In this session they are NON-BLOCKING BY DESIGN: you dispatch a
2989
+ background \`worker-*\` subagent, get control back immediately, and receive the
2990
+ worker's result as a completion notification when it finishes. Your turn is
2991
+ never blocked waiting on a worker, and the worker's tool output never fills your
2992
+ context (only its final result comes back).
2993
+
2994
+ ## How to run a worker
2995
+
2996
+ Dispatch the matching dispatcher subagent with the Agent tool:
2997
+
2998
+ - worker-explore: read-only investigation / codebase gathering, returns a summary.
2999
+ - worker-review: reads the code itself to verify a change or claim; findings with severity + file:line.
3000
+ - worker-plan: returns an ordered implementation plan from a task + acceptance criteria.
3001
+ - worker-implement: read/write/bash coding worker; pass worktree: true for isolated-worktree execution + a returned diff.
3002
+ - worker-test: independent test author that writes tests trying to break the implementation and reports pass/fail.
3003
+
3004
+ Put the full worker brief in the subagent's prompt (and an absolute workspace
3005
+ path, or model/thinking/worktree, only if you need to override the defaults).
3006
+ The dispatcher calls the worker once and relays its result verbatim.
3007
+
3008
+ ## What to expect
3009
+
3010
+ - The dispatch returns immediately; you can keep working or start other workers.
3011
+ - When the worker finishes you get a completion notification carrying its result.
3012
+ - Up to 8 workers run concurrently (the worker-semaphore cap); further dispatches queue.
3013
+ - You do NOT call the raw mcp__...__ worker tools from the main thread: a guard
3014
+ denies that and points you at the matching worker-* subagent. That guard is the
3015
+ guarantee your turn never blocks; dispatching worker-* directly is the normal path.
3016
+
3017
+ ## Notes
3018
+
3019
+ - Large worker output may be summarized by the dispatcher relay; for a big
3020
+ artifact, have the worker write it to a file and return the path.
3021
+ - Dispatching the same worker twice runs it twice (no dedup); avoid double-dispatch
3022
+ for side-effecting work like worker-implement.
3023
+ - Background subagents + completion notifications are the interactive default. In
3024
+ headless (claude -p) runs the task surface behaves differently; prefer interactive
3025
+ for long worker fan-out.
3026
+ `
3027
+ };
3028
+
2726
3029
  //#endregion
2727
3030
  //#region src/lib/injected-skills/artifact-review-skill.ts
2728
3031
  const ARTIFACT_REVIEW_SKILL = {
2729
3032
  name: "gh-artifact-review",
2730
3033
  md: `---
2731
3034
  name: gh-artifact-review
2732
- description: Review plans and artifacts in the ai-or-die panel. Default to authoring a self-contained HTML artifact (rich, annotatable) and opening THAT for the human, then poll for feedback, revise, and end the loop. Use when running inside an ai-or-die tab and you have a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
3035
+ description: Review plans and artifacts in the ai-or-die panel. Default to authoring a self-contained HTML artifact (rich, annotatable, optionally interactive) and opening THAT for the human, then drain feedback with artifact_await, revise, and end the loop. Use when running inside an ai-or-die tab and you have a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
2733
3036
  user-invocable: true
2734
3037
  ---
2735
3038
 
2736
3039
  # gh-artifact-review: human review in the ai-or-die panel
2737
3040
 
2738
- Use this when you finish a plan or produce something a user should review and you are inside an ai-or-die tab (the \`mcp__peers__artifact_*\` tools drive a live panel). The human can click any block or select text to attach a comment that comes back to you.
3041
+ Use this when you finish a plan or produce something a user should review and you are inside an ai-or-die tab (the \`mcp__peers__artifact_*\` tools drive a live panel). The human can click any block or select text to attach a comment, and can click declarative action controls you emit — both come back to you as typed events.
2739
3042
 
2740
3043
  ## Default: present HTML, not raw markdown
2741
3044
 
@@ -2763,14 +3066,36 @@ Artifacts stay portable (they must render identically opened standalone), so do
2763
3066
 
2764
3067
  ## Loop
2765
3068
 
2766
- 1. Open: \`mcp__peers__artifact_open\` with the absolute path of the \`.html\` (or the file). Relay the returned \`viewUrl\` and tell the user to review in the panel — they can click a block or select text to comment.
2767
- 2. Poll: \`mcp__peers__artifact_poll\`. If status is waiting, poll again. Each returned prompt may carry a \`selector\`, quoted \`text\`, and \`sourceLine\` pinpointing what the comment is about act on that exact spot.
2768
- 3. Apply: make the requested edits, then \`mcp__peers__artifact_reply\` with a concise summary of what changed.
2769
- 4. Repeat 2-3 until the user is satisfied; then \`mcp__peers__artifact_end\`.
3069
+ 1. Open: \`mcp__peers__artifact_open\` with the absolute path of the \`.html\` (or the file). Pass \`mode:"interactive"\` when the HTML carries \`data-aod-*\` action controls (below). Relay the returned \`viewUrl\` and tell the user to review in the panel.
3070
+ 2. Drain: \`mcp__peers__artifact_await\`. It long-holds for the human's next events and returns \`{events, status, cursor}\`. **Pass the returned \`cursor\` on your next call** so you only receive newer events; if \`events\` is empty, call again with that cursor.
3071
+ 3. Act on each event by \`kind\`:
3072
+ - \`comment\` — a free-text note anchored to the artifact (\`selector\`, quoted \`text\`, \`sourceLine\`); apply the requested change at that exact spot.
3073
+ - \`action\` — the human clicked a control you emitted (\`action\` verb, \`elementId\`, optional \`value\`; a multi-select submit arrives as one action carrying the selected set). Do what the verb means (approve a step, choose an option, apply a toggle set).
3074
+ 4. Reply: \`mcp__peers__artifact_reply\` with a concise summary of what you changed. Optionally \`mcp__peers__artifact_update({file})\` or \`({html})\` to replace the artifact content in place, or \`mcp__peers__artifact_refresh\` to reload it from disk.
3075
+ 5. Repeat 2-4 until the user is satisfied; then \`mcp__peers__artifact_end\`. Use \`mcp__peers__artifact_dismiss\` to hide the panel while keeping the review alive (queued feedback preserved) if the user wants it out of the way without ending.
3076
+
3077
+ ### Push arrival (you do not have to be polling)
3078
+
3079
+ When you are idle at the prompt, panel feedback can arrive on its own as a new turn (the tab injects it). Structured actions that answer a pending decision are routed to you directly. Either way, the durable record is the \`artifact_await\` drain, so when in doubt call \`artifact_await\` (with your last cursor) to reconcile — it replays anything you missed.
3080
+
3081
+ ### \`artifact_poll\` (frozen legacy)
3082
+
3083
+ \`mcp__peers__artifact_poll\` still resolves for back-compat but returns the OLD payload (human comments only, no structured actions). Prefer \`artifact_await\`.
3084
+
3085
+ ## Interactive controls (data-aod-* authoring)
3086
+
3087
+ To let the human act on the artifact (not just comment), emit declarative controls; the panel wires them and delivers a typed \`action\` event. No JS in the artifact.
3088
+
3089
+ - **choose-one (fires immediately):** each option is a button that posts the moment it is clicked.
3090
+ \`<button data-aod-action="choose" data-aod-group="decision-1" data-aod-id="opt-jwt" data-aod-value="jwt">JWT</button>\`
3091
+ - **multi-select (toggle then submit):** checkboxes sharing a \`data-aod-group\`, plus one submit button with the same group. The submit delivers ONE action carrying \`{group, selected:[{elementId, value?}]}\`.
3092
+ \`<input type="checkbox" data-aod-action="check" data-aod-group="opts" data-aod-id="a" data-aod-value="a">\` … \`<button data-aod-action="submit" data-aod-group="opts" data-aod-id="opts-submit">Apply</button>\`
3093
+ - **plan steps:** \`<li class="aod-step" data-aod-id="plan-step-3" data-source-line="14">\` with per-step \`data-aod-action="approve"/"skip"\` buttons.
3094
+ - **Required attrs:** \`data-aod-action\` (verb), \`data-aod-id\` (stable, echoed back as \`elementId\`; a control missing it is ignored), optional \`data-aod-value\`. Keep \`data-source-line\` for comment mapping.
2770
3095
 
2771
3096
  ## Honest limits
2772
3097
 
2773
- - If a tool errors (e.g. \`NOT_IN_AIORDIE_TAB\`, \`UNREACHABLE\`), report the code/message verbatim; do not claim the panel opened.
3098
+ - If a tool errors (e.g. \`NOT_IN_AIORDIE_TAB\`, \`UNREACHABLE\`, \`INVALID_REQUEST\`), report the code/message verbatim; do not claim the panel opened.
2774
3099
  - The panel is a review surface, not an approver: outward/irreversible actions still need explicit user confirmation.
2775
3100
  `
2776
3101
  };
@@ -2792,11 +3117,13 @@ const PEER_MARKER_OPEN = "<!-- gh-router peer-mcp awareness — auto-injected, r
2792
3117
  const PEER_MARKER_CLOSE = "<!-- /gh-router peer-mcp awareness -->";
2793
3118
  const STYLE_MARKER_OPEN = "<!-- gh-router style directive — auto-injected, regenerated per launch -->";
2794
3119
  const STYLE_MARKER_CLOSE = "<!-- /gh-router style directive -->";
3120
+ const OPERATING_MARKER_OPEN = "<!-- gh-router operating defaults — auto-injected, regenerated per launch -->";
3121
+ const OPERATING_MARKER_CLOSE = "<!-- /gh-router operating defaults -->";
2795
3122
  const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injected, regenerated per launch -->";
2796
3123
  const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
2797
3124
  const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
2798
3125
  const ARTIFACT_MARKER_CLOSE = "<!-- /gh-router artifact-panel directive -->";
2799
- const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by default)\n\nYou are running inside an ai-or-die tab, so the `mcp__peers__artifact_open/poll/reply/end` tools drive a live human-review panel. Default to an HTML artifact for anything the user should review before you proceed, not just plans but also design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Author a self-contained `.html` (inline CSS, no external deps, readable typography) and open it with `mcp__peers__artifact_open`; tell the user to review (they can click a block or select text to comment), then `mcp__peers__artifact_poll` for feedback, revise, `mcp__peers__artifact_reply`, and `mcp__peers__artifact_end` when done. Plan-mode plans are auto-rendered to HTML and auto-opened for you; raw markdown is only a fallback. Skip the panel only for trivial one-line answers.\n\nMake the HTML good: match the subject project's design system (its Tailwind / theme / tokens) when the artifact represents a specific app, otherwise clean readable defaults. Per-type cheatsheet. plan: goal, current state, proposed approach, risks / open questions. comparison: options as columns with trade-off rows and a recommendation. table: scannable rows with a sticky header. diagram: boxes + arrows (SVG/CSS or Mermaid). code / diff: `<pre>` with before/after. The `gh-artifact-review` skill carries the fuller playbook.";
3126
+ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by default)\n\nYou are running inside an ai-or-die tab, so the `mcp__peers__artifact_*` tools drive a live human-review panel. Default to an HTML artifact for anything the user should review before you proceed, not just plans but also design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Author a self-contained `.html` (inline CSS, no external deps, readable typography) and open it with `mcp__peers__artifact_open` (pass `mode:\"interactive\"` if it carries `data-aod-*` action controls); tell the user to review, then drain their feedback with `mcp__peers__artifact_await` (pass back the returned `cursor` each call), revise, `mcp__peers__artifact_reply`, and `mcp__peers__artifact_end` when done. `artifact_await` returns typed events: `comment` (free-text anchored by selector/text/sourceLine) and `action` (the human clicked a control you emitted). Use `mcp__peers__artifact_update`/`artifact_refresh` to change the shown content and `mcp__peers__artifact_dismiss` to hide the panel while keeping the review alive. `mcp__peers__artifact_poll` is a frozen legacy alias (comments only). Plan-mode plans are auto-rendered to HTML and auto-opened for you; raw markdown is only a fallback. Skip the panel only for trivial one-line answers.\n\nMake the HTML good: match the subject project's design system (its Tailwind / theme / tokens) when the artifact represents a specific app, otherwise clean readable defaults. Per-type cheatsheet. plan: goal, current state, proposed approach, risks / open questions. comparison: options as columns with trade-off rows and a recommendation. table: scannable rows with a sticky header. diagram: boxes + arrows (SVG/CSS or Mermaid). code / diff: `<pre>` with before/after. To let the user act (not just comment), emit declarative controls (no JS): `data-aod-action` (verb) + `data-aod-id` (stable) [+ `data-aod-value`] — a choose-one option fires on click, a multi-select is checkboxes sharing `data-aod-group` plus a submit button with that group. The `gh-artifact-review` skill carries the fuller playbook.";
2800
3127
  /**
2801
3128
  * Writing / communication style directive injected at the TOP of the
2802
3129
  * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
@@ -2807,6 +3134,28 @@ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by defau
2807
3134
  */
2808
3135
  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).";
2809
3136
  /**
3137
+ * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md
3138
+ * AND into the main agent's system prompt (`--append-system-prompt`), so it is
3139
+ * the highest-salience behavioral default. Two defaults, both explicitly
3140
+ * overridden by the user's own direction and the domain's standards:
3141
+ *
3142
+ * 1. Orchestrate (strong default): delegate the heavy / parallel /
3143
+ * context-heavy work to the right subagent / worker / model, keeping the
3144
+ * main context free to reason and collaborate with the user, while still
3145
+ * doing trivial / surgical / last-mile work directly (delegating that
3146
+ * would only add relay-fidelity loss + latency).
3147
+ * 2. Excellence lens (HYBRID per the peer review): the reliable control
3148
+ * signal is the PRINCIPLE stated plainly; the well-known names appear only
3149
+ * as a "bar to clear" calibration, NOT as "channel X", with an explicit
3150
+ * no-impersonation / no-theatrics guardrail (cross-lab critics: a named
3151
+ * entity is a dense, high-variance vector that pulls in noise + persona
3152
+ * mannerisms at top salience, so the principle leads and the name calibrates).
3153
+ *
3154
+ * Self-referentially compliant with the style directive: no em dashes, no
3155
+ * Claude / Anthropic attribution.
3156
+ */
3157
+ const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (worker-* agents for background non-blocking runs, Task subagents for parallel work, peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context.\n\nAim high. Default to radical simplicity and relentless focus on the user's real experience (the Jobs and Ive bar for design), whole-systems first-principles thinking that anticipates scale and the long arc (the Gates bar for architecture and tech), and customer obsession that works backwards from the outcome (the Bezos bar for product and business). Question every assumption and prefer what you can derive, reproduce, or test. Adopt the principles, not a persona: no impersonation, name-dropping, or theatrics.";
3158
+ /**
2810
3159
  * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
2811
3160
  * the would-be post-write file) has grown past this size.
2812
3161
  * Read-modify-write becomes pathological at very large sizes; CLAUDE.md
@@ -3144,6 +3493,23 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
3144
3493
  });
3145
3494
  }
3146
3495
  /**
3496
+ * Prepend the operating-defaults directive (orchestrator posture + hybrid
3497
+ * excellence lens; `OPERATING_DEFAULTS_DIRECTIVE` above) to the TOP of the
3498
+ * mirrored CLAUDE.md so descendant agents (Agent subagents, agent-teams
3499
+ * teammates) inherit it. The main agent gets the same text at higher salience
3500
+ * via `--append-system-prompt`. Separate marker fence from the style / peer
3501
+ * blocks so all coexist; best-effort (warn-and-continue) like its siblings.
3502
+ */
3503
+ async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_DEFAULTS_DIRECTIVE) {
3504
+ await injectMarkerBlock({
3505
+ snippet: directive,
3506
+ markerOpen: OPERATING_MARKER_OPEN,
3507
+ markerClose: OPERATING_MARKER_CLOSE,
3508
+ position: "top",
3509
+ label: "operating-defaults"
3510
+ });
3511
+ }
3512
+ /**
3147
3513
  * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
3148
3514
  * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
3149
3515
  * subagents, agent-teams teammates) learn about the provisioned tools.
@@ -3235,6 +3601,7 @@ const INJECTED_SKILLS = [
3235
3601
  RESEARCH_SKILL,
3236
3602
  ORCHESTRATE_SKILL,
3237
3603
  FLOOR_KEEPER_SKILL,
3604
+ WORKER_SKILL,
3238
3605
  FIRST_MATE_SKILL
3239
3606
  ];
3240
3607
 
@@ -3717,7 +4084,7 @@ function initProxyFromEnv() {
3717
4084
  //#endregion
3718
4085
  //#region package.json
3719
4086
  var name = "github-router";
3720
- var version$1 = "0.3.151";
4087
+ var version$1 = "0.3.153";
3721
4088
 
3722
4089
  //#endregion
3723
4090
  //#region src/lib/approval.ts
@@ -5868,6 +6235,7 @@ const claude = defineCommand({
5868
6235
  await removeOwnClaudeConfigMirror();
5869
6236
  };
5870
6237
  let onShutdown = baseShutdown;
6238
+ let peerAwarenessSnippet;
5871
6239
  if (args["codex-mcp"] !== false) try {
5872
6240
  const requestedCli = args["codex-cli"] ?? false;
5873
6241
  const backend = resolveCodexCliBackend({
@@ -5890,7 +6258,9 @@ const claude = defineCommand({
5890
6258
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
5891
6259
  codexCli: backend === "cli",
5892
6260
  geminiAvailable,
5893
- groupKeys
6261
+ groupKeys,
6262
+ workerToolsAvailable: workerToolsEnabled(),
6263
+ browseAvailable: browseAgentEnabled()
5894
6264
  });
5895
6265
  state.peerMcpNonce = runtime.nonce;
5896
6266
  envVars.GH_ROUTER_HOOK_MCP_URL = serverUrl;
@@ -5923,6 +6293,16 @@ const claude = defineCommand({
5923
6293
  } catch (err) {
5924
6294
  consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`);
5925
6295
  }
6296
+ if (!injected.ok) consola.warn("Workers non-blocking guard NOT registered: subagent MCP injection fell back to parent-only (--mcp-config), so worker-* dispatchers cannot reach the workers server. Raw (blocking) worker tools remain usable on the main thread this session.");
6297
+ else if (process$1.env.GH_ROUTER_DISABLE_WORKER_GUARD === "1") consola.info("Workers non-blocking guard disabled via GH_ROUTER_DISABLE_WORKER_GUARD=1 — raw mcp__workers__* is callable on the main thread (blocking); the worker-* background agents remain the steered, non-blocking default.");
6298
+ else try {
6299
+ const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
6300
+ const workersKey = workersKeyOf(groupKeys);
6301
+ const modes = activeDispatchModes({ browse: browseAgentEnabled() });
6302
+ await injectStopHookIntoSettingsFile(settingsPath, buildWorkerGuardHookCommand(process$1.execPath, process$1.argv[1], workersKey, modes), "PreToolUse", 10, guardToolMatcher(workersKey, modes));
6303
+ } catch (err) {
6304
+ consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`);
6305
+ }
5926
6306
  if (skillsWritten > 0) {
5927
6307
  const skillNames = skillsToWrite.map((s) => `/${s.name}`).join(", ");
5928
6308
  process$1.stderr.write(`Injected skills (${skillsWritten}/${skillsToWrite.length}): ${skillNames}.\n`);
@@ -6062,7 +6442,7 @@ const claude = defineCommand({
6062
6442
  agentToolsAvailable: agentToolsEnabled(),
6063
6443
  groupKeys
6064
6444
  });
6065
- extraArgs.push("--append-system-prompt", peerSnippet);
6445
+ peerAwarenessSnippet = peerSnippet;
6066
6446
  try {
6067
6447
  await appendPeerAwarenessToMirroredClaudeMd(peerSnippet);
6068
6448
  } catch (err) {
@@ -6076,6 +6456,12 @@ const claude = defineCommand({
6076
6456
  } catch (err) {
6077
6457
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
6078
6458
  }
6459
+ extraArgs.push("--append-system-prompt", peerAwarenessSnippet ? `${OPERATING_DEFAULTS_DIRECTIVE}\n\n${peerAwarenessSnippet}` : OPERATING_DEFAULTS_DIRECTIVE);
6460
+ try {
6461
+ await prependOperatingDefaultsToMirroredClaudeMd();
6462
+ } catch (err) {
6463
+ consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
6464
+ }
6079
6465
  launchChild({
6080
6466
  kind: "claude-code",
6081
6467
  envVars,
@@ -6351,7 +6737,7 @@ async function postJson(url, payload, opts) {
6351
6737
  * no such handle. Hooks always receive piped/redirected stdin, so this never
6352
6738
  * blocks (guarded against an interactive TTY, and any error -> "").
6353
6739
  */
6354
- function readStdin$3() {
6740
+ function readStdin$4() {
6355
6741
  try {
6356
6742
  if (process.stdin.isTTY) return "";
6357
6743
  return readFileSync(0, "utf8");
@@ -6382,7 +6768,7 @@ const internalPromptSubmit = defineCommand({
6382
6768
  },
6383
6769
  async run() {
6384
6770
  try {
6385
- const stdin = readStdin$3();
6771
+ const stdin = readStdin$4();
6386
6772
  const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
6387
6773
  const runtime = hookMcpRuntimeFromEnv();
6388
6774
  let decision;
@@ -6442,7 +6828,7 @@ const internalPromptSubmit = defineCommand({
6442
6828
  * teardown and trips a `uv_async_send` assertion; a synchronous read has no such
6443
6829
  * handle. Hooks always receive piped stdin (guarded against a TTY; any error -> "").
6444
6830
  */
6445
- function readStdin$2() {
6831
+ function readStdin$3() {
6446
6832
  try {
6447
6833
  if (process.stdin.isTTY) return "";
6448
6834
  return readFileSync(0, "utf8");
@@ -6532,7 +6918,7 @@ const internalSessionBind = defineCommand({
6532
6918
  try {
6533
6919
  const out = typeof args.out === "string" ? args.out.trim() : "";
6534
6920
  if (!out) return;
6535
- const record = decodeSessionBind(readStdin$2());
6921
+ const record = decodeSessionBind(readStdin$3());
6536
6922
  if (record) writeSidecar(out, record);
6537
6923
  } catch {}
6538
6924
  process.exitCode = 0;
@@ -6641,15 +7027,9 @@ function renderMarkdownBody(source) {
6641
7027
  * readable typography (works even before ai-or-die injects its annotation SDK).
6642
7028
  */
6643
7029
  function renderPlanHtml(source, title = "Plan") {
6644
- const body = renderMarkdownBody(source);
6645
- return `<!doctype html>
6646
- <html lang="en">
6647
- <head>
6648
- <meta charset="utf-8">
6649
- <meta name="viewport" content="width=device-width, initial-scale=1">
6650
- <title>${escapeHtml(title)}</title>
6651
- <style>
6652
- :root {
7030
+ return wrapArtifactDocument(renderMarkdownBody(source), title);
7031
+ }
7032
+ const BASE_STYLE = ` :root {
6653
7033
  --bg: #0f1115; --fg: #f7f3ea; --muted: #aeb6c6; --border: #2a2f3a;
6654
7034
  --accent: #f4c95d; --code-bg: #171a21;
6655
7035
  --serif: 'Iowan Old Style', Georgia, 'Times New Roman', serif;
@@ -6678,7 +7058,22 @@ function renderPlanHtml(source, title = "Plan") {
6678
7058
  th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
6679
7059
  th { background: var(--code-bg); }
6680
7060
  hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }
6681
- ul, ol { padding-left: 1.4em; }
7061
+ ul, ol { padding-left: 1.4em; }`;
7062
+ /**
7063
+ * Wrap rendered inner HTML in the self-contained document shell. `extraStyle` is
7064
+ * appended to the inline stylesheet so the interactive variant can add its own
7065
+ * rules without a second `<style>` block. Static callers pass "" (default), so
7066
+ * the static document is byte-identical to the pre-refactor output.
7067
+ */
7068
+ function wrapArtifactDocument(body, title, extraStyle = "") {
7069
+ return `<!doctype html>
7070
+ <html lang="en">
7071
+ <head>
7072
+ <meta charset="utf-8">
7073
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7074
+ <title>${escapeHtml(title)}</title>
7075
+ <style>
7076
+ ${BASE_STYLE}${extraStyle}
6682
7077
  </style>
6683
7078
  </head>
6684
7079
  <body>
@@ -6692,7 +7087,7 @@ ${body}
6692
7087
 
6693
7088
  //#endregion
6694
7089
  //#region src/internal-artifact-open.ts
6695
- function readStdin$1() {
7090
+ function readStdin$2() {
6696
7091
  try {
6697
7092
  if (process.stdin.isTTY) return "";
6698
7093
  return readFileSync(0, "utf8");
@@ -6786,7 +7181,7 @@ const internalArtifactOpen = defineCommand({
6786
7181
  },
6787
7182
  async run() {
6788
7183
  try {
6789
- const raw = readStdin$1();
7184
+ const raw = readStdin$2();
6790
7185
  let payload;
6791
7186
  try {
6792
7187
  payload = JSON.parse(raw);
@@ -6814,7 +7209,7 @@ const internalArtifactOpen = defineCommand({
6814
7209
  * no such handle. Hooks always receive piped/redirected stdin, so this never
6815
7210
  * blocks (guarded against an interactive TTY, and any error -> "").
6816
7211
  */
6817
- function readStdin() {
7212
+ function readStdin$1() {
6818
7213
  try {
6819
7214
  if (process.stdin.isTTY) return "";
6820
7215
  return readFileSync(0, "utf8");
@@ -6941,7 +7336,7 @@ const internalStopHook = defineCommand({
6941
7336
  description: "Internal: the structural-gate Stop hook. Reads the Claude Code hook payload on stdin, runs the sealed gate, exits 2 (blocks the stop) on a red gate or gate-weakening diff."
6942
7337
  },
6943
7338
  async run() {
6944
- const stdin = readStdin();
7339
+ const stdin = readStdin$1();
6945
7340
  const reviewEnabled = stopReviewEnabled() && hookMcpRuntimeFromEnv() !== void 0;
6946
7341
  let transcriptPath = "";
6947
7342
  let userPrompt = "";
@@ -7078,6 +7473,56 @@ const internalStopReview = defineCommand({
7078
7473
  }
7079
7474
  });
7080
7475
 
7476
+ //#endregion
7477
+ //#region src/internal-worker-guard.ts
7478
+ /**
7479
+ * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`) — same
7480
+ * rationale as `internal-prompt-submit`: an async stdin read leaves an in-flight
7481
+ * libuv FS request that races Windows process teardown. Hooks always receive
7482
+ * piped stdin (guarded against an interactive TTY; any error → "").
7483
+ */
7484
+ function readStdin() {
7485
+ try {
7486
+ if (process.stdin.isTTY) return "";
7487
+ return readFileSync(0, "utf8");
7488
+ } catch {
7489
+ return "";
7490
+ }
7491
+ }
7492
+ const internalWorkerGuard = defineCommand({
7493
+ meta: {
7494
+ name: "internal-worker-guard",
7495
+ description: "Internal: the workers PreToolUse guard. Denies a raw mcp__<workers>__<mode> call from the main agent (redirecting to the worker-<mode> background subagent) and allows it from the dispatcher subagent. Fails closed."
7496
+ },
7497
+ args: {
7498
+ "workers-key": {
7499
+ type: "string",
7500
+ description: "Resolved workers MCP config key (bare `workers` or `gh-router-workers`).",
7501
+ required: true
7502
+ },
7503
+ modes: {
7504
+ type: "string",
7505
+ description: "CSV of active worker modes, e.g. `explore,implement,review,plan,test`.",
7506
+ required: false
7507
+ }
7508
+ },
7509
+ run(ctx) {
7510
+ try {
7511
+ const stdin = readStdin();
7512
+ const workersKey = String(ctx.args["workers-key"] ?? "").trim();
7513
+ if (workersKey.length > 0) {
7514
+ const { output } = decideWorkerGuard({
7515
+ stdin,
7516
+ workersKey,
7517
+ modes: parseModesCsv(typeof ctx.args.modes === "string" ? ctx.args.modes : void 0)
7518
+ });
7519
+ if (output) process.stdout.write(output);
7520
+ }
7521
+ } catch {}
7522
+ process.exitCode = 0;
7523
+ }
7524
+ });
7525
+
7081
7526
  //#endregion
7082
7527
  //#region src/models.ts
7083
7528
  const models = defineCommand({
@@ -7363,7 +7808,7 @@ process.on("uncaughtException", (error) => {
7363
7808
  const version = getPackageVersion();
7364
7809
  const argv = process.argv.slice(2);
7365
7810
  const isVersionFlag = argv.includes("--version");
7366
- 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";
7811
+ 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";
7367
7812
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
7368
7813
  await runMain(defineCommand({
7369
7814
  meta: {
@@ -7383,7 +7828,8 @@ await runMain(defineCommand({
7383
7828
  "internal-prompt-submit": internalPromptSubmit,
7384
7829
  "internal-stop-review": internalStopReview,
7385
7830
  "internal-session-bind": internalSessionBind,
7386
- "internal-artifact-open": internalArtifactOpen
7831
+ "internal-artifact-open": internalArtifactOpen,
7832
+ "internal-worker-guard": internalWorkerGuard
7387
7833
  }
7388
7834
  }));
7389
7835