github-router 0.3.249 → 0.3.255

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.
Files changed (95) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/background.js +8 -1
  6. package/dist/browser-ext/manifest.json +1 -1
  7. package/dist/check-usage-6PB5IUFb.js +43 -0
  8. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  9. package/dist/claude-BiZxoNWq.js +593 -0
  10. package/dist/claude-BiZxoNWq.js.map +1 -0
  11. package/dist/client-CQMfroGV.js +413 -0
  12. package/dist/client-CQMfroGV.js.map +1 -0
  13. package/dist/codex-ST21X3WF.js +85 -0
  14. package/dist/codex-ST21X3WF.js.map +1 -0
  15. package/dist/debug-CjmE9sgG.js +81 -0
  16. package/dist/debug-CjmE9sgG.js.map +1 -0
  17. package/dist/engine-DNCX0Hfd.js +2 -0
  18. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  19. package/dist/exec-DTO2oin0.js.map +1 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  21. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  22. package/dist/gate-discovery-CepmbZnc.js +635 -0
  23. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  25. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  26. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  27. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  29. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  30. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  32. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  34. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  36. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  38. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  40. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  41. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  42. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  44. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  46. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  47. package/dist/lifecycle-Byw5s1G8.js +134 -0
  48. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  49. package/dist/lifecycle-D80CaWie.js +2 -0
  50. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  51. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  52. package/dist/lifecycle-DzqCqDlM.js +2 -0
  53. package/dist/main.js +43 -12884
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  56. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  57. package/dist/models-B6UnGyRo.js +158 -0
  58. package/dist/models-B6UnGyRo.js.map +1 -0
  59. package/dist/orchestration-hRU4TdWr.js +39 -0
  60. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  61. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  62. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  63. package/dist/paths-CwKwc2pi.js +2 -0
  64. package/dist/{peer-mcp-personas-DJzLpfDJ.js → peer-mcp-personas-BDet588n.js} +1006 -2488
  65. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  67. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  69. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  70. package/dist/provision-DSkPa5hY.js +656 -0
  71. package/dist/provision-DSkPa5hY.js.map +1 -0
  72. package/dist/serve-sa_7c-yD.js +1511 -0
  73. package/dist/serve-sa_7c-yD.js.map +1 -0
  74. package/dist/server-setup-DcO9qvN3.js +4776 -0
  75. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  76. package/dist/start-siGxuZhO.js +129 -0
  77. package/dist/start-siGxuZhO.js.map +1 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  79. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  81. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  82. package/dist/token-R2ocFC3M.js +1054 -0
  83. package/dist/token-R2ocFC3M.js.map +1 -0
  84. package/dist/version-_Q1WpsQp.js +32 -0
  85. package/dist/version-_Q1WpsQp.js.map +1 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  87. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  88. package/package.json +1 -1
  89. package/dist/engine-BuuHUb4t.js +0 -2
  90. package/dist/lifecycle-0rXrwjWk.js +0 -2
  91. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  92. package/dist/lifecycle-hzPwXc3I.js +0 -2
  93. package/dist/paths-BjTMI_xK.js.map +0 -1
  94. package/dist/paths-D3XgmTcQ.js +0 -2
  95. package/dist/peer-mcp-personas-DJzLpfDJ.js.map +0 -1
@@ -0,0 +1,2347 @@
1
+ import { St as DEFINITION_OF_GREATNESS, c as personasFor, i as buildAgentPrompt, n as MCP_GROUPS, t as GROUP_META, xt as CONDENSED_OPERATING_SEQUENCE } from "./peer-mcp-personas-BDet588n.js";
2
+ import { a as isUnderClaudeConfigMirror, d as writeRuntimeFileSecure, t as PATHS } from "./paths-C1cLZ7Ga.js";
3
+ import { a as STRIPPED_AUTH_ROUTING_ENV_KEYS, n as buildCodexProviderConfigFlags } from "./provision-DSkPa5hY.js";
4
+ import { n as buildWorkspaceHeaderHelperCommand } from "./mcp-workspace-header-CGJbNeHb.js";
5
+ import { a as dispatcherAgentName, c as dispatcherTools, n as activeDispatchModes, o as dispatcherDescription, s as dispatcherPrompt } from "./worker-dispatch-Bj1uYyG9.js";
6
+ import consola from "consola";
7
+ import path from "node:path";
8
+ import { randomBytes } from "node:crypto";
9
+ import fs from "node:fs/promises";
10
+ //#region src/lib/codex-mcp-config.ts
11
+ /** The `peers` server is always enabled, so its resolved key always exists;
12
+ * this convenience reads it with the bare-key fallback for safety. */
13
+ function peersKeyOf(groupKeys) {
14
+ return groupKeys.peers ?? GROUP_META.peers.preferredKey;
15
+ }
16
+ /** The resolved `workers` server key (bare `workers`, or the `gh-router-workers`
17
+ * fallback on collision). Used to name the dispatcher tools and the guard
18
+ * matcher. Falls back to the preferred bare key when the group is absent (the
19
+ * caller only builds worker dispatchers when the group is enabled anyway). */
20
+ function workersKeyOf(groupKeys) {
21
+ return groupKeys.workers ?? GROUP_META.workers.preferredKey;
22
+ }
23
+ /**
24
+ * Decide which MCP backend serves the codex personas.
25
+ *
26
+ * - User passed `--codex-cli` AND codex 0.129+ is on PATH → "cli".
27
+ * The peer config registers `codex-cli` as a stdio MCP server
28
+ * spawning `codex mcp-server`; codex personas route there;
29
+ * gemini-critic stays on the HTTP backend (Codex CLI can't run
30
+ * Gemini).
31
+ * - User passed `--codex-cli` but codex is missing or < 0.129 →
32
+ * fallback to "http" with a warning. Never break
33
+ * `github-router claude` over a missing optional dep.
34
+ * - User did not pass `--codex-cli` → "http", read-only personas only.
35
+ */
36
+ function resolveCodexCliBackend(opts) {
37
+ if (!opts.requested) return "http";
38
+ if (!opts.codexInfo || !opts.codexInfo.ok) {
39
+ const detail = opts.codexInfo?.version ? `installed version "${opts.codexInfo.version}" is too old (need 0.129+)` : "codex CLI not found on PATH";
40
+ consola.warn(`--codex-cli requested but ${detail}; falling back to HTTP-only Codex MCP backend (codex-implementer will not be registered).`);
41
+ return "http";
42
+ }
43
+ return "cli";
44
+ }
45
+ /**
46
+ * Build one scoped HTTP `mcpServers` entry for a group: `type: http`, the
47
+ * `/mcp/<urlSuffix>` URL, the Bearer-nonce Authorization header, and the
48
+ * per-session workspace `headersHelper` when supplied. Shared by
49
+ * `buildPeerMcpConfig` (the session `.claude.json` entries) AND
50
+ * `buildPeerAgentDefinitions` (the per-subagent inline `mcpServers` frontmatter
51
+ * that sidesteps Claude Code's Agent-tool MCP-inheritance bug, anthropics/
52
+ * claude-code#30280 — a bare name-reference "shares the parent connection" and
53
+ * re-triggers it, so subagents MUST inline the full HTTP config to connect
54
+ * independently). One source of truth so the two can't drift.
55
+ */
56
+ function httpEntryFor(serverUrl, group, nonce, workspaceHeaderCmd) {
57
+ const entry = {
58
+ type: "http",
59
+ url: `${serverUrl}/mcp/${GROUP_META[group].urlSuffix}`,
60
+ headers: { Authorization: `Bearer ${nonce}` }
61
+ };
62
+ const ws = workspaceHeaderCmd?.trim();
63
+ if (ws) entry.headersHelper = ws;
64
+ return entry;
65
+ }
66
+ /**
67
+ * Build the JSON payload for `claude --mcp-config <path>` (and the same
68
+ * entries that get merged into the mirrored `.claude.json`).
69
+ *
70
+ * Emits one HTTP `mcpServers` entry per enabled group present in
71
+ * `opts.groupKeys`, each pointing at its scoped `/mcp/<group>` endpoint
72
+ * under the resolved (bare or prefixed-fallback) config key. When
73
+ * `codexCli` is true, also registers `codex-cli` (stdio) which spawns
74
+ * `codex mcp-server` with the proxy's provider-config flags so codex runs
75
+ * through our Copilot-routed billing path rather than its default
76
+ * api.openai.com.
77
+ */
78
+ function buildPeerMcpConfig(serverUrl, opts) {
79
+ const mcpServers = {};
80
+ const workspaceHeaderCmd = opts.workspaceHeaderCmd?.trim();
81
+ for (const group of MCP_GROUPS) {
82
+ const key = opts.groupKeys[group];
83
+ if (!key) continue;
84
+ mcpServers[key] = httpEntryFor(serverUrl, group, opts.nonce, workspaceHeaderCmd);
85
+ }
86
+ if (opts.codexCli) mcpServers["codex-cli"] = {
87
+ command: "codex",
88
+ args: ["mcp-server", ...buildCodexProviderConfigFlags(serverUrl)],
89
+ env: {
90
+ OPENAI_BASE_URL: `${serverUrl}/v1`,
91
+ OPENAI_API_KEY: "dummy",
92
+ CODEX_HOME: opts.codexHome
93
+ }
94
+ };
95
+ return { mcpServers };
96
+ }
97
+ /**
98
+ * The `peer-review-coordinator` Claude Code subagent — the strongest
99
+ * auto-invocation lever per Phase 2A of the peer-MCP plan. It is NOT a
100
+ * peer-MCP tool; it is a regular Claude Code subagent that runs inside
101
+ * the spawned-claude context, has tool access to the peer MCP tools
102
+ * (codex_critic, codex_reviewer, gemini_critic), and fans out + aggregates.
103
+ *
104
+ * The "use proactively" phrase in the description is the documented Claude
105
+ * Code idiom for subagents the parent (Opus) should delegate to without
106
+ * explicit user request — at the natural checkpoints listed below.
107
+ *
108
+ * Empirically the polling-loop reliability for "use proactively" is ~60%
109
+ * (claude-code-guide expert estimate); the plan calls for an acceptance
110
+ * test (≥7/10 sessions delegate at the right checkpoints) before declaring
111
+ * "auto-invoked". If <7/10 we flip the optional PreToolUse hook on
112
+ * ExitPlanMode to default-on (env-disable-able).
113
+ */
114
+ function buildCoordinatorAgent(opts) {
115
+ const peers = ["codex-critic", "opus-critic"];
116
+ if (opts.geminiAvailable) peers.push("gemini-critic");
117
+ peers.push("codex-reviewer");
118
+ if (opts.geminiAvailable) peers.push("gemini-reviewer");
119
+ return {
120
+ description: "Coordinates cross-lab adversarial review across codex-critic, opus-critic, gemini-critic, codex-reviewer. Use proactively before non-trivial plans and after non-trivial commits. Always pass artifacts verbatim — peers are fresh-context.",
121
+ prompt: [
122
+ "# Subagent: peer-review-coordinator",
123
+ "",
124
+ "You orchestrate cross-lab adversarial review for the lead orchestrator (Opus). You have access to these peer-MCP subagents:",
125
+ "",
126
+ peers.map((p) => `- \`${p}\``).join("\n"),
127
+ "",
128
+ "## When the lead invokes you",
129
+ "",
130
+ "The lead's brief will include an artifact (plan, design, diff, or code) and a goal (e.g. 'review before exit-plan', 'review the commit I just made', 'cross-check codex-critic's verdict'). Pick the right peers for the artifact type:",
131
+ "",
132
+ "- **Plan / design / architecture choice** → fan out to `codex-critic` (gpt-5.6-sol, strongest reasoning, cross-lab)" + (opts.geminiAvailable ? " AND `gemini-critic` (third-lab triangulation, strong on formal reasoning) in parallel" : "") + ". codex-reviewer is the wrong tool for plans (it's a code-specialist, not an architecture critic).",
133
+ "- **Concrete diff or single file** → fan out to `codex-reviewer` (gpt-5.3-codex, line-level code specialist, fastest at ~16s)" + (opts.geminiAvailable ? " AND `gemini-reviewer` (gemini-3.1-pro, second-lab line-level review)" : "") + (opts.geminiAvailable ? " AND `gemini-critic` for cross-lab triangulation" : "") + ". For very small changes (<20 lines), one `codex-reviewer` call is enough.",
134
+ "- **Large artifact** → the only peers that take a large artifact WHOLE are `codex-critic` (gpt-5.6-sol, ≈1M-token input window) and `opus-critic` (Opus-4.7-1M, ≈936K-token input on enterprise catalogs; ≈168K otherwise). Route the full artifact to those for cross-lab coverage. `codex-reviewer` (≈272K) and `gemini-critic` (≈136K) have small windows — see Decomposition below: never summarize or downsize the request to squeeze a large artifact into a small-window peer.",
135
+ "- **Formal reasoning, proofs, or invariants** → prefer `gemini-critic`" + (opts.geminiAvailable ? " (gemini-3.1-pro, strong on math and formally-stated properties)" : " (NOT REGISTERED in this session — gemini-3.x not in catalog)") + ".",
136
+ "- **Tie-breaker after codex-critic has weighed in** → call `gemini-critic`" + (opts.geminiAvailable ? "" : " (NOT REGISTERED in this session)") + " or `opus-critic` with the artifact AND codex-critic's verdict for cross-check.",
137
+ "- **Fast sanity check** → `opus-critic` (~22s, same lab as lead but fresh context — catches confabulation and motivated reasoning).",
138
+ "",
139
+ "## Decomposition for large artifacts",
140
+ "",
141
+ "Route by the peer's real PROMPT WINDOW (input tokens): `codex-critic` gpt-5.6-sol ≈1M · `opus-critic` Opus-4.7-1M ≈936K (enterprise catalogs; ≈168K otherwise) · `codex-reviewer` gpt-5.3-codex ≈272K · `gemini-critic` gemini-3.1-pro ≈136K. The proxy REJECTS (with an actionable message) any single call whose brief exceeds the target peer's window — it will NOT silently truncate, because dropping lines from a review artifact is worse than a clear error. So: send the full artifact only to peers whose window fits it (large artifacts → `codex-critic` and/or `opus-critic`). When a peer's window is too small (commonly `gemini-critic` at ≈136K, or `codex-reviewer` at ≈272K), do NOT summarize or downsize the request to include it — either skip that peer, or split the artifact into 2-4 logical batches BY CONCERN (not by raw size — semantic batches give better per-batch reviews) that each fit, and call in parallel. Use the big-window peers for the whole and reserve a small-window peer like gemini for the concerns it can actually hold. The proxy's MCP cap allows up to 8 in-flight calls. Aggregate findings yourself before reporting back. (Separately, on the JSON transport a per-effort `predictedTooLong` byte cap still guards the ~60s tools/call timeout for non-SSE clients; Claude Code uses SSE, which streams with heartbeats and isn't subject to that cap.)",
142
+ "",
143
+ "## Aggregation contract",
144
+ "",
145
+ "When fan-out completes, return a SEVERITY-GROUPED, DEDUPLICATED finding list. Format:",
146
+ "",
147
+ " ## Findings",
148
+ " ### HIGH",
149
+ " 1. <one-line title> — `<file:line>` — sources: codex-critic, gemini-critic (3-lab confirmed if applicable)",
150
+ " - bug: <one sentence>",
151
+ " - mitigation: <one sentence>",
152
+ " ### MEDIUM",
153
+ " ...",
154
+ " ### LOW",
155
+ " ...",
156
+ "",
157
+ "Cite which peer raised each finding. If two or more peers raised the SAME finding (cross-lab confirmation), call it out — those are the highest-confidence bugs.",
158
+ "",
159
+ "## What NOT to do",
160
+ "",
161
+ "- Do not paraphrase or summarize per-peer verdicts BEFORE aggregating; aggregate from the raw verdicts.",
162
+ "- Do not invent severity labels not present in the source verdicts.",
163
+ "- Do not call peers serially (waste of wall-clock); always fan out in parallel.",
164
+ "- Do not consult yourself — you are the coordinator, not a critic.",
165
+ "",
166
+ "Self-reminder (read before every reply):",
167
+ " Did I fan out in parallel to the right peers for this artifact type?",
168
+ " Did I aggregate findings by severity, citing which peer raised each?",
169
+ " If two peers agreed, did I flag the cross-lab confirmation?"
170
+ ].join("\n")
171
+ };
172
+ }
173
+ /**
174
+ * Claude Code's built-in subagents, which the interactive CLI provides natively
175
+ * but the Agent SDK (used by CloudCLI under `serve`) does NOT register — so a
176
+ * serve session shows `Agent type 'Explore' not found`. We re-register them as
177
+ * custom subagents so the model's habitual `Agent(subagent_type:"…")` calls
178
+ * resolve. SERVE-ONLY: never inject these for `github-router claude` (the CLI's
179
+ * native, tuned built-ins would be shadowed by a same-name custom agent).
180
+ *
181
+ * No `tools:` restriction — each inherits the session's full toolset; the role
182
+ * is steered by the prompt (matching the built-ins' behavior without the risk of
183
+ * an over-narrow allowlist). `statusline-setup`/`output-style-setup` are omitted
184
+ * (niche, rarely invoked).
185
+ */
186
+ const BUILTIN_SUBAGENT_DEFINITIONS = {
187
+ "general-purpose": {
188
+ description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use when searching for a keyword or file and you are not confident you will find the right match in the first few tries.",
189
+ prompt: "You are a general-purpose agent. Research the question or carry out the multi-step task you are given, using the full toolset (read, search, edit, run commands as needed). Work autonomously and return a single, complete final answer — your final message is the whole result, so include the findings, file paths, and any code the caller needs."
190
+ },
191
+ Explore: {
192
+ description: "Read-only search agent for broad fan-out searches — when answering means sweeping many files or directories and you only need the conclusion, not the file dumps. It locates code; it does not modify it.",
193
+ prompt: "You are a read-only exploration agent. Investigate the codebase to answer the question by reading and searching (Read/Glob/Grep and semantic code search); do NOT modify any files, run mutating commands, or make commits. Cast a wide net, then return a concise conclusion with the relevant file paths and line references — your final message is the whole answer."
194
+ },
195
+ Plan: {
196
+ description: "Software architect agent for designing implementation plans. Use when you need to plan the implementation strategy for a task. Returns a step-by-step plan, identifies critical files, and considers architectural trade-offs.",
197
+ prompt: "You are a planning agent. Read the codebase (read-only — do not modify files) to design a concrete, ordered implementation plan for the task: the approach, the specific files to change, reuse of existing utilities, risks, and how the result will be verified. Return the plan as your final message."
198
+ }
199
+ };
200
+ /** Empty-string-safe read of an optional model id. */
201
+ function nonEmptyModel(id) {
202
+ return id && id.length > 0 ? id : void 0;
203
+ }
204
+ /**
205
+ * The shared "prefer the dedicated tools over shell" steer, appended to every
206
+ * native subagent prompt. `bashUses` names the work that legitimately belongs in
207
+ * Bash for that agent (`builds` for a coder, `repros` for an investigator), the
208
+ * only token that ever differed across the three hand-copied variants this
209
+ * replaces.
210
+ *
211
+ * Deliberately NOT merged with `FILE_TOOL_GUIDANCE` in
212
+ * `anthropic-translate/anthropic-request.ts`: that one is injected at the shim
213
+ * boundary and therefore only reaches shim-routed (non-Claude) models. When a
214
+ * native falls back to the lead's Claude model the shim is bypassed entirely, so
215
+ * this prompt-level copy is the only coverage that survives. Two layers, two
216
+ * different reasons to exist.
217
+ */
218
+ function fileToolSteer(bashUses) {
219
+ return `Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running ${bashUses}, tests, and git. Do not shell out (sed/awk/python/here-docs) to read or edit files.`;
220
+ }
221
+ /** The read-only half of `fileToolSteer`, for agents that never write. */
222
+ function readOnlyToolSteer() {
223
+ return "Use Read to read files and Grep/Glob plus the semantic code search tool to find them; Bash is for read-only inspection such as git log, git blame, and git show. Do not modify any file, and do not run mutating commands.";
224
+ }
225
+ /**
226
+ * `tools:` allowlist for the read-only natives (`scout`, `brainstorm`), modelled
227
+ * on Claude Code's own `Explore`/`Plan` built-ins, which run with every tool
228
+ * EXCEPT Agent / Artifact / ExitPlanMode / Edit / Write / NotebookEdit — note
229
+ * that Anthropic's own read-only agent keeps Bash, which is what makes `git log`
230
+ * and `git blame` reachable.
231
+ *
232
+ * Two deliberate deviations, both forced by the frontmatter format:
233
+ *
234
+ * 1. `tools:` is a POSITIVE allowlist with no "all except" form, so the
235
+ * complement has to be spelled out. Rather than enumerate every harness tool
236
+ * (which varies by Claude Code version, and would silently drop anything
237
+ * added later), this lists the read / search / shell core that a read-only
238
+ * agent actually uses. Tools added by a future release are NOT inherited by
239
+ * these two agents; that is the price of real enforcement over a prompt that
240
+ * merely asks nicely.
241
+ * 2. The workers and orchestrate MCP groups are excluded on the same reasoning
242
+ * that makes `Explore` drop `Agent`: both spawn further agents, so leaving
243
+ * them in would reintroduce exactly the recursion that exclusion prevents.
244
+ *
245
+ * NOT A SANDBOX. `Bash` is retained because Anthropic's own read-only built-in
246
+ * retains it, and without it an explorer cannot run `git log` / `git blame` /
247
+ * `git show`, which is most of what repository archaeology needs. A shell is a
248
+ * general write primitive, so "read-only" here means the agent has no dedicated
249
+ * mutation tool and is instructed not to mutate, NOT that mutation is
250
+ * impossible. Dropping Edit/Write still removes the path a model reaches for by
251
+ * default. If a future change needs a hard guarantee, this allowlist is not
252
+ * where it can be made.
253
+ *
254
+ * `searchKey` is the RESOLVED group key, not the bare literal: a user-side
255
+ * `mcpServers` collision renames the group (`gh-router-search`, …), and a
256
+ * hardcoded `mcp__search__*` would then grant nothing at all.
257
+ */
258
+ function readOnlyToolAllowlist(searchKey) {
259
+ return [
260
+ "Read",
261
+ "Grep",
262
+ "Glob",
263
+ "Bash",
264
+ "WebFetch",
265
+ "WebSearch",
266
+ `mcp__${searchKey}__*`
267
+ ];
268
+ }
269
+ /**
270
+ * Build the JSON payload for `claude --agents <path>`.
271
+ *
272
+ * Always includes the read-only personas applicable to the mode (gemini
273
+ * is dropped if absent from the catalog); adds `codex-implementer` only
274
+ * when `codexCli` is true. Always appends the `peer-review-coordinator`
275
+ * meta-subagent — the strongest "use proactively" auto-invocation lever
276
+ * per Phase 2A of the peer-MCP plan.
277
+ */
278
+ function buildPeerAgentDefinitions(opts) {
279
+ const out = {};
280
+ const personas = personasFor({
281
+ codexCli: opts.codexCli,
282
+ geminiAvailable: opts.geminiAvailable
283
+ });
284
+ const peersKey = peersKeyOf(opts.groupKeys);
285
+ const peersMcp = opts.serverUrl ? { mcpServers: { [peersKey]: httpEntryFor(opts.serverUrl, "peers", opts.nonce, opts.workspaceHeaderCmd) } } : {};
286
+ for (const persona of personas) out[persona.agentName] = {
287
+ description: persona.description,
288
+ prompt: buildAgentPrompt(persona, {
289
+ codexCli: opts.codexCli,
290
+ peersKey
291
+ }),
292
+ ...peersMcp
293
+ };
294
+ out["peer-review-coordinator"] = {
295
+ ...buildCoordinatorAgent({
296
+ codexCli: opts.codexCli,
297
+ geminiAvailable: opts.geminiAvailable
298
+ }),
299
+ ...peersMcp
300
+ };
301
+ const nativeModel = nonEmptyModel(opts.nativeSubagentModel);
302
+ const reviewerModel = nonEmptyModel(opts.reviewerModel);
303
+ const brainstormModel = nonEmptyModel(opts.brainstormModel);
304
+ const scoutModel = nonEmptyModel(opts.scoutModel);
305
+ const scribeModel = nonEmptyModel(opts.scribeModel);
306
+ const modelField = nativeModel ? { model: nativeModel } : {};
307
+ const searchKey = opts.groupKeys.search ?? GROUP_META.search.preferredKey;
308
+ const searchMcp = opts.serverUrl ? { mcpServers: { [searchKey]: httpEntryFor(opts.serverUrl, "search", opts.nonce, opts.workspaceHeaderCmd) } } : {};
309
+ out.implementer = {
310
+ description: nativeModel ? `Bounded implementation subagent running ${nativeModel} (strong non-Claude coder, maximum reasoning). Use proactively for well-scoped coding tasks — edits, small features, fixes — to keep the lead's context focused; runs in its own context. Model is overridable at spawn.` : `Bounded implementation subagent (native tools, runs on the lead's model in its own context). Use proactively for well-scoped coding tasks — edits, small features, fixes — to keep the lead's context focused. Model is overridable at spawn.`,
311
+ prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. " + fileToolSteer("builds") + " Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
312
+ ...modelField
313
+ };
314
+ out.reviewer = {
315
+ description: reviewerModel ? `Feedback subagent running ${reviewerModel}, a DIFFERENT lab from both the lead and the implementer, so its blind spots are decorrelated from whoever produced the work. Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context (reproduce a failure, run the suite, bisect); prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. It can also REVIEW SCREENSHOTS and other images: just point it at the file and it will look at them (its model accepts up to 10 images per request, more than any other lane here). Model is overridable at spawn.` : `Feedback subagent (native tools, runs on the lead's model in its own context). Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context; prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. Model is overridable at spawn.`,
316
+ prompt: "You are a feedback subagent. Your job is to tell the caller what is actually true about the artifact you are given — code, a plan, a document, a failure report — and what is wrong with it. Verify against the ACTUAL code by reading it; never assume. Do whatever the assessment requires: reproduce a failure end to end as close to how a real user hits it as you can, form hypotheses and test them against the code and runtime, and isolate the true root cause rather than a symptom. Where the change warrants it, author tests that try to BREAK the implementation (edge cases, error paths, and the acceptance criteria as executable checks), run them, and report which pass and which fail; do NOT modify production code just to make tests pass. " + fileToolSteer("builds") + " Do the work yourself — do not spawn further subagents. Report severity-ranked findings with `file:line` citations, the evidence behind each, and end with a clear go/no-go.",
317
+ ...reviewerModel ? { model: reviewerModel } : {}
318
+ };
319
+ out.brainstorm = {
320
+ description: brainstormModel ? `Divergent-options subagent running ${brainstormModel} (third lab, for approaches the lead would not generate). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.` : `Divergent-options subagent (runs on the lead's model in its own context). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.`,
321
+ prompt: "You are a divergent-options subagent: the lead's sounding board while an approach is still open. Your job is to surface the option the lead would not have reached on its own. If the caller states its current leading approach, your job is to try to beat it, and you close with one verdict: `replace`, `retain`, or `insufficient evidence`. `replace` requires naming a concrete alternative that dominates it and the evidence that decides between them. `retain` is a real answer and a useful one: say it plainly when the approach survives a genuine attempt to beat it. Manufactured disagreement is as useless as agreement; do neither. If the caller states no leading approach, generate independently and let the lead compare. Return 3 to 5 approaches that differ in MECHANISM, not in phrasing. For each: how it works, what it costs, what would have to be true for it to be the right answer, and the failure mode that would kill it. Near-duplicate options are the failure mode to avoid — if only one real approach exists, say so plainly and explain why the alternatives are dead, because one honest option beats four padded ones. Screen EVERY candidate against this repository and this environment before you rank them, then verify the one you are about to recommend can actually run here: read the code path it depends on, the guard that would refuse it, the artifact it assumes exists. A recommendation that cannot execute is worse than no recommendation. If checking kills your front-runner, rerank and say so. Ground every option in what the repository actually contains, and prefer reusing what is already there over inventing something new. " + readOnlyToolSteer() + " Do the work yourself — do not spawn further subagents.",
322
+ tools: readOnlyToolAllowlist(searchKey),
323
+ ...brainstormModel ? { model: brainstormModel } : {},
324
+ ...searchMcp
325
+ };
326
+ if (scoutModel) out.scout = {
327
+ description: `Read-only exploration subagent running ${scoutModel} (fast and cheap, so repository lookups do not run at the lead's model rates). Use proactively to find or understand something in the codebase: it sweeps widely and returns conclusions with file:line references rather than file dumps. Model is overridable at spawn.`,
328
+ prompt: "You are a read-only exploration subagent. Answer the question by investigating the repository: cast a wide net, then narrow. Return the conclusion, not the raw material — cite `file:line` for anything load-bearing and quote only the lines that matter. If the answer is that something does not exist, say so explicitly and describe where you looked. " + readOnlyToolSteer() + " Do the work yourself — do not spawn further subagents.",
329
+ tools: readOnlyToolAllowlist(searchKey),
330
+ model: scoutModel,
331
+ ...searchMcp
332
+ };
333
+ out.scribe = {
334
+ description: scribeModel ? `Documentation subagent running ${scribeModel}. Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Keeps low-glamour upkeep off the lead's context. Model is overridable at spawn.` : `Documentation subagent (runs on the lead's model in its own context). Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Model is overridable at spawn.`,
335
+ prompt: "You are a documentation subagent. Write and maintain the prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, README rows. Read the code before describing it — every claim you write must be checkable against the repository as it is now, not as a summary said it was. Match the surrounding document's voice, structure, and level of detail. Prefer updating an existing document over adding a new one, and delete what has become false rather than layering a correction on top of it. " + fileToolSteer("builds") + " Do the work yourself — do not spawn further subagents. Report which documents changed and any claim you could not verify.",
336
+ ...scribeModel ? { model: scribeModel } : {}
337
+ };
338
+ if (opts.workerToolsAvailable) {
339
+ const workersKey = workersKeyOf(opts.groupKeys);
340
+ const workersMcp = opts.serverUrl ? { mcpServers: { [workersKey]: httpEntryFor(opts.serverUrl, "workers", opts.nonce, opts.workspaceHeaderCmd) } } : {};
341
+ for (const mode of activeDispatchModes({ browse: opts.browseAvailable === true })) out[dispatcherAgentName(mode)] = {
342
+ description: dispatcherDescription(mode),
343
+ prompt: dispatcherPrompt(mode, workersKey),
344
+ tools: dispatcherTools(mode, workersKey),
345
+ ...workersMcp
346
+ };
347
+ }
348
+ return out;
349
+ }
350
+ /**
351
+ * Default location Claude Code reads subagent .md files from at session
352
+ * startup. Files placed here populate the Task `subagent_type` enum.
353
+ *
354
+ * We point at the router-owned `PATHS.CLAUDE_CONFIG_DIR/agents/` because
355
+ * `getClaudeCodeEnvVars` sets `CLAUDE_CONFIG_DIR=PATHS.CLAUDE_CONFIG_DIR`
356
+ * (the snapshot-mirror substrate fix that gives spawned teammates an
357
+ * authenticatable on-disk credential). The user's own custom-agent .md
358
+ * files were copied into this same dir by `ensureClaudeConfigMirror`,
359
+ * so writing peer-* files here doesn't conflict — and the boot-time
360
+ * sweep is scoped to peer-* names only via the persona-name allowlist.
361
+ */
362
+ function defaultAgentsDir() {
363
+ return path.join(PATHS.CLAUDE_CONFIG_DIR, "agents");
364
+ }
365
+ /**
366
+ * YAML frontmatter string-escape — sufficient for our use case where
367
+ * descriptions can contain colons, quotes, newlines. Wraps the value
368
+ * in double-quotes and escapes:
369
+ * - `\` and `"` (canonical YAML)
370
+ * - `\n`, `\r`, `\t` (whitespace controls — `\r` matters on Windows-edited
371
+ * literals; strict YAML 1.2 parsers reject raw `\r` in double-quoted
372
+ * scalars)
373
+ * - other C0 control chars (\x00-\x08, \x0B, \x0C, \x0E-\x1F) and
374
+ * DEL (\x7F) — encoded as `\xNN` so the YAML stays valid even if
375
+ * a future description sources data from an external file
376
+ *
377
+ * NOT a general-purpose YAML serializer; we control the inputs.
378
+ */
379
+ function escapeYamlString(s) {
380
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`)}"`;
381
+ }
382
+ /**
383
+ * Strict allowlist for subagent names — controls both the YAML
384
+ * frontmatter `name:` field AND the filename suffix. Defense-in-depth:
385
+ * even if a future contributor wires in a dynamic agent name from
386
+ * outside, the validator at the top of `writePeerAgentMdFiles` rejects
387
+ * anything that wouldn't be a safe bare YAML scalar AND a safe path
388
+ * component.
389
+ */
390
+ const VALID_AGENT_NAME = /^[A-Za-z][A-Za-z0-9-]*$/;
391
+ /**
392
+ * Emit the subagent-frontmatter `mcpServers` block as a YAML sequence of
393
+ * single-key maps (the INLINE form that connects on subagent start — a bare
394
+ * name-reference would re-trigger claude-code#30280). Scalars are
395
+ * JSON.stringify-quoted, which is valid YAML double-quoted syntax (handles the
396
+ * Windows-path/backslash headersHelper command safely).
397
+ *
398
+ * ```yaml
399
+ * mcpServers:
400
+ * - workers:
401
+ * type: http
402
+ * url: "http://127.0.0.1:PORT/mcp/workers"
403
+ * headers:
404
+ * Authorization: "Bearer <nonce>"
405
+ * headersHelper: "<cmd>"
406
+ * ```
407
+ */
408
+ function emitMcpServersYaml(mcpServers) {
409
+ const lines = ["mcpServers:"];
410
+ for (const [key, entry] of Object.entries(mcpServers)) {
411
+ lines.push(` - ${JSON.stringify(key)}:`);
412
+ lines.push(` type: ${JSON.stringify(entry.type)}`);
413
+ lines.push(` url: ${JSON.stringify(entry.url)}`);
414
+ lines.push(` headers:`);
415
+ for (const [hk, hv] of Object.entries(entry.headers)) lines.push(` ${JSON.stringify(hk)}: ${JSON.stringify(hv)}`);
416
+ if (entry.headersHelper !== void 0) lines.push(` headersHelper: ${JSON.stringify(entry.headersHelper)}`);
417
+ }
418
+ return lines;
419
+ }
420
+ /** Build a single subagent .md file body (frontmatter + system prompt).
421
+ *
422
+ * `tools` (optional) becomes a `tools:` frontmatter allowlist RESTRICTING the
423
+ * subagent to exactly those tools (omission inherits the parent's full toolset,
424
+ * per Claude Code semantics). Used by the `worker-*` dispatchers to pin each to
425
+ * its single `mcp__<workersKey>__<mode>` tool — which physically prevents them
426
+ * from spawning other agents or doing extra work. `mcpServers` (optional) is
427
+ * emitted as an inline `mcpServers` frontmatter list so the subagent connects
428
+ * directly to its scoped MCP server(s) — a workaround for claude-code#30280
429
+ * (Agent-tool subagents don't reliably inherit the session's HTTP MCP servers).
430
+ * Names are validated by the caller (`writePeerAgentMdFiles`) / are
431
+ * proxy-generated, so no escaping needed beyond the comma-join Claude Code's
432
+ * frontmatter parser expects. */
433
+ function buildAgentMd(spec) {
434
+ const lines = [
435
+ "---",
436
+ `name: ${spec.name}`,
437
+ `description: ${escapeYamlString(spec.description)}`
438
+ ];
439
+ if (spec.model) lines.push(`model: ${escapeYamlString(spec.model)}`);
440
+ if (spec.tools && spec.tools.length > 0) lines.push(`tools: [${spec.tools.map((t) => JSON.stringify(t)).join(", ")}]`);
441
+ if (spec.mcpServers && Object.keys(spec.mcpServers).length > 0) lines.push(...emitMcpServersYaml(spec.mcpServers));
442
+ lines.push("---", "", spec.prompt, "");
443
+ return lines.join("\n");
444
+ }
445
+ /**
446
+ * Write per-launch subagent .md files into the user's `~/.claude/agents/`
447
+ * directory so they appear in Claude Code's Task `subagent_type` enum
448
+ * (which `--agents` JSON files do NOT, per claude-code-guide expert).
449
+ *
450
+ * Filenames follow `peer-<pid>-<rand>-<agentName>.md` so the boot-time
451
+ * sweep (`sweepStalePeerAgentMdFiles` in paths.ts) can drop orphans
452
+ * from crashed prior proxy sessions without touching the user's other
453
+ * `.claude/agents/` files. The `name:` field in the frontmatter is the
454
+ * canonical agent identifier — matching across files would cause Claude
455
+ * Code to (un)deterministically pick one, so concurrent proxies running
456
+ * the same agents need different filenames but resolve to the same
457
+ * agent name (intended — they're the same subagent, just registered
458
+ * twice).
459
+ *
460
+ * Returns the file paths plus a cleanup() that unlinks them.
461
+ */
462
+ async function writePeerAgentMdFiles(agents, opts) {
463
+ for (const name of Object.keys(agents)) if (!VALID_AGENT_NAME.test(name)) throw new Error(`writePeerAgentMdFiles: invalid agent name ${JSON.stringify(name)} — must match ${VALID_AGENT_NAME.source}`);
464
+ const dir = opts.agentsDir ?? defaultAgentsDir();
465
+ await fs.mkdir(dir, { recursive: true });
466
+ const paths = [];
467
+ try {
468
+ for (const [name, def] of Object.entries(agents)) {
469
+ const filePath = path.join(dir, `peer-${opts.fileSuffix}-${name}.md`);
470
+ await fs.unlink(filePath).catch(() => {});
471
+ await writeRuntimeFileSecure(filePath, buildAgentMd({
472
+ name,
473
+ description: def.description,
474
+ prompt: def.prompt,
475
+ model: def.model,
476
+ tools: def.tools,
477
+ mcpServers: def.mcpServers
478
+ }));
479
+ paths.push(filePath);
480
+ }
481
+ } catch (err) {
482
+ await Promise.allSettled(paths.map((p) => fs.unlink(p)));
483
+ throw err;
484
+ }
485
+ const cleanup = async () => {
486
+ await Promise.allSettled(paths.map((p) => fs.unlink(p)));
487
+ };
488
+ return {
489
+ paths,
490
+ cleanup
491
+ };
492
+ }
493
+ /**
494
+ * Read just the `mcpServers` object from a mirrored `.claude.json` (or `{}`
495
+ * on missing / malformed). Used by `resolveGroupKeysFromMirror` to detect
496
+ * which of our bare group keys would collide with a user-side entry.
497
+ */
498
+ async function readMcpServersSnapshot(target) {
499
+ try {
500
+ const raw = await fs.readFile(target, "utf8");
501
+ const parsed = JSON.parse(raw);
502
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
503
+ const servers = parsed.mcpServers;
504
+ if (servers && typeof servers === "object" && !Array.isArray(servers)) return servers;
505
+ }
506
+ } catch {}
507
+ return {};
508
+ }
509
+ /**
510
+ * Resolve a config-entry key for each enabled group, defending against
511
+ * collisions with the user's own `mcpServers`. Prefer the bare key
512
+ * (`peers`/`search`/…); on collision walk the numbered fallback sequence
513
+ * `gh-router-<group>`, `gh-router-<group>-2`, `gh-router-<group>-3`, …
514
+ * until a free name is found. This NEVER skips and NEVER returns a name the
515
+ * user already owns: every enabled group is guaranteed a key WE control, so
516
+ * a capability is never silently dropped AND the model is never routed at
517
+ * the user's same-named server (the caller threads these resolved keys into
518
+ * both the `mcpServers` entries AND the persona `.md` routing strings). The
519
+ * `skipped` field is retained for API stability but is always empty now.
520
+ *
521
+ * Reads the mirror snapshot once; the caller passes the result to BOTH
522
+ * `writePeerMcpRuntimeFiles` and `injectPeerMcpIntoMirror`. The mirror is a
523
+ * per-launch dir written ONLY by us (after `ensureClaudeConfigMirror`
524
+ * snapshotted the user's config) and nothing mutates it between this read
525
+ * and `injectPeerMcpIntoMirror`'s write, so the two reads see identical
526
+ * state — no TOCTOU window, and the inject-side defensive conflict check
527
+ * never fires for these resolved keys.
528
+ */
529
+ async function resolveGroupKeysFromMirror(enabledGroups, claudeConfigDir) {
530
+ const dir = claudeConfigDir ?? PATHS.CLAUDE_CONFIG_DIR;
531
+ const existing = await readMcpServersSnapshot(path.join(dir, ".claude.json"));
532
+ const keys = {};
533
+ for (const group of enabledGroups) {
534
+ const bare = GROUP_META[group].preferredKey;
535
+ if (existing[bare] === void 0) {
536
+ keys[group] = bare;
537
+ continue;
538
+ }
539
+ let candidate = `gh-router-${group}`;
540
+ let n = 1;
541
+ while (existing[candidate] !== void 0) {
542
+ n += 1;
543
+ candidate = `gh-router-${group}-${n}`;
544
+ }
545
+ keys[group] = candidate;
546
+ }
547
+ return {
548
+ keys,
549
+ skipped: []
550
+ };
551
+ }
552
+ /**
553
+ * Mutate the mirrored `<CLAUDE_CONFIG_DIR>/.claude.json` to add the
554
+ * `gh-router-peers` entry (and `codex-cli` when enabled) under
555
+ * `mcpServers`. This is the load-bearing fix for subagent MCP visibility.
556
+ *
557
+ * Subagents — Agent-tool subagents, forks, and agent-teams subprocesses
558
+ * — discover MCP servers from persistent scopes (`.claude.json` and
559
+ * project-scope `.mcp.json`), NOT from the parent's `--mcp-config` CLI
560
+ * flag. Writing into the per-launch mirror's `.claude.json` makes the
561
+ * MCP entry visible to subagents transparently: they inherit
562
+ * `CLAUDE_CONFIG_DIR` from the parent's env, so they read the same
563
+ * config file we just mutated.
564
+ *
565
+ * Safety:
566
+ * - Refuses to overwrite a same-named user-side entry (the snapshot
567
+ * copied their `.claude.json` first, so an existing entry would
568
+ * belong to the user). Returns `{ ok: false }` so the caller can
569
+ * fall back to leaving `--mcp-config` in place for the parent.
570
+ * - Preserves all other top-level fields and other `mcpServers`
571
+ * entries.
572
+ * - Atomic write: temp-file with `wx` (`O_CREAT | O_EXCL`) followed by
573
+ * `rename`, mirroring the synthetic-credentials write pattern in
574
+ * `ensureClaudeConfigMirror`. Mode 0o600. The per-launch
575
+ * `CLAUDE_CONFIG_DIR` means there are no cross-launch racers.
576
+ */
577
+ async function injectPeerMcpIntoMirror(serverUrl, opts) {
578
+ const dir = opts.claudeConfigDir ?? PATHS.CLAUDE_CONFIG_DIR;
579
+ const target = path.join(dir, ".claude.json");
580
+ let existing = {};
581
+ try {
582
+ const raw = await fs.readFile(target, "utf8");
583
+ try {
584
+ const parsed = JSON.parse(raw);
585
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
586
+ else consola.warn(`injectPeerMcpIntoMirror: ${target} parsed to non-object (typeof=${typeof parsed}); discarding contents and starting fresh.`);
587
+ } catch (err) {
588
+ consola.warn(`injectPeerMcpIntoMirror: cannot parse ${target} as JSON; starting fresh (existing contents will be overwritten):`, err);
589
+ }
590
+ } catch (err) {
591
+ if (err.code !== "ENOENT") consola.debug(`injectPeerMcpIntoMirror: cannot read ${target}:`, err);
592
+ }
593
+ let mcpServers;
594
+ const rawServers = existing.mcpServers;
595
+ if (rawServers !== void 0 && rawServers !== null && typeof rawServers === "object" && !Array.isArray(rawServers)) mcpServers = rawServers;
596
+ else {
597
+ if (rawServers !== void 0 && rawServers !== null) consola.warn(`injectPeerMcpIntoMirror: mcpServers field in ${target} is not an object (typeof=${typeof rawServers}); replacing with our entry.`);
598
+ mcpServers = {};
599
+ }
600
+ const peerConfig = buildPeerMcpConfig(serverUrl, {
601
+ codexCli: opts.codexCli,
602
+ geminiAvailable: opts.geminiAvailable,
603
+ groupKeys: opts.groupKeys,
604
+ nonce: opts.nonce,
605
+ codexHome: opts.codexHome ?? PATHS.CODEX_HOME,
606
+ workspaceHeaderCmd: buildWorkspaceHeaderHelperCommand(process.execPath, process.argv[1])
607
+ });
608
+ const conflicts = [];
609
+ for (const name of Object.keys(peerConfig.mcpServers)) if (mcpServers[name] !== void 0) conflicts.push(name);
610
+ if (conflicts.length > 0) {
611
+ consola.warn(`injectPeerMcpIntoMirror: your ~/.claude/.claude.json already has mcpServers entries named [${conflicts.join(", ")}]; refusing to overwrite. Subagents will not see those tools — only the parent session via --mcp-config fallback. To resolve, rename the user-side server(s) (e.g. via \`claude mcp remove\`) and relaunch.`);
612
+ return {
613
+ ok: false,
614
+ reason: "user-has-conflicting-entry",
615
+ conflictingServers: conflicts
616
+ };
617
+ }
618
+ for (const [name, entry] of Object.entries(peerConfig.mcpServers)) mcpServers[name] = entry;
619
+ existing.mcpServers = mcpServers;
620
+ const desiredJson = JSON.stringify(existing, null, 2) + "\n";
621
+ await fs.mkdir(dir, { recursive: true });
622
+ const tempPath = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
623
+ try {
624
+ await fs.writeFile(tempPath, desiredJson, {
625
+ mode: 384,
626
+ flag: "wx"
627
+ });
628
+ await fs.rename(tempPath, target);
629
+ } catch (err) {
630
+ await fs.unlink(tempPath).catch(() => {});
631
+ throw err;
632
+ }
633
+ return {
634
+ ok: true,
635
+ serversAdded: Object.keys(peerConfig.mcpServers)
636
+ };
637
+ }
638
+ /**
639
+ * Generate a per-launch nonce, write the MCP config + agents JSON
640
+ * tempfiles under `CLAUDE_RUNTIME_DIR` with mode 0o600 and `O_EXCL`,
641
+ * and return a `cleanup()` to unlink them on shutdown.
642
+ *
643
+ * Filenames are `peer-mcp-<pid>-<rand>.json` and `peer-agents-<pid>-<rand>.json`.
644
+ * The PID prefix is what the boot-time sweep (`sweepStaleRuntimeFiles` in
645
+ * paths.ts) keys off to drop orphans from crashed prior sessions; the
646
+ * random suffix prevents two concurrent calls within the same process
647
+ * from clobbering each other's files (e.g., a proxy that internally
648
+ * relaunches its spawned child without restarting itself).
649
+ */
650
+ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
651
+ const nonce = opts.nonce ?? randomBytes(32).toString("hex");
652
+ const runtimeDir = opts.runtimeDir ?? PATHS.CLAUDE_RUNTIME_DIR;
653
+ const codexHome = opts.codexHome ?? PATHS.CODEX_HOME;
654
+ await fs.mkdir(runtimeDir, { recursive: true });
655
+ if (process.platform !== "win32") await fs.chmod(runtimeDir, 448).catch(() => {});
656
+ const fileSuffix = `${process.pid}-${randomBytes(4).toString("hex")}`;
657
+ const mcpConfigPath = path.join(runtimeDir, `peer-mcp-${fileSuffix}.json`);
658
+ const agentsPath = path.join(runtimeDir, `peer-agents-${fileSuffix}.json`);
659
+ const mcpConfig = buildPeerMcpConfig(serverUrl, {
660
+ codexCli: opts.codexCli,
661
+ geminiAvailable: opts.geminiAvailable,
662
+ groupKeys: opts.groupKeys,
663
+ nonce,
664
+ codexHome,
665
+ workspaceHeaderCmd: buildWorkspaceHeaderHelperCommand(process.execPath, process.argv[1])
666
+ });
667
+ const agents = buildPeerAgentDefinitions({
668
+ codexCli: opts.codexCli,
669
+ geminiAvailable: opts.geminiAvailable,
670
+ groupKeys: opts.groupKeys,
671
+ workerToolsAvailable: opts.workerToolsAvailable,
672
+ browseAvailable: opts.browseAvailable,
673
+ nativeSubagentModel: opts.nativeSubagentModel,
674
+ reviewerModel: opts.reviewerModel,
675
+ brainstormModel: opts.brainstormModel,
676
+ scoutModel: opts.scoutModel,
677
+ scribeModel: opts.scribeModel,
678
+ nonce,
679
+ codexHome,
680
+ serverUrl,
681
+ workspaceHeaderCmd: buildWorkspaceHeaderHelperCommand(process.execPath, process.argv[1])
682
+ });
683
+ await fs.unlink(mcpConfigPath).catch(() => {});
684
+ await fs.unlink(agentsPath).catch(() => {});
685
+ await writeRuntimeFileSecure(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
686
+ await writeRuntimeFileSecure(agentsPath, JSON.stringify(agents, null, 2));
687
+ const mdResult = await writePeerAgentMdFiles(opts.builtinSubagents ? {
688
+ ...agents,
689
+ ...opts.builtinSubagents
690
+ } : agents, {
691
+ agentsDir: opts.agentsDir,
692
+ fileSuffix
693
+ });
694
+ const personas = personasFor({
695
+ codexCli: opts.codexCli,
696
+ geminiAvailable: opts.geminiAvailable
697
+ });
698
+ const cleanup = async () => {
699
+ await Promise.allSettled([
700
+ fs.unlink(mcpConfigPath),
701
+ fs.unlink(agentsPath),
702
+ mdResult.cleanup()
703
+ ]);
704
+ };
705
+ return {
706
+ mcpConfigPath,
707
+ agentsPath,
708
+ agentMdPaths: mdResult.paths,
709
+ nonce,
710
+ personas,
711
+ cleanup
712
+ };
713
+ }
714
+ //#endregion
715
+ //#region src/lib/mcp-permissions-settings.ts
716
+ /**
717
+ * CLAUDE_CODE_* / auth / routing env keys that must NOT reach the CloudCLI-spawned
718
+ * serve agent through the mirror `settings.json` `env` block. CloudCLI applies
719
+ * that block via the Agent SDK's `settingSources`, so anything a user set in their
720
+ * real `~/.claude/settings.json` (for their own workflow) is mirrored in and
721
+ * mis-shapes the single serve chat agent — the exact mechanism behind the
722
+ * coordinator-mode bug.
723
+ *
724
+ * - {@link STRIPPED_AUTH_ROUTING_ENV_KEYS} (shared with the process-env strip in
725
+ * `launch.ts`): auth/routing/remote keys that would re-route the agent OFF the
726
+ * github-router proxy (`ANTHROPIC_BASE_URL`, `CLAUDE_CODE_USE_BEDROCK/VERTEX/
727
+ * FOUNDRY`, a personal gateway), inject real auth over the synthetic
728
+ * credential (`ANTHROPIC_API_KEY`/`AUTH_TOKEN`/`OAUTH_TOKEN`), pin a
729
+ * non-Copilot `ANTHROPIC_MODEL`, or activate the unimplemented Bridge/remote
730
+ * path. The settings.json `env` block is a SECOND vector for the same keys the
731
+ * process-env strip already blocks.
732
+ * - `CLAUDE_CODE_COORDINATOR_MODE`: strips the single agent to delegation-only
733
+ * (Glob/Read/Bash fail "not enabled in this context").
734
+ * - `CLAUDE_CODE_SUBAGENT_MODEL`: a rank-#1 hard override that would retarget
735
+ * the injected implementer/peer-critic subagents off their frontmatter models.
736
+ *
737
+ * `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is intentionally NOT stripped — purely
738
+ * additive, matching `github-router claude` parity.
739
+ */
740
+ const SERVE_STRIP_ENV_KEYS = [
741
+ ...STRIPPED_AUTH_ROUTING_ENV_KEYS,
742
+ "CLAUDE_CODE_COORDINATOR_MODE",
743
+ "CLAUDE_CODE_SUBAGENT_MODEL"
744
+ ];
745
+ async function readSettingsObject(settingsPath) {
746
+ let raw;
747
+ try {
748
+ raw = await fs.readFile(settingsPath, "utf8");
749
+ } catch (err) {
750
+ if (err.code === "ENOENT") return null;
751
+ throw err;
752
+ }
753
+ const parsed = JSON.parse(raw);
754
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
755
+ throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`);
756
+ }
757
+ async function writeSettingsObject(settingsPath, obj, suffix) {
758
+ const tmp = `${settingsPath}.${process.pid}.${suffix}.tmp`;
759
+ await fs.writeFile(tmp, `${JSON.stringify(obj, null, 2)}\n`, { mode: 384 });
760
+ await fs.rename(tmp, settingsPath);
761
+ }
762
+ /**
763
+ * Remove serve-inappropriate CLAUDE_CODE_* keys (see {@link SERVE_STRIP_ENV_KEYS})
764
+ * from the serve mirror `settings.json` `env` block. UNCONDITIONAL — unlike the
765
+ * permission bypass this is NOT gated by `GH_ROUTER_SERVE_NO_AUTO_APPROVE`,
766
+ * because a user who only wants permission prompts still needs a working toolset.
767
+ * Serve mirror only; never the operator's real `~/.claude/settings.json`. Atomic
768
+ * temp+rename, mode 0o600. Returns the keys it removed (empty if none present).
769
+ */
770
+ async function sanitizeServeSettingsEnv(settingsPath) {
771
+ const existing = await readSettingsObject(settingsPath);
772
+ if (!existing) return { removed: [] };
773
+ const envRaw = existing.env;
774
+ if (!envRaw || typeof envRaw !== "object" || Array.isArray(envRaw)) return { removed: [] };
775
+ const env = { ...envRaw };
776
+ const removed = [];
777
+ for (const key of SERVE_STRIP_ENV_KEYS) if (key in env) {
778
+ delete env[key];
779
+ removed.push(key);
780
+ }
781
+ if (removed.length === 0) return { removed: [] };
782
+ await writeSettingsObject(settingsPath, {
783
+ ...existing,
784
+ env
785
+ }, "serveenv");
786
+ return { removed };
787
+ }
788
+ /**
789
+ * Native Claude Code tools worth auto-approving alongside our injected MCP
790
+ * servers so PLAN mode is frictionless for research. These are read-only
791
+ * discovery tools that plan mode would otherwise prompt for.
792
+ *
793
+ * Deliberately MINIMAL and safe:
794
+ * - `WebSearch` / `WebFetch` — network reads; exactly the research surface a
795
+ * planner reaches for, and they prompt by default in plan mode.
796
+ * - EXCLUDES `Read`/`Glob`/`Grep` — Claude Code already runs them in plan mode
797
+ * without a rule (its built-in safe list), so a rule would be redundant.
798
+ * - EXCLUDES `Bash` — can mutate (`rm`, redirects); blanket-allow is unsafe.
799
+ * Curated `Bash(<readonly cmd> *)` rules are the user's own call.
800
+ * - EXCLUDES `Task`/`Skill`/`Workflow`/`SendMessage` — spawn/delegate; a
801
+ * subagent launched in plan mode isn't guaranteed to inherit plan's
802
+ * read-only restriction, so auto-approving them is a plan-mode-bypass vector.
803
+ * - EXCLUDES `Edit`/`Write`/`NotebookEdit` etc. — plan mode gates them by
804
+ * design and we must not override that.
805
+ */
806
+ const NATIVE_RESEARCH_ALLOW_RULES = ["WebSearch", "WebFetch"];
807
+ /**
808
+ * The routine native Claude Code tools serve auto-approves so ordinary agent
809
+ * work stays seamless (no per-call prompt) — the exact surface a coding agent
810
+ * uses turn to turn. Seeded into CloudCLI's `localStorage['claude-settings']`
811
+ * `allowedTools` (feeds the Agent SDK `canUseTool` fast-path + `sdkOptions.allowedTools`).
812
+ *
813
+ * DELIBERATELY EXCLUDES the two interaction tools `AskUserQuestion` and
814
+ * `ExitPlanMode`. CloudCLI hard-codes both into its `TOOLS_REQUIRING_INTERACTION`
815
+ * set (claude-sdk.js) and renders a real question widget / plan Approve-Reject
816
+ * card for them — but ONLY when `canUseTool` runs (non-bypass) AND they are NOT
817
+ * pre-approved. Leaving them off this list is what routes them to the human
818
+ * instead of letting the model auto-answer its own question. This is the same
819
+ * line the interactive Claude CLI draws: routine tools seamless, decisions to
820
+ * the user. Never add either name here.
821
+ *
822
+ * A tool omitted from this list degrades to a one-off Allow/Deny prompt in
823
+ * CloudCLI — safe, not broken — so the list is a superset of the common surface
824
+ * rather than an exact registry match (extra names for tools this Claude build
825
+ * lacks are harmless no-ops).
826
+ */
827
+ const SEAMLESS_BUILTIN_TOOLS = [
828
+ "Task",
829
+ "Bash",
830
+ "BashOutput",
831
+ "KillShell",
832
+ "Glob",
833
+ "Grep",
834
+ "Read",
835
+ "Edit",
836
+ "Write",
837
+ "NotebookEdit",
838
+ "WebFetch",
839
+ "WebSearch",
840
+ "TodoWrite",
841
+ "Skill",
842
+ "SlashCommand",
843
+ "EnterPlanMode"
844
+ ];
845
+ /**
846
+ * Merge arbitrary allow rules into a mirror `settings.json` `permissions.allow`
847
+ * so the listed tools auto-run WITHOUT a permission prompt in every mode —
848
+ * including plan mode, where a matched allow rule bypasses the prompt for any
849
+ * tool NOT on plan mode's file-edit/shell-write restricted list (MCP tools and
850
+ * read-only natives like WebSearch/WebFetch). See {@link NATIVE_RESEARCH_ALLOW_RULES}
851
+ * for the native additions and the "Configure permissions" Claude Code doc for
852
+ * the plan-mode semantics.
853
+ *
854
+ * Rules are literal allow strings (`mcp__peers`, `WebSearch`, …). Existing
855
+ * `allow` entries and `deny`/`ask` are preserved; merge is idempotent (no write
856
+ * when every rule is already present). Mirror only; never the operator's real
857
+ * settings. Atomic temp+rename, mode 0o600.
858
+ */
859
+ async function injectAllowRules(settingsPath, rules) {
860
+ const want = [...new Set(rules.filter(Boolean))];
861
+ if (want.length === 0) return { added: [] };
862
+ const existing = await readSettingsObject(settingsPath) ?? {};
863
+ const permsRaw = existing.permissions;
864
+ const perms = permsRaw && typeof permsRaw === "object" && !Array.isArray(permsRaw) ? { ...permsRaw } : {};
865
+ const allow = Array.isArray(perms.allow) ? [...perms.allow] : [];
866
+ const present = new Set(allow.filter((x) => typeof x === "string"));
867
+ const added = want.filter((rule) => !present.has(rule));
868
+ if (added.length === 0) return { added: [] };
869
+ perms.allow = [...allow, ...added];
870
+ await writeSettingsObject(settingsPath, {
871
+ ...existing,
872
+ permissions: perms
873
+ }, "allowrules");
874
+ return { added };
875
+ }
876
+ /**
877
+ * Build the full plan-mode allow-rule set: a bare `mcp__<server>` rule per
878
+ * injected MCP server (whole-server allow) plus the native research tools. The
879
+ * server keys are the RESOLVED mcpServers config keys (collision-aware —
880
+ * `peers` or `gh-router-peers`, `codex-cli`, …) so each rule matches the server
881
+ * name the model actually sees.
882
+ */
883
+ function planModeAllowRules(serverKeys) {
884
+ return [...[...new Set(serverKeys.filter(Boolean))].map((k) => `mcp__${k}`), ...NATIVE_RESEARCH_ALLOW_RULES];
885
+ }
886
+ /**
887
+ * Ensure the serve mirror's `settings.json` `permissions.defaultMode` is NOT
888
+ * `bypassPermissions` — the load-bearing half of serve's "seamless routine +
889
+ * decisions to the user" model.
890
+ *
891
+ * Under `bypassPermissions` the Agent SDK resolves approval at the permission-mode
892
+ * step and SKIPS the `canUseTool` callback entirely (claude-sdk.js) — the only
893
+ * channel CloudCLI's interactive widgets ride on, so `AskUserQuestion` and
894
+ * `ExitPlanMode` auto-resolve on a model-generated answer and never reach the human.
895
+ * CloudCLI leaves `sdkOptions.permissionMode` unset when the composer is in default
896
+ * mode, so the binary falls back to THIS `defaultMode` — a stale `bypassPermissions`
897
+ * here would silently re-defeat the fix through the settings vector.
898
+ *
899
+ * MINIMAL override: only rewrite when the mode is absent or an explicit
900
+ * `bypassPermissions` (→ `"default"`, keeping `canUseTool` live). A user's
901
+ * deliberate NON-bypass posture (`plan` / `acceptEdits` / `default`) is preserved
902
+ * verbatim — all three already keep `canUseTool` live, so there is nothing to fix
903
+ * and no reason to override their choice. `allow`/`deny`/`ask` are always untouched.
904
+ *
905
+ * Only ever rewrites the per-launch serve mirror, never the operator's real
906
+ * `~/.claude/settings.json`. Atomic temp+rename, mode 0o600. A non-object
907
+ * settings.json throws; the caller wraps this in warn-and-continue.
908
+ *
909
+ * Opt out with `GH_ROUTER_SERVE_NO_AUTO_APPROVE=1` (the caller skips this AND the
910
+ * allow-list/localStorage seed) — the mirrored `defaultMode` is then left exactly
911
+ * as snapshotted and CloudCLI prompts on every not-yet-approved tool.
912
+ */
913
+ async function configureServeDefaultPermissionMode(settingsPath) {
914
+ const existing = await readSettingsObject(settingsPath) ?? {};
915
+ const permsRaw = existing.permissions;
916
+ const perms = permsRaw && typeof permsRaw === "object" && !Array.isArray(permsRaw) ? { ...permsRaw } : {};
917
+ if (perms.defaultMode !== void 0 && perms.defaultMode !== "bypassPermissions") return { written: false };
918
+ perms.defaultMode = "default";
919
+ await writeSettingsObject(settingsPath, {
920
+ ...existing,
921
+ permissions: perms
922
+ }, "servemode");
923
+ return { written: true };
924
+ }
925
+ //#endregion
926
+ //#region src/lib/injected-skills/first-mate-conduct-skill.ts
927
+ const FIRST_MATE_CONDUCT_SKILL = {
928
+ name: "gh-first-mate-conduct",
929
+ md: `---
930
+ name: gh-first-mate-conduct
931
+ description: Fleet conductor for first-mate — one durable heartbeat loop drives a FLEET of per-repo CEO meta-subagents to greatness. Arms the deterministic loop, sweeps the whole portfolio once, fans out a fresh CEO subagent per repo that needs judgment, batches their verdicts, couriers human decisions, and re-arms. Use when first-mate should be the default durable driver for one or many repos.
932
+ user-invocable: true
933
+ ---
934
+
935
+ # gh-first-mate-conduct: the deterministic-loop fleet conductor
936
+
937
+ You are the **fleet conductor**. One correctly-armed heartbeat loop, run by you (the main session), drives a fleet of GitHub repos — each by its own per-repo CEO meta-subagent — to greatness. This is the ONLY way one instance drives many repos durably: only the main REPL can arm a durable cron, so you own the heartbeat; the per-repo CEOs own judgment; the durable ledger + strategy store own memory. You hold almost nothing in context.
938
+
939
+ You carry the whole brain by REFERENCE, not by memorizing missions: the CEO/CTO/CPO operating protocol (\`/gh-first-mate-operate\`), the per-CEO driving loop (\`/gh-first-mate\`), and the definition of repo greatness (below). Your context each wake is only the compact board + each CEO's compact return — never diffs, logs, or transcripts.
940
+
941
+ ## The loop each wake (arm it right, then fan out)
942
+
943
+ 1. **One global sweep.** Call \`mcp__first-mate__advance\` ONCE with no \`mission_id\` — it sweeps the whole portfolio and returns \`board\`, \`needsModel[]\`, \`needsHuman[]\`, \`nextWakeSeconds\`. Tier1 auto-answers have ALREADY fired inside the tool for the safe \`author_fix\`/\`answer_agent_question\`/\`decompose\` envelope, so the cheap mechanical loop is handled for free and most wakes surface little. Do NOT disable tier1 — it is what keeps you from spawning a CEO for trivia.
944
+ 2. **Partition by mission/repo.** Group the residual \`needsModel\` (escalated \`review_plan\`/\`judge_review\` + anything tier1 declined) and open \`needsHuman\` and active board rows by \`missionId\`.
945
+ 3. **Surgical per-repo CEO fan-out.** For each mission that has an open judgment \`needsModel\`, OR a strategic checkpoint due (a phase to advance, a greatness item to verify, a pre-registered kill/pivot threshold reached), spawn a **fresh** CEO meta-subagent — Agent tool, in PARALLEL (multiple Agent calls in one message), capped at a few per wake (fleet fan-out cap; the MCP inflight budget is shared). A mission whose agents are grinding with no \`needsModel\` and no checkpoint due needs ZERO CEO spawns this wake. Hand each CEO its brief (below).
946
+ 4. **Batch + apply + courier.** Collect each CEO's returned \`model_answers\`; call \`mcp__first-mate__advance\` ONCE more with all of them (\`model_answers: [...]\`) to apply — the CONDUCTOR owns the single drive lease, CEOs never call \`advance\` themselves (that would contend the lease). Courier every \`needsHuman\` packet to the user (open \`packetHtmlPath\` in the artifact panel); never decide merges/abandons yourself.
947
+ 5. **Re-arm ONE heartbeat** from the MINIMUM \`nextWakeSeconds\` across the portfolio (the tightest cadence wins, so an imminent-work repo tightens the whole fleet).
948
+
949
+ ## The CEO spawn brief (hand this to each fresh per-repo CEO)
950
+
951
+ > You are the CEO of repo <owner/name> (mission <id>), spawned fresh for one turn with COMPLETE authority over that repo on the GitHub platform. Do NOT arm a heartbeat (you are a subagent — you cannot) and do NOT call \`mcp__first-mate__advance\` (the conductor applies your verdicts). Steps: (1) \`mcp__first-mate__read_strategy({mission_id})\` + \`mcp__first-mate__mission_status({mission_id})\` to re-hydrate strategy + state; (2) follow \`/gh-first-mate\` (per-CEO driving) + \`/gh-first-mate-operate\` (CEO protocol) + the greatness bar — you DELEGATE all buildable work (code, docs, README/website content, UI, CI, tests) to cloud-agent units and do little yourself; your hands are for orchestration, verification, and decisions, not building; and you VERIFY every user-viewable surface (product UI, README-as-rendered, Pages, docs, release, og-card) by VIEWING the rendered pixels (\`mcp__browser__*\` / screenshots), never guessing from code; (3) for each of YOUR \`needsModel\` requests, VERIFY the deliverable against external evidence (delegate heavy reads to worker-explore/worker-review to stay context-thin) and produce the typed verdict (decompose with disjoint \`fileScopes\`, review_plan, judge_review, author_fix, answer_agent_question); (4) \`mcp__first-mate__write_strategy({mission_id, currentPhase, activeBet, greatnessChecklist, decisionLog:[one entry], nextStrategicAction})\` to persist your strategy delta; (5) RETURN a compact \`{ model_answers:[{requestId,verdict}], needsHuman:[…to courier], strategy_written:true }\` — no prose, no diffs.
952
+
953
+ Because each CEO is FRESH per wake, its strategic continuity comes ONLY from the strategy store — so a rich \`write_strategy\` (phase, pre-registered bet + thresholds, greatness checklist with evidence handles, an append-only decision-log entry, what-was-tried) is what stops the next wake's CEO from drifting or re-litigating a dead end.
954
+
955
+ ## Self-driving heartbeat (arm / disarm — you own the ONLY one)
956
+
957
+ ONE durable cron, marker \`[fm-heartbeat]\`, is the dead-man's-switch that survives idle, compaction, restart, and /clear. There is exactly ONE first-mate heartbeat regardless of which driver skill armed it; manage it create-fresh-then-reap-the-rest:
958
+
959
+ Arm (nextWakeSeconds is a number):
960
+ 1. Cadence bucket from nextWakeSeconds (fixed cron, no time math): \`<=120 → "1-59/2 * * * *"\`; \`<=600 → "2,7,12,17,22,27,32,37,42,47,52,57 * * * *"\`; else \`"3,13,23,33,43,53 * * * *"\`.
961
+ 2. CronCreate the new job (durable:true, recurring:true, the chosen cron, prompt \`"/gh-first-mate-conduct [fm-heartbeat] wake the fleet, sweep, fan out CEOs, apply verdicts, reschedule."\`) and capture its id.
962
+ 3. CronList, then CronDelete every job whose prompt contains \`[fm-heartbeat]\` EXCEPT the id you just created — converges to exactly one, reaps duplicates/old-version orphans (including a stray standalone \`/gh-first-mate\` heartbeat), resets the 7-day expiry.
963
+
964
+ Disarm (nextWakeSeconds is null AND no pending needsHuman): CronList and CronDelete every \`[fm-heartbeat]\` job; report the fleet is idle and resumes when a mission is next started/advanced.
965
+
966
+ MCP unavailable / not \`--agents\`: do not advance; reap all \`[fm-heartbeat]\` jobs and report "re-run under \`github-router claude --agents\`." No scheduler tool: report the next wake is in nextWakeSeconds and stop.
967
+
968
+ ## Context discipline & report
969
+
970
+ The ledger is durable memory for unit state; the strategy store is durable memory for CEO strategy; your context is neither. Never read a full diff/log/transcript. Report compactly from the board: per mission — id, repos, phase counts, blocked count, the greatness-checklist progress (leading done + which LAGGING signals moved), needsHuman awaiting the user, and the next wake. A repo is only "great" when a LAGGING signal has moved, never when leading boxes are merely ticked.
971
+
972
+ ## Definition of greatness (the bar every repo is driven toward)
973
+
974
+ ${DEFINITION_OF_GREATNESS}
975
+ `
976
+ };
977
+ //#endregion
978
+ //#region src/lib/injected-skills/first-mate-operate-skill.ts
979
+ const FIRST_MATE_OPERATE_SKILL = {
980
+ name: "gh-first-mate-operate",
981
+ md: `---
982
+ name: gh-first-mate-operate
983
+ description: Operator-facing CEO/CTO/CPO operating protocol for autonomously driving a product with first-mate — shape each mission from a real struggling moment, make acceptance criteria externally verifiable, sequence discovery through growth, and escalate launch, spend, and pricing to the human. Use when deciding WHAT product work first-mate should drive, not only how to execute it.
984
+ user-invocable: true
985
+ ---
986
+
987
+ # gh-first-mate-operate: drive a product as CEO + CTO + CPO
988
+
989
+ You are the CEO of the product. The GitHub cloud coding agents are your team — they carry the CTO/CPO/engineering execution roles (scaffolded into each repo). Your job is to think like a CEO and get real, verified work out of that team: decide the product direction (the niche, the riskiest assumption, the MVP scope, when to launch, what to measure, what to iterate), turn each decision into a scoped mission, drive the agents to deliver it, and hold the result to evidence.
990
+
991
+ You do not write the product code. You orchestrate: shape missions, review plans, answer the team's questions fast so they never idle, verify deliverables, and sequence the whole effort toward an outcome. Seed the team's playbook once with \`mcp__first-mate__scaffold_repo\` (it commits \`docs/playbook/README.md\` plus the \`ceo\`/\`cto\`/\`cpo\` role agents the cloud agents read); then use THIS protocol to run the company.
992
+
993
+ ## Delegate the work — your hands are for orchestration, not building
994
+
995
+ You are the orchestrator; the GitHub cloud coding agents are the workers. The whole point of first-mate is to get work OUT of the team — so DELEGATE, and do little yourself.
996
+
997
+ - **Everything BUILDABLE is a cloud-agent mission/unit, never hand-written by you:** product code, bug fixes, README/docs/website CONTENT, CI/workflows, tests, the UI. Dispatch it via \`mcp__first-mate__start_mission\` / \`add_units\` / \`decompose\` — do NOT open an editor and write it yourself.
998
+ - **Your OWN hands are only for:** strategy & decisions, decomposition, answering the team (plan review, questions, fix instructions, judge verdicts), VERIFYING deliverables (browse / read / screenshot — **observe, never build**), persisting strategy, and GitHub-**platform** governance via \`gh\` (merge, release, branch protection, secrets, labels, issues, triage). That is orchestration, not building.
999
+ - **Resist the "I'll just quickly fix it myself" urge — it is the failure mode.** A quick hand-edit feels faster but doesn't scale, produces no reusable team capability, and isn't your job. If you catch yourself about to write code, docs, or UI: STOP and dispatch it as a scoped unit instead. The only exception is a genuine one-line last-mile config the cloud-agent loop cannot reach — and even then, prefer a unit.
1000
+
1001
+ ## Drive the team (get work out of them)
1002
+
1003
+ - **Verify, never trust "done".** Every deliverable clears an external checkpoint — a real HTTP 200, green CI, an observed analytics event, a real survey N — or it is not done. Reject self-reported completion and send it back with a concrete gap.
1004
+ - **Keep the team unblocked and busy.** A blocked agent produces nothing: answer \`answer_agent_question\` promptly, dispatch independent units in parallel, and re-steer a stalled or underdelivering agent instead of waiting. Idle or looping agents are wasted throughput.
1005
+ - **Set the bar as acceptance criteria.** The mission's acceptance criteria = the phase's externally verifiable checkpoint. Vague criteria produce vague work; make the bar reproducible.
1006
+ - **Own the P&L of attention.** Kill low-value missions, double down on what moves the outcome metric, and escalate only the genuinely human-gated calls (launch to real channels, spend, pricing, merges).
1007
+
1008
+ ## The one rule that makes autonomy safe
1009
+
1010
+ Every phase advances only on an EXTERNALLY VERIFIABLE checkpoint — a real HTTP 200, a green CI run, an observed analytics event, or a real survey sample size — never a self-reported "done". Autonomous agents fail or hallucinate "done" a large fraction of the time, so an unverified claim is not progress. Encode the checkpoint as the mission's acceptance criteria and refuse to advance without the evidence.
1011
+
1012
+ ## Operating loop (OODA inside Build-Measure-Learn)
1013
+
1014
+ - Inner loop, each turn: OBSERVE fresh evidence (issues, mentions, downloads, analytics), ORIENT against the current job/segment/assumptions, DECIDE one reversible next action against a pre-set threshold, ACT by delegating a scoped mission to the cloud agents.
1015
+ - Outer loop, each phase: build the smallest testable increment, measure externally observable behavior, learn against the pre-registered threshold, then persist or pivot. Do not enter the next phase until its checkpoint is independently reproducible.
1016
+
1017
+ ## Shaping a mission by phase
1018
+
1019
+ When you call \`mcp__first-mate__start_mission\` (or \`mcp__first-mate__add_units\`), set the fields from the CURRENT phase:
1020
+
1021
+ - **goal**: the phase objective, grounded in a real struggling moment — not a feature wish.
1022
+ - **acceptance_criteria**: the phase's externally verifiable exit checkpoint, stated as evidence a reviewer can reproduce (e.g. "cold-start quickstart under five minutes, timed from a fresh checkout, recorded in the PR"; "Sean Ellis survey with N≥40 responses and ≥40% 'very disappointed'").
1023
+ - **house_rules**: any hard constraint (privacy, license, brand, spend limit).
1024
+ - Unlock real parallelism the right way: give each independent unit a DISJOINT \`fileScopes\` allowlist so their builds run CONCURRENTLY (the controller proves non-overlap and dispatches in parallel up to the mission's \`maxConcurrentBuilds\`), and/or split fully independent workstreams into SEPARATE missions (the build gate is per-mission, so N missions build N units at once). \`max_in_flight_per_provider\` is a global provider cap, NOT the build-concurrency lever — disjoint \`fileScopes\` and separate missions are. Never race overlapping work on the same files.
1025
+
1026
+ Let the controller drive decomposition and steering (see \`gh-first-mate\`); this skill decides the PHASE and the checkpoint, not the controller mechanics.
1027
+
1028
+ ## Phased sequence (shared with the scaffolded playbook)
1029
+
1030
+ ${CONDENSED_OPERATING_SEQUENCE}
1031
+
1032
+ ## Definition of greatness (the shipping bar every repo must clear)
1033
+
1034
+ Driving a product to greatness is not just shipping features — the repo itself must clear a verifiable shipping-infrastructure bar. Drive each repo toward it and gate each item on real, third-party-checkable evidence (a green check, a \`gh api\`, a \`curl\`, a \`cosign verify\`), never a self-report. This is the SAME bar the scaffolded playbook and the eval use.
1035
+
1036
+ ${DEFINITION_OF_GREATNESS}
1037
+
1038
+ ## Iterate to polish — never guess the UI, drive it and view the pixels
1039
+
1040
+ UI/UX quality (and every user-viewable surface) is DRIVEN, SEEN, and ITERATED, never designed once and asserted. For ANY unit that touches the product UI, the README, the website, docs, the release page, or the og-card, require the cloud agent to — and verify yourself by — VIEW the rendered result before "done":
1041
+ 1. Build & run the real artifact (the deployed Pages / live URL, or a dev server) — a local happy-path screenshot is not the product.
1042
+ 2. Drive every state & flow with \`mcp__browser__*\` (navigate/act/observe): first-run/empty, real input, forced error, success, edge/overflow — not just the ideal state.
1043
+ 3. Screenshot the matrix: each state at mobile/tablet/desktop × light+dark, honoring \`prefers-color-scheme\` and \`prefers-reduced-motion\`. These pixels, not the code, are the evidence.
1044
+ 4. Critique the pixels against the professional bar (Pillar D of the greatness definition); for each defect name the concrete problem AND the screenshot it is in. Rank by severity.
1045
+ 5. Fix the top defects (dispatch to the implementer role; prefer token/design-system fixes over one-off patches).
1046
+ 6. Re-drive & re-capture (repeat 2–5) until the vision rubric is clean and the deterministic gates (visual-regression / axe / contrast / CWV) are green. Update \`toHaveScreenshot\` baselines only on an intentional, reviewed change.
1047
+ 7. Lock it in: commit the visual-regression baselines + axe/Lighthouse/CWV CI so polish can't silently regress.
1048
+
1049
+ The no-guessing rule: never mark a user-viewable surface — including the README as GitHub renders it — "done" from code/markdown review alone; back every claim with a screenshot of the actual running/rendered artifact.
1050
+
1051
+ ## Every iteration must move the end-user experience forward — and regress nothing
1052
+
1053
+ This is the objective the whole loop is tuned to, and the operating expression of the CEO eval (\`docs/first-mate-ceo-eval-framework.md\`, the End-User Experience Delta lens): after each iteration (or a bounded set), measure what VERIFIABLY improved for the END USER, and confirm nothing regressed.
1054
+
1055
+ - **Drive the pinned user journeys before AND after.** Keep a pinned set of golden journeys per repo (the aha path + key flows) plus user-facing facets — journey completion, time-to-first-value, capability, performance, accessibility, user-facing correctness, the rendered README/Pages/docs/Release, state robustness. Capture them on the inherited state, then again after the iteration, by DRIVING the real artifact — never inferred from code or a 200.
1056
+ - **Ship only a net improvement.** An iteration is done only when it shows at least one MATERIAL, verified user-facing improvement (past a pre-set threshold, tied to a real user job) AND zero detected unintended regression under the coverage you actually ran. Report coverage honestly: "no regression detected in the pinned journeys," never an absolute "zero."
1057
+ - **A detected regression is stop-the-line.** A journey that worked now failing, a metric crossing its budget the wrong way, a removed/broken capability, a new user-hitting bug, a broken rendered surface, or a slower quickstart CAPS the iteration: fix the regression before the improvement counts. Recovering a regression you caused only retires the debt; it is not new progress.
1058
+ - **Intended user-facing changes are approved tradeoffs, not free.** A deliberate deprecation / redesign / pivot is recorded as an approved tradeoff (old baseline preserved, migration path, human approval) — never silently relabeled "not a regression."
1059
+ - **Necessary invisible work is legitimate but bounded.** Security hardening, migrations, and refactors that de-risk future UX are enabling-investment, not churn — but bounded: after ~2 consecutive iterations with no user-facing delta (or ~a third of a milestone's budget), the next iteration must show a material user improvement or you flag the strand.
1060
+ - **The agent must not own the ruler.** The journey manifest + measurement harness are yours (or human-blessed), not authored by the cloud agent being measured; a change to them is reviewed and logged, like a merge approval.
1061
+
1062
+ ## Anti-patterns (hard stops)
1063
+
1064
+ - **Over-building without distribution:** run a reachability/channel test before extending product scope. "Build it and they will come" is not a plan.
1065
+ - **Hallucinated progress:** require real evidence (HTTP 200, green CI, observed analytics, real survey N); never convert activity or a narrative into completion.
1066
+ - **Viral ≠ product-market fit:** attention, stars, and shares do not replace the Sean Ellis threshold plus a flattening retention curve.
1067
+ - **Metrics after the fact:** pre-register kill/pivot/continue thresholds before collecting results.
1068
+
1069
+ ## Escalate to the human (never decide autonomously)
1070
+
1071
+ Hard authority limits: launching to real external channels, any spend or paid acquisition, setting or changing pricing, issuing discounts, entering contracts, expanding privileges, and any regulated/legal/privacy commitment require an explicit human boundary or approval. Within those limits, proceed on best judgment and record assumptions rather than pausing. Merge approval and abandonment remain human-gated per \`gh-first-mate\`.
1072
+
1073
+ ## Report
1074
+
1075
+ Report the current phase, its checkpoint and whether it is met with reproducible evidence, the active mission(s) and their phase-appropriate acceptance criteria, and the next decision or escalation.
1076
+ `
1077
+ };
1078
+ //#endregion
1079
+ //#region src/lib/injected-skills/first-mate-setup-skill.ts
1080
+ const FIRST_MATE_SETUP_SKILL = {
1081
+ name: "gh-first-mate-scaffold",
1082
+ 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/{ceo,cto,cpo,planner,implementer,reviewer,researcher,tester}.md` mirrored into `.claude/agents/` — C-suite operator hats plus execution roles, each with frontmatter, cold-start contract, method, quality bar, output contract, and self-reminder.\n- `docs/playbook/README.md` — the autonomous DISCOVER → NICHE → POSITION → SCOPE → BUILD → LAUNCH → MEASURE → ITERATE → GROW protocol, externally verifiable phase gates, OODA / Build-Measure-Learn governance, and authority limits.\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, product playbook, 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"
1083
+ };
1084
+ //#endregion
1085
+ //#region src/lib/injected-skills/first-mate-skill.ts
1086
+ const FIRST_MATE_SKILL = {
1087
+ name: "gh-first-mate",
1088
+ md: `---
1089
+ name: gh-first-mate
1090
+ 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.
1091
+ user-invocable: true
1092
+ ---
1093
+
1094
+ # gh-first-mate: durable cloud-agent controller loop
1095
+
1096
+ Use this skill when the user wants first-mate to drive GitHub cloud coding agents across one or more repositories.
1097
+ The first-mate controller is the durable system of record: missions, units, decisions, handles, and controller state live in its registry and ledger.
1098
+ Your job is to run the thin protocol, not to hold the mission in context.
1099
+
1100
+ ## You are the CEO
1101
+
1102
+ You are the CEO of the product. The GitHub cloud coding agents are your team; your job is to get real, verified work out of them and drive the product to an outcome — not to write the code yourself. Operate like a CEO every turn:
1103
+
1104
+ - **Drive results, not activity.** Hold every deliverable to external evidence (a real HTTP 200, green CI, an observed metric, a real survey N). Never accept a self-reported "done" — autonomous agents fail or hallucinate completion a large fraction of the time.
1105
+ - **Set clear expectations.** Every mission's acceptance criteria IS the bar: a phase's externally verifiable checkpoint, stated so a reviewer can reproduce it.
1106
+ - **Keep the team unblocked and busy.** Answer agent questions fast (the controller surfaces \`answer_agent_question\`), dispatch independent units in parallel, and re-steer a stalled or weak agent promptly rather than letting it idle.
1107
+ - **Own the outcome.** Sequence missions toward the product result (niche → MVP → launch → traction), kill low-value work, and iterate on evidence. Think in bets — hypothesis, metric, threshold — and delegate execution to the cloud-agent team.
1108
+
1109
+ For the full operating protocol — discovery, positioning, MVP scope, launch, measure, iterate, grow — invoke \`/gh-first-mate-operate\`.
1110
+
1111
+ ## Foundation-first mandate
1112
+
1113
+ 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.
1114
+
1115
+ 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.
1116
+
1117
+ ## Scoped-work discipline
1118
+
1119
+ Well-scoped, testable work items succeed; vague meta-work fails. Discovery/decompose must emit concrete units with acceptance criteria, expected evidence, and dependencies. **Parallelize deliberately.** Independent build units that declare DISJOINT \`fileScopes\` build CONCURRENTLY — the controller proves independence (no unmet deps + non-overlapping declared scopes) and dispatches them in parallel up to the mission's \`maxConcurrentBuilds\` cap; units with overlapping or undeclared scope serialize their builds (the safe default). For fully independent workstreams you can also run SEPARATE missions — the build gate is per-mission, so N missions build N units at once. Never race overlapping work on the same files.
1120
+
1121
+ 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.
1122
+
1123
+ ## Start a mission
1124
+
1125
+ For a new goal, call mcp__first-mate__start_mission with:
1126
+
1127
+ - goal: the user's goal in one sentence.
1128
+ - repos: repository strings as owner/name.
1129
+ - acceptance_criteria: explicit user-blessed acceptance criteria.
1130
+ - priority and house_rules only when the user supplied them or they are necessary constraints.
1131
+
1132
+ If acceptance criteria are missing or ambiguous, ask the user before starting.
1133
+ 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.
1134
+
1135
+ 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.
1136
+
1137
+ ## Controller loop (push-based, self-driving)
1138
+
1139
+ The loop is push-based: each turn you DRAIN all ready work, then ARM the next wake and YIELD the turn. You never sit polling advance in a tight loop — that does not scale and wastes context. A durable heartbeat re-invokes this skill while the session is idle, and one advance call sweeps the WHOLE portfolio (every mission, every repo) at once, so a single heartbeat drives everything.
1140
+
1141
+ Each turn:
1142
+
1143
+ 1. Call mcp__first-mate__advance, batching any answers you already have.
1144
+ 2. Read the compact response: board, needsModel, needsHuman, applied_count, nextWakeAt, and nextWakeSeconds.
1145
+ 3. Answer every needsModel item with the typed verdict for its kind, and courier every needsHuman packet. If you produced any model_answers this turn, call advance again to apply them and pick up the resulting state. Stop this inner drain once advance returns no new needsModel you can answer without the user; keep it bounded (at most a few iterations), never a busy loop.
1146
+ 4. Manage the heartbeat from nextWakeSeconds (see "Self-driving heartbeat").
1147
+ 5. Report compactly from the board, then YIELD. Do not call advance again until the next scheduled wake or a new user message.
1148
+
1149
+ When submitting answers, batch what you have:
1150
+
1151
+ - model_answers: [{ requestId, verdict }]
1152
+ - human_decisions: [{ requestId, choice }]
1153
+
1154
+ Keep verdicts small and typed to the request kind.
1155
+
1156
+ ## Model request verdicts
1157
+
1158
+ Use the request's kind and payload as the contract:
1159
+
1160
+ - decompose: split a unit-less active mission into dispatchable units. Return { units: [{ title, repo?, agent?, dependsOn?, model?, fileScopes? }] }. \`dependsOn\` entries are 0-based indices into the same units list. **\`fileScopes\` is the parallelism lever** — declare each unit's disjoint file allowlist (paths or \`dir/**\` prefixes it may touch) so the controller can prove independence and build those units CONCURRENTLY; units with overlapping or absent scopes serialize their builds. Emit once per unit-less active mission; the controller creates durable unit ids and will not ask again after units exist.
1161
+ - 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.
1162
+ - 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.
1163
+ - author_fix: author a concise fix instruction for the cloud agent. Return { instruction: "..." } with the failure, expected behavior, and any bounded check to run.
1164
+ - judge_review: judge whether review/CI/floor evidence is sufficient. Return { pass: true } only when the acceptance criteria and floor are satisfied; otherwise return { pass: false } with a compact reason when useful.
1165
+
1166
+ Delegate heavy reading to workers:
1167
+
1168
+ - Use the worker-explore subagent (Agent tool) for focused source or history gathering.
1169
+ - Use the worker-review subagent (Agent tool) for compact review of a specific plan, PR summary, or suspicious change.
1170
+ - Use the worker-test subagent (Agent tool) when a missing executable check is the blocker.
1171
+
1172
+ 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.
1173
+
1174
+ 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.
1175
+
1176
+ ## Human requests
1177
+
1178
+ For each needsHuman item:
1179
+
1180
+ 1. If packetHtmlPath is present, open that HTML file in the ai-or-die artifact panel for the user.
1181
+ 2. Tell the user the reason, repo, issue/PR handles, and the available decision choices.
1182
+ 3. Wait for the user's decision.
1183
+ 4. Submit the choice back with mcp__first-mate__advance({ human_decisions: [...] }).
1184
+
1185
+ If an artifact-panel tool is unavailable, give the user the local packetHtmlPath and the compact summary from needsHuman.
1186
+ Never decide a merge approval or abandonment choice on the user's behalf.
1187
+
1188
+ ## Self-driving heartbeat (arm / disarm)
1189
+
1190
+ The loop keeps itself alive with ONE durable cron job — a dead-man's-switch that survives idle, compaction, restart, and /clear because it lives on disk, not in your context. The controller hands you nextWakeSeconds so you never do arithmetic: it is a ready-to-use delay in seconds, or null when the whole portfolio is idle. Every heartbeat carries the exact marker token [fm-heartbeat] in its prompt so you can identify it unambiguously (never a fuzzy match against unrelated crons). Manage exactly one heartbeat:
1191
+
1192
+ Arm — "create fresh, then reap the rest" (nextWakeSeconds is a number, i.e. there is active work):
1193
+
1194
+ 1. Pick the cadence bucket from nextWakeSeconds (fixed cron expressions, no time math), so the heartbeat tracks the controller's own cadence:
1195
+ - nextWakeSeconds <= 120 → "1-59/2 * * * *" (about every 2 min)
1196
+ - nextWakeSeconds <= 600 → "2,7,12,17,22,27,32,37,42,47,52,57 * * * *" (about every 5 min)
1197
+ - otherwise → "3,13,23,33,43,53 * * * *" (about every 10 min)
1198
+ 2. CronCreate the new heartbeat and capture its id: durable: true, recurring: true, the chosen cron, prompt "/gh-first-mate [fm-heartbeat] wake the controller loop, answer ready requests, reschedule." Creating first (before deleting) guarantees at least one heartbeat always exists.
1199
+ 3. CronList, then CronDelete every job whose prompt contains [fm-heartbeat] EXCEPT the id you just created. This converges to exactly one, reaps duplicates and old-version orphans, and — because you recreate each wake — resets the 7-day recurring-cron expiry so a long mission never silently stops.
1200
+
1201
+ Disarm (nextWakeSeconds is null AND there are no pending needsHuman): nothing is active. CronList and CronDelete every [fm-heartbeat] job. Report that first-mate is idle and resumes when the user next starts or advances a mission (safe because, per the Start-a-mission invariant, only a turn can reactivate work, and that turn re-arms).
1202
+
1203
+ Responsiveness (optional): the buckets above already tighten cadence to ~2 min for imminent work. If this session is a /loop you MAY additionally ScheduleWakeup(delaySeconds: nextWakeSeconds, ...) for an exact one-shot; keep at most one outstanding.
1204
+
1205
+ 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.
1206
+
1207
+ 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.
1208
+
1209
+ ## Board reports
1210
+
1211
+ Use mcp__first-mate__board or mcp__first-mate__mission_status for read-only status checks.
1212
+ Report compactly:
1213
+
1214
+ - mission id and title
1215
+ - repositories
1216
+ - phase counts
1217
+ - blocked count and why, when available
1218
+ - per-unit handles for non-terminal units: unitId, issue/PR, phase, provider, validation, model, and blockedReason when present
1219
+ - resolved cloud-agent model per unit (surfaced on the board — verify model choice before approving plans)
1220
+ - terminal work only as summary counts; use include_all only when the user asks for completed/abandoned history
1221
+ - next wake time or the next requested action
1222
+
1223
+ Never reconstruct status by rereading raw logs when the controller board already has the handles.
1224
+
1225
+ ## Context discipline
1226
+
1227
+ - The ledger is the durable memory; do not paste large artifacts into the chat.
1228
+ - Never read a full diff/log/transcript unless the user explicitly asks and it is essential.
1229
+ - Prefer handles: mission id, repo, issue, PR, request id, packet path.
1230
+ - Keep every answer to first-mate compact and action-oriented.
1231
+ - If controller state and chat memory disagree, trust the controller state and ask for clarification only when it affects acceptance criteria or human approval.
1232
+
1233
+ ## Return format
1234
+
1235
+ When reporting progress, return:
1236
+
1237
+ - Mission: id and one-line goal.
1238
+ - Board: compact phase counts and blocked count.
1239
+ - Applied: count or short list of important controller actions.
1240
+ - Needs model: request ids and kinds answered or pending.
1241
+ - Needs human: decision ids/packet paths and the user's required choice.
1242
+ - Next wake: the heartbeat state — armed and next check in nextWakeSeconds seconds, or idle (disarmed).
1243
+ `
1244
+ };
1245
+ //#endregion
1246
+ //#region src/lib/injected-skills/floor-keeper-skill.ts
1247
+ const FLOOR_KEEPER_SKILL = {
1248
+ name: "gh-floor-keeper",
1249
+ md: `---
1250
+ name: gh-floor-keeper
1251
+ 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.
1252
+ user-invocable: true
1253
+ ---
1254
+
1255
+ # gh-floor-keeper: done-checkpoint verification
1256
+
1257
+ Invoke this before declaring a non-trivial change done.
1258
+ It is the final floor check: executable gate first, cross-lab review second, advisor third, severity reconciliation last.
1259
+ It does not prove the change is correct; it reports what was checked and what remains residual.
1260
+
1261
+ ## Operating contract
1262
+
1263
+ - Input: the user ask, user-blessed acceptance criteria, current diff, and any research or plan pointers.
1264
+ - Output: go/no-go with binding executable results, advisory review findings, and residual risks.
1265
+ - Scope: changed behavior and changed files, not a full repo audit unless requested.
1266
+ - Reuse /gh-research for claim verification instead of re-deriving complex facts.
1267
+ - Keep attempts bounded and ask before expanding into a large new test harness.
1268
+
1269
+ ## Honest limits
1270
+
1271
+ - The executable gate is binding only for what it covers.
1272
+ - A green gate does not rule out wrong-spec or missing coverage.
1273
+ - Cross-lab review reduces correlated blind spots but is advisory.
1274
+ - Advisor output is judgment-only unless converted into tests, source changes, or a gate.
1275
+ - Different-lab test authorship is an advisory practice, not enforceable provenance.
1276
+
1277
+ ## Step 1: gather the done context
1278
+
1279
+ Collect:
1280
+
1281
+ - Original ask and acceptance criteria.
1282
+ - Current working-tree diff.
1283
+ - Commands already run and their outputs.
1284
+ - Research brief pointer, if one exists.
1285
+ - Plan or orchestration summary, if one exists.
1286
+ - Known residual risks from earlier phases.
1287
+
1288
+ If acceptance criteria are absent, stop and ask for them or state that wrong-spec risk remains high.
1289
+
1290
+ ## Step 2: run the executable gate
1291
+
1292
+ Run the repo-appropriate executable checks for the changed slice:
1293
+
1294
+ - typecheck, tests, lint, build, or focused command named by the repo/user.
1295
+ - Prefer the existing gate command when available.
1296
+ - Capture exact command, exit code, duration, and relevant output.
1297
+ - If the command times out or cannot run, report unknown, not pass.
1298
+
1299
+ Binding rule:
1300
+
1301
+ - Red gate for covered behavior means no-go until fixed or explicitly waived by the user.
1302
+ - Green gate means only that the checks that ran passed.
1303
+ - Missing checks or unavailable commands remain residual risk.
1304
+
1305
+ ## Step 3: identify missing test coverage
1306
+
1307
+ Ask whether changed behavior has executable coverage.
1308
+
1309
+ - 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.
1310
+ - Cap missing-test attempts; default to a small number of focused tries.
1311
+ - Run the new test and then the relevant existing gate.
1312
+ - If creating a large new harness, broad fixture system, or slow integration environment is required, ask the user before proceeding.
1313
+ - If a model-authored test is the only oracle, label it honestly as helpful but not a complete correctness guarantee.
1314
+
1315
+ ## Step 4: fan out cross-lab review
1316
+
1317
+ Send the same diff, acceptance criteria, and gate results in parallel to:
1318
+
1319
+ - mcp__peers__codex_reviewer (OpenAI)
1320
+ - mcp__peers__gemini_reviewer (Google)
1321
+
1322
+ Ask both reviewers for:
1323
+
1324
+ - correctness bugs
1325
+ - acceptance-criteria misses
1326
+ - regressions
1327
+ - security or data-loss risks
1328
+ - test gaps
1329
+ - maintainability issues that matter for this change
1330
+ - severity for each finding: blocker, high, medium, low, nit
1331
+
1332
+ Do not treat reviewer agreement as proof. Treat it as advisory signal to investigate or fix.
1333
+
1334
+ ## Step 5: consult advisor
1335
+
1336
+ Consult the advisor with a focused concern:
1337
+
1338
+ - whether the diff satisfies the acceptance criteria
1339
+ - whether the gate covers the risky behavior
1340
+ - whether reviewer findings indicate no-go
1341
+ - what residual risk should be surfaced to the user
1342
+
1343
+ Advisor output is advisory unless you convert it into a source-verified claim, executable test, or code change.
1344
+
1345
+ ## Step 6: verify disputed or load-bearing claims
1346
+
1347
+ For any important claim from a reviewer, advisor, or your own reading:
1348
+
1349
+ - If it needs research, invoke /gh-research and use its persisted brief pointer.
1350
+ - Prefer reproducing the issue or running a focused test: verified-executable.
1351
+ - Otherwise read the actual source and cite it: verified-source.
1352
+ - If neither is possible within budget, mark unverified and include it in residual risk.
1353
+
1354
+ Do not re-derive complex repo facts from memory when /gh-research is the right tool.
1355
+
1356
+ ## Step 7: reconcile by severity
1357
+
1358
+ Build a reconciliation table:
1359
+
1360
+ - Finding.
1361
+ - Source: gate, codex reviewer, gemini reviewer, advisor, research, or self.
1362
+ - Severity: blocker, high, medium, low, nit.
1363
+ - Evidence tag: verified-executable, verified-source, cross-lab-agreed, or unverified.
1364
+ - Decision: fix now, accept residual, ask user, or no action.
1365
+
1366
+ Decision rules:
1367
+
1368
+ - Any covered executable failure is no-go.
1369
+ - Any credible blocker or high correctness/security/data-loss issue is no-go unless disproven or explicitly waived.
1370
+ - Medium issues usually require fixing when cheap; otherwise surface as residual.
1371
+ - Low and nit findings do not block unless they violate acceptance criteria.
1372
+ - Wrong-spec residual is always listed unless the user explicitly blessed the acceptance criteria for this exact done state.
1373
+
1374
+ ## Step 8: return go/no-go
1375
+
1376
+ Return a compact final checkpoint:
1377
+
1378
+ - Verdict: go or no-go.
1379
+ - Executable gate: commands, pass/fail/unknown, and why it is binding or not.
1380
+ - Missing-test handling: tests authored, skipped, capped, or user approval needed.
1381
+ - Cross-lab review summary: OpenAI findings, Google findings, agreements, disagreements.
1382
+ - Advisor summary.
1383
+ - Reconciliation table with severity and evidence tags.
1384
+ - Residual risks, explicitly including wrong-spec if applicable.
1385
+ - Required next actions before declaring done.
1386
+
1387
+ ## Non-goals
1388
+
1389
+ - Do not claim the change is correct merely because tests passed.
1390
+ - Do not let advisory reviewers override a covered red executable gate.
1391
+ - Do not spend unbounded attempts creating tests.
1392
+ - Do not bury cap-hit or unknown states in a green-sounding summary.
1393
+ `
1394
+ };
1395
+ //#endregion
1396
+ //#region src/lib/injected-skills/orchestrate-skill.ts
1397
+ const ORCHESTRATE_SKILL = {
1398
+ name: "gh-orchestrate",
1399
+ md: `---
1400
+ name: gh-orchestrate
1401
+ 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.
1402
+ user-invocable: true
1403
+ ---
1404
+
1405
+ # gh-orchestrate: right-sized blind-spot elimination
1406
+
1407
+ Use this skill when the user asks for a non-trivial change and the composed workflow can reduce real blind spots.
1408
+ The sole objective is: how does the composed workflow deterministically raise the floor for THIS ask, and what blind spots does it eliminate with which tools?
1409
+
1410
+ ## Right-size first
1411
+
1412
+ - For trivial asks, skip this pipeline and say why.
1413
+ - A three-line obvious fix, typo, small config read, or simple explanation should not pay orchestration cost.
1414
+ - If the ask has multiple files, unclear behavior, risky migration, uncertain tests, or high user impact, orchestration is likely worth it.
1415
+ - The pipeline is a tool, not a ritual.
1416
+
1417
+ ## Honest limits
1418
+
1419
+ - User-blessed acceptance criteria are the only defense against the wrong-spec hole.
1420
+ - Executable gates do not catch a model solving the wrong task.
1421
+ - Cross-lab review is advisory unless a code rule or executable gate consumes its output.
1422
+ - The native Workflow path approximates but does not carry the kernel's hard max(orchestrated, baseline) guarantee.
1423
+ - Use mcp__orchestrate__run_workflow instead when the user wants the hard floor from the frozen kernel.
1424
+
1425
+ ## Phase 0: scope and acceptance criteria
1426
+
1427
+ 1. Restate the user's goal in one sentence.
1428
+ 2. Capture explicit USER-BLESSED acceptance criteria before planning.
1429
+ 3. If acceptance criteria are missing or ambiguous, ask the user or present a short candidate list for confirmation.
1430
+ 4. State plainly: these criteria are the only guard against wrong-spec; green tests can still be green for the wrong interpretation.
1431
+ 5. Identify constraints: files, APIs, compatibility, performance, security, release risk, and forbidden changes.
1432
+
1433
+ ## Phase 1: delegate research
1434
+
1435
+ 1. Invoke /gh-research for the ask and acceptance criteria.
1436
+ 2. Wait for its bounded saturated brief.
1437
+ 3. If the brief is cap-hit-with-residuals, surface that status; do not treat it as complete.
1438
+ 4. Read the persisted research file by pointer when needed and check freshness metadata.
1439
+ 5. If HEAD or the working-tree diff hash moved, re-verify stale load-bearing claims.
1440
+
1441
+ ## Phase 2: blind-spot analysis
1442
+
1443
+ Create a blind-spot table before decomposing:
1444
+
1445
+ - Wrong-spec risk: judgment-only, mitigated only by user-blessed acceptance criteria and checkpoint.
1446
+ - Root-cause risk: executable-checkable if reproduced or covered by a failing test; otherwise advisory.
1447
+ - Integration risk: usually source-verified plus tests where possible.
1448
+ - Regression risk: executable-checkable when tests/typecheck/lint cover it.
1449
+ - Review risk: advisory cross-lab reviewers reduce correlated blind spots.
1450
+ - Concurrency or merge risk: source-verified and sometimes executable-checkable.
1451
+ - Missing-test risk: executable-checkable only after a test exists and runs.
1452
+
1453
+ Tag every blind spot as executable-checkable or judgment-only.
1454
+
1455
+ ## Phase 3 and 4: decompose and plan (run in parallel)
1456
+
1457
+ 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.
1458
+
1459
+ - 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.
1460
+ - 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.
1461
+
1462
+ ## Phase 5: compose a native Workflow
1463
+
1464
+ Compose a native Workflow using the Workflow tool where every node has:
1465
+
1466
+ - goal
1467
+ - input artifacts
1468
+ - output artifact
1469
+ - gh-router tool to call
1470
+ - blind spot it kills
1471
+ - deterministic or advisory annotation
1472
+ - producer and checker lab where relevant
1473
+
1474
+ Parallelism (the Workflow tool's core optimization rule):
1475
+
1476
+ - DEFAULT to pipeline(): items flow through stages with NO barrier, so the slowest single item, not the slowest stage, sets wall-clock.
1477
+ - Use parallel() ONLY at a genuine barrier — a stage that needs ALL prior results at once (dedup/merge across the set, an early-exit on the total, or a cross-item comparison). "It is cleaner" or "I need to map/flatten first" is NOT a barrier; do that transform inside a pipeline stage.
1478
+ - Independent nodes within a phase run concurrently; never serialize work that has no data dependency.
1479
+
1480
+ Role to tool mapping:
1481
+
1482
+ - research: worker-explore (Agent subagent) and mcp__search__code for focused follow-ups.
1483
+ - plan: worker-plan (Agent subagent).
1484
+ - implement: worker-implement (Agent subagent), with worktree:true for parallel writers.
1485
+ - test: worker-test (Agent subagent), authored by a DIFFERENT LAB than the implementer when possible. This is an advisory practice, not enforced provenance.
1486
+ - review: mcp__peers__codex_reviewer plus mcp__peers__gemini_reviewer. Advisory unless findings are converted into executable checks or code changes.
1487
+ - 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.
1488
+ - verify: cross-lab checker plus mcp__orchestrate__attest_step with producer not equal to checker lab.
1489
+
1490
+ No nesting:
1491
+
1492
+ - A Workflow node must not invoke /gh-orchestrate.
1493
+ - Workflow-spawned workers are internal sessions.
1494
+ - Internal sessions must not get prompt steering or stop-gate blocking.
1495
+ - Carry a depth or call budget and stop with a diagnostic if it would recurse.
1496
+
1497
+ ## Phase 6: verify the workflow
1498
+
1499
+ 1. Call mcp__orchestrate__verify_workflow.
1500
+ 2. Fix drift between the ask, acceptance criteria, research, plan, and node graph.
1501
+ 3. Bound this repair loop to at most 3 verification rounds.
1502
+ 4. If drift remains after the cap, checkpoint with the drift as residual risk instead of pretending it is solved.
1503
+
1504
+ ## Phase 7: checkpoint, then run
1505
+
1506
+ Before running, present:
1507
+
1508
+ - Goal and user-blessed acceptance criteria.
1509
+ - Node to tool map.
1510
+ - Per-node blind spot killed.
1511
+ - Per-node deterministic or advisory annotation.
1512
+ - Residual-risk list, including the wrong-spec residual.
1513
+ - Research saturation status and any open residual unknowns.
1514
+ - Cost estimate: workers, peer calls, tests, and whether baseline plus selector is enabled.
1515
+ - The statement that native Workflow approximates, but does not guarantee, hard max(orchestrated, baseline).
1516
+
1517
+ After the checkpoint, run the Workflow only if it still appears right-sized for the ask.
1518
+ If the user rejects scope or cost, downshift to the smallest workflow that kills the important blind spots.
1519
+
1520
+ ## Return format
1521
+
1522
+ Return:
1523
+
1524
+ - Whether orchestration was skipped or run, with the right-sizing reason.
1525
+ - Acceptance criteria used.
1526
+ - Research brief pointer and freshness status.
1527
+ - Workflow summary and node annotations.
1528
+ - Executable gate results, if any.
1529
+ - Advisory review results, if any.
1530
+ - Final residual risks and next action.
1531
+ `
1532
+ };
1533
+ //#endregion
1534
+ //#region src/lib/injected-skills/research-skill.ts
1535
+ const RESEARCH_SKILL = {
1536
+ name: "gh-research",
1537
+ md: `---
1538
+ name: gh-research
1539
+ 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.
1540
+ user-invocable: true
1541
+ ---
1542
+
1543
+ # gh-research: bounded saturation engine
1544
+
1545
+ Use this skill when an ask needs grounded investigation before planning or editing.
1546
+ Your output is a compact confidence-tagged root-cause brief plus a pointer to the durable full brief.
1547
+ Do not try to be exhaustive forever; saturation is bounded by explicit caps.
1548
+
1549
+ ## Operating contract
1550
+
1551
+ - Objective: find the most likely root cause, integration constraints, or decision facts for this ask.
1552
+ - Prefer primary sources over summaries.
1553
+ - Prefer executable proof over all other evidence.
1554
+ - Be honest about uncertainty: only verified-executable is deterministic.
1555
+ - Delegate heavy gather to workers so the top-level context stays compact.
1556
+ - Never silently claim completeness after hitting a cap.
1557
+
1558
+ ## Evidence tags
1559
+
1560
+ Use these exact tags on every finding and claim:
1561
+
1562
+ - verified-executable: reproduced the symptom, ran the failing test, or ran a check that directly proves the claim. This is the only deterministic confidence tag.
1563
+ - verified-source: read the actual source, config, logs, docs, or primary artifact and cited the relevant locations. This is model-mediated and can still be wrong.
1564
+ - cross-lab-agreed: a different-lab reviewer or critic independently agreed with the claim. This reduces correlated blind spots but is advisory.
1565
+ - unverified: plausible but not confirmed; treat as residual risk.
1566
+
1567
+ ## Bounded loop
1568
+
1569
+ Default caps unless the user explicitly gives a smaller or larger budget:
1570
+
1571
+ - Maximum rounds: about 3.
1572
+ - Maximum parallel explore workers per round: finite and right-sized to the ask.
1573
+ - Maximum search and peer-review calls: finite; do not spend unbounded context.
1574
+ - Terminate at the first of saturation or a cap.
1575
+ - On cap-hit, return with open unknowns flagged as residual. Do not loop forever.
1576
+
1577
+ ## Procedure
1578
+
1579
+ 1. Restate the ask and define the research target.
1580
+ - Identify whether this is a bug, feature, refactor, incident, or design question.
1581
+ - Name the expected downstream consumer: implementer, orchestrator, floor-keeper, or user.
1582
+
1583
+ 2. Enumerate unknowns as an explicit worklist.
1584
+ - Include facts needed to decide the root cause or safe implementation path.
1585
+ - Mark each unknown as code, behavior, dependency, history, external, or acceptance-criteria related.
1586
+ - Add newly discovered unknowns as they appear.
1587
+
1588
+ 3. Fan out in parallel.
1589
+ - Run independent code, web, history, and explore calls concurrently where possible; only the semantic-to-lexical code-search refinement is ordered. Issue the independent calls in a SINGLE turn (one message, multiple tool calls) so the harness actually runs them in parallel rather than serializing.
1590
+ - Use mcp__search__code semantically first to find concepts and likely files.
1591
+ - Then use mcp__search__code lexically for exact symbols, filenames, errors, routes, flags, and config keys.
1592
+ - Use git blame or history when authorship, regression timing, or intent matters.
1593
+ - Use mcp__search__web for upstream APIs, package behavior, protocol docs, or public issues.
1594
+ - 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.
1595
+ - Keep worker results summarized; do not paste every detail into the main context.
1596
+
1597
+ 4. Form a root-cause hypothesis.
1598
+ - For bugs: describe the causal chain from trigger to observed symptom.
1599
+ - For features: identify integration points, constraints, and likely implementation seams.
1600
+ - For design questions: identify the decision, alternatives, and primary constraints.
1601
+ - State what would falsify the hypothesis.
1602
+
1603
+ 5. Verify load-bearing claims adversarially.
1604
+ - First preference: reproduce the bug, run the failing test, or run the direct check. Tag verified-executable.
1605
+ - If executable proof is not available, read the actual source or primary artifact and cite the lines. Tag verified-source.
1606
+ - Dispatch the worker-review subagent (via the Agent tool) to confirm the source-reading for important claims.
1607
+ - Ask a different-lab refuter through mcp__peers__codex_critic or mcp__peers__gemini_critic to try to refute the hypothesis.
1608
+ - Give the refuter the symptom, observed facts, and acceptance criteria, but not your proposed root cause. Avoid anchoring them.
1609
+ - If the refuter finds a plausible alternative, add it to the worklist and spend at most one bounded round resolving it.
1610
+
1611
+ 6. Run a completeness pass.
1612
+ - Ask: what do we still not know?
1613
+ - Ask: what claim, if false, would break the conclusion?
1614
+ - Ask: have we checked primary sources for every load-bearing claim?
1615
+ - Ask: did a further bounded round surface anything material?
1616
+ - If no material unknowns remain and the root cause is at least verified-source, stop for saturation.
1617
+
1618
+ 7. Persist the full brief.
1619
+ - Write a durable markdown file such as .docs/research/<slug>.md.
1620
+ - Include freshness metadata: HEAD commit, working-tree diff hash, timestamp, repo path, and command/search date.
1621
+ - Include the unknown worklist, searches run, workers consulted, evidence table, refuter result, residuals, and full citations.
1622
+ - Downstream phases should read by pointer and check freshness instead of re-injecting the whole brief.
1623
+
1624
+ ## Return format
1625
+
1626
+ Return a compact brief, not the whole research dump:
1627
+
1628
+ - Research file: path to the durable brief.
1629
+ - Freshness: HEAD commit, diff hash, timestamp.
1630
+ - Termination: saturated or cap-hit; if cap-hit, name the cap.
1631
+ - Root-cause hypothesis: 3-8 bullets with confidence tags.
1632
+ - Evidence table: claim, tag, primary source or command, reviewer/refuter status.
1633
+ - Residual unknowns: explicit list, or none.
1634
+ - Downstream guidance: recommended next action and what must be rechecked if the tree changes.
1635
+
1636
+ ## Non-goals
1637
+
1638
+ - Do not present verified-source or cross-lab-agreed as deterministic.
1639
+ - Do not hide open unknowns because the answer looks useful.
1640
+ - Do not keep searching after the cap.
1641
+ - Do not paste the entire persisted brief into later turns unless the user asks.
1642
+ `
1643
+ };
1644
+ //#endregion
1645
+ //#region src/lib/injected-skills/worker-skill.ts
1646
+ /**
1647
+ * The `/gh-worker` skill: the operating model for the NON-BLOCKING workers
1648
+ * surface. Materialized into the per-launch mirror whenever `workerToolsEnabled()`
1649
+ * (same gate as the other floor-raising skills). Discoverability + playbook; the
1650
+ * load-bearing enforcement is the PreToolUse guard + the `worker-*` dispatcher
1651
+ * subagents (see src/lib/worker-dispatch.ts).
1652
+ */
1653
+ const WORKER_SKILL = {
1654
+ name: "gh-worker",
1655
+ md: `---
1656
+ name: gh-worker
1657
+ 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.
1658
+ user-invocable: true
1659
+ ---
1660
+
1661
+ # gh-worker: non-blocking workers
1662
+
1663
+ Worker tasks (explore, implement, review, plan, test) can run for up to 6 hours.
1664
+ In this session they are NON-BLOCKING BY DESIGN: you dispatch a
1665
+ background \`worker-*\` subagent, get control back immediately, and receive the
1666
+ worker's result as a completion notification when it finishes. Your turn is
1667
+ never blocked waiting on a worker, and the worker's tool output never fills your
1668
+ context (only its final result comes back).
1669
+
1670
+ ## How to run a worker
1671
+
1672
+ Dispatch the matching dispatcher subagent with the Agent tool:
1673
+
1674
+ - worker-explore: read-only investigation / codebase gathering, returns a summary.
1675
+ - worker-review: reads the code itself to verify a change or claim; findings with severity + file:line.
1676
+ - worker-plan: returns an ordered implementation plan from a task + acceptance criteria.
1677
+ - worker-implement: read/write/bash coding worker; pass worktree: true for isolated-worktree execution + a returned diff.
1678
+ - worker-test: independent test author that writes tests trying to break the implementation and reports pass/fail.
1679
+
1680
+ Put the full worker brief in the subagent's prompt (and an absolute workspace
1681
+ path, or model/thinking/worktree, only if you need to override the defaults).
1682
+ The dispatcher calls the worker once and relays its result verbatim.
1683
+
1684
+ ## What to expect
1685
+
1686
+ - The dispatch returns immediately; you can keep working or start other workers.
1687
+ - When the worker finishes you get a completion notification carrying its result.
1688
+ - Up to 8 workers run concurrently (the worker-semaphore cap); further dispatches queue.
1689
+ - You do NOT call the raw mcp__...__ worker tools from the main thread: a guard
1690
+ denies that and points you at the matching worker-* subagent. That guard is the
1691
+ guarantee your turn never blocks; dispatching worker-* directly is the normal path.
1692
+
1693
+ ## Notes
1694
+
1695
+ - Large worker output may be summarized by the dispatcher relay; for a big
1696
+ artifact, have the worker write it to a file and return the path.
1697
+ - Dispatching the same worker twice runs it twice (no dedup); avoid double-dispatch
1698
+ for side-effecting work like worker-implement.
1699
+ - Background subagents + completion notifications are the interactive default. In
1700
+ headless (claude -p) runs the task surface behaves differently; prefer interactive
1701
+ for long worker fan-out.
1702
+ `
1703
+ };
1704
+ //#endregion
1705
+ //#region src/lib/claude-md-injection.ts
1706
+ /**
1707
+ * Marker fences for each injection block. The literal text of each
1708
+ * fence is intentionally specific enough that a content collision with
1709
+ * user prose is implausible. Each block's parser only matches its own
1710
+ * marker pair, so blocks operate independently.
1711
+ *
1712
+ * Writer-side guard: the injector refuses to write a snippet that
1713
+ * itself contains its own marker literals (that would create
1714
+ * ambiguous state on the next launch where the inner literal would
1715
+ * parse as a new open or close marker).
1716
+ */
1717
+ const PEER_MARKER_OPEN = "<!-- gh-router peer-mcp awareness — auto-injected, regenerated per launch -->";
1718
+ const PEER_MARKER_CLOSE = "<!-- /gh-router peer-mcp awareness -->";
1719
+ const STYLE_MARKER_OPEN = "<!-- gh-router style directive — auto-injected, regenerated per launch -->";
1720
+ const STYLE_MARKER_CLOSE = "<!-- /gh-router style directive -->";
1721
+ const OPERATING_MARKER_OPEN = "<!-- gh-router operating defaults — auto-injected, regenerated per launch -->";
1722
+ const OPERATING_MARKER_CLOSE = "<!-- /gh-router operating defaults -->";
1723
+ const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injected, regenerated per launch -->";
1724
+ const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
1725
+ const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
1726
+ const ARTIFACT_MARKER_CLOSE = "<!-- /gh-router artifact-panel directive -->";
1727
+ function ARTIFACT_PANEL_DIRECTIVE(peersKey = "peers") {
1728
+ const toolPrefix = `mcp__${peersKey}__artifact_`;
1729
+ return `## Review in the artifact panel (HTML by default)
1730
+
1731
+ 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.`;
1732
+ }
1733
+ /**
1734
+ * Writing / communication style directive injected at the TOP of the
1735
+ * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
1736
+ * agent-teams teammate) reads it before the user's own CLAUDE.md body.
1737
+ *
1738
+ * Self-referentially compliant: the directive itself uses no em
1739
+ * dashes and does not mention any Claude / Anthropic attribution.
1740
+ */
1741
+ 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).";
1742
+ /**
1743
+ * Operating-defaults directive injected at the TOP of the mirrored CLAUDE.md.
1744
+ * The main agent's system prompt (`--append-system-prompt`) gets
1745
+ * OPERATING_DEFAULTS_DIGEST instead, with this full statement available through
1746
+ * CLAUDE.md. Three defaults, each explicitly overridden by the user's own
1747
+ * direction and the domain's standards:
1748
+ *
1749
+ * 1. Orchestrate (strong default): delegate the heavy / parallel /
1750
+ * context-heavy work to the right subagent / worker / model, keeping the
1751
+ * main context free to reason and collaborate with the user, while still
1752
+ * doing trivial / surgical / last-mile work directly (delegating that
1753
+ * would only add relay-fidelity loss + latency).
1754
+ * 2. Adversarial review: WHEN a peer critic earns its keep and, equally
1755
+ * important, when reaching for one is ritual rather than review. Same
1756
+ * failure shape the delegation default had: "consult a critic for
1757
+ * non-trivial changes" is unfalsifiable in advance, so it collapses into
1758
+ * either never (four of four unprimed agents) or always (worse than
1759
+ * never). The discriminator is whether the conclusion still turns on
1760
+ * judgment once the direct evidence is in: a consequential recommendation
1761
+ * cannot be run, which is exactly where confabulation hides, while a
1762
+ * tracing question a search already proved gains nothing from a second
1763
+ * model re-deriving it. The roster, the lens-to-artifact match, the
1764
+ * advisor-complements-rather-than-substitutes distinction, and the
1765
+ * do-not-anchor-the-critic rule live here; the digest carries only the
1766
+ * trigger and the ritual exclusion.
1767
+ * 3. Excellence lens: the principles stated plainly and concretely (radical
1768
+ * simplicity + real-user focus; whole-system first-principles thinking that
1769
+ * anticipates scale; work back from the customer outcome). Named exemplars
1770
+ * were dropped per the injected-surface review: a named entity is a dense,
1771
+ * high-variance vector that pulls in persona mannerisms at top salience, and
1772
+ * the guidance favors specific functional framing over comparison, so
1773
+ * specificity carries the vividness instead.
1774
+ * 4. Engineering excellence: quality / robustness / maintainability over
1775
+ * development cost; reproduce a bug end-to-end (as a real user hits it)
1776
+ * before fixing so the fix targets the real cause; a pixel-perfect UI bar;
1777
+ * and fix any lint error / test failure / flake on sight, whoever caused it,
1778
+ * folded into the current work rather than derailing the user's task (the
1779
+ * scope guardrail keeps proactive quality from becoming yak-shaving). The
1780
+ * digest carries a one-line form; the full statement lives here so it does
1781
+ * not cost the context window every turn.
1782
+ *
1783
+ * Self-referentially compliant with the style directive: no em dashes, no
1784
+ * Claude / Anthropic attribution.
1785
+ */
1786
+ 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. Reach for `implementer` when you know what to build, `reviewer` when something exists and you want it assessed (including reproducing and root-causing a failure), `brainstorm` when you do not yet know which approach to take, `scout` to find or understand something in the repo, and `scribe` for docs and ADRs that trail the code; worker-* agents for background non-blocking runs; Task subagents for parallel work; peer critics for review. That keeps your own context free to reason and collaborate with the user. Prefer parallel delegation for independent work. Delegation pays when the work is WIDE (many files or sources to sweep) or SLOW, and you need only the conclusion: the main thread is where you think with and respond to the user, and its context window is a finite shared resource. It does NOT pay merely because a sub-question is separable. A narrow, deep question whose whole value is file:line fidelity loses exactly that through a summarization layer, and a sub-question you could answer in one command is cheaper done directly than paying a subagent's startup. Do trivial, surgical, and last-mile work yourself.\n\nAdversarial review. The peer critics (`codex_critic` and `codex_reviewer`, `gemini_critic` and `gemini_reviewer`, `opus_critic`, and the `peer-review-coordinator` that fans out to several of them) are fresh-context models, so what they add is a blind spot that whoever produced the work cannot reach by thinking harder about it; prefer a critic from a different lab than the producer, since blind spots correlate within a lab. The `advisor` is a complement and not a substitute: it sees your transcript, so it catches your own drift and momentum, but it inherits your framing, which is exactly what a fresh-context critic does not. They earn their keep on consequential design choices, recommendations, and hard-to-reverse decisions: the cases where plausible alternatives remain and the conclusion rests on judgment rather than on something you can verify directly. That is where confabulation hides, so budget the wait even under delivery pressure. Always consult one when the change touches auth, user input, database queries, crypto, or serialization. They do NOT pay for read-only tracing, ordinary repository lookup, or a conclusion that a focused test, a direct reproduction, or unambiguous code evidence already settles. Asking a critic to re-derive a proven fact returns a confident answer either way, which is ritual skepticism rather than review, and skipping them there is the right call and not a shortcut. Match the lens to the artifact: a strategic critic for plans and trade-offs, a code reviewer for a concrete diff, the coordinator only when the risk warrants several independent lenses. Give whichever you pick the artifact and the constraints and not your rationale, since justification anchors the review and dulls it.\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.";
1787
+ /**
1788
+ * Condensed digest of OPERATING_DEFAULTS_DIRECTIVE for the spawned session's
1789
+ * system prompt (--append-system-prompt). The FULL directive is prepended to
1790
+ * the mirrored CLAUDE.md (read by the main agent and descendants); this digest
1791
+ * keeps both behavioral directives at top salience without duplicating the full
1792
+ * ~310-token block in the context window every turn. Points to the full copy.
1793
+ *
1794
+ * The unverifiable-claim rule is here rather than in CLAUDE.md alone because it
1795
+ * fires at a moment that suppresses lookups: an agent racing to deliver a
1796
+ * recommendation is precisely the one that will not stop to consult its project
1797
+ * instructions, which is how the check got crowded out in the first place. It
1798
+ * also closes a hole in the rule above it, which only covers claims that direct
1799
+ * evidence CAN settle. The roster of critics, the lens-to-artifact match, and
1800
+ * the reasoning stay in the directive; the always-resident copy carries only
1801
+ * the trigger and the ritual exclusion.
1802
+ */
1803
+ const OPERATING_DEFAULTS_DIGEST = "## Operating defaults (the user's explicit direction and the domain's standards always override)\n\nDelegate when the work is WIDE (many files or sources to sweep) or SLOW and you need only the conclusion, to protect the main thread's finite context and keep it free for reasoning and interacting with the user; prefer parallel delegation for independent work. Do NOT delegate merely because a sub-question is separable: a narrow, deep question whose value is file:line fidelity loses exactly that through a summarization layer, and one answerable in a single command is cheaper done directly. Do trivial, surgical, and last-mile work yourself.\n\nVerify, do not assert. Run the code, read the file, check the exit code. A claim in prose is worth nothing against state you did not check, and a check that cannot fail proves nothing. Reproduce a bug end to end, the way a real user hits it, before fixing it. Fix a lint error, failing test, or flake the moment you see it, whoever introduced it, without letting it derail the task at hand. Prefer quality and long-term maintainability over development cost.\n\nVerification has a blind spot: a consequential recommendation, a design or trade-off call, or a hard-to-reverse decision that still turns on judgment among plausible alternatives once the direct evidence is in. That is where confabulation hides, so put it past a peer critic before you ship it and budget the wait even under delivery pressure. When a test, a run, a reproduction, or a search would settle the claim, settle it that way instead; a critic asked to re-derive what you can already prove is ritual, not review.\n\nThe agent roster, the tool surface, and the reasoning behind these defaults are in your CLAUDE.md project instructions. Read them when choosing HOW to work; the rules above apply without a lookup.";
1804
+ /**
1805
+ * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
1806
+ * the would-be post-write file) has grown past this size.
1807
+ * Read-modify-write becomes pathological at very large sizes; CLAUDE.md
1808
+ * should never legitimately be a database. The main agent still gets
1809
+ * the awareness via `--append-system-prompt`, so skipping here only
1810
+ * loses descendant-reach.
1811
+ */
1812
+ const MAX_CLAUDE_MD_BYTES = 1048576;
1813
+ /**
1814
+ * Bounded retry budget for the temp → rename step on Windows where
1815
+ * `fs.rename` can transiently fail with EBUSY / EPERM / EACCES when
1816
+ * CLAUDE.md is open in an editor, scanned by AV, or indexed by the
1817
+ * search service. Mirrors the verify-on-rename-fail pattern at
1818
+ * `paths.ts:795-818`. POSIX renames almost never fail this way; the
1819
+ * cost on Linux/macOS is one extra `lstat` in the unhappy path.
1820
+ */
1821
+ const RENAME_RETRY_DELAYS_MS = [
1822
+ 50,
1823
+ 200,
1824
+ 500
1825
+ ];
1826
+ /**
1827
+ * Grep-able error-code prefix. Every warn-and-continue path here
1828
+ * starts its message with this token so a Windows user who never sees
1829
+ * a fresh marker block in their mirror can `grep CLAUDE_MD_WRITE` in
1830
+ * the launcher output and land on the actionable line directly.
1831
+ */
1832
+ const ERROR_CODE$1 = "CLAUDE_MD_WRITE";
1833
+ /**
1834
+ * Find every well-formed marker block matching the given `markerOpen`
1835
+ * + `markerClose` pair. A well-formed block is an exact `markerOpen`
1836
+ * line followed somewhere later (any number of intervening lines) by
1837
+ * an exact `markerClose` line, with no intervening `markerOpen`.
1838
+ * Multiple stale blocks all surface here so the caller can remove
1839
+ * all of them.
1840
+ *
1841
+ * Malformed state (open without close, or close without open) is
1842
+ * reported separately via the second return value so the caller can
1843
+ * `warn` and leave user prose untouched. We never try to "fix"
1844
+ * malformed marker state — that risks corrupting user content.
1845
+ */
1846
+ function findMarkerBlocks(lines, markerOpen = PEER_MARKER_OPEN, markerClose = PEER_MARKER_CLOSE) {
1847
+ const blocks = [];
1848
+ let pendingOpen = null;
1849
+ let malformed = false;
1850
+ for (let i = 0; i < lines.length; i++) {
1851
+ const line = lines[i];
1852
+ if (line === markerOpen) {
1853
+ if (pendingOpen !== null) malformed = true;
1854
+ pendingOpen = i;
1855
+ } else if (line === markerClose) if (pendingOpen === null) malformed = true;
1856
+ else {
1857
+ blocks.push({
1858
+ openLineIndex: pendingOpen,
1859
+ closeLineIndex: i
1860
+ });
1861
+ pendingOpen = null;
1862
+ }
1863
+ }
1864
+ if (pendingOpen !== null) malformed = true;
1865
+ return {
1866
+ blocks,
1867
+ malformed
1868
+ };
1869
+ }
1870
+ /**
1871
+ * Detect line-ending style of `content`. Returns `"\r\n"` if `\r\n`
1872
+ * sequences outnumber bare `\n`; otherwise `"\n"`. Empty content
1873
+ * defaults to `\n` (POSIX-style new file).
1874
+ *
1875
+ * Preserves CRLF on Windows users' existing CLAUDE.md — flipping their
1876
+ * line endings under them would be a regression even though Claude
1877
+ * Code itself reads either style.
1878
+ */
1879
+ function detectLineEnding(content) {
1880
+ if (content.length === 0) return "\n";
1881
+ const crlf = (content.match(/\r\n/g) ?? []).length;
1882
+ return crlf > (content.match(/\n/g) ?? []).length - crlf ? "\r\n" : "\n";
1883
+ }
1884
+ /**
1885
+ * Strip a leading UTF-8 BOM (`U+FEFF`) if present so the first line's
1886
+ * marker comparison is byte-exact. CLAUDE.md authored on Windows in
1887
+ * Notepad / VS Code sometimes carries a BOM; without this strip the
1888
+ * first marker line would never match (`<BOM><!--...` !== `<!--...`)
1889
+ * and successive launches would loop into malformed-state warn paths.
1890
+ */
1891
+ function stripLeadingBom(content) {
1892
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
1893
+ }
1894
+ /**
1895
+ * Split `content` into lines without losing the line-ending style.
1896
+ * The split is done on `\n`; trailing `\r` (from CRLF) is stripped
1897
+ * from each line for marker comparison, but the original ending is
1898
+ * reconstructed via `detectLineEnding` + `joinLines`.
1899
+ */
1900
+ function splitLines(content) {
1901
+ if (content.length === 0) return [];
1902
+ return content.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
1903
+ }
1904
+ function joinLines(lines, eol) {
1905
+ return lines.join(eol);
1906
+ }
1907
+ /**
1908
+ * Containment check that defeats symlink/junction tricks (peer-review
1909
+ * C3). `isUnderClaudeConfigMirror` is purely lexical via
1910
+ * `path.resolve()` — it does NOT dereference symlinks, so an attacker
1911
+ * (or an unfortunate `~/.claude` symlinked into Dropbox) could escape
1912
+ * the mirror while passing the lexical guard. This helper resolves
1913
+ * BOTH paths to their canonical form via `fs.realpath()` first.
1914
+ *
1915
+ * **Fail-closed semantics (advisor follow-up):**
1916
+ *
1917
+ * - If the mirror root itself is a symlink (`lstat` reports
1918
+ * `isSymbolicLink() === true`), refuse. A symlinked mirror root
1919
+ * means writes flow through the link to whatever the user (or an
1920
+ * attacker) targeted — the boundary's whole point is to never
1921
+ * mutate real `~/.claude/`, so accepting any symlinked root
1922
+ * undermines it.
1923
+ * - If `realpath` fails on the mirror root OR the target parent,
1924
+ * refuse. The mirror dir is provisioned by `ensureClaudeConfigMirror`
1925
+ * before this helper runs (documented ordering invariant); a
1926
+ * `realpath` failure here signals an unexpected state, and after
1927
+ * the root check has already succeeded a missing parent means the
1928
+ * root vanished between checks (TOCTOU race).
1929
+ */
1930
+ async function isUnderClaudeConfigMirrorRealpath(target) {
1931
+ if (!isUnderClaudeConfigMirror(target)) return false;
1932
+ const mirrorRoot = PATHS.CLAUDE_CONFIG_DIR;
1933
+ try {
1934
+ if ((await fs.lstat(mirrorRoot)).isSymbolicLink()) {
1935
+ consola.warn(`${ERROR_CODE$1}: mirror root is a symlink (${mirrorRoot}); refusing to write through it`);
1936
+ return false;
1937
+ }
1938
+ } catch (err) {
1939
+ consola.warn(`${ERROR_CODE$1}: cannot lstat mirror root ${mirrorRoot}: ${err instanceof Error ? err.message : String(err)}`);
1940
+ return false;
1941
+ }
1942
+ let resolvedRoot;
1943
+ try {
1944
+ resolvedRoot = await fs.realpath(mirrorRoot);
1945
+ } catch (err) {
1946
+ consola.warn(`${ERROR_CODE$1}: realpath failed on mirror root ${mirrorRoot}: ${err instanceof Error ? err.message : String(err)}`);
1947
+ return false;
1948
+ }
1949
+ const targetParent = path.dirname(target);
1950
+ let resolvedTargetParent;
1951
+ try {
1952
+ resolvedTargetParent = await fs.realpath(targetParent);
1953
+ } catch (err) {
1954
+ consola.warn(`${ERROR_CODE$1}: realpath failed on target parent ${targetParent} after root check (TOCTOU?): ${err instanceof Error ? err.message : String(err)}`);
1955
+ return false;
1956
+ }
1957
+ if (resolvedTargetParent === resolvedRoot) return true;
1958
+ return resolvedTargetParent.startsWith(resolvedRoot + path.sep);
1959
+ }
1960
+ /**
1961
+ * Try `fs.rename(temp, target)` with bounded retry + verify-on-fail.
1962
+ * Mirrors `injectSyntheticClaudeJsonFields` in `paths.ts`. Windows
1963
+ * `fs.rename` can transiently fail with EBUSY / EPERM / EACCES when
1964
+ * the destination is held by another process (editor, AV, search
1965
+ * indexer). Returns `true` on eventual success, `false` after all
1966
+ * retries are exhausted (caller will warn-and-continue).
1967
+ *
1968
+ * On final failure we read the destination back and check whether it
1969
+ * already matches `desiredContent` — a concurrent racer may have
1970
+ * landed the same bytes (the snippet is deterministic per launch).
1971
+ * In that case treat as success.
1972
+ *
1973
+ * **No `copyFile` fallback** (peer-review codex-critic C2). `fs.copyFile`
1974
+ * follows the destination path — if `target` was replaced with a
1975
+ * symlink/junction between our earlier `lstat` and now (TOCTOU), or
1976
+ * if `target` is a hardlink to the real `~/.claude/CLAUDE.md`,
1977
+ * `copyFile` would mutate user files through the link. The boundary
1978
+ * we are defending says "never mutate the real `~/.claude/`". Rename
1979
+ * is safe because replacing a path entry doesn't follow the link; the
1980
+ * `copyFile` degradation reintroduces the escape. Fail-closed instead.
1981
+ */
1982
+ async function renameWithRetry(tempPath, target, desiredContent) {
1983
+ let lastErr;
1984
+ for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt++) try {
1985
+ await fs.rename(tempPath, target);
1986
+ return true;
1987
+ } catch (err) {
1988
+ lastErr = err;
1989
+ if (attempt < RENAME_RETRY_DELAYS_MS.length) await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_DELAYS_MS[attempt]));
1990
+ }
1991
+ try {
1992
+ if (await fs.readFile(target, "utf8") === desiredContent) {
1993
+ await fs.unlink(tempPath).catch(() => {});
1994
+ consola.debug(`${ERROR_CODE$1}: rename failed but target already holds expected content (racer-won-race): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
1995
+ return true;
1996
+ }
1997
+ } catch {}
1998
+ await fs.unlink(tempPath).catch(() => {});
1999
+ consola.warn(`${ERROR_CODE$1}: rename failed for ${target} after ${RENAME_RETRY_DELAYS_MS.length + 1} attempts (no copyFile fallback to avoid symlink/hardlink escape; descendant-reach via CLAUDE.md disabled this launch; main agent still has --append-system-prompt). rename err: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
2000
+ return false;
2001
+ }
2002
+ async function injectMarkerBlock(opts) {
2003
+ const { snippet, markerOpen, markerClose, position, label } = opts;
2004
+ if (snippet.includes(markerOpen) || snippet.includes(markerClose)) {
2005
+ consola.warn(`${ERROR_CODE$1}: refusing to inject ${label} snippet that contains marker literal; this would corrupt idempotency on the next launch`);
2006
+ return;
2007
+ }
2008
+ const target = path.join(PATHS.CLAUDE_CONFIG_DIR, "CLAUDE.md");
2009
+ if (!await isUnderClaudeConfigMirrorRealpath(target)) {
2010
+ consola.warn(`${ERROR_CODE$1}: refusing to write outside resolved mirror dir (target=${target}, mirror=${PATHS.CLAUDE_CONFIG_DIR}) [${label}]`);
2011
+ return;
2012
+ }
2013
+ let existingContent;
2014
+ let targetExists;
2015
+ try {
2016
+ const linkStat = await fs.lstat(target);
2017
+ if (linkStat.isSymbolicLink()) {
2018
+ consola.warn(`${ERROR_CODE$1}: refusing to write through symlinked CLAUDE.md (target=${target}) [${label}]`);
2019
+ return;
2020
+ }
2021
+ if (!linkStat.isFile()) {
2022
+ consola.warn(`${ERROR_CODE$1}: refusing to write non-regular target (target=${target}, mode=${linkStat.mode.toString(8)}) [${label}]`);
2023
+ return;
2024
+ }
2025
+ if (linkStat.size > MAX_CLAUDE_MD_BYTES) {
2026
+ consola.warn(`${ERROR_CODE$1}: skipping oversized CLAUDE.md (${linkStat.size} bytes > ${MAX_CLAUDE_MD_BYTES}) [${label}]; descendant-reach disabled this launch`);
2027
+ return;
2028
+ }
2029
+ if (linkStat.nlink > 1) {
2030
+ consola.warn(`${ERROR_CODE$1}: refusing to write to hardlinked CLAUDE.md (nlink=${linkStat.nlink}) [${label}]; would mutate shared inode`);
2031
+ return;
2032
+ }
2033
+ targetExists = true;
2034
+ existingContent = await fs.readFile(target, "utf8");
2035
+ } catch (err) {
2036
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
2037
+ existingContent = "";
2038
+ targetExists = false;
2039
+ } else {
2040
+ consola.warn(`${ERROR_CODE$1}: failed to stat/read target (${target}) [${label}]: ${err instanceof Error ? err.message : String(err)}`);
2041
+ return;
2042
+ }
2043
+ }
2044
+ const hadBom = existingContent.charCodeAt(0) === 65279;
2045
+ const normalizedContent = stripLeadingBom(existingContent);
2046
+ const eol = detectLineEnding(normalizedContent);
2047
+ const lines = splitLines(normalizedContent);
2048
+ const { blocks, malformed } = findMarkerBlocks(lines, markerOpen, markerClose);
2049
+ if (malformed) {
2050
+ consola.warn(`${ERROR_CODE$1}: malformed marker state in ${target} (open without close or vice versa) [${label}]; leaving file untouched`);
2051
+ return;
2052
+ }
2053
+ const cleanedLines = [...lines];
2054
+ for (let i = blocks.length - 1; i >= 0; i--) {
2055
+ const block = blocks[i];
2056
+ cleanedLines.splice(block.openLineIndex, block.closeLineIndex - block.openLineIndex + 1);
2057
+ if (position === "bottom") while (block.openLineIndex - 1 >= 0 && cleanedLines[block.openLineIndex - 1] === "" && cleanedLines.slice(0, block.openLineIndex - 1).some((l) => l !== "")) cleanedLines.splice(block.openLineIndex - 1, 1);
2058
+ else while (block.openLineIndex < cleanedLines.length && cleanedLines[block.openLineIndex] === "" && cleanedLines.slice(block.openLineIndex + 1).some((l) => l !== "")) cleanedLines.splice(block.openLineIndex, 1);
2059
+ }
2060
+ if (position === "bottom") while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") cleanedLines.pop();
2061
+ else while (cleanedLines.length > 0 && cleanedLines[0] === "") cleanedLines.shift();
2062
+ const markerBlockLines = [
2063
+ markerOpen,
2064
+ ...snippet.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l),
2065
+ markerClose
2066
+ ];
2067
+ let finalLines;
2068
+ if (cleanedLines.length === 0) finalLines = [...markerBlockLines, ""];
2069
+ else if (position === "bottom") finalLines = [
2070
+ ...cleanedLines,
2071
+ "",
2072
+ ...markerBlockLines,
2073
+ ""
2074
+ ];
2075
+ else finalLines = [
2076
+ ...markerBlockLines,
2077
+ "",
2078
+ ...cleanedLines,
2079
+ ""
2080
+ ];
2081
+ const bodyContent = joinLines(finalLines, eol);
2082
+ const finalContent = hadBom ? "" + bodyContent : bodyContent;
2083
+ if (Buffer.byteLength(finalContent, "utf8") > MAX_CLAUDE_MD_BYTES) {
2084
+ consola.warn(`${ERROR_CODE$1}: post-build content exceeds ${MAX_CLAUDE_MD_BYTES} bytes [${label}]; skipping update (descendant-reach disabled this launch)`);
2085
+ return;
2086
+ }
2087
+ const tempPath = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
2088
+ try {
2089
+ await fs.writeFile(tempPath, finalContent, {
2090
+ encoding: "utf8",
2091
+ flag: "wx"
2092
+ });
2093
+ } catch (err) {
2094
+ await fs.unlink(tempPath).catch(() => {});
2095
+ consola.warn(`${ERROR_CODE$1}: temp-file write failed for ${tempPath} [${label}]: ${err instanceof Error ? err.message : String(err)}`);
2096
+ return;
2097
+ }
2098
+ if (!await renameWithRetry(tempPath, target, finalContent)) return;
2099
+ consola.debug(`${ERROR_CODE$1}: ${targetExists ? "updated" : "created"} ${target} [${label}] (${finalContent.length} bytes, eol=${eol === "\r\n" ? "CRLF" : "LF"})`);
2100
+ }
2101
+ /**
2102
+ * Append the peer-MCP awareness `snippet` to the mirrored
2103
+ * `<CLAUDE_CONFIG_DIR>/CLAUDE.md`. Idempotent across launches: prior
2104
+ * well-formed peer-marker blocks are removed before appending a fresh
2105
+ * one at the bottom. The original user content is preserved
2106
+ * byte-for-byte at the top (modulo line-ending normalization to the
2107
+ * file's detected style; leading UTF-8 BOM is preserved).
2108
+ *
2109
+ * Failures `warn` and return — this surface is the descendant-reach
2110
+ * enhancement; the main agent still gets the awareness via
2111
+ * `--append-system-prompt`. Every warn message starts with
2112
+ * `CLAUDE_MD_WRITE` so users can grep launcher output.
2113
+ */
2114
+ async function appendPeerAwarenessToMirroredClaudeMd(snippet) {
2115
+ await injectMarkerBlock({
2116
+ snippet,
2117
+ markerOpen: PEER_MARKER_OPEN,
2118
+ markerClose: PEER_MARKER_CLOSE,
2119
+ position: "bottom",
2120
+ label: "peer-mcp-awareness"
2121
+ });
2122
+ }
2123
+ /**
2124
+ * Prepend a writing / communication style directive to the TOP of the
2125
+ * mirrored `<CLAUDE_CONFIG_DIR>/CLAUDE.md` so every spawned agent
2126
+ * reads it first. The directive itself is hard-coded to
2127
+ * `STYLE_DIRECTIVE` above; the parameter exists for tests / future
2128
+ * configurability. Idempotent across launches via the
2129
+ * style-marker fence (separate from the peer-awareness fence, so the
2130
+ * two blocks coexist without colliding).
2131
+ */
2132
+ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTIVE) {
2133
+ await injectMarkerBlock({
2134
+ snippet: directive,
2135
+ markerOpen: STYLE_MARKER_OPEN,
2136
+ markerClose: STYLE_MARKER_CLOSE,
2137
+ position: "top",
2138
+ label: "style-directive"
2139
+ });
2140
+ }
2141
+ /**
2142
+ * Prepend the operating-defaults directive (orchestrator posture + hybrid
2143
+ * excellence lens; `OPERATING_DEFAULTS_DIRECTIVE` above) to the TOP of the
2144
+ * mirrored CLAUDE.md so the main agent and descendant agents (Agent subagents,
2145
+ * agent-teams teammates) inherit the full statement. The main agent also gets
2146
+ * OPERATING_DEFAULTS_DIGEST at higher salience via `--append-system-prompt`.
2147
+ * Separate marker fence from the style / peer blocks so all coexist;
2148
+ * best-effort (warn-and-continue) like its siblings.
2149
+ */
2150
+ async function prependOperatingDefaultsToMirroredClaudeMd(directive = OPERATING_DEFAULTS_DIRECTIVE) {
2151
+ await injectMarkerBlock({
2152
+ snippet: directive,
2153
+ markerOpen: OPERATING_MARKER_OPEN,
2154
+ markerClose: OPERATING_MARKER_CLOSE,
2155
+ position: "top",
2156
+ label: "operating-defaults"
2157
+ });
2158
+ }
2159
+ /**
2160
+ * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
2161
+ * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
2162
+ * subagents, agent-teams teammates) and the main agent learn about the
2163
+ * provisioned tools via the mirrored CLAUDE.md. This line is not sent via
2164
+ * `--append-system-prompt`.
2165
+ * Separate marker fence from the peer-awareness / style blocks.
2166
+ */
2167
+ async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
2168
+ await injectMarkerBlock({
2169
+ snippet,
2170
+ markerOpen: TOOLBELT_MARKER_OPEN,
2171
+ markerClose: TOOLBELT_MARKER_CLOSE,
2172
+ position: "bottom",
2173
+ label: "toolbelt-awareness"
2174
+ });
2175
+ }
2176
+ /**
2177
+ * Prepend the artifact-panel review directive to the TOP of the mirrored
2178
+ * CLAUDE.md so plans/artifacts are reviewed in the ai-or-die panel by default.
2179
+ * Gated by the caller on AIORDIE_SESSION_ID (only useful inside a tab). Separate
2180
+ * marker fence; best-effort like the style/peer blocks.
2181
+ */
2182
+ async function prependArtifactPanelDirectiveToMirroredClaudeMd(peersKey = "peers") {
2183
+ await injectMarkerBlock({
2184
+ snippet: ARTIFACT_PANEL_DIRECTIVE(peersKey),
2185
+ markerOpen: ARTIFACT_MARKER_OPEN,
2186
+ markerClose: ARTIFACT_MARKER_CLOSE,
2187
+ position: "top",
2188
+ label: "artifact-panel-directive"
2189
+ });
2190
+ }
2191
+ //#endregion
2192
+ //#region src/lib/injected-skills/write.ts
2193
+ /**
2194
+ * Writer for injected skills: materializes a `SKILL.md` into the per-launch
2195
+ * `CLAUDE_CONFIG_DIR` mirror so the spawned Claude Code session discovers it as a
2196
+ * user-scope skill (`<CLAUDE_CONFIG_DIR>/skills/<name>/SKILL.md`, where the folder
2197
+ * name MUST equal the frontmatter `name` — the loader enforces this).
2198
+ *
2199
+ * Safety mirrors `claude-md-injection.ts`:
2200
+ * - mirror-only write guard (symlink-resolving) so we never touch the user's
2201
+ * real `~/.claude/skills/`;
2202
+ * - ATOMIC temp+rename so a concurrent child process can never read a
2203
+ * half-written `SKILL.md` (Claude Code watches the skills dir);
2204
+ * - warn-and-continue on every failure — an injected skill is an enhancement,
2205
+ * never a launch blocker.
2206
+ *
2207
+ * No per-skill stale sweep is needed: the whole per-launch mirror dir is GC'd by
2208
+ * `removeOwnClaudeConfigMirror` / `sweepStaleClaudeConfigMirrors`.
2209
+ */
2210
+ /** Grep-able prefix on every warn path (mirrors the CLAUDE_MD_WRITE convention). */
2211
+ const ERROR_CODE = "INJECTED_SKILL_WRITE";
2212
+ /**
2213
+ * Strict skill-name allowlist. Lowercase kebab so the folder name is a safe path
2214
+ * segment AND a valid Claude Code skill `name` (loader asserts folder == name).
2215
+ * All our injected skills (`gh-research`, `gh-orchestrate`, `gh-floor-keeper`,
2216
+ * `gh-first-mate`) pass.
2217
+ */
2218
+ const VALID_SKILL_NAME = /^[a-z][a-z0-9-]*$/;
2219
+ /**
2220
+ * Write `md` to `<CLAUDE_CONFIG_DIR>/skills/<name>/SKILL.md`. `md` must already be
2221
+ * a complete `SKILL.md` (YAML frontmatter with `name: <name>` + `description`,
2222
+ * then the body). Idempotent across launches (overwrite); the per-launch mirror
2223
+ * dir is disposable.
2224
+ */
2225
+ async function writeInjectedSkill(name, md) {
2226
+ if (!VALID_SKILL_NAME.test(name)) {
2227
+ consola.warn(`${ERROR_CODE}: invalid skill name "${name}" (need lowercase kebab); skipping`);
2228
+ return { written: false };
2229
+ }
2230
+ const dir = path.join(PATHS.CLAUDE_CONFIG_DIR, "skills", name);
2231
+ const target = path.join(dir, "SKILL.md");
2232
+ try {
2233
+ await fs.mkdir(dir, { recursive: true });
2234
+ } catch (err) {
2235
+ consola.warn(`${ERROR_CODE}: mkdir failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2236
+ return { written: false };
2237
+ }
2238
+ if (!await isUnderClaudeConfigMirrorRealpath(target)) {
2239
+ consola.warn(`${ERROR_CODE}: refusing to write outside the resolved mirror dir (target=${target}, mirror=${PATHS.CLAUDE_CONFIG_DIR})`);
2240
+ return { written: false };
2241
+ }
2242
+ const tempPath = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
2243
+ try {
2244
+ await fs.writeFile(tempPath, md, {
2245
+ encoding: "utf8",
2246
+ flag: "wx"
2247
+ });
2248
+ } catch (err) {
2249
+ await fs.unlink(tempPath).catch(() => {});
2250
+ consola.warn(`${ERROR_CODE}: temp-file write failed for ${tempPath}: ${err instanceof Error ? err.message : String(err)}`);
2251
+ return { written: false };
2252
+ }
2253
+ if (!await renameWithRetry(tempPath, target, md)) return { written: false };
2254
+ consola.debug(`${ERROR_CODE}: wrote ${target} (${md.length} bytes)`);
2255
+ return {
2256
+ written: true,
2257
+ path: target
2258
+ };
2259
+ }
2260
+ //#endregion
2261
+ //#region src/lib/injected-skills/index.ts
2262
+ /**
2263
+ * Injected-skill registry: the floor-raising / controller skills the `claude`
2264
+ * launcher materializes into the per-launch `CLAUDE_CONFIG_DIR` mirror so the
2265
+ * spawned Claude Code session discovers them (`/gh-research`,
2266
+ * `/gh-orchestrate`, `/gh-floor-keeper`, `/gh-first-mate`). See
2267
+ * `docs/floor-raising-agent-surface.md`.
2268
+ */
2269
+ /** All injected skills, in dependency order (research underpins the others). */
2270
+ const INJECTED_SKILLS = [
2271
+ RESEARCH_SKILL,
2272
+ ORCHESTRATE_SKILL,
2273
+ FLOOR_KEEPER_SKILL,
2274
+ WORKER_SKILL,
2275
+ FIRST_MATE_SKILL,
2276
+ FIRST_MATE_SETUP_SKILL,
2277
+ FIRST_MATE_OPERATE_SKILL,
2278
+ FIRST_MATE_CONDUCT_SKILL
2279
+ ];
2280
+ //#endregion
2281
+ //#region src/lib/attribution-settings.ts
2282
+ /**
2283
+ * Deterministic backstop for the injected "no attribution" style directive.
2284
+ *
2285
+ * github-router injects a CLAUDE.md style directive telling the agent not to
2286
+ * attribute work to Claude / AI / Anthropic. That directive is ADVISORY (prose
2287
+ * the model may or may not follow). Claude Code also exposes a HARNESS-ENFORCED
2288
+ * control that suppresses attribution at the source: the `attribution`
2289
+ * settings.json key (Claude Code v2.0.62+; string `commit` / `pr` sub-fields),
2290
+ * which supersedes the deprecated boolean `includeCoAuthoredBy`. Setting both
2291
+ * sub-fields to the empty string removes the "Generated with Claude Code"
2292
+ * commit footer and the "Co-Authored-By: Claude" byline from commits and PRs.
2293
+ * Because the harness reads and applies this itself, it holds even when the
2294
+ * model ignores the prose directive — a deterministic backstop, not a second
2295
+ * suggestion.
2296
+ *
2297
+ * PRESENCE-GUARDED. If the mirrored settings already carry `attribution` OR
2298
+ * `includeCoAuthoredBy`, the user expressed a deliberate preference in their
2299
+ * real config (which the one-way mirror snapshotted); we DO NOT override it.
2300
+ * Only a user who has expressed no preference receives github-router's
2301
+ * no-attribution default. This keeps faith with "the user's explicit direction
2302
+ * always overrides".
2303
+ *
2304
+ * The mirror is a per-launch snapshot of the user's real `~/.claude` config, so
2305
+ * this write never propagates back to the user's own settings.json. A
2306
+ * project-scope `<repo>/.claude/settings.json` still overrides the mirror
2307
+ * (global scope) at runtime, so the default remains overridable per-repo.
2308
+ *
2309
+ * Failure model matches the sibling settings writers: a transient read error
2310
+ * or a non-object settings.json throws (never clobber a file we don't
2311
+ * understand); the caller wraps this in warn-and-continue so a settings-write
2312
+ * hiccup never blocks launch.
2313
+ */
2314
+ async function injectAttributionSuppressionIntoSettingsFile(settingsPath) {
2315
+ let existing = {};
2316
+ let raw;
2317
+ try {
2318
+ raw = await fs.readFile(settingsPath, "utf8");
2319
+ } catch (err) {
2320
+ if (err.code !== "ENOENT") throw err;
2321
+ raw = void 0;
2322
+ }
2323
+ if (raw !== void 0) {
2324
+ const parsed = JSON.parse(raw);
2325
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
2326
+ else throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`);
2327
+ }
2328
+ if ("attribution" in existing || "includeCoAuthoredBy" in existing) return {
2329
+ written: false,
2330
+ reason: "user-set"
2331
+ };
2332
+ const merged = {
2333
+ ...existing,
2334
+ attribution: {
2335
+ commit: "",
2336
+ pr: ""
2337
+ }
2338
+ };
2339
+ const tmp = `${settingsPath}.${process.pid}.attr.tmp`;
2340
+ await fs.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
2341
+ await fs.rename(tmp, settingsPath);
2342
+ return { written: true };
2343
+ }
2344
+ //#endregion
2345
+ export { injectPeerMcpIntoMirror as _, OPERATING_DEFAULTS_DIRECTIVE as a, workersKeyOf as b, prependArtifactPanelDirectiveToMirroredClaudeMd as c, SEAMLESS_BUILTIN_TOOLS as d, configureServeDefaultPermissionMode as f, BUILTIN_SUBAGENT_DEFINITIONS as g, sanitizeServeSettingsEnv as h, OPERATING_DEFAULTS_DIGEST as i, prependOperatingDefaultsToMirroredClaudeMd as l, planModeAllowRules as m, INJECTED_SKILLS as n, appendPeerAwarenessToMirroredClaudeMd as o, injectAllowRules as p, writeInjectedSkill as r, appendToolbeltAwarenessToMirroredClaudeMd as s, injectAttributionSuppressionIntoSettingsFile as t, prependStyleDirectiveToMirroredClaudeMd as u, resolveCodexCliBackend as v, writePeerMcpRuntimeFiles as x, resolveGroupKeysFromMirror as y };
2346
+
2347
+ //# sourceMappingURL=attribution-settings-CegtE0TY.js.map