@integrity-labs/agt-cli 0.28.526 → 0.28.528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +4 -4
- package/dist/{chunk-ONTG4Z5K.js → chunk-7N55RDMG.js} +138 -4
- package/dist/chunk-7N55RDMG.js.map +1 -0
- package/dist/{chunk-Z4P4VEDU.js → chunk-PR3QSOLI.js} +23 -5
- package/dist/chunk-PR3QSOLI.js.map +1 -0
- package/dist/{claude-pair-runtime-V7PPPPU5.js → claude-pair-runtime-KQOPJSUJ.js} +2 -2
- package/dist/lib/manager-worker.js +10 -10
- package/dist/{persistent-session-C7ETZOWH.js → persistent-session-KAHG73SM.js} +2 -2
- package/dist/{responsiveness-probe-WHA7YNFM.js → responsiveness-probe-MKUXLX4F.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-ONTG4Z5K.js.map +0 -1
- package/dist/chunk-Z4P4VEDU.js.map +0 -1
- /package/dist/{claude-pair-runtime-V7PPPPU5.js.map → claude-pair-runtime-KQOPJSUJ.js.map} +0 -0
- /package/dist/{persistent-session-C7ETZOWH.js.map → persistent-session-KAHG73SM.js.map} +0 -0
- /package/dist/{responsiveness-probe-WHA7YNFM.js.map → responsiveness-probe-MKUXLX4F.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/core/src/provisioning/mcp-config-guards.ts","../../../packages/core/src/provisioning/mcp-secret-lint.ts","../../../packages/core/src/provisioning/hook-env.ts","../../../packages/core/src/provisioning/env-integrations-file.ts","../../../packages/core/src/provisioning/frameworks/claudecode/index.ts","../../../packages/core/src/integrations/xurl-config.ts","../../../packages/core/src/crypto/secret.ts","../../../packages/core/src/crypto/integration-credentials.ts","../../../packages/core/src/provisioning/github-broker-credentials.ts","../src/lib/globals.ts","../src/lib/config.ts","../src/lib/auth-exchange-error.ts","../src/lib/api-client.ts","../src/lib/atomic-write.ts","../src/lib/feature-flags-host.ts","../src/lib/stale-mcp-reaper.ts","../src/lib/mcp-presence-reaper.ts","../src/lib/restart-breaker.ts","../src/lib/mcp-config-lookup.ts","../src/lib/connectivity-probe-context.ts","../src/lib/mcp-stdio-probe.ts","../src/lib/cli-probe.ts","../src/lib/session-tool-probe.ts","../src/lib/session-tool-bind-runner.ts","../src/lib/session-tool-bind-probe-host.ts","../../../packages/core/src/provisioning/provisioner.ts","../src/lib/self-update-pin.ts","../src/commands/manager.ts","../src/lib/watchdog.ts","../src/lib/output.ts","../src/lib/connectivity-probe-executor.ts"],"sourcesContent":["// ENG-4787: defensive guards for `.mcp.json` writes by the manager and\n// provisioning writers. ENG-4744 forced an operator workaround\n// (chattr +i on the agent's .mcp.json) because the manager's 10s\n// integration-sync tick was overwriting the file with a broken\n// cloud-broker entry — literal `${AGT_API_KEY}` placeholders and a\n// missing `AGT_AGENT_ID`. The underlying writer bug was fixed in\n// ENG-4739, but the manager had no guardrails against the *next*\n// writer regression.\n//\n// This module provides two layers:\n// 1. `validateRenderedMcpConfig` — a pure function that catches\n// unexpanded ${...} placeholders, missing required env keys per\n// MCP server, and JSON-shape failures. Cheap to call before any\n// write.\n// 2. `safeWriteJsonAtomic` — a writer that does a temp-file +\n// rename dance with a single .bak snapshot. Recovery from a bad\n// write becomes `mv file.bak file` instead of an SSH session.\n//\n// Both are pure / fs-isolated so the rest of the provisioning code\n// can be tested without an integration-tick fixture.\n\nimport {\n chmodSync,\n existsSync,\n readFileSync,\n renameSync,\n writeFileSync,\n unlinkSync,\n} from 'node:fs';\n\n// ENG-5901 (ADR-0018 Phase 1): `.mcp.json` carries `${VAR}` placeholders\n// whose raw values live in `.env.integrations` (mode 0600). The file\n// itself should be owner-only too — symmetric with `.env.integrations`\n// and a defence-in-depth step against a co-located reader. Audit run\n// 2026-06-02 found it world-readable (0644).\nexport const MCP_FILE_MODE = 0o600;\n\n// ENG-5901 Track D: value-shape lint for literal secrets. Type-only\n// imports flow the OTHER way (mcp-secret-lint imports McpConfig types\n// from here), so this value import creates no runtime cycle.\nimport {\n scanConfigForLiteralSecrets,\n formatLiteralSecretRejection,\n} from './mcp-secret-lint.js';\n\n// ENG-5901 PR 3: last logged rejection fingerprint per .mcp.json path —\n// the dedupe memory for the armed lint's stderr lines (see\n// safeWriteMcpJson). Process-lifetime state is intentional: the manager\n// is long-lived and the goal is exactly to stop per-tick repeats.\nconst lastRejectionFingerprintByPath = new Map<string, string>();\n\n// ── Validator ─────────────────────────────────────────────────────────────\n\nexport interface McpServerEntry {\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n // ENG-4806/etc forward-compat — http transports etc. live alongside\n // command-based stdio. Validator ignores fields it doesn't recognise.\n type?: string;\n url?: string;\n headers?: Record<string, string>;\n}\n\nexport interface McpConfig {\n mcpServers?: Record<string, McpServerEntry>;\n}\n\nexport type McpValidationFailure =\n | 'unexpanded_placeholder'\n | 'missing_required_env'\n | 'invalid_json_shape'\n // ENG-5074: URL-based entries (Streamable HTTP / SSE remote MCPs)\n // require a `type` field per Claude Code's MCP schema. Pre-fix the\n // writer omitted it and the sanitizer's preserve-headers path also\n // didn't add one, leading to claude rejecting the config at startup\n // and the agent's tmux session looping forever. Surface the missing\n // field at write time so the bug can't ship to the host again.\n | 'remote_mcp_missing_type'\n // ENG-5901 Track D: a literal credential shape (xoxb-/xapp-/tlk_/ak_/\n // Telegram/JWT) in an env value or HTTP header value. Every secret in\n // .mcp.json must be a `${VAR}` template with the raw value in\n // .env.integrations; a literal here is a writer regression and the\n // write is rejected (previous good file stays in place).\n | 'literal_secret';\n\nexport interface McpValidationError {\n kind: McpValidationFailure;\n /** Server key in `mcpServers` (e.g. 'cloud-broker'); '*' if structural. */\n server: string;\n /** Human-readable message safe to log. */\n message: string;\n}\n\nexport type McpValidationResult =\n | { ok: true }\n | { ok: false; errors: McpValidationError[] };\n\n/**\n * Required env keys per known MCP server with their substitution\n * contract:\n *\n * mustBeConcrete: true — value is baked at provision time (e.g.\n * AGT_AGENT_ID is the agent's literal UUID); a `${...}`\n * placeholder here is the writer-regression shape ENG-4744 hit.\n * mustBeConcrete: false — value is intentionally a `${VAR}`\n * placeholder that Claude / the manager substitutes at MCP-\n * launch time (AGT_HOST, AGT_API_KEY follow this pattern; see\n * packages/core/src/provisioning/frameworks/claudecode/index.ts\n * ~line 1217).\n *\n * Keep the list small — only servers where a missing or wrongly-shaped\n * key produces broken runtime behaviour.\n */\ninterface RequiredEnvRule {\n key: string;\n mustBeConcrete: boolean;\n}\n\nconst REQUIRED_ENV_RULES_BY_SERVER: Readonly<Record<string, readonly RequiredEnvRule[]>> = {\n 'cloud-broker': [\n { key: 'AGT_HOST', mustBeConcrete: false },\n { key: 'AGT_API_KEY', mustBeConcrete: false },\n // ENG-4744: this is the bug shape — writer used to omit this\n // entirely, or render it as a literal `${AGT_AGENT_ID}` instead\n // of the agent's real UUID. The broker has no way to substitute\n // it post-spawn, so a placeholder here = silently broken agent.\n { key: 'AGT_AGENT_ID', mustBeConcrete: true },\n ],\n};\n\nconst PLACEHOLDER_RE = /\\$\\{[^}]+\\}/;\n\n/**\n * Validate a rendered `.mcp.json` config object before it's written.\n *\n * Catches the three failure modes that produced ENG-4744 and similar:\n * - Unexpanded `${VAR}` placeholders left in env values (writer bug\n * where the env var wasn't substituted before serialising).\n * - Missing required env keys for known MCP servers.\n * - Invalid JSON shape (mcpServers is not an object).\n *\n * Returns structured errors so callers can pick a single-line log\n * message; never throws.\n */\nexport function validateRenderedMcpConfig(config: unknown): McpValidationResult {\n const errors: McpValidationError[] = [];\n\n if (typeof config !== 'object' || config === null || Array.isArray(config)) {\n // CodeRabbit ENG-4787: arrays pass `typeof === 'object'`, so\n // without the Array.isArray guard `[]` would slip through and\n // then hit the `root.mcpServers === undefined` early return\n // (arrays don't have an `mcpServers` key) — accepting an array\n // root as a valid config.\n return {\n ok: false,\n errors: [\n {\n kind: 'invalid_json_shape',\n server: '*',\n message: 'config root must be a non-null object',\n },\n ],\n };\n }\n\n const root = config as { mcpServers?: unknown };\n if (root.mcpServers === undefined) {\n // Empty config (no servers) is valid — agents legitimately ship\n // without any MCP servers configured.\n return { ok: true };\n }\n if (typeof root.mcpServers !== 'object' || root.mcpServers === null) {\n return {\n ok: false,\n errors: [\n {\n kind: 'invalid_json_shape',\n server: '*',\n message: 'mcpServers must be an object',\n },\n ],\n };\n }\n\n if (Array.isArray(root.mcpServers)) {\n return {\n ok: false,\n errors: [\n {\n kind: 'invalid_json_shape',\n server: '*',\n message: 'mcpServers must be an object',\n },\n ],\n };\n }\n\n for (const [serverKey, raw] of Object.entries(root.mcpServers)) {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n errors.push({\n kind: 'invalid_json_shape',\n server: serverKey,\n message: `entry must be an object`,\n });\n continue;\n }\n const entry = raw as McpServerEntry;\n\n // ENG-5074: URL-based remote MCPs require a `type` field per Claude\n // Code's MCP schema ('http' for Streamable HTTP, 'sse' for legacy\n // Server-Sent Events). The writer + sanitizer were both shipping\n // url+headers without a type, and claude rejected the whole config\n // at startup. Catch it here at write time instead of letting it\n // ride to the host. Stdio entries (command-based, no url) skip\n // this check entirely.\n const entryRecord = entry as Record<string, unknown>;\n if (typeof entryRecord['url'] === 'string') {\n const type = entryRecord['type'];\n if (type !== 'http' && type !== 'sse') {\n errors.push({\n kind: 'remote_mcp_missing_type',\n server: serverKey,\n message: `url-based entry must include \\`type: \"http\"\\` or \\`type: \"sse\"\\` (Claude Code MCP schema requires it)`,\n });\n }\n }\n\n // Required env keys for known servers per their substitution\n // contract (see REQUIRED_ENV_RULES_BY_SERVER). For mustBeConcrete\n // keys, a `${...}` placeholder is the ENG-4744 writer-regression\n // shape and we reject. For non-concrete keys, a placeholder is\n // legitimate (Claude / manager substitutes at exec time).\n const rules = REQUIRED_ENV_RULES_BY_SERVER[serverKey];\n if (rules) {\n const env = (entry.env ?? {}) as Record<string, string>;\n for (const rule of rules) {\n const value = env[rule.key];\n if (typeof value !== 'string' || value.length === 0) {\n errors.push({\n kind: 'missing_required_env',\n server: serverKey,\n message: `missing required env key: ${rule.key}`,\n });\n continue;\n }\n if (rule.mustBeConcrete && PLACEHOLDER_RE.test(value)) {\n errors.push({\n kind: 'unexpanded_placeholder',\n server: serverKey,\n message: `env.${rule.key} contains an unexpanded \\${...} placeholder; expected a concrete value`,\n });\n }\n }\n }\n }\n\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n/** Format the error list into a single log line for `manager.log`. */\nexport function formatValidationErrors(errors: McpValidationError[]): string {\n return errors.map((e) => `${e.server}:${e.kind}=${e.message}`).join('; ');\n}\n\n// ── Atomic writer ─────────────────────────────────────────────────────────\n\nexport interface AtomicWriteOptions {\n /** When true (default) write a `<path>.bak` snapshot of the existing\n * file before swapping in the new one. Disable for paths where the\n * caller manages backups itself. */\n keepBackup?: boolean;\n /**\n * Override for the rename syscall, used by the rollback regression\n * test (vitest can't spy on `node:fs` ESM exports). Production\n * callers should leave this unset — it defaults to `node:fs`'s\n * synchronous `renameSync`.\n */\n renamer?: (src: string, dest: string) => void;\n /**\n * ENG-5901: when set, chmod the temp file to this mode *before* the\n * rename, so the installed file lands with the right permissions\n * atomically. If the chmod fails the whole write aborts and rolls\n * back — a secret file with the wrong mode must never ship.\n */\n mode?: number;\n}\n\n/**\n * Atomically write `content` to `path` via temp-file + rename. Mirrors\n * the pattern already used for `daily-session.json.bak*`:\n *\n * 1. Write to <path>.new (same directory, same volume → rename is\n * guaranteed atomic on POSIX).\n * 2. Rename existing <path> → <path>.bak (overwriting any prior\n * .bak — single-slot snapshot for cheap recovery).\n * 3. Rename <path>.new → <path>.\n *\n * If any step fails the partial files are cleaned up so a half-\n * written state can't survive on disk.\n */\nexport function safeWriteJsonAtomic(\n path: string,\n content: string,\n opts: AtomicWriteOptions = {},\n): void {\n const keepBackup = opts.keepBackup !== false;\n const rename = opts.renamer ?? renameSync;\n const tmpPath = `${path}.new`;\n const bakPath = `${path}.bak`;\n // CodeRabbit follow-up: track whether we've already moved the\n // original aside so the catch path can roll back. Without this,\n // a failure between the two renames would leave `path` missing\n // entirely (original moved to .bak, new file never installed) —\n // worse than a stale write because consumers can't even read.\n let movedOriginalToBackup = false;\n\n try {\n writeFileSync(tmpPath, content);\n // ENG-5901: set the final mode on the temp file before it's renamed\n // into place, so the install is atomic w.r.t. permissions too.\n if (opts.mode !== undefined) {\n chmodSync(tmpPath, opts.mode);\n }\n } catch (err) {\n // Couldn't write (or chmod) the temp file — clean up any partial\n // temp and bail without touching the original.\n try {\n if (existsSync(tmpPath)) unlinkSync(tmpPath);\n } catch {\n /* best-effort */\n }\n throw err;\n }\n\n try {\n if (keepBackup && existsSync(path)) {\n // overwriting the prior .bak is intentional — single-slot.\n rename(path, bakPath);\n movedOriginalToBackup = true;\n }\n rename(tmpPath, path);\n } catch (err) {\n // Clean up the orphan temp file so we don't leave debris.\n try {\n if (existsSync(tmpPath)) unlinkSync(tmpPath);\n } catch {\n /* best-effort */\n }\n // Roll back to last-known-good when we already moved the\n // original aside but failed to install the new file. Best-\n // effort: if the rollback itself fails, .bak still has the\n // previous content for manual recovery. Uses the real\n // renameSync (not the injected `rename`) so a test that throws\n // on rename for fault injection can still rely on the rollback\n // path running.\n if (movedOriginalToBackup && !existsSync(path) && existsSync(bakPath)) {\n try {\n renameSync(bakPath, path);\n } catch {\n /* best-effort */\n }\n }\n throw err;\n }\n}\n\n/** Read the most recent `.bak` snapshot for a path, if any. */\nexport function readBackup(path: string): string | null {\n const bakPath = `${path}.bak`;\n if (!existsSync(bakPath)) return null;\n try {\n return readFileSync(bakPath, 'utf-8');\n } catch {\n return null;\n }\n}\n\n// ── Combined entry point ──────────────────────────────────────────────────\n\nexport interface SafeWriteMcpResult {\n written: boolean;\n /** Validation errors when written=false. Empty array on success. */\n errors: McpValidationError[];\n}\n\n/**\n * Validate `config` and atomically write to `path` if validation\n * passes. Returns `{ written: false, errors }` on failure so callers\n * can log a single structured line and skip rather than corrupting\n * the existing file.\n *\n * This is the recommended entry point for all `.mcp.json` writers in\n * the manager's integration-sync loop; the previous `writeFileSync`\n * pattern at every callsite is the regression surface ENG-4744 hit.\n */\nexport function safeWriteMcpJson(\n path: string,\n config: McpConfig | unknown,\n): SafeWriteMcpResult {\n const validation = validateRenderedMcpConfig(config);\n if (!validation.ok) {\n return { written: false, errors: validation.errors };\n }\n // ENG-5901 Track D: literal-secret lint, armed in the same change that\n // converted every writer to `${VAR}` templates (arming it earlier would\n // have rejected the writers' own literal output). A match rejects the\n // write — the previous good file stays — and emits one structured,\n // secret-free stderr line per finding (the contributor-side gate: a\n // future copy-paste of the old literal pattern fails at provision time,\n // not in production).\n //\n // ENG-5901 PR 3: per-path log dedupe. A hot writer loop re-attempting\n // the same rejected write was producing ~50% of manager.log on\n // agt-aws-1 — keep rejecting every attempt, but only LOG when the\n // finding fingerprint for this path changes (and clear the memory on a\n // clean write so a relapse logs again).\n const secretFindings = scanConfigForLiteralSecrets(config);\n if (secretFindings.length > 0) {\n const fingerprint = secretFindings\n .map((f) => `${f.server}.${f.field}.${f.location}`)\n .sort()\n .join('|');\n if (lastRejectionFingerprintByPath.get(path) !== fingerprint) {\n lastRejectionFingerprintByPath.set(path, fingerprint);\n for (const f of secretFindings) {\n process.stderr.write(`${formatLiteralSecretRejection(f)}\\n`);\n }\n }\n return {\n written: false,\n errors: secretFindings.map((f) => ({\n kind: 'literal_secret' as const,\n server: f.server,\n message: `literal secret in ${f.location} field '${f.field}' (pattern ${f.pattern}); expected a \\${VAR} template`,\n })),\n };\n }\n lastRejectionFingerprintByPath.delete(path);\n // ENG-5901: `.mcp.json` is a secret-bearing file (even post-templating\n // it carries the `${VAR}` contract and historically held literals) —\n // install it owner-only, atomically.\n safeWriteJsonAtomic(path, JSON.stringify(config, null, 2), { mode: MCP_FILE_MODE });\n return { written: true, errors: [] };\n}\n\n// ── ENG-5901: provision ⇄ project mirror parity ────────────────────────────\n//\n// `syncMcpToProject` copies `provision/.mcp.json` to `project/.mcp.json`\n// verbatim. If a future change ever transforms content on the way (or a\n// writer mutates one mirror without the other), the two could diverge on\n// a secret-bearing field — the exact drift class ENG-4793 lineage warns\n// about. This pure helper compares the secret-bearing fields of two\n// rendered configs so the sync chokepoint (and tests) can assert parity.\n\n/** Field-name shapes that carry a secret (env keys / header names). */\nconst SECRET_FIELD_NAME_RE = /TOKEN|KEY|SECRET|BEARER|PASSWORD/i;\n\nexport interface McpMirrorMismatch {\n server: string;\n field: string;\n location: 'env' | 'header';\n reason: 'missing-in-project' | 'missing-in-provision' | 'value-diverges';\n}\n\nfunction collectSecretFields(\n config: unknown,\n): Map<string, { location: 'env' | 'header'; value: string }> {\n const out = new Map<string, { location: 'env' | 'header'; value: string }>();\n if (typeof config !== 'object' || config === null) return out;\n const servers = (config as McpConfig).mcpServers;\n if (typeof servers !== 'object' || servers === null) return out;\n for (const [server, raw] of Object.entries(servers)) {\n if (typeof raw !== 'object' || raw === null) continue;\n const entry = raw as McpServerEntry;\n for (const [block, location] of [\n [entry.env, 'env'],\n [entry.headers, 'header'],\n ] as Array<[Record<string, unknown> | undefined, 'env' | 'header']>) {\n if (!block) continue;\n for (const [field, value] of Object.entries(block)) {\n if (typeof value !== 'string') continue;\n if (!SECRET_FIELD_NAME_RE.test(field)) continue;\n out.set(`${server}\u0000${field}`, { location, value });\n }\n }\n }\n return out;\n}\n\n/**\n * Compare the secret-bearing fields of the provision and project mirrors.\n * Returns an empty array when they agree. Pure — no I/O.\n */\nexport function mcpMirrorParityErrors(\n provision: unknown,\n project: unknown,\n): McpMirrorMismatch[] {\n const a = collectSecretFields(provision);\n const b = collectSecretFields(project);\n const mismatches: McpMirrorMismatch[] = [];\n const keys = new Set<string>([...a.keys(), ...b.keys()]);\n for (const key of keys) {\n const [server, field] = key.split('\u0000') as [string, string];\n const pv = a.get(key);\n const pj = b.get(key);\n if (pv && !pj) {\n mismatches.push({ server, field, location: pv.location, reason: 'missing-in-project' });\n } else if (!pv && pj) {\n mismatches.push({ server, field, location: pj.location, reason: 'missing-in-provision' });\n } else if (pv && pj && pv.value !== pj.value) {\n mismatches.push({ server, field, location: pv.location, reason: 'value-diverges' });\n }\n }\n return mismatches;\n}\n\n/** Secret-free structured log line for a mirror mismatch. */\nexport function formatMirrorMismatch(m: McpMirrorMismatch): string {\n return `[mcp-mirror] [parity-violation] server=${m.server} field=${m.field} location=${m.location} reason=${m.reason}`;\n}\n","// ENG-5901 (Phase 1 of ADR-0018): runtime lint that rejects *literal*\n// secrets in a rendered `.mcp.json` before it is written to disk.\n//\n// Why this exists\n// ---------------\n// The audit run on 2026-06-02 found `.mcp.json` carrying plaintext\n// SLACK_BOT_TOKEN / SLACK_APP_TOKEN / TELEGRAM_BOT_TOKEN / AGT_API_KEY /\n// Composio `x-api-key` values — the same secrets that should be `${VAR}`\n// placeholders resolved at MCP-launch time from `.env.integrations`\n// (see ADR-0006 §D2 and the channel/Composio templating in this PR).\n//\n// This module is the *contributor-side* gate: once the writers in\n// `claudecode/index.ts` emit `${VAR}` templates, a future copy-paste of\n// the old literal pattern is caught at write time (provision/sync) rather\n// than shipping to a host and leaking. It scans both stdio `env` blocks\n// and `http`/`sse`/`ws` `headers` objects, because Claude Code substitutes\n// `${VAR}` in both (verified against the Claude Code MCP docs — expansion\n// locations: command, args, env, url, headers).\n//\n// IMPORTANT — rollout coupling: arming this scan in the write path\n// (`safeWriteMcpJson`) while any writer still emits a literal token would\n// reject that write and silently kill the channel. The scan must be wired\n// in *lockstep* with converting the literal writers to `${VAR}`. This\n// module is pure (no fs, no process) so it can be unit-tested and reasoned\n// about independently of that wiring.\n//\n// A templated value (`${SLACK_BOT_TOKEN}`) never matches these patterns,\n// so legitimate post-conversion configs pass cleanly.\n\nimport type { McpConfig, McpServerEntry } from './mcp-config-guards.js';\n\n/**\n * Value-shape patterns for credentials that must never appear literally in\n * `.mcp.json`. Anchored at the start of the value so a `${VAR}` template\n * (which begins with `$`) can't match.\n *\n * Extending this list is the intended way to cover new credential shapes —\n * keep each entry documented with the provider it guards.\n */\nexport const LITERAL_SECRET_PATTERNS: ReadonlyArray<{\n readonly name: string;\n readonly re: RegExp;\n}> = [\n // Slack bot token — `xoxb-<workspace>-<...>`\n { name: 'slack_bot_token', re: /^xoxb-/ },\n // Slack app-level token (Socket Mode) — `xapp-<...>`\n { name: 'slack_app_token', re: /^xapp-/ },\n // AGT host API key — `tlk_<...>` (see claudecode-plugin-augmented README).\n { name: 'agt_host_api_key', re: /^tlk_/ },\n // Composio / generic api-key prefix — `ak_<...>`\n { name: 'composio_api_key', re: /^ak_/ },\n // Telegram bot token — `<bot id>:AA<...>` (BotFather format). ENG-5901\n // PR 4: the original `\\d{10}:AAE` (from the issue AC) was too narrow —\n // live tokens on agt-aws-1 carry `AA` + a varying third character\n // (don/scout/stirling all had AA-not-E tokens the lint and migration\n // missed). Bot ids are 8–12 digits; the token part always starts `AA`.\n { name: 'telegram_bot_token', re: /^\\d{8,12}:AA[A-Za-z0-9_-]/ },\n // ENG-5901 extension beyond the original AC's five patterns: a literal\n // JWT (`eyJ...`) is the shape of a leaked AGT_API_KEY, which the\n // value-prefix patterns above would otherwise miss. Header values often\n // carry it behind `Bearer ` (or a copy-pasted `Authorization: Bearer `),\n // so those prefixes are optionally consumed (CodeRabbit #1731).\n // Templates (`Bearer ${AGT_API_KEY}`) and concrete non-secret values\n // (UUIDs, hosts) never put `eyJ` after the prefix, so this stays\n // false-positive-safe inside .mcp.json.\n { name: 'jwt_agt_api_key', re: /^(?:authorization:\\s*)?(?:bearer\\s+)?eyJ[A-Za-z0-9_-]+\\./i },\n];\n\nexport type LiteralSecretLocation = 'env' | 'header';\n\nexport interface LiteralSecretFinding {\n /** Server key in `mcpServers` (e.g. 'slack', 'composio_googledrive'). */\n readonly server: string;\n /** The offending env key or header name (e.g. 'SLACK_BOT_TOKEN'). */\n readonly field: string;\n /** Whether the literal was found in an `env` value or a `headers` value. */\n readonly location: LiteralSecretLocation;\n /** Which pattern matched (from {@link LITERAL_SECRET_PATTERNS}). */\n readonly pattern: string;\n}\n\n/** Match a single value against every known literal-secret pattern. */\nfunction matchLiteralSecret(value: string): string | null {\n for (const { name, re } of LITERAL_SECRET_PATTERNS) {\n if (re.test(value)) return name;\n }\n return null;\n}\n\nfunction scanRecord(\n server: string,\n record: Record<string, unknown> | undefined,\n location: LiteralSecretLocation,\n findings: LiteralSecretFinding[],\n): void {\n if (!record) return;\n for (const [field, value] of Object.entries(record)) {\n if (typeof value !== 'string') continue;\n const pattern = matchLiteralSecret(value);\n if (pattern) findings.push({ server, field, location, pattern });\n }\n}\n\n/**\n * Scan a rendered `.mcp.json` config for literal secrets in any server's\n * `env` block or `headers` object. Pure: returns findings, logs nothing,\n * throws nothing. An empty array means the config is clean.\n */\nexport function scanConfigForLiteralSecrets(\n config: McpConfig | unknown,\n): LiteralSecretFinding[] {\n const findings: LiteralSecretFinding[] = [];\n if (typeof config !== 'object' || config === null) return findings;\n const servers = (config as McpConfig).mcpServers;\n if (typeof servers !== 'object' || servers === null) return findings;\n\n for (const [server, raw] of Object.entries(servers)) {\n if (typeof raw !== 'object' || raw === null) continue;\n const entry = raw as McpServerEntry;\n scanRecord(server, entry.env, 'env', findings);\n scanRecord(server, entry.headers, 'header', findings);\n }\n return findings;\n}\n\n/**\n * The structured, secret-free stderr line emitted when a write is\n * rejected. Names the field and server but NEVER the value — per the\n * \"never expose secrets in output\" rule. Stable, greppable format so\n * operators can alert on it.\n */\nexport function formatLiteralSecretRejection(f: LiteralSecretFinding): string {\n return `[mcp-write] [literal-secret-rejected] field=${f.field} server=${f.server} location=${f.location} pattern=${f.pattern}`;\n}\n","/**\n * Build a PATH for plugin install-hook execution that includes the standard\n * Homebrew + system bin directories. The manager process is often spawned\n * by cloud-init under a minimal PATH that omits `/home/linuxbrew/.linuxbrew/bin`,\n * so a hook script that calls `npm`, `npx`, `qmd`, `xurl`, etc. exits 127\n * (\"command not found\") even though the binaries are installed.\n *\n * Order:\n * 1. The current process PATH (operator overrides win)\n * 2. Linuxbrew prefix (EC2 / Linux hosts)\n * 3. macOS Apple-silicon brew prefix\n * 4. Intel macOS / generic /usr/local\n * 5. Standard system bins, in case the inherited PATH was empty.\n *\n * Callers should pass `process.env.PATH` so an existing operator-augmented\n * PATH is preserved at the front.\n */\nexport function augmentedHookPath(currentPath: string | undefined): string {\n const extras = [\n '/home/linuxbrew/.linuxbrew/bin',\n '/opt/homebrew/bin',\n '/usr/local/bin',\n '/usr/bin',\n '/bin',\n ];\n const seen = new Set<string>();\n const parts: string[] = [];\n const push = (p: string): void => {\n if (!p || seen.has(p)) return;\n seen.add(p);\n parts.push(p);\n };\n for (const p of (currentPath ?? '').split(':')) push(p);\n for (const p of extras) push(p);\n return parts.join(':');\n}\n\n/**\n * When a hook exits 127, bash itself prints `bash: line N: <cmd>: command not found`\n * to stderr before any user code runs, so the message does not contain\n * secrets the script may have echoed. Extract the first such line so logs\n * can show the missing binary without leaking the rest of stderr.\n *\n * Returns null if no recognisable not-found line is present (e.g. the\n * script returned 127 by itself for some other reason).\n */\nexport function extractCommandNotFound(stderr: string): string | null {\n if (!stderr) return null;\n // Restrict to shells we actually invoke (`bash -c`/`sh -c`) so an attacker\n // can't craft a fake `evil-shell: foo: command not found` line in script\n // stdout that gets reflected through to logs. The captured token is logged\n // raw, so validate it is a basename-style command name: starts with a\n // letter, charset limited to alnum + `._-`, max 64 chars. This keeps the\n // leakage surface tight even if the regex above lets something unexpected\n // through.\n const SAFE_CMD = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;\n for (const line of stderr.split(/\\r?\\n/)) {\n const m = line.match(/^(?:bash|sh): (?:line \\d+: )?([^:\\s]+): command not found$/);\n if (m?.[1]) {\n // Defensive: strip any path prefix before validating, so a future\n // change to the line regex can't accidentally let a full path leak.\n const rawCmd = m[1].trim();\n const cmd = rawCmd.split('/').pop() ?? rawCmd;\n if (SAFE_CMD.test(cmd)) return cmd;\n }\n }\n return null;\n}\n","// ENG-5901 Track D (ADR-0018 Phase 1): pure content model for the\n// `.env.integrations` file, shared by its two writers.\n//\n// Why this exists\n// ---------------\n// `.env.integrations` historically had ONE writer (`writeIntegrations`),\n// which rebuilt the whole file from the integration list each tick —\n// full-overwrite semantics were how stale keys of disconnected\n// integrations got pruned. Track D adds a SECOND writer: the channel\n// credential path now stores raw channel tokens here (templated as\n// `${VAR}` in `.mcp.json`). Two writers with full-overwrite semantics\n// clobber each other's keys — the channel tick would wipe integration\n// tokens and vice versa, killing whichever MCP loses the race.\n//\n// The fix is explicit key ownership, expressed as two merge modes over\n// the same parsed model:\n//\n// - 'upsert' (channel writer): overwrite ONLY the given keys,\n// preserve every other existing line.\n// - 'replace-preserving' (writeIntegrations): rebuild from the given\n// keys (so disconnected-integration pruning still works), carrying\n// over ONLY the channel-owned keys from the existing file.\n//\n// Pure functions, no fs — the claudecode adapter owns the read/write\n// (agent dir + project mirror, both SECRET_FILE_MODE 0600).\n\n/**\n * Wrap a value in single quotes and escape any embedded single quotes\n * using the bash idiom `'\\''`. Safe for `source`-d shell files: bash\n * never interprets metacharacters inside single-quoted strings, so a\n * value like `$(rm -rf /)` becomes a literal string instead of being\n * executed.\n *\n * Lives here (not in the claudecode adapter) since ENG-5901 Track D so\n * the env-file content model has no import cycle with the adapter; the\n * adapter re-exports it for back-compat.\n */\nexport function shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Channel-owned secret keys. The channel credential writer upserts\n * these; `writeIntegrations`' rebuild carries them over. Extend this\n * list when a new channel gains a secret env var — forgetting to do so\n * means the next integration tick wipes the new channel's token.\n */\nexport const CHANNEL_SECRET_ENV_KEYS: readonly string[] = [\n 'SLACK_BOT_TOKEN',\n 'SLACK_APP_TOKEN',\n 'TELEGRAM_BOT_TOKEN',\n 'MSTEAMS_CLIENT_SECRET',\n];\n\n/**\n * Secrets hoisted out of url-MCP server entries by `writeMcpServer`\n * (Composio `x-api-key`, Pipedream client secret). Like channel keys,\n * they are upserted outside `writeIntegrations`' rebuild and must be\n * carried over by 'replace-preserving' or the next integration tick\n * wipes them and the templated header/env substitutes to nothing.\n */\nexport const MCP_SERVER_SECRET_ENV_KEYS: readonly string[] = [\n 'COMPOSIO_API_KEY',\n 'PIPEDREAM_CLIENT_SECRET',\n];\n\n/** Default carry-over set for 'replace-preserving' merges. */\nexport const PRESERVED_ENV_KEYS: readonly string[] = [\n ...CHANNEL_SECRET_ENV_KEYS,\n ...MCP_SERVER_SECRET_ENV_KEYS,\n];\n\nconst HEADER = '# Augmented integrations — auto-generated, do not edit';\n\n/**\n * Parse `.env.integrations` content into an ordered map of\n * key → rendered value text (still shellQuoted exactly as on disk —\n * carrying lines over must not re-quote them). Comments/blank lines are\n * dropped; the canonical header is re-added on render.\n */\nexport function parseEnvFileEntries(content: string): Map<string, string> {\n const out = new Map<string, string>();\n for (const line of content.split('\\n')) {\n if (!line || line.startsWith('#') || !line.includes('=')) continue;\n const eqIdx = line.indexOf('=');\n out.set(line.slice(0, eqIdx), line.slice(eqIdx + 1));\n }\n return out;\n}\n\n/** Render the canonical file: header + one KEY=<rendered> line each. */\nexport function renderEnvIntegrations(entries: Map<string, string>): string {\n const lines = [HEADER];\n for (const [key, rendered] of entries) lines.push(`${key}=${rendered}`);\n return lines.join('\\n') + '\\n';\n}\n\nexport interface MergeEnvIntegrationsArgs {\n /**\n * 'upsert': overwrite only `updates` keys, keep everything else\n * (channel credential writer).\n * 'replace-preserving': rebuild from `updates`, carrying over only\n * `preserveKeys` from the existing content (writeIntegrations —\n * keeps its stale-key pruning while sparing channel tokens).\n */\n mode: 'upsert' | 'replace-preserving';\n /**\n * RAW (unquoted) values — shellQuoted on render. A `null` value is an\n * explicit DELETE (CodeRabbit #1745): removal paths use it to evict a\n * writer-owned secret on disconnect; without it, preserved keys would\n * be carried forward indefinitely. Deletion wins over `preserveKeys`.\n */\n updates: Record<string, string | null>;\n /** Keys carried over verbatim in 'replace-preserving' mode.\n * Defaults to {@link PRESERVED_ENV_KEYS} (channel + hoisted url-MCP\n * secrets). */\n preserveKeys?: readonly string[];\n}\n\n/**\n * Merge new entries into existing `.env.integrations` content and\n * return the full new file body. `existing` is null when the file\n * doesn't exist yet.\n */\nexport function mergeEnvIntegrationsContent(\n existing: string | null,\n args: MergeEnvIntegrationsArgs,\n): string {\n const current = existing === null ? new Map<string, string>() : parseEnvFileEntries(existing);\n\n let next: Map<string, string>;\n if (args.mode === 'upsert') {\n next = new Map(current);\n for (const [key, raw] of Object.entries(args.updates)) {\n if (raw === null) next.delete(key);\n else next.set(key, shellQuote(raw));\n }\n } else {\n next = new Map<string, string>();\n for (const [key, raw] of Object.entries(args.updates)) {\n if (raw !== null) next.set(key, shellQuote(raw));\n }\n const preserve = args.preserveKeys ?? PRESERVED_ENV_KEYS;\n for (const key of preserve) {\n // An explicitly-addressed key (set OR null-deleted) is never\n // resurrected by preserve; otherwise carry the existing rendered\n // value forward.\n if (key in args.updates) continue;\n if (!next.has(key) && current.has(key)) {\n next.set(key, current.get(key)!);\n }\n }\n }\n return renderEnvIntegrations(next);\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, readdirSync, rmSync, copyFileSync, lstatSync, realpathSync, symlinkSync, readlinkSync, renameSync } from 'node:fs';\nimport { join, relative, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execFile } from 'node:child_process';\n// ENG-4787: validate + atomic-write `.mcp.json` to prevent\n// ENG-4744-class regressions where a writer bug overwrites the file\n// with broken content (unexpanded ${...}, missing required env keys).\nimport {\n safeWriteMcpJson,\n formatValidationErrors,\n mcpMirrorParityErrors,\n formatMirrorMismatch,\n MCP_FILE_MODE,\n} from '../../mcp-config-guards.js';\nimport type { FrameworkAdapter, AuthProfileInput, ProvisionArtifact, PluginHookContext, PluginHookResult } from '../../framework-adapter.js';\nimport type { ScheduledTaskRow } from '../../../types/scheduled-task.js';\nimport { wrapScheduledTaskPrompt } from '../../../scheduled-tasks/prompt-wrapper.js';\nimport type { ResolvedIntegration } from '../../../types/integration.js';\nimport type { CapabilitySkillFile } from '../../../types/capability.js';\nimport { registerFramework } from '../../framework-registry.js';\nimport { mcpWildcardsForServers } from '../../mcp-tool-patterns.js';\nimport { resolveAvatarEnvUrl } from '../../avatar-env.js';\nimport type { ProvisionInput } from '../../types.js';\nimport {\n generateClaudeMd,\n buildIntegrationsSection,\n estimateActiveTasksTokens,\n INTEGRATIONS_SECTION_START,\n INTEGRATIONS_SECTION_END,\n type IntegrationSummary,\n} from './identity.js';\n\n// ENG-5794: re-export the sentinel constants so the manager (which imports\n// from the published `@augmented/core/provisioning/frameworks/claudecode/index.js`\n// subpath) can target the same range the side-effect writer uses, without\n// reaching into a deeper subpath that isn't published in package.json\n// exports.\nexport { INTEGRATIONS_SECTION_START, INTEGRATIONS_SECTION_END };\n\n// ENG-5380: re-export so the manager (which imports from\n// `@augmented/core/provisioning/frameworks/claudecode/index.js`) can log\n// the rendered token cost per refresh without depending on a deeper\n// subpath that isn't published in package.json exports.\nexport { estimateActiveTasksTokens };\nimport { INTEGRATION_REGISTRY } from '../../../integrations/registry.js';\nimport { writeXurlStoreForIntegrations } from '../../../integrations/xurl-config.js';\nimport { decryptIntegrationCredentials } from '../../../crypto/integration-credentials.js';\n// ENG-8440: buildHostBrokeredRemoteMcpEntry is no longer imported here —\n// Higgsfield was its last caller in this adapter (Vercel left in ENG-8421).\n// The helper itself is retained in remote-mcp.ts for any future host-brokered\n// remote; see the host_oauth decision on ENG-8440 before adding one.\nimport { buildRemoteMcpEntry, buildOAuthRemoteMcpProxyEntry, buildLiveHeaderRemoteMcpProxyEntry } from '../../remote-mcp.js';\nimport { buildNativeMcpEntry } from '../../native-mcp.js';\nimport { OAUTH_PROVIDERS } from '../../../integrations/oauth-providers.js';\n// ENG-8359: the per-connection naming rule, shared with the host-side probe +\n// orphan reaper so a named connection is written, probed and reaped under one key.\nimport {\n remoteMcpServerKey,\n remoteMcpEnvPrefix,\n remoteMcpConnectionScopedEnvVar,\n} from '../../../integrations/remote-mcp-connection.js';\nimport { augmentedHookPath } from '../../hook-env.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst VALID_CODE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SECRET_FILE_MODE = 0o600;\n\n// ENG-5901 Track D: shellQuote moved to env-integrations-file.ts (the\n// .env.integrations content model needs it without an import cycle back\n// into this adapter). Imported for local use + re-exported for\n// back-compat with existing importers; used when writing `.env*` files\n// that get shell-sourced by the persistent-session wrapper (ENG-4717).\nimport {\n shellQuote,\n mergeEnvIntegrationsContent,\n parseEnvFileEntries,\n type MergeEnvIntegrationsArgs,\n} from '../../env-integrations-file.js';\nimport { scanConfigForLiteralSecrets } from '../../mcp-secret-lint.js';\nimport {\n BROKER_SCRIPT_MODE,\n GH_SHIM_BASENAME,\n GITHUB_BROKER_BIN_DIR,\n GIT_CREDENTIAL_HELPER_BASENAME,\n buildGitCredentialEnv,\n renderGhShim,\n renderGitCredentialHelper,\n} from '../../github-broker-credentials.js';\nexport { shellQuote };\n\n/**\n * ENG-5901 Track D: single chokepoint for `.env.integrations` writes.\n * Reads the existing agent-dir copy, merges per the caller's mode\n * ('replace-preserving' for writeIntegrations, 'upsert' for the channel\n * credential writers), and installs BOTH copies — the agent-dir source\n * and the project-dir mirror that the spawned Claude process actually\n * sources — at SECRET_FILE_MODE (0600), created with the right mode.\n *\n * No-ops when there is nothing on disk and nothing to write, so a\n * fresh agent without integrations or channels doesn't grow an empty\n * secrets file.\n */\nfunction writeEnvIntegrationsForAgent(\n codeName: string,\n args: MergeEnvIntegrationsArgs,\n): void {\n const agentDir = getAgentDir(codeName);\n const envPath = join(agentDir, '.env.integrations');\n let existing: string | null = null;\n try {\n existing = readFileSync(envPath, 'utf-8');\n } catch {\n /* fresh file */\n }\n if (existing === null && Object.keys(args.updates).length === 0) return;\n\n const content = mergeEnvIntegrationsContent(existing, args);\n writeFileSync(envPath, content, { mode: SECRET_FILE_MODE });\n try { chmodSync(envPath, SECRET_FILE_MODE); } catch { /* best-effort */ }\n\n // Mirror to the project dir — the persistent wrapper, scheduled-task\n // and direct-chat spawn paths all source the PROJECT copy, so a failed\n // mirror means the running session keeps stale/missing secrets.\n // CodeRabbit #1745: don't fail silently — emit a structured, secret-free\n // stderr line so operators can grep for divergence. Still non-throwing:\n // every writer re-runs on the next manager tick (which re-mirrors), and\n // a throw here would abort the remaining channel writes in the same\n // tick — a worse failure than one stale mirror interval.\n try {\n const projectDir = getProjectDir(codeName);\n mkdirSync(projectDir, { recursive: true });\n const dest = join(projectDir, '.env.integrations');\n writeFileSync(dest, content, { mode: SECRET_FILE_MODE });\n try { chmodSync(dest, SECRET_FILE_MODE); } catch { /* best-effort */ }\n } catch (err) {\n process.stderr.write(\n `[env-integrations] [mirror-write-failed] agent=${codeName} error=${(err as Error).message}\\n`,\n );\n }\n}\n\n// ── ENG-5901 PR 3: on-disk literal-secret migration ─────────────────────────\n//\n// Track D armed the literal-secret lint and converted every WRITER to\n// `${VAR}` templates — but pre-existing `.mcp.json` files still carry\n// literal entries written by older releases. Every incremental writer\n// (writeMcpServer, the manager artifact-merge loop, per-channel\n// writeChannelCredentials) preserves the OTHER servers' entries verbatim,\n// so on a file with any old literal, every write re-carries it and the\n// armed lint rejects the whole write: the file can never self-heal\n// (observed live on agt-aws-1, 2026-06-04 — ~50% of manager.log was\n// `[literal-secret-rejected]` while channel-bearing agents' configs were\n// frozen). This one-time pass hoists the known literals into\n// `.env.integrations` and rewrites the file templated, after which every\n// writer passes the lint again.\n\n/**\n * Known secret FIELD name → the env var its value belongs to. AGT_API_KEY\n * maps to itself but is template-only (the manager exports the real value\n * to every spawn env; it must NOT be persisted to `.env.integrations`).\n * Unmapped literal fields are left in place and reported — better a\n * loud rejection loop on an unknown shape than silently moving a value\n * we don't understand.\n */\nconst MIGRATABLE_FIELD_TO_ENV_VAR: Readonly<Record<string, string>> = {\n SLACK_BOT_TOKEN: 'SLACK_BOT_TOKEN',\n SLACK_APP_TOKEN: 'SLACK_APP_TOKEN',\n TELEGRAM_BOT_TOKEN: 'TELEGRAM_BOT_TOKEN',\n MSTEAMS_CLIENT_SECRET: 'MSTEAMS_CLIENT_SECRET',\n PIPEDREAM_CLIENT_SECRET: 'PIPEDREAM_CLIENT_SECRET',\n 'x-api-key': 'COMPOSIO_API_KEY',\n AGT_API_KEY: 'AGT_API_KEY',\n};\n\n/**\n * Hoist literal secrets out of the agent's provision `.mcp.json` into\n * `.env.integrations` (raw values, 0600, both mirrors) and rewrite the\n * file with `${VAR}` templates through the guarded writer. Idempotent —\n * a clean file returns immediately. Secret values never appear in any\n * log line.\n */\nfunction migrateExistingLiteralSecrets(codeName: string): void {\n const mcpJsonPath = join(getAgentDir(codeName), 'provision', '.mcp.json');\n let config: { mcpServers?: Record<string, unknown> };\n try {\n config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));\n } catch {\n return; // nothing provisioned yet — nothing to migrate\n }\n\n // CodeRabbit #1780: the deadlock shape can already have FRESHER values\n // in .env.integrations than the stale .mcp.json literal — the live\n // writers persist the raw value first and only then attempt the guarded\n // .mcp.json write, so a rejected write leaves .mcp.json stale while\n // .env.integrations is current (observed on agt-aws-1: COMPOSIO_API_KEY\n // hoisted, x-api-key literal still on disk). Never roll a fresh env\n // entry back to the old literal — env wins; we still template the field.\n let existingEnvKeys = new Set<string>();\n try {\n existingEnvKeys = new Set(\n parseEnvFileEntries(\n readFileSync(join(getAgentDir(codeName), '.env.integrations'), 'utf-8'),\n ).keys(),\n );\n } catch {\n /* no env file yet */\n }\n\n // ENG-5901 PR 4: KEY-NAME-driven, not value-scan-driven. The first\n // iteration walked scanConfigForLiteralSecrets findings, which silently\n // skipped any secret whose VALUE has no recognisable shape — observed on\n // agt-aws-1: MSTEAMS_CLIENT_SECRET (random string) and Telegram tokens\n // whose third char isn't `E` survived migration while the key-name-based\n // audit kept flagging them. Iterating the field-name map directly\n // migrates every known secret field regardless of value shape; the\n // value scan runs afterwards purely to report unknowns.\n const updates: Record<string, string> = {};\n let hoisted = 0;\n for (const raw of Object.values(config.mcpServers ?? {})) {\n if (typeof raw !== 'object' || raw === null) continue;\n const entry = raw as { env?: Record<string, string>; headers?: Record<string, string> };\n for (const block of [entry.env, entry.headers]) {\n if (!block) continue;\n for (const [field, envVar] of Object.entries(MIGRATABLE_FIELD_TO_ENV_VAR)) {\n const value = block[field];\n if (typeof value !== 'string' || value.length === 0 || value.includes('${')) continue;\n if (envVar !== 'AGT_API_KEY' && !existingEnvKeys.has(envVar)) {\n updates[envVar] = value;\n }\n block[field] = `\\${${envVar}}`;\n hoisted++;\n }\n }\n }\n\n // Value-scan AFTER templating the mapped fields: whatever still trips\n // the lint is a shape we don't know how to relocate — left in place,\n // reported loudly (the guarded write below will keep rejecting it).\n const unmapped = scanConfigForLiteralSecrets(config).map((f) => `${f.server}.${f.field}`);\n\n if (hoisted === 0 && unmapped.length === 0) return; // clean file\n\n if (hoisted === 0) {\n process.stderr.write(\n `[mcp-migrate] [no-mappable-literals] agent=${codeName} unmapped=${unmapped.join(',')}\\n`,\n );\n return;\n }\n\n // Raw values land first (same ordering contract as the live writers) so\n // no spawn can observe a template whose value isn't on disk yet.\n if (Object.keys(updates).length > 0) {\n writeEnvIntegrationsForAgent(codeName, { mode: 'upsert', updates });\n }\n if (writeMcpJsonGuarded(codeName, mcpJsonPath, config)) {\n syncMcpToProject(codeName);\n process.stderr.write(\n `[mcp-migrate] [literals-hoisted] agent=${codeName} hoisted=${hoisted}${\n unmapped.length > 0 ? ` unmapped=${unmapped.join(',')}` : ''\n }\\n`,\n );\n }\n}\n\nfunction assertValidCodeName(codeName: string): void {\n if (!VALID_CODE_NAME.test(codeName)) {\n throw new Error(`Invalid agent code_name: \"${codeName}\". Must be kebab-case.`);\n }\n}\n\nfunction assertSafeRelativePath(relativePath: string): void {\n if (relativePath.includes('..') || relativePath.startsWith('/') || relativePath.includes('\\0')) {\n throw new Error(`Unsafe relative path: ${relativePath}`);\n }\n}\n\nfunction getHomeDir(): string {\n return process.env['HOME'] ?? process.env['USERPROFILE'] ?? homedir();\n}\n\n// ---------------------------------------------------------------------------\n// ENG-7891 / ADR-0049: agent_id-keyed host directory layout\n// ---------------------------------------------------------------------------\n//\n// The per-agent host dir is moving from ~/.augmented/{codeName}/ to the real\n// ~/.augmented/{agent_id}/, with ~/.augmented/{codeName} kept as a relative\n// compatibility symlink (operator-facing read API). Spawning Claude with a cwd\n// inside the real id-keyed path makes the transcript store key (a flattened\n// cwd) rename-stable and turns the filesystem step of a rename into an atomic\n// symlink swap.\n//\n// This ships DARK. The self-detecting resolver (`resolveRealAgentPath`) and the\n// `getRegisteredAgents` symlink-dedupe are backward-compatible and always on:\n// with no symlink on disk they behave exactly as before. The layout-CREATING\n// behavior (id-keyed provisioning + symlink) and the id-keyed spawn cwd are\n// gated on this constant, flipped to `true` only after the test-host rehearsal\n// (ENG-7891 acceptance criterion). Existing agents are never converted here;\n// that is the dark migration engine's job (ENG-7891 slice 3).\nconst ID_KEYED_LAYOUT_ENABLED = false;\n\nconst VALID_AGENT_ID =\n /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nfunction assertValidAgentId(agentId: string): void {\n if (!VALID_AGENT_ID.test(agentId)) {\n throw new Error(`Invalid agent_id: \"${agentId}\". Must be a UUID.`);\n }\n}\n\n/**\n * Resolve a codename-keyed ~/.augmented path to its real on-disk location.\n * Under the id-keyed layout the codename is a symlink to the real\n * ~/.augmented/{agent_id} dir; following it here means every caller lands on\n * the same inode regardless of layout, and paths baked into on-disk artifacts\n * are id-keyed (rename-stable). Legacy agents (no symlink) are unchanged: the\n * codename dir IS the real dir, and a not-yet-created path resolves to itself.\n */\nfunction resolveRealAgentPath(codeNamePath: string): string {\n try {\n if (lstatSync(codeNamePath).isSymbolicLink()) {\n return realpathSync(codeNamePath);\n }\n } catch {\n // Path not created yet (fresh agent) - fall through to the codename path.\n }\n return codeNamePath;\n}\n\n/**\n * Create the id-keyed layout for a fresh agent: the real\n * ~/.augmented/{agent_id} dir plus a relative ~/.augmented/{codeName} ->\n * {agent_id} compatibility symlink. Returns the real agent dir. Gated by\n * `ID_KEYED_LAYOUT_ENABLED` at the call site. A legacy agent (a real codename\n * dir already on disk) is NOT converted - that is the dark migration engine's\n * job (ENG-7891 slice 3) - so this is a safe no-op on every existing host.\n */\nfunction ensureIdKeyedLayout(codeName: string, agentId: string): string {\n assertValidCodeName(codeName);\n assertValidAgentId(agentId);\n const home = getHomeDir();\n const codeNamePath = join(home, '.augmented', codeName); // agent-dir-allow: symlink-creation site (ADR-0049)\n const idPath = join(home, '.augmented', agentId);\n\n let state: 'symlink' | 'realdir' | 'absent';\n try {\n state = lstatSync(codeNamePath).isSymbolicLink() ? 'symlink' : 'realdir';\n } catch {\n state = 'absent';\n }\n\n if (state === 'realdir') {\n // Legacy layout already on disk; leave it for the migration engine.\n return codeNamePath;\n }\n if (state === 'symlink') {\n // Idempotent re-registration: the link must already point at THIS agent's\n // id dir. A mismatch means the codename maps to a different agent_id on\n // disk (a rename that bypassed the migration engine, or a stale link) -\n // fail closed rather than silently resolving to the wrong agent's dir.\n const target = readlinkSync(codeNamePath);\n if (target !== agentId) {\n throw new Error(\n `Codename symlink ${codeNamePath} points at \"${target}\", expected \"${agentId}\"; ` +\n `refusing to provision over a mismatched id-keyed layout.`,\n );\n }\n mkdirSync(idPath, { recursive: true });\n return idPath;\n }\n // state === 'absent': create the real id dir and the relative codename link\n // (bare {agent_id}, same parent dir, so it survives a homedir move - ADR-0049).\n mkdirSync(idPath, { recursive: true });\n symlinkSync(agentId, codeNamePath);\n return idPath;\n}\n\n/**\n * Terminal state reached by {@link migrateAgentDirToIdKeyed}. See that function\n * for what each value means.\n */\nexport type IdKeyedMigrationStatus =\n | 'migrated'\n | 'recovered'\n | 'already-id-keyed'\n | 'conflict'\n | 'no-agent';\n\n/** Staged (temporary) symlink name for the swap, in the SAME parent dir as the\n * codename so the final rename onto the codename path is atomic (same fs). */\nfunction stagedMigrationLinkPath(home: string, codeName: string): string {\n return join(home, '.augmented', `.${codeName}.migrating`); // agent-dir-allow: staged symlink temp name (ADR-0049 slice 3)\n}\n\n/** Remove a leftover staged link from a prior interrupted attempt. Best-effort;\n * only unlinks when the path is actually a symlink so a real dir is never\n * touched. */\nfunction cleanupStaleStagedLink(staged: string): void {\n try {\n if (lstatSync(staged).isSymbolicLink()) rmSync(staged, { force: true });\n } catch {\n // Nothing staged - fine.\n }\n}\n\n/** True iff `path` is a symlink whose target is exactly `target`. */\nfunction isSymlinkTo(path: string, target: string): boolean {\n try {\n return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target;\n } catch {\n return false;\n }\n}\n\n/**\n * ENG-7891 / ADR-0049 slice 3 - the dark migration engine's filesystem core:\n * convert an EXISTING legacy codename-keyed agent dir to the id-keyed layout.\n *\n * Moves the real `~/.augmented/{codeName}` dir to `~/.augmented/{agent_id}` and\n * leaves `~/.augmented/{codeName}` as a relative symlink -> `{agent_id}`. The\n * swap is two atomic `rename(2)` calls with the symlink pre-staged under a temp\n * name in the same parent dir, so the codename entry is observably absent only\n * for the microseconds between them. Per ADR-0049 §4 the availability guarantee\n * is delivered by the caller's quiescence (the agent - and therefore its\n * in-session channel MCP writers - is not running when this is called) plus\n * these atomic renames, NOT by checkpointing; checkpointing only covers the\n * crash case, which is completed forward idempotently on the next call.\n *\n * Ordering note (dir-first): this moves the dir + creates the symlink; the\n * caller moves the transcript store AFTER a symlink exists. That makes \"codename\n * is a symlink\" mean \"the dir move is committed\", so the transcript completion\n * is flag-independent and there is no window where a legacy-cwd agent's\n * transcript has been relocated out from under it.\n *\n * This is the DIR+SYMLINK step ONLY - no transcript move, no hook regeneration,\n * no flag/quiesce policy. ADR-0049 §4 places the engine in the manager; the\n * manager orchestrator (apps/cli `id-keyed-migration.ts`) owns the flag gate,\n * the WhatsApp-writer quiesce check, and the transcript move, and relies on the\n * pre-spawn funnel's own `provisionIsolationHook(codeName, agent_id)` call\n * (which runs after this and before spawn, every tick) to keep the isolation\n * hook id-aware. This lives in core so the symlink contract stays beside\n * `ensureIdKeyedLayout` and is unit-testable without the manager.\n *\n * Idempotent and total over every on-disk state:\n * - `migrated` a legacy real dir was moved + symlinked this call.\n * - `recovered` a prior crash left `{agent_id}` present with the codename\n * entry absent (died between the two renames); the symlink\n * was completed this call.\n * - `already-id-keyed` the codename is already a symlink -> `{agent_id}`\n * (verified to point at THIS agent); no-op.\n * - `conflict` BOTH a real `{codeName}` dir AND a real `{agent_id}` dir\n * exist (a stray writer re-created the codename dir). Refuse\n * and leave both for an operator rather than risk data loss.\n * - `no-agent` neither exists; nothing on disk to migrate.\n *\n * Throws only on an invalid codename/agent_id or a codename symlink that points\n * at a DIFFERENT agent (fail closed, same posture as `ensureIdKeyedLayout`).\n */\nexport function migrateAgentDirToIdKeyed(\n codeName: string,\n agentId: string,\n opts: { home?: string } = {},\n): IdKeyedMigrationStatus {\n assertValidCodeName(codeName);\n assertValidAgentId(agentId);\n const home = opts.home ?? getHomeDir();\n const codeNamePath = join(home, '.augmented', codeName); // agent-dir-allow: migration engine source path (ADR-0049 slice 3)\n const idPath = join(home, '.augmented', agentId);\n const staged = stagedMigrationLinkPath(home, codeName);\n\n let codeNameKind: 'symlink' | 'realdir' | 'absent';\n try {\n codeNameKind = lstatSync(codeNamePath).isSymbolicLink() ? 'symlink' : 'realdir';\n } catch {\n codeNameKind = 'absent';\n }\n const idExists = existsSync(idPath);\n\n // Already migrated: the codename is the compatibility symlink. Verify it\n // points at THIS agent (fail closed on a mismatch) and no-op.\n if (codeNameKind === 'symlink') {\n const target = readlinkSync(codeNamePath);\n if (target !== agentId) {\n throw new Error(\n `Codename symlink ${codeNamePath} points at \"${target}\", expected \"${agentId}\"; refusing to migrate.`,\n );\n }\n return 'already-id-keyed';\n }\n\n // Crash recovery: a prior run moved the dir (rename #1) but died before the\n // symlink rename (#2) - the id dir exists and the codename entry is gone.\n // Complete the symlink forward, reusing the staged link if it survived. This\n // path is deliberately flag-independent (see the orchestrator): a half-moved\n // agent is BROKEN until the codename symlink exists, so it must always finish.\n if (codeNameKind === 'absent' && idExists) {\n if (isSymlinkTo(staged, agentId)) {\n renameSync(staged, codeNamePath);\n } else {\n cleanupStaleStagedLink(staged);\n symlinkSync(agentId, codeNamePath);\n }\n return 'recovered';\n }\n\n // No codename dir and no id dir: nothing on disk for this agent (a fresh agent\n // is created id-keyed by ensureIdKeyedLayout, not here).\n if (codeNameKind === 'absent') return 'no-agent';\n\n // codeNameKind === 'realdir' from here.\n if (idExists) {\n // A real codename dir AND a real id dir both exist - the corruption the\n // orchestrator's WhatsApp-writer quiesce exists to prevent (a stray external\n // writer re-created the codename dir during a prior move window). Merging\n // could lose data; refuse and leave both for an operator.\n return 'conflict';\n }\n\n // Full migration: pre-stage the relative symlink, then two atomic renames.\n // A crash after rename #1 lands in the `absent && idExists` recovery branch\n // above on the next call; a crash after only the stage leaves an orphan link\n // cleaned up here on the next attempt.\n cleanupStaleStagedLink(staged);\n symlinkSync(agentId, staged); // relative target, same parent as the codename\n renameSync(codeNamePath, idPath); // rename #1: codename dir -> id dir (atomic)\n renameSync(staged, codeNamePath); // rename #2: staged link -> codename (atomic)\n return 'migrated';\n}\n\n/**\n * Per-agent root config directory: ~/.augmented/{codeName}/\n *\n * ENG-4418: collapsed the previous `~/.augmented/{codeName}/claudecode/`\n * subdirectory into the agent root. The `claudecode/` intermediate was\n * cruft — one agent has one framework, and the split caused a whole class\n * of \"manager wrote to X, adapter read from Y\" bugs (see ENG-4419 slack\n * clobber, ENG-4421 CLAUDE.md churn). `migrateLegacyClaudecodeDir` below\n * moves any stale `{codeName}/claudecode/` contents up one level on the\n * first call per agent, so existing hosts converge without operator action.\n *\n * Layout after collapse:\n * ~/.augmented/{codeName}/\n * ├── provision/ ← generated artifacts (CLAUDE.md, .mcp.json, ...)\n * ├── project/ ← runtime project dir (Claude Code cwd)\n * └── .tokens.json ← integration tokens\n */\nfunction getAgentDir(codeName: string): string {\n assertValidCodeName(codeName);\n // ENG-7891 / ADR-0049: resolve through the codename compatibility symlink to\n // the real ~/.augmented/{agent_id} dir when the id-keyed layout is present;\n // otherwise the codename path (legacy / not-yet-created) is the real dir.\n return resolveRealAgentPath(join(getHomeDir(), '.augmented', codeName)); // agent-dir-allow: canonical seam resolver (ADR-0049)\n}\n\n/**\n * Idempotent migration from the old `{codeName}/claudecode/` tree to the\n * unified `{codeName}/` tree. Runs on every call site that reads/writes\n * the agent's provision dir — the `migratedCodeNames` guard makes repeat\n * calls free after the first.\n *\n * Strategy:\n * 1. If `{codeName}/claudecode/` is absent → mark migrated, return.\n * 2. For each file under the old tree, compute the new destination.\n * - `.mcp.json`: merge keys (new wins for overlap, old fills gaps)\n * - Other files: copy if destination missing OR source is newer\n * 3. After every file is accounted for, `rm -rf {codeName}/claudecode/`.\n *\n * On any mid-flight error the routine bails without deleting, so operators\n * can inspect. Next call retries.\n */\nconst migratedCodeNames = new Set<string>();\n\nfunction migrateLegacyClaudecodeDir(codeName: string, log?: (msg: string) => void): void {\n // Validate BEFORE any filesystem ops so a malformed codeName can never\n // reach existsSync / readdirSync / rmSync with crafted path traversal.\n assertValidCodeName(codeName);\n if (migratedCodeNames.has(codeName)) return;\n\n const legacyRoot = join(getHomeDir(), '.augmented', codeName, 'claudecode');\n if (!existsSync(legacyRoot)) {\n migratedCodeNames.add(codeName);\n return;\n }\n\n const newRoot = getAgentDir(codeName);\n const emit = (msg: string): void => { log?.(msg); };\n\n try {\n const walkAndMigrate = (srcDir: string, destDir: string): void => {\n mkdirSync(destDir, { recursive: true });\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n const src = join(srcDir, entry.name);\n const dest = join(destDir, entry.name);\n if (entry.isDirectory()) {\n walkAndMigrate(src, dest);\n continue;\n }\n if (entry.name === '.mcp.json' && existsSync(dest)) {\n // Merge mcpServers: new tree's entries take precedence (they're\n // the hot path); old tree fills any gaps.\n //\n // If either side fails to parse, ABORT the migration — do NOT\n // fall through to the legacy-tree delete. The walkAndMigrate\n // recursion throws out of walkAndMigrate, past rmSync, into the\n // outer catch which leaves legacyRoot in place for retry.\n try {\n const oldCfg = JSON.parse(readFileSync(src, 'utf-8')) as { mcpServers?: Record<string, unknown> };\n const newCfg = JSON.parse(readFileSync(dest, 'utf-8')) as { mcpServers?: Record<string, unknown> };\n const merged = { mcpServers: { ...(oldCfg.mcpServers ?? {}), ...(newCfg.mcpServers ?? {}) } };\n writeFileSync(dest, JSON.stringify(merged, null, 2));\n emit(`[migrate] '${codeName}' merged .mcp.json (${Object.keys(merged.mcpServers).length} servers)`);\n } catch (err) {\n throw new Error(`Failed merging .mcp.json (${src} → ${dest}): ${(err as Error).message}`);\n }\n continue;\n }\n if (!existsSync(dest)) {\n copyFileSync(src, dest);\n continue;\n }\n // Destination exists and isn't .mcp.json — keep whichever is newer.\n try {\n const srcStat = readFileSync(src);\n const destStat = readFileSync(dest);\n if (!srcStat.equals(destStat)) {\n // Content differs — prefer destination (new tree is live);\n // silently drop old copy.\n }\n } catch (err) {\n // Read failure is a real signal (permissions, disk issue) — abort\n // rather than silently dropping data on the floor.\n throw new Error(`Failed comparing ${src} vs ${dest}: ${(err as Error).message}`);\n }\n }\n };\n\n walkAndMigrate(legacyRoot, newRoot);\n rmSync(legacyRoot, { recursive: true, force: true });\n emit(`[migrate] '${codeName}': collapsed ~/.augmented/${codeName}/claudecode/ into ~/.augmented/${codeName}/`);\n migratedCodeNames.add(codeName);\n } catch (err) {\n emit(`[migrate] '${codeName}': migration failed — leaving legacy dir in place: ${(err as Error).message}`);\n // Don't mark migrated — retry next call.\n }\n}\n\n/**\n * Per-agent project directory where Claude Code actually runs.\n * Each agent gets its own isolated directory with CLAUDE.md, settings.json,\n * .mcp.json, etc. This ensures multiple agents on the same machine don't\n * collide — each runs as a separate Claude Code session in its own project dir.\n *\n * Layout: ~/.augmented/{codeName}/project/\n * ├── CLAUDE.md (agent identity)\n * ├── settings.json (agent config)\n * ├── .mcp.json (MCP servers)\n * ├── CHARTER.md (governance)\n * ├── TOOLS.md (tool manifest)\n * └── .claude/ (Claude Code session data, auto-created)\n *\n * Host prerequisite (ENG-5786): channels are delivered via Claude Code's\n * `--dangerously-load-development-channels`, which CC 2.1.158+ blocks unless\n * `channelsEnabled: true` is present in the *host-level managed-settings* file\n * (Linux `/etc/claude-code/managed-settings.json`, macOS `/Library/Application\n * Support/ClaudeCode/managed-settings.json`). That file is NOT per-agent and is\n * not written here — host provisioning (`host-bootstrap.ts`) writes it and the\n * manager re-asserts it on start (`ensureClaudeManagedSettings`). Without it,\n * every channel's inbound is silently dropped while the agent looks healthy.\n */\n/**\n * ENG-8344: install (or remove) the use-time GitHub credential programs.\n *\n * Returns the `.env.integrations` entries that point `git` at the helper —\n * empty when broker delivery is off, which matters because `writeIntegrations`\n * merges in 'replace-preserving' mode: a key absent from the update map is\n * dropped from the file, so flipping the flag back off un-configures git on the\n * very next tick with no explicit deletion step.\n *\n * Removal is equally load-bearing. Leaving a stale `gh` shim on PATH after a\n * rollback would keep intercepting `gh` while the token is back in the\n * environment — working, but not the behaviour the operator rolled back TO.\n * `rmSync` runs unconditionally in the off branch so the two directions are\n * genuinely symmetric.\n */\nfunction syncGitHubBrokerCredentialTooling(\n projectDir: string,\n enabled: boolean,\n): Record<string, string> {\n const binDir = join(projectDir, GITHUB_BROKER_BIN_DIR);\n if (!enabled) {\n rmSync(binDir, { recursive: true, force: true });\n return {};\n }\n\n const helperPath = join(binDir, GIT_CREDENTIAL_HELPER_BASENAME);\n const shimPath = join(binDir, GH_SHIM_BASENAME);\n mkdirSync(binDir, { recursive: true });\n // `mode` on writeFileSync is only honoured when the file is CREATED, so\n // chmod after every write or a rewrite silently keeps the old (possibly\n // wider) mode. Same reasoning as writePersistentClaudeWrapper.\n writeFileSync(helperPath, renderGitCredentialHelper(), { mode: BROKER_SCRIPT_MODE });\n chmodSync(helperPath, BROKER_SCRIPT_MODE);\n writeFileSync(shimPath, renderGhShim(), { mode: BROKER_SCRIPT_MODE });\n chmodSync(shimPath, BROKER_SCRIPT_MODE);\n\n return buildGitCredentialEnv(helperPath);\n}\n\nfunction getProjectDir(codeName: string): string {\n // Resolve the (possibly id-keyed) real agent dir first, then append the\n // runtime project subdir - so the Claude Code cwd never contains the\n // codename symlink and the transcript-store key stays rename-stable.\n return join(getAgentDir(codeName), 'project');\n}\n\n/**\n * Sync .mcp.json from the agent config dir to the project dir.\n * Called after any MCP server or channel mutation.\n */\nfunction syncMcpToProject(codeName: string): void {\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n const provisionMcpPath = join(agentDir, 'provision', '.mcp.json');\n const projectMcpPath = join(projectDir, '.mcp.json');\n\n try {\n const content = readFileSync(provisionMcpPath, 'utf-8');\n mkdirSync(projectDir, { recursive: true });\n // ENG-5901 (CodeRabbit #1731): the project mirror is secret-bearing —\n // create it 0600 from the start (the `mode` option applies at file\n // creation), with a chmod fallback for files that already exist at a\n // legacy mode (writeFileSync leaves perms of existing files alone).\n writeFileSync(projectMcpPath, content, { mode: MCP_FILE_MODE });\n try {\n chmodSync(projectMcpPath, MCP_FILE_MODE);\n } catch {\n /* best-effort: a chmod failure shouldn't break the sync */\n }\n // ENG-5901: parity guard. The copy above is verbatim, so a mismatch\n // here means a future change diverged the mirrors on a secret field —\n // log a structured, secret-free line so CI / log-grep catches it.\n try {\n const mismatches = mcpMirrorParityErrors(\n JSON.parse(content),\n JSON.parse(readFileSync(projectMcpPath, 'utf-8')),\n );\n for (const m of mismatches) {\n process.stderr.write(`${formatMirrorMismatch(m)} agent=${codeName}\\n`);\n }\n } catch {\n /* parity check is best-effort defence-in-depth */\n }\n } catch {\n // No MCP config to sync\n }\n\n // ENG-4793: keep the channel-message-handler subagent allowlist in sync\n // with the just-written `.mcp.json`. Every incremental mutation path\n // (writeMcpServer / removeMcpServer / writeChannelCredentials / etc.)\n // funnels through here, so this is the single chokepoint that prevents\n // the subagent's tools list from drifting from the actual MCP server\n // set across a session's lifetime.\n renderChannelMessageHandlerForAgent(codeName);\n // ENG-5905: keep the augmented-worker subagent allowlist in sync via\n // the same chokepoint — same dynamic-render shape, same `.mcp.json`\n // source of truth, sibling project-scope sub-agent for general\n // multi-step background work.\n renderAugmentedWorkerForAgent(codeName);\n}\n\n// ENG-4821: integration manifest sidecar. Persisted by writeIntegrations\n// (and the buildArtifacts initial-provision path) so the subagent renderer\n// can fold integrations into the system prompt without re-parsing CLAUDE.md\n// or re-decrypting credentials. Same shape as the `IntegrationSummary[]`\n// fed into CLAUDE.md's `## Integrations` section — write once, read in two\n// places, no drift.\nconst INTEGRATIONS_SUMMARY_FILE = 'integrations-summary.json';\n\nfunction integrationsSummaryPath(codeName: string): string {\n return join(getAgentDir(codeName), 'provision', INTEGRATIONS_SUMMARY_FILE);\n}\n\nfunction writeIntegrationsSummaryForAgent(codeName: string, summaries: IntegrationSummary[]): void {\n const target = integrationsSummaryPath(codeName);\n try {\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, JSON.stringify(summaries, null, 2));\n } catch {\n // Non-fatal: subagent will render without the integrations block until\n // the next sync. Channel messages still work — they just may answer\n // \"no GitHub\" until the file lands.\n }\n}\n\nfunction readIntegrationsSummaryForAgent(codeName: string): IntegrationSummary[] {\n try {\n const raw = readFileSync(integrationsSummaryPath(codeName), 'utf-8');\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as IntegrationSummary[]) : [];\n } catch {\n return [];\n }\n}\n\n/**\n * ENG-4793 / ENG-4821: re-render `.claude/agents/channel-message-handler.md`\n * from the agent's current `.mcp.json` `mcpServers` keys (ENG-4793) plus the\n * persisted `integrations-summary.json` manifest (ENG-4821). Called from\n * syncMcpToProject so every `.mcp.json` mutation refreshes the subagent\n * allowlist; ENG-4821 piggybacks on that same chokepoint so an integration\n * that adds an MCP server (e.g. Xero) refreshes the integrations block too.\n * No-op when `.mcp.json` is missing or unreadable — the next write will\n * recreate both.\n */\nfunction renderChannelMessageHandlerForAgent(codeName: string): void {\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n const provisionMcpPath = join(agentDir, 'provision', '.mcp.json');\n\n let mcpServerKeys: string[];\n try {\n const config = JSON.parse(readFileSync(provisionMcpPath, 'utf-8')) as {\n mcpServers?: Record<string, unknown>;\n };\n mcpServerKeys = Object.keys(config.mcpServers ?? {});\n } catch {\n return; // No `.mcp.json` yet — nothing to mirror.\n }\n\n const integrations = readIntegrationsSummaryForAgent(codeName);\n const content = buildChannelMessageHandlerAgent({ mcpServerKeys, integrations });\n // Write to both provision dir (canonical) and project dir (active workspace),\n // mirroring the .mcp.json sync pattern above.\n for (const baseDir of [agentDir, projectDir]) {\n const target = join(baseDir, '.claude', 'agents', 'channel-message-handler.md');\n try {\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, content);\n } catch {\n // Non-fatal: the artifact pipeline will recreate it on next full provision.\n }\n }\n}\n\n/**\n * ENG-4787: write `.mcp.json` through validate + atomic + .bak\n * snapshot. On validation failure, leave the existing file untouched\n * and emit a single structured stderr line (`manager.log` captures\n * stderr) so the regression is visible without flooding. Returns\n * `true` on a successful write so callers can short-circuit\n * downstream syncs that assume a fresh `.mcp.json` is on disk.\n */\nfunction writeMcpJsonGuarded(\n codeName: string,\n path: string,\n config: { mcpServers?: Record<string, unknown> },\n): boolean {\n const result = safeWriteMcpJson(path, config);\n if (!result.written) {\n process.stderr.write(\n `[manager-worker] [mcp-validate] skipping write for '${codeName}': ${formatValidationErrors(result.errors)}\\n`,\n );\n return false;\n }\n return true;\n}\n\n/**\n * Read a single env var from an MCP server entry in the agent's .mcp.json.\n * Returns undefined if the file, server, or env key is missing — callers\n * use this to preserve baked-in values (e.g. AGT_AGENT_ID) across\n * incremental sync rewrites without re-plumbing the full agent context.\n */\nfunction readExistingMcpEnvVar(\n codeName: string,\n serverId: string,\n envKey: string,\n): string | undefined {\n const mcpJsonPath = join(getAgentDir(codeName), 'provision', '.mcp.json');\n try {\n const raw = readFileSync(mcpJsonPath, 'utf-8');\n const config = JSON.parse(raw) as { mcpServers?: Record<string, unknown> };\n const server = config.mcpServers?.[serverId];\n if (!server || typeof server !== 'object') return undefined;\n const env = (server as { env?: Record<string, unknown> }).env;\n const value = env?.[envKey];\n if (typeof value !== 'string' || value === '' || value === `\\${${envKey}}`) {\n return undefined;\n }\n return value;\n } catch {\n return undefined;\n }\n}\n\n/**\n * ENG-4823: resolve the real agent UUID for cloud-broker's AGT_AGENT_ID,\n * walking a deterministic fallback chain so we NEVER write the literal\n * `${AGT_AGENT_ID}` placeholder into a cloud-broker entry.\n *\n * The bug this fixes: pre-fix, writeIntegrations used `existingAgentId ??\n * '${AGT_AGENT_ID}'` as the fallback. Claude Code only substitutes\n * `${VAR}` in env values from the parent claude's spawn env at MCP-launch\n * time. AGT_AGENT_ID is NOT exported into the parent claude's env (the\n * augmented server entry instead has it baked literally), so the\n * substitution never fires and the broker boots with the literal string\n * \"${AGT_AGENT_ID}\". The API correctly 404s — and Vigil sat broken for\n * 3 hours unable to request AWS credentials. See ENG-4823 issue for the\n * full triage; the validator (ENG-4787) catches new occurrences but\n * can't repair existing ones because the same fallback fires every sync.\n *\n * Fallback chain:\n * 1. Existing cloud-broker entry's AGT_AGENT_ID (filtered for placeholders\n * via readExistingMcpEnvVar). The healthy steady-state value.\n * 2. Existing augmented server's AGT_AGENT_ID. buildMcpJson always bakes\n * the literal UUID into the augmented entry's env block (line 1116\n * ish — `AGT_AGENT_ID: input.agent.agent_id`), so this is the\n * canonical authoritative source on disk.\n * 3. Caller-provided fallback (only the buildArtifacts path passes one,\n * because input.agent.agent_id is in scope there). Layered AFTER the\n * augmented-entry read so a stale-but-correct on-disk value wins\n * over a re-derivation if both exist.\n * 4. undefined. Caller MUST handle this — return a structured error and\n * skip the write rather than poison the file.\n */\nconst PLACEHOLDER_LITERAL_RE = /\\$\\{[^}]+\\}/;\n\nexport function resolveBrokerAgentId(\n codeName: string,\n fallback?: string,\n): string | undefined {\n // CodeRabbit (PR #793): readExistingMcpEnvVar only rejects the exact\n // ${AGT_AGENT_ID} token (the original poisoning shape). A different\n // placeholder literal — say ${BROKER_ID} from a refactor that\n // half-renamed the env key, or ${TBD} from a partial write — would\n // pass through and violate the resolver's \"never placeholder\"\n // guarantee. Apply the regex defence to every candidate, not just\n // the caller-provided fallback.\n const existing = readExistingMcpEnvVar(codeName, 'cloud-broker', 'AGT_AGENT_ID');\n if (existing && !PLACEHOLDER_LITERAL_RE.test(existing)) return existing;\n const fromAugmented = readExistingMcpEnvVar(codeName, 'augmented', 'AGT_AGENT_ID');\n if (fromAugmented && !PLACEHOLDER_LITERAL_RE.test(fromAugmented)) return fromAugmented;\n if (fallback && !PLACEHOLDER_LITERAL_RE.test(fallback)) return fallback;\n return undefined;\n}\n\n/**\n * Deploy provision artifacts into the agent's project directory.\n * Called after buildArtifacts() writes to the provision dir — this copies\n * the artifacts into the per-agent project dir where Claude Code will\n * actually read them at runtime.\n */\nfunction deployArtifactsToProject(codeName: string, provisionDir: string): void {\n const projectDir = getProjectDir(codeName);\n mkdirSync(projectDir, { recursive: true });\n\n const artifactFiles = ['CLAUDE.md', 'settings.json', '.mcp.json', 'CHARTER.md', 'TOOLS.md'];\n\n // Markers for the skills index section managed by the CLI\n const SKILLS_START = '<!-- AGT:SKILLS_INDEX_START -->';\n const SKILLS_END = '<!-- AGT:SKILLS_INDEX_END -->';\n\n for (const file of artifactFiles) {\n const src = join(provisionDir, file);\n const dest = join(projectDir, file);\n try {\n const srcContent = readFileSync(src, 'utf-8');\n\n // For CLAUDE.md: preserve the skills index section that the CLI manages.\n // Compare only the non-index content to avoid a rewrite-loop where deploy\n // strips the index and refreshSkillsIndex re-adds it every cycle.\n if (file === 'CLAUDE.md' && existsSync(dest)) {\n const destContent = readFileSync(dest, 'utf-8');\n const stripIndex = (s: string) => s.replace(new RegExp(`${SKILLS_START}[\\\\s\\\\S]*?${SKILLS_END}`), '').trimEnd();\n if (stripIndex(srcContent) === stripIndex(destContent)) continue; // no change\n // Content changed — preserve existing skills index if present\n const indexMatch = destContent.match(new RegExp(`${SKILLS_START}[\\\\s\\\\S]*?${SKILLS_END}`));\n if (indexMatch) {\n writeFileSync(dest, srcContent.trimEnd() + '\\n\\n' + indexMatch[0] + '\\n');\n continue;\n }\n }\n\n // ENG-5901 (CodeRabbit #1731): the initial-deploy path bypasses\n // safeWriteMcpJson — create the secret-bearing artifact 0600 from\n // the start (`mode` applies at creation) with a chmod fallback for\n // pre-existing files, so a fresh agent's project .mcp.json never\n // exists world-readable even briefly.\n if (file === '.mcp.json') {\n writeFileSync(dest, srcContent, { mode: MCP_FILE_MODE });\n try { chmodSync(dest, MCP_FILE_MODE); } catch { /* best-effort */ }\n } else {\n writeFileSync(dest, srcContent);\n }\n } catch {\n // Artifact may not exist (e.g., optional .mcp.json)\n }\n }\n\n // Deploy skill files (e.g., .claude/skills/core-knowledge/SKILL.md)\n // Manages core-knowledge and legacy knowledge-* folders — leaves other skills untouched.\n const skillsDir = join(provisionDir, '.claude', 'skills');\n const destSkillsDir = join(projectDir, '.claude', 'skills');\n try {\n // Prune stale managed skill folders from destination\n if (existsSync(destSkillsDir)) {\n const srcFolders = existsSync(skillsDir) ? new Set(readdirSync(skillsDir)) : new Set<string>();\n for (const folder of readdirSync(destSkillsDir)) {\n // Prune legacy knowledge-* folders (replaced by core-knowledge) and\n // core-knowledge itself if no longer in source\n if (folder.startsWith('knowledge-') || (folder === 'core-knowledge' && !srcFolders.has(folder))) {\n try { rmSync(join(destSkillsDir, folder), { recursive: true }); } catch { /* ignore */ }\n }\n }\n }\n\n // Copy new/updated skills from provision dir\n if (existsSync(skillsDir)) {\n for (const skillFolder of readdirSync(skillsDir)) {\n const srcSkillFile = join(skillsDir, skillFolder, 'SKILL.md');\n if (!existsSync(srcSkillFile)) continue;\n const destFolder = join(destSkillsDir, skillFolder);\n const destFile = join(destFolder, 'SKILL.md');\n const srcContent = readFileSync(srcSkillFile, 'utf-8');\n // Skip write if content unchanged\n try { if (existsSync(destFile) && readFileSync(destFile, 'utf-8') === srcContent) continue; } catch { /* write anyway */ }\n mkdirSync(destFolder, { recursive: true });\n writeFileSync(destFile, srcContent);\n }\n }\n } catch {\n // Non-fatal — skills are optional\n }\n\n // ENG-4684: deploy named subagent files from .claude/agents/. The\n // channel-message-handler agent powers the dispatcher pattern in the\n // CLAUDE.md \"Channel message triage\" instruction. Same write-when-changed\n // pattern as skills.\n const agentsDir = join(provisionDir, '.claude', 'agents');\n const destAgentsDir = join(projectDir, '.claude', 'agents');\n try {\n if (existsSync(agentsDir)) {\n const sourceAgentFiles = new Set(\n readdirSync(agentsDir).filter((f) => f.endsWith('.md')),\n );\n\n // Prune stale files in dest first — anything no longer in source\n // (renamed or removed managed agent) gets cleaned up so the\n // .claude/agents/ directory mirrors the provision dir, not\n // accumulates leftovers across reprovisions.\n if (existsSync(destAgentsDir)) {\n for (const destFile of readdirSync(destAgentsDir)) {\n if (!destFile.endsWith('.md')) continue;\n if (sourceAgentFiles.has(destFile)) continue;\n try { rmSync(join(destAgentsDir, destFile)); } catch { /* non-fatal */ }\n }\n }\n\n // Then write/refresh from source.\n for (const agentFile of sourceAgentFiles) {\n const srcPath = join(agentsDir, agentFile);\n const destPath = join(destAgentsDir, agentFile);\n const srcContent = readFileSync(srcPath, 'utf-8');\n try { if (existsSync(destPath) && readFileSync(destPath, 'utf-8') === srcContent) continue; } catch { /* write anyway */ }\n mkdirSync(destAgentsDir, { recursive: true });\n writeFileSync(destPath, srcContent);\n }\n }\n } catch {\n // Non-fatal\n }\n\n // ADR-0012 / ENG-6352: deploy down-synced dynamic workflows from\n // .claude/workflows/. Same prune-then-write-when-changed shape as the\n // managed agents above — a renamed/revoked workflow's stale `.js` is pruned\n // so the project dir mirrors the resolved set rather than accumulating\n // leftovers. The whole set is server-resolved + flag-gated, so when the\n // feature is off the provision dir has no workflows and dest is emptied.\n const workflowsDir = join(provisionDir, '.claude', 'workflows');\n const destWorkflowsDir = join(projectDir, '.claude', 'workflows');\n try {\n const sourceWorkflowFiles = existsSync(workflowsDir)\n ? new Set(readdirSync(workflowsDir).filter((f) => f.endsWith('.js')))\n : new Set<string>();\n\n // Prune stale workflow files in dest — anything no longer in source.\n if (existsSync(destWorkflowsDir)) {\n for (const destFile of readdirSync(destWorkflowsDir)) {\n if (!destFile.endsWith('.js')) continue;\n if (sourceWorkflowFiles.has(destFile)) continue;\n try { rmSync(join(destWorkflowsDir, destFile)); } catch { /* non-fatal */ }\n }\n }\n\n // Then write/refresh from source.\n for (const workflowFile of sourceWorkflowFiles) {\n const srcPath = join(workflowsDir, workflowFile);\n const destPath = join(destWorkflowsDir, workflowFile);\n const srcContent = readFileSync(srcPath, 'utf-8');\n try { if (existsSync(destPath) && readFileSync(destPath, 'utf-8') === srcContent) continue; } catch { /* write anyway */ }\n mkdirSync(destWorkflowsDir, { recursive: true });\n writeFileSync(destPath, srcContent);\n }\n } catch {\n // Non-fatal — workflows are optional\n }\n\n // Merge any additional .mcp.json entries from the agent config dir\n // (channels, extra MCP servers added after initial provisioning)\n const agentMcpPath = join(getAgentDir(codeName), 'provision', '.mcp.json');\n const projectMcpPath = join(projectDir, '.mcp.json');\n\n try {\n const agentMcp = JSON.parse(readFileSync(agentMcpPath, 'utf-8'));\n let projectMcp: Record<string, unknown>;\n try {\n projectMcp = JSON.parse(readFileSync(projectMcpPath, 'utf-8'));\n } catch {\n projectMcp = { mcpServers: {} };\n }\n\n const projectServers = (projectMcp['mcpServers'] ?? {}) as Record<string, unknown>;\n const agentServers = (agentMcp['mcpServers'] ?? {}) as Record<string, unknown>;\n\n // Remove managed toolkit entries with invalid relative URLs (e.g., /mcp-proxy/...)\n // These get re-added with absolute URLs by the manager's integration provisioning step.\n // Filter both sources — agentServers can also contain stale relative URLs.\n const stripRelativeUrls = (servers: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(\n Object.entries(servers).filter(([, val]) => {\n const entry = val as Record<string, unknown> | null;\n return !(entry && typeof entry['url'] === 'string' && entry['url'].startsWith('/'));\n }),\n );\n\n // Merge agent-level MCP servers into project (agent-level wins on conflict)\n projectMcp['mcpServers'] = { ...stripRelativeUrls(projectServers), ...stripRelativeUrls(agentServers) };\n // ENG-5901 (CodeRabbit #1745 class): secret-bearing — born 0600.\n writeFileSync(projectMcpPath, JSON.stringify(projectMcp, null, 2), { mode: MCP_FILE_MODE });\n try { chmodSync(projectMcpPath, MCP_FILE_MODE); } catch { /* best-effort */ }\n } catch {\n // No agent-level MCP config to merge\n }\n\n // Copy .env files (auth profiles, integrations) into project dir.\n // ENG-5901 (CodeRabbit #1745): these carry raw secrets — create the\n // project copies 0600 (mode applies at creation) with a chmod fallback\n // for pre-existing files, so a fresh registerAgent()/deploy never\n // leaves the runtime copy world-readable until the next env rewrite.\n const agentDir = getAgentDir(codeName);\n for (const envFile of ['.env', '.env.integrations']) {\n try {\n const content = readFileSync(join(agentDir, envFile), 'utf-8');\n const envDest = join(projectDir, envFile);\n writeFileSync(envDest, content, { mode: SECRET_FILE_MODE });\n try { chmodSync(envDest, SECRET_FILE_MODE); } catch { /* best-effort */ }\n } catch {\n // File doesn't exist\n }\n }\n\n // Install pre-commit hook if the project dir is a git repo.\n // The hook content lives in the provision dir under .git-hooks/pre-commit;\n // we copy it to .git/hooks/pre-commit and make it executable.\n // Safe to re-run — we skip the write if the content is already current,\n // but always re-assert the executable bit in case a previous install\n // wrote the file without it.\n try {\n const gitDir = join(projectDir, '.git');\n const hookSrc = join(provisionDir, '.git-hooks', 'pre-commit');\n if (existsSync(gitDir) && existsSync(hookSrc)) {\n const hooksDir = join(gitDir, 'hooks');\n mkdirSync(hooksDir, { recursive: true });\n const hookDest = join(hooksDir, 'pre-commit');\n const srcContent = readFileSync(hookSrc, 'utf-8');\n const upToDate =\n existsSync(hookDest) && readFileSync(hookDest, 'utf-8') === srcContent;\n if (!upToDate) writeFileSync(hookDest, srcContent);\n chmodSync(hookDest, 0o755);\n }\n } catch {\n // Non-fatal — project may not be a git repo yet\n }\n}\n\n/**\n * Provision Stop hook for persistent session result capture.\n * The hook fires after every assistant turn and checks for a pending task marker.\n * If a marker exists (written by the manager when injecting a scheduled task),\n * the hook extracts the last assistant message from the transcript and POSTs it\n * to the Augmented API.\n */\nexport function provisionStopHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // Write the Stop hook script\n const hookScriptPath = join(claudeDir, 'agt-stop-hook.sh');\n // ENG-4660: previously this script ran with `set -euo pipefail` and used\n // the `[ test ] && exit 0` early-return pattern throughout. When the test\n // was false (the common case — e.g. AGENT_ID is set, RESP is non-empty),\n // `[ ... ]` returns 1, `&&` short-circuits, the whole statement returns 1,\n // and `set -e` exits the script silently. Claude Code then reported\n // \"Stop hook error: Failed with non-blocking status code: No stderr output\"\n // every turn. Switched to `if ... then exit 0; fi` for early-returns and\n // added an ERR trap so any unexpected non-zero exit carries a real reason.\n const hookScript = [\n '#!/bin/bash',\n '# Auto-generated by Augmented — captures persistent session task results.',\n 'set -uo pipefail',\n 'trap \\'ec=$?; echo \"agt-stop-hook failed (exit $ec) at line $LINENO: $BASH_COMMAND\" >&2\\' ERR',\n 'INPUT=$(cat)',\n 'TRANSCRIPT_PATH=$(echo \"$INPUT\" | jq -r \\'.transcript_path // empty\\')',\n 'if [ -z \"$TRANSCRIPT_PATH\" ] || [ ! -f \"$TRANSCRIPT_PATH\" ]; then exit 0; fi',\n 'CWD=$(echo \"$INPUT\" | jq -r \\'.cwd // empty\\')',\n 'MARKER=\"${CWD}/.claude/.agt-pending-task.json\"',\n 'if [ ! -f \"$MARKER\" ]; then exit 0; fi',\n 'AGENT_ID=$(jq -r \\'.agent_id // empty\\' \"$MARKER\")',\n 'TEMPLATE_ID=$(jq -r \\'.template_id // empty\\' \"$MARKER\")',\n 'if [ -z \"$AGENT_ID\" ]; then rm -f \"$MARKER\"; exit 0; fi',\n 'RESP=$(tail -50 \"$TRANSCRIPT_PATH\" | jq -rs \\'[.[] | select(.type == \"assistant\") | .message.content[]? | select(.type == \"text\") | .text] | last // empty\\' 2>/dev/null || true)',\n 'if [ -z \"$RESP\" ]; then RESP=$(tail -50 \"$TRANSCRIPT_PATH\" | jq -rs \\'[.[] | select(.role == \"assistant\") | .content[]? | select(.type == \"text\") | .text] | last // empty\\' 2>/dev/null || true); fi',\n 'if [ -z \"$RESP\" ]; then rm -f \"$MARKER\"; exit 0; fi',\n 'rm -f \"$MARKER\"',\n 'AGT_HOST=\"${AGT_HOST:-}\"; AGT_API_KEY=\"${AGT_API_KEY:-}\"',\n 'if [ -z \"$AGT_HOST\" ] || [ -z \"$AGT_API_KEY\" ]; then exit 0; fi',\n 'JWT=$(curl -sf -X POST \"${AGT_HOST}/host/exchange\" -H \"Content-Type: application/json\" -d \"{\\\\\"api_key\\\\\": \\\\\"${AGT_API_KEY}\\\\\"}\" | jq -r \\'.token // empty\\' 2>/dev/null || true)',\n 'if [ -z \"$JWT\" ]; then exit 0; fi',\n 'case \"$TEMPLATE_ID\" in',\n ' daily-standup|standup|weekly-standup)',\n ' curl -sf -X POST \"${AGT_HOST}/host/agent-status\" -H \"Content-Type: application/json\" -H \"Authorization: Bearer $JWT\" -d \"$(jq -n --arg a \\\\\"$AGENT_ID\\\\\" --arg s \\\\\"$RESP\\\\\" \\'{agent_id:$a,standup:$s,current_status:\"idle\"}\\')\" >/dev/null 2>&1 & ;;',\n ' *)',\n ' curl -sf -X POST \"${AGT_HOST}/host/agent-status\" -H \"Content-Type: application/json\" -H \"Authorization: Bearer $JWT\" -d \"$(jq -n --arg a \\\\\"$AGENT_ID\\\\\" --arg t \\\\\"$RESP\\\\\" \\'{agent_id:$a,current_tasks:$t}\\')\" >/dev/null 2>&1 & ;;',\n 'esac',\n 'exit 0',\n ].join('\\n') + '\\n';\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n // ENG-4569: ghost-reply detector. Fires on every Stop event and checks\n // whether the assistant just emitted reply text without dispatching the\n // matching channel tool (slack.reply / telegram.reply / teams.reply) while a\n // pending-inbound marker is present. If so, drops a recovery file in\n // the channel's outbox dir; the channel MCP server (which holds the bot\n // token) picks it up via fs.watch and sends it through the same\n // chat.postMessage / sendMessage path normal replies use.\n //\n // Lives in a second script so the existing scheduled-task capture and\n // the ghost-reply detection stay independently testable. Both run on\n // every Stop, neither depends on the other's marker.\n const ghostHookPath = join(claudeDir, 'agt-ghost-reply-hook.sh');\n // Markers + outbox payloads use atomic writes (temp file in the same dir,\n // then rename) — addresses CodeRabbit ENG-4569 review on PR #527: the\n // recovery consumers parse new files immediately and would punt on a\n // partial write. Each channel has its own per-conversation marker DIR\n // (not a single file) so a multi-pending agent doesn't lose markers.\n // Correlation: pick the channel whose marker is the LATEST (received_at).\n // The agent's text is most likely in response to the most-recent inbound;\n // older markers stay armed and rely on the 5-min timeout instead. If the\n // agent did call slack.reply / telegram.reply / teams.reply, the channel server has\n // already removed the corresponding marker, so we won't recover that\n // channel.\n // Message `content` shows up in transcripts as a content-block array OR a\n // plain string (channel notifications) — and map() over anything that isn't\n // an array is a jq type error the pipeline's 2>/dev/null swallows, which\n // silently no-ops recovery (the ENG-6288 Slack incident). Keep this\n // normalization TOTAL: string → one text block, array → as-is, any other\n // shape → [] so extraction degrades to empty instead of a hidden error.\n // Element selects below also guard on `type == \"object\"` for the same\n // reason (a mixed array with bare-string elements errors inside map()).\n const jqNormalizeContent =\n '(.message.content // .content // []) | if type == \"string\" then [{type: \"text\", text: .}] elif type == \"array\" then . else [] end';\n\n const ghostHookScript = [\n '#!/bin/bash',\n '# Auto-generated by Augmented (ENG-4569) — detects ghost replies and',\n '# drops recovery files in the channel outbox dirs. Runs on every Stop.',\n '# ENG-4660: switched off `set -e` and converted `[ test ] && exit 0`',\n '# guards to explicit `if`/`then`/`fi`. The old form silently exited',\n '# non-zero whenever the test was false (the common case), producing the',\n '# \"No stderr output\" stop-hook errors. ERR trap reports any unexpected',\n '# failure to stderr so future regressions surface immediately.',\n 'set -uo pipefail',\n 'trap \\'ec=$?; echo \"agt-ghost-reply-hook failed (exit $ec) at line $LINENO: $BASH_COMMAND\" >&2\\' ERR',\n 'INPUT=$(cat)',\n 'TRANSCRIPT_PATH=$(echo \"$INPUT\" | jq -r \\'.transcript_path // empty\\')',\n 'if [ -z \"$TRANSCRIPT_PATH\" ] || [ ! -f \"$TRANSCRIPT_PATH\" ]; then exit 0; fi',\n 'CWD=$(echo \"$INPUT\" | jq -r \\'.cwd // empty\\')',\n 'CODE_NAME=$(echo \"$CWD\" | sed -nE \\'s|.*/\\\\.augmented/([^/]+)/project/?$|\\\\1|p\\')',\n 'if [ -z \"$CODE_NAME\" ]; then exit 0; fi',\n 'AGENT_DIR=\"$(dirname \"$CWD\")\"',\n '# ENG-6567: observability. The recover_* gates used to return SILENTLY, so a',\n '# missed reply was undiagnosable in production — you could not tell',\n '# \"fired-but-failed correlation\" from \"never-fired\". log_ghost appends one',\n '# bounded line per decision to ${AGENT_DIR}/ghost-reply-hook.log (greppable',\n '# via the aws-host-agent-diagnostics SSM sweep). Bounded: trimmed to the last',\n '# 200 lines once it passes ~512KB, so a long-lived session cannot grow it',\n \"# unboundedly. IDs are logged in their sanitized marker-filename form (already\",\n '# present as cleartext marker filenames on disk), so this leaks nothing new.',\n 'GHOST_LOG=\"${AGENT_DIR}/ghost-reply-hook.log\"',\n 'log_ghost() {',\n ' echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) $*\" >> \"$GHOST_LOG\" 2>/dev/null || true',\n ' local sz; sz=$(wc -c < \"$GHOST_LOG\" 2>/dev/null || echo 0)',\n ' if [ \"${sz:-0}\" -gt 524288 ]; then tail -n 200 \"$GHOST_LOG\" > \"${GHOST_LOG}.tmp\" 2>/dev/null && mv -f \"${GHOST_LOG}.tmp\" \"$GHOST_LOG\" 2>/dev/null || true; fi',\n '}',\n 'TG_MARKER_DIR=\"${AGENT_DIR}/telegram-pending-inbound\"',\n 'SL_MARKER_DIR=\"${AGENT_DIR}/slack-pending-inbound\"',\n 'MS_MARKER_DIR=\"${AGENT_DIR}/msteams-pending-inbound/.markers\"',\n '# ENG-7808 (ENG-6722 fm4): direct-chat markers (written by the direct-chat MCP',\n '# into the CODE-NAME dir, at parity with the others). direct-chat has no',\n \"# recovery-outbox yet (that's ENG-7814/E), so it participates only in the\",\n '# block-turn-end paths, never in recover_*_for. Its markers carry neither',\n '# discretionary nor undeliverable, so the owed-reply predicate always treats',\n '# them as owed.',\n 'DC_MARKER_DIR=\"${AGENT_DIR}/direct-chat-pending-inbound\"',\n '# CodeRabbit ENG-4569 round-3: latest-marker correlation could leak chat',\n '# A\\'s composed reply into chat B if B arrived later. Now correlate by',\n '# scanning the transcript for the LAST channel-source <channel ...> tag',\n '# in user/notification turns and routing recovery to the EXACT marker',\n '# whose chat_id+message_id (Telegram) / channel+thread_ts (Slack) match.',\n '# If the tag isn\\'t found, skip recovery — the timeout will catch it.',\n 'pending_markers_count() {',\n ' local dir=\"$1\"',\n ' if [ ! -d \"$dir\" ]; then echo 0; return; fi',\n ' shopt -s nullglob',\n ' local files=(\"$dir\"/*.json)',\n ' echo \"${#files[@]}\"',\n '}',\n 'TG_PENDING=$(pending_markers_count \"$TG_MARKER_DIR\")',\n 'SL_PENDING=$(pending_markers_count \"$SL_MARKER_DIR\")',\n 'MS_PENDING=$(pending_markers_count \"$MS_MARKER_DIR\")',\n 'DC_PENDING=$(pending_markers_count \"$DC_MARKER_DIR\")',\n 'if [ \"$TG_PENDING\" = \"0\" ] && [ \"$SL_PENDING\" = \"0\" ] && [ \"$MS_PENDING\" = \"0\" ] && [ \"$DC_PENDING\" = \"0\" ]; then exit 0; fi',\n '# ENG-6727 (failure mode 3 — hook slowness): read the transcript tail ONCE',\n '# into a temp file and reuse it for every jq pass below (last-assistant text,',\n '# channel-tag scan, and the per-source recency check). The old hook re-ran',\n '# `tail -400 \"$TRANSCRIPT_PATH\" | jq` three to four times per Stop; on a long',\n '# session that repeated seek-to-end + pipe over a multi-MB transcript could',\n \"# exceed Claude Code's stop-hook timeout, so the safety net was killed mid-run\",\n '# (vera: \"running stop hooks… 0/2 · 3m 15s\"). One bounded read, many cheap',\n '# passes over the small tail, removes that failure mode.',\n 'RECENT_FILE=\"$(mktemp \"${TMPDIR:-/tmp}/agt-ghost-recent.XXXXXX\" 2>/dev/null || true)\"',\n 'if [ -z \"$RECENT_FILE\" ]; then RECENT_FILE=\"${AGENT_DIR}/.ghost-recent.$$\"; : > \"$RECENT_FILE\" 2>/dev/null || true; fi',\n '# Transcript text can contain user/secret content — keep the snapshot',\n '# owner-only (mktemp is already 0600; this covers the umask-dependent',\n '# fallback path), matching atomic_write_payload\\'s posture.',\n 'chmod 600 \"$RECENT_FILE\" 2>/dev/null || true',\n 'trap \\'rm -f \"$RECENT_FILE\" 2>/dev/null || true\\' EXIT',\n 'tail -400 \"$TRANSCRIPT_PATH\" > \"$RECENT_FILE\" 2>/dev/null || true',\n '# ENG-8142: collect the tool_use ids whose tool_result came back',\n '# `is_error: true`. A REFUSED reply is not a delivered reply, but the',\n '# replied_this_conv_* predicates below only ever saw the tool_use half of',\n '# the pair, so any errored exit read as \"the agent replied\" — returning',\n '# early before the recovery outbox is written AND setting SL_REPLIED=yes so',\n '# emit_block_if_obligated skipped the owed-reply re-prompt. Both halves of',\n '# the safety net stood down and the human just saw silence. slack.reply',\n '# alone has eight isError branches, so this is a whole class, not one bug.',\n '# Computed once here (not per predicate) to hold the ENG-6288 budget of a',\n '# single jq pass over the snapshot.',\n 'ERRORED_TOOL_IDS=$(jq -r \\'(.message.content // .content // []) | if type==\"array\" then .[] else empty end | select((type==\"object\") and (.type==\"tool_result\") and (.is_error==true)) | (.tool_use_id // empty)\\' \"$RECENT_FILE\" 2>/dev/null | sort -u || true)',\n 'LAST_ASSISTANT=$(jq -cs \\'[.[] | select(.type == \"assistant\" or .role == \"assistant\")] | last // empty\\' \"$RECENT_FILE\" 2>/dev/null || true)',\n 'if [ -z \"$LAST_ASSISTANT\" ] || [ \"$LAST_ASSISTANT\" = \"null\" ]; then exit 0; fi',\n '# Assistant content is array-shaped today; normalize anyway so a shape',\n '# change can never resurrect the swallowed-jq-error no-op (ENG-6288).',\n `TEXT=$(echo \"$LAST_ASSISTANT\" | jq -r '${jqNormalizeContent} | map(select(type == \"object\" and .type == \"text\") | .text) | join(\"\\\\n\\\\n\")' 2>/dev/null || true)`,\n '# Strip whitespace and bail only on truly-empty (vs the previous <4-char',\n '# threshold that dropped legit short replies like \"ok\", \"yes\", emoji).',\n 'if [ -z \"${TEXT//[[:space:]]/}\" ]; then exit 0; fi',\n `TOOL_NAMES=$(echo \"$LAST_ASSISTANT\" | jq -r '${jqNormalizeContent} | map(select(type == \"object\" and .type == \"tool_use\") | .name) | .[]' 2>/dev/null || true)`,\n '# Find the LAST <channel ...> tag in user/notification turns. Channel',\n '# notifications are forwarded into the session as text containing this',\n '# tag (slack-channel.ts:1247 / telegram-channel.ts:776 emit content +',\n '# meta which Claude Code wraps in a <channel ...> preamble). Searching',\n '# the raw text gives us the exact pending conversation key.',\n '# User-event content is frequently a PLAIN STRING (channel notifications',\n '# land that way), not a content-block array — map() over a string is a',\n '# jq error, which the 2>/dev/null swallowed, so the tag came back empty',\n '# and recovery silently no-oped on every Slack ghost reply (confirmed',\n '# live on sherlock/agt-aws-1 2026-06-10). Normalize before mapping.',\n '# ENG-6467: grep is line-oriented, so `[^>]+` can never span a newline. A',\n \"# channel preamble whose thread_context embeds a multi-paragraph prior reply\",\n '# (exactly the high-value brief/research threads) splits the opening tag',\n '# across lines, so the regex matched nothing → TAG_SOURCE=none → the whole',\n \"# recovery (and block-turn-end) chain was skipped (Sophie / two-tractors-host,\",\n '# 2026-06-17). Flatten newlines to spaces PER TEXT BLOCK inside jq (gsub),',\n '# before join: each transcript record stays on its own output line, so a',\n \"# partial/unterminated tag in one record can't be closed by a `>` in the\",\n '# next (a post-join `tr` would synthesize that false tag). A `>` inside',\n \"# thread_context can truncate the match early, but every scalar attr we read\",\n '# (source/channel/thread_ts/message_ts/chat_id/conversation_id) precedes',\n '# thread_context, so the truncated tag still carries them.',\n '# ENG-6727: skip assistant-authored records when scanning for the inbound',\n '# <channel> tag. If the agent quotes/summarizes a <channel ...> tag in its',\n '# own final text, that assistant tag could otherwise become CHANNEL_TAG and',\n '# correlate recovery/block against the wrong conversation. Exclude assistant',\n '# turns (not an allowlist of user/notification, which could miss a real',\n '# notification record shape).',\n `CHANNEL_TAG=$(jq -r 'select((.type // \"\") != \"assistant\" and (.role // \"\") != \"assistant\") | ${jqNormalizeContent} | map(select(type == \"object\" and .type == \"text\") | .text | gsub(\"[\\\\n\\\\r]+\"; \" \")) | join(\" \")' \"$RECENT_FILE\" 2>/dev/null | grep -oE '<channel [^>]+>' | tail -1 || true)`,\n 'TAG_SOURCE=\"\"',\n 'if [ -n \"$CHANNEL_TAG\" ]; then TAG_SOURCE=$(echo \"$CHANNEL_TAG\" | grep -oE \\'source=\"[^\"]+\"\\' | head -1 | sed \\'s/source=\"\\\\(.*\\\\)\"/\\\\1/\\' || true); fi',\n '# ENG-6567: one context line per Stop that reached here (non-empty assistant',\n '# text + at least one pending marker). This is the load-bearing diagnostic:',\n '# it records the inbound source, pending-marker counts, whether the agent',\n '# called a reply tool in the final turn, and the text length — enough to',\n '# classify a miss without guessing. A missing TAG_SOURCE here explains the',\n \"# 'marker present but never recovered' mystery (no <channel> tag correlated).\",\n 'REPLY_IN_LAST=no',\n \"if echo \\\"$TOOL_NAMES\\\" | grep -qE '(^|__)(slack|telegram|teams)[._](reply|send_message)$|(^|__)direct_chat[._](reply|consume)$'; then REPLY_IN_LAST=yes; fi\",\n 'log_ghost \"stop source=${TAG_SOURCE:-none} pending(tg=$TG_PENDING sl=$SL_PENDING ms=$MS_PENDING dc=$DC_PENDING) text_len=${#TEXT} reply_tool_in_final_turn=$REPLY_IN_LAST\"',\n 'extract_attr() { echo \"$1\" | grep -oE \"$2=\\\\\"[^\\\\\\\"]+\\\\\"\" | head -1 | sed -E \"s/$2=\\\\\\\"(.*)\\\\\\\"/\\\\\\\\1/\"; }',\n '# Atomic write helper: jq → tmp file in same dir, then rename. The',\n '# channel-side fs.watch only fires on rename, so the file is never',\n '# observed mid-write.',\n 'atomic_write_payload() {',\n ' local out_dir=\"$1\" final=\"$2\" jq_expr=\"$3\"; shift 3',\n ' if ! mkdir -p \"$out_dir\" 2>/dev/null; then return 1; fi',\n ' local tmp=\"$out_dir/.${final##*/}.tmp\"',\n ' # ENG-7805 (CodeRabbit): fail (non-zero) on a jq/write fault instead of',\n ' # mv-ing a half-written tmp - the script has no set -e, so an unguarded jq',\n ' # error would otherwise ship an empty payload AND let callers believe the',\n ' # write succeeded (leaving a recovery-ledger entry orphaned until GC).',\n ' if ! jq -n \"$jq_expr\" \"$@\" > \"$tmp\" 2>/dev/null; then rm -f \"$tmp\" 2>/dev/null || true; return 1; fi',\n ' chmod 600 \"$tmp\" 2>/dev/null || true',\n ' mv -f \"$tmp\" \"$out_dir/$final\"',\n '}',\n '# Sanitize an ID the same way the channel servers do (safeMarkerName /',\n '# safeSlackMarkerName): replace anything outside [A-Za-z0-9_-] with `_`.',\n 'safe_id() { echo -n \"$1\" | sed -E \\'s|[^A-Za-z0-9_-]|_|g\\'; }',\n '# ENG-6727 (failure mode 1 — over-suppression): per-conversation \"did the',\n '# agent reply to THIS conversation in the final turn?\" checks. The old',\n \"# same-turn guards grepped TOOL_NAMES for ANY reply tool, so a multi-thread\",\n '# agent that replied to thread B suppressed the owed reply/block for thread A',\n '# (the chronic sherlock symptom). These scope the check to the conversation',\n '# keyed by the inbound tag, comparing the reply tool\\'s input against it.',\n '# Output \"yes\"/\"no\" (never a jq nonzero exit) to stay clear of the ERR trap.',\n '# ENG-8142: `$bad` is the errored-tool_use id set; a tool_use whose id is in',\n '# it was REFUSED, so it must not count as a reply. A tool_use with no `.id`',\n '# cannot be correlated and deliberately still counts as a reply — that is the',\n '# pre-ENG-8142 behaviour, and it is the safe direction, because the failure',\n '# mode of guessing the other way is double-delivering to the customer.',\n 'replied_this_conv_slack() {',\n ' local channel=\"$1\" thread_ts=\"$2\" hit',\n ' hit=$(printf \\'%s\\' \"$LAST_ASSISTANT\" | jq -r --arg ch \"$channel\" --arg th \"$thread_ts\" --arg errids \"$ERRORED_TOOL_IDS\" \\'(($errids // \"\") | split(\"\\\\n\") | map(select(length > 0))) as $bad | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | (if any(.[]; (type==\"object\") and (.type==\"tool_use\") and ((.name|tostring)|test(\"(^|__)slack[._]reply$\")) and ((.id // \"\") as $tid | ($bad | index($tid)) == null) and ((.input.channel // \"\") as $c | (.input.thread_ts // \"\") as $t | (.input.message_ts // \"\") as $m | (($th != \"\" and ($t==$th or $m==$th)) or ($th==\"\" and $c!=\"\" and $c==$ch)))) then \"yes\" else \"no\" end)\\' 2>/dev/null || echo no)',\n ' [ \"$hit\" = \"yes\" ]',\n '}',\n 'replied_this_conv_telegram() {',\n ' local chat_id=\"$1\" hit',\n ' hit=$(printf \\'%s\\' \"$LAST_ASSISTANT\" | jq -r --arg cid \"$chat_id\" --arg errids \"$ERRORED_TOOL_IDS\" \\'(($errids // \"\") | split(\"\\\\n\") | map(select(length > 0))) as $bad | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | (if any(.[]; (type==\"object\") and (.type==\"tool_use\") and ((.name|tostring)|test(\"(^|__)telegram[._](reply|send_message)$\")) and ((.id // \"\") as $tid | ($bad | index($tid)) == null) and (((.input.chat_id // \"\")|tostring)==$cid)) then \"yes\" else \"no\" end)\\' 2>/dev/null || echo no)',\n ' [ \"$hit\" = \"yes\" ]',\n '}',\n 'replied_this_conv_teams() {',\n ' local conversation_id=\"$1\" hit',\n ' hit=$(printf \\'%s\\' \"$LAST_ASSISTANT\" | jq -r --arg cid \"$conversation_id\" --arg errids \"$ERRORED_TOOL_IDS\" \\'(($errids // \"\") | split(\"\\\\n\") | map(select(length > 0))) as $bad | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | (if any(.[]; (type==\"object\") and (.type==\"tool_use\") and ((.name|tostring)|test(\"(^|__)teams[._]reply$\")) and ((.id // \"\") as $tid | ($bad | index($tid)) == null) and (((.input.conversation_id // \"\")|tostring)==$cid)) then \"yes\" else \"no\" end)\\' 2>/dev/null || echo no)',\n ' [ \"$hit\" = \"yes\" ]',\n '}',\n '# ENG-7808 (ENG-6722 fm4): \"did the agent reply to THIS direct-chat session in',\n '# the final turn?\" A direct_chat.reply OR direct_chat.consume settles the',\n '# obligation (consume is the explicit handled-without-reply ack, the same',\n '# session-scoped drain the MCP applies), so either counts as answered. session_id',\n '# is unambiguous (one session per marker), so there is no cross-thread class here.',\n 'replied_this_conv_directchat() {',\n ' local session_id=\"$1\" hit',\n ' hit=$(printf \\'%s\\' \"$LAST_ASSISTANT\" | jq -r --arg sid \"$session_id\" \\'(.message.content // .content // []) | (if type==\"array\" then . else [] end) | (if any(.[]; (type==\"object\") and (.type==\"tool_use\") and ((.name|tostring)|test(\"(^|__)direct_chat[._](reply|consume)$\")) and (((.input.session_id // \"\")|tostring)==$sid)) then \"yes\" else \"no\" end)\\' 2>/dev/null || echo no)',\n ' [ \"$hit\" = \"yes\" ]',\n '}',\n 'recover_telegram_for() {',\n ' local chat_id=\"$1\" msg_id=\"$2\"',\n ' if [ -z \"$chat_id\" ] || [ -z \"$msg_id\" ]; then return; fi',\n ' # ENG-6727 (mode 1): scoped same-turn guard. Suppress only if the agent',\n ' # replied to THIS chat in the final turn (a final reply clears the marker',\n ' # anyway; this catches the interim-ack case where the marker is downgraded',\n ' # to seen, not deleted). A reply to a DIFFERENT chat no longer suppresses —',\n ' # marker existence + the recency guard below decide that. Was a global grep',\n ' # over TOOL_NAMES that dropped owed replies on multi-chat agents.',\n ' if replied_this_conv_telegram \"$chat_id\"; then log_ghost \"telegram skip=replied_this_turn chat=$(safe_id \"$chat_id\")\"; return; fi',\n ' # ENG-6405 / ENG-6467 — recency-aware koda suppression (KEPT). If the agent',\n ' # has telegram.replied/sent to a DIFFERENT chat_id AFTER this inbound tag,',\n ' # it moved on and TEXT is not a reliably-correlated ghost of THIS inbound —',\n ' # recovering it would post mis-correlated text. Stay silent: a silent gap the',\n ' # operator re-pings beats wrong content that poisons trust.',\n ' # ENG-7806 (fm1): delivery-ledger read replaces the transcript scan (conv_key',\n ' # = chat_id, Telegram\\'s conversation grain). no_ledger falls back to the scan.',\n ' local _tg_m=\"${TG_MARKER_DIR}/$(safe_id \"$chat_id\")__$(safe_id \"$msg_id\").json\" _tg_iid=\"\" _tg_recv=\"\"',\n ' if [ -f \"$_tg_m\" ]; then _tg_iid=$(jq -r \\'.inbound_id // \"\"\\' \"$_tg_m\" 2>/dev/null || echo \"\"); _tg_recv=$(jq -r \\'.received_at // \"\"\\' \"$_tg_m\" 2>/dev/null || echo \"\"); fi',\n ' case \"$(delivery_ledger_verdict \"$_tg_iid\" \"$chat_id\" \"$_tg_recv\")\" in',\n ' self) log_ghost \"telegram skip=already_delivered chat=$(safe_id \"$chat_id\")\"; return ;;',\n ' moved_on) log_ghost \"telegram skip=moved_on_after_tag_ledger chat=$(safe_id \"$chat_id\")\"; return ;;',\n ' no_ledger)',\n ' local replied_other',\n ' replied_other=$(jq -s --arg cid \"$chat_id\" \\'def ctext: (.message.content // .content // []) | if type==\"string\" then . elif type==\"array\" then (map(select(type==\"object\" and .type==\"text\")|.text)|join(\" \")) else \"\" end; . as $all | ([ range(0; ($all|length)) | select((($all[.]|ctext)|test(\"<channel \")) and (($all[.]|(.type // .role // \"\")) != \"assistant\")) ] | last) as $idx | [ $all[(($idx // -1)+1):][] | select((.type // .role)==\"assistant\") | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | .[] | select((.type==\"tool_use\") and ((.name|tostring)|test(\"telegram[._](reply|send_message)$\")) and (((.input.chat_id // \"\")|tostring) as $c | ($c != $cid and $c != \"\"))) ] | length\\' \"$RECENT_FILE\" 2>/dev/null || true)',\n ' if [ \"${replied_other:-0}\" -gt 0 ] 2>/dev/null; then return; fi',\n ' ;;',\n ' esac',\n ' local marker_name',\n ' marker_name=\"$(safe_id \"$chat_id\")__$(safe_id \"$msg_id\").json\"',\n ' local marker_path=\"${TG_MARKER_DIR}/${marker_name}\"',\n ' if [ ! -f \"$marker_path\" ]; then log_ghost \"telegram skip=no_pending_marker chat=$(safe_id \"$chat_id\")\"; return; fi',\n ' # ENG-7805 (fm2): confirm-before-clear. Cap re-fire with a per-marker ledger and',\n ' # LEAVE the marker for the consumer to clear on confirmed send. Ledger-first',\n ' # fail-safe (same as recover_directchat_for / block-turn-end): if the ledger',\n ' # write fails, log a skip and write no payload rather than re-fire every Stop.',\n ' if ! mkdir -p \"$TG_RECOVERY_LEDGER_DIR\" 2>/dev/null; then log_ghost \"telegram skip=recovery_ledger_unwritable chat=$(safe_id \"$chat_id\")\"; return; fi',\n ' # ENG-7805 (CodeRabbit): atomic acquire - noclobber create fails if the entry',\n ' # already exists (recovery in flight) OR the write faults; the follow-up [ -f ]',\n ' # disambiguates. Closes the \"[ -f ] then : >\" TOCTOU where two concurrent Stops',\n ' # both observe no entry and both enqueue a duplicate payload.',\n ' if ! ( set -o noclobber; : > \"${TG_RECOVERY_LEDGER_DIR}/${marker_name}\" ) 2>/dev/null; then',\n ' if [ -f \"${TG_RECOVERY_LEDGER_DIR}/${marker_name}\" ]; then log_ghost \"telegram skip=recovery_in_flight chat=$(safe_id \"$chat_id\")\"; else log_ghost \"telegram skip=recovery_ledger_unwritable chat=$(safe_id \"$chat_id\")\"; fi',\n ' return',\n ' fi',\n ' local TS',\n ' TS=$(date -u +%Y%m%dT%H%M%S%N)',\n ' if ! atomic_write_payload \"${AGENT_DIR}/telegram-recovery-outbox\" \"${TS}.json\" \\\\',\n ' \\'{chat_id:$c, message_id:$m, text:$t, marker_name:$mn, source:\"ghost-reply-recovery\"}\\' \\\\',\n ' --arg c \"$chat_id\" --arg m \"$msg_id\" --arg t \"$TEXT\" --arg mn \"$marker_name\"; then',\n ' # ENG-7805 (CodeRabbit): the write faulted after the ledger was acquired -',\n ' # drop the ledger entry so replay + the next Stop are not suppressed until GC.',\n ' rm -f \"${TG_RECOVERY_LEDGER_DIR}/${marker_name}\" 2>/dev/null || true',\n ' log_ghost \"telegram skip=recovery_payload_write_failed chat=$(safe_id \"$chat_id\")\"',\n ' return',\n ' fi',\n ' log_ghost \"telegram RECOVERED chat=$(safe_id \"$chat_id\") msg=$(safe_id \"$msg_id\") text_len=${#TEXT}\"',\n '}',\n 'recover_slack_for() {',\n ' local channel=\"$1\" thread_ts=\"$2\"',\n ' if [ -z \"$channel\" ]; then return; fi',\n ' # ENG-6727 (mode 1): scoped same-turn guard. Suppress only if the agent',\n ' # replied to THIS thread/channel in the final turn (a final reply clears the',\n ' # marker anyway; this catches the interim-ack case where the marker is',\n ' # downgraded to seen, not deleted). A reply to a DIFFERENT thread no longer',\n ' # suppresses here — marker existence + the recency guard below decide that.',\n ' # Was a global grep over TOOL_NAMES (any slack.reply), which dropped owed',\n ' # replies on multi-thread agents (the chronic sherlock symptom).',\n ' if replied_this_conv_slack \"$channel\" \"$thread_ts\"; then log_ghost \"slack skip=replied_this_turn thread=$(safe_id \"$thread_ts\")\"; return; fi',\n ' # ENG-6387 plus ENG-6467: recency-aware correlation + fail-silent (KEPT). A reply and',\n ' # trailing narration can straddle turns, so a single-turn check is not',\n ' # enough. ENG-6387 bailed on a reply to ANY other thread ANYWHERE in the',\n ' # window; a multi-thread agent (sherlock) satisfies that on nearly every',\n ' # turn, so it silently dropped composed replies (ENG-6467). Narrowed to',\n ' # RECENCY: suppress only if the agent replied to a DIFFERENT thread AFTER',\n \" # this inbound's <channel> tag (it moved on, the koda 2026-06-12 case: a DM\",\n ' # tag followed by a reply to the kickoff thread, so the trailing text is the',\n ' # kickoff\\'s, not the DM\\'s). Replies BEFORE this inbound no longer suppress,',\n ' # so answering an older thread then this one recovers correctly. Silence on',\n ' # a genuine move-on still beats posting mis-correlated text.',\n ' # ENG-7806 (fm1): read the inbound-delivery ledger instead of scraping the',\n ' # transcript. Find the target marker for its inbound_id + received_at, then ask',\n ' # the ledger \"already delivered / moved on to another conversation?\" - a',\n ' # byte-exact record, not coordinate/recency inference. no_ledger (a pre-C',\n ' # session with no ledger data) falls back to the old transcript scan below.',\n ' # CodeRabbit: select the SAME marker recovery will process (oldest for this',\n ' # channel+thread) ONCE, so the self-check reads the exact inbound being recovered,',\n ' # never a sibling message in a busy thread. Reused by the CS-1444 guard + recovery below.',\n ' local prefix=\"$(safe_id \"$channel\")__$(safe_id \"$thread_ts\")__\"',\n ' local marker_path=\"\"',\n ' shopt -s nullglob',\n ' for f in \"$SL_MARKER_DIR\"/${prefix}*.json; do if [ -z \"$marker_path\" ]; then marker_path=\"$f\"; fi; done',\n ' if [ -z \"$marker_path\" ]; then log_ghost \"slack skip=no_pending_marker channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\")\"; return; fi',\n ' local _sl_iid=\"\" _sl_recv=\"\"',\n ' _sl_iid=$(jq -r \\'.inbound_id // \"\"\\' \"$marker_path\" 2>/dev/null || echo \"\"); _sl_recv=$(jq -r \\'.received_at // \"\"\\' \"$marker_path\" 2>/dev/null || echo \"\")',\n ' case \"$(delivery_ledger_verdict \"$_sl_iid\" \"$thread_ts\" \"$_sl_recv\")\" in',\n ' self) log_ghost \"slack skip=already_delivered thread=$(safe_id \"$thread_ts\")\"; return ;;',\n ' moved_on) log_ghost \"slack skip=moved_on_after_tag_ledger thread=$(safe_id \"$thread_ts\")\"; return ;;',\n ' no_ledger)',\n ' local replied_other',\n ' replied_other=$(jq -s --arg th \"$thread_ts\" \\'def ctext: (.message.content // .content // []) | if type==\"string\" then . elif type==\"array\" then (map(select(type==\"object\" and .type==\"text\")|.text)|join(\" \")) else \"\" end; . as $all | ([ range(0; ($all|length)) | select((($all[.]|ctext)|test(\"<channel \")) and (($all[.]|(.type // .role // \"\")) != \"assistant\")) ] | last) as $idx | [ $all[(($idx // -1)+1):][] | select((.type // .role)==\"assistant\") | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | .[] | select((.type==\"tool_use\") and ((.name|tostring)|test(\"slack[._]reply$\")) and (((.input.thread_ts // .input.message_ts // \"\")) as $t | ($t != $th and $t != \"\"))) ] | length\\' \"$RECENT_FILE\" 2>/dev/null || true)',\n ' if [ \"${replied_other:-0}\" -gt 0 ] 2>/dev/null; then log_ghost \"slack skip=moved_on_after_tag thread=$(safe_id \"$thread_ts\")\"; return; fi',\n ' ;;',\n ' esac',\n ' # marker_path (the oldest for this channel+thread, closest to firing the timeout)',\n ' # was selected above and reused here - one selection shared by the ledger read,',\n ' # the CS-1444 guard, and recovery.',\n ' # CS-1444: precedence guard against cross-thread mis-delivery. The last',\n ' # <channel> tag can point at a DISCRETIONARY (auto-followed, un-mentioned)',\n ' # inbound the agent never owed a reply to (ENG-6319). If the trailing text',\n ' # was produced while working a DIFFERENT, ENGAGED conversation, posting it',\n ' # into this discretionary thread leaks one conversation into another - the',\n ' # koda cross-thread failure the recency guard above misses when the agent',\n ' # replied to NOTHING this turn and only narrated. (CS-1444: an agent filed a',\n ' # ticket in a DM and ended with \"Filed... Standing by\"; an unrelated thread B',\n ' # had an auto_followed pending inbound as the LAST tag, so the DM summary',\n ' # posted into thread B.) When THIS target marker is discretionary AND an',\n ' # engaged (non-discretionary, non-undeliverable) marker for another inbound',\n ' # is also pending, do NOT synthesize a post: leave this marker so the',\n ' # bounded durable-replay scanner re-delivers the ORIGINAL inbound (convert',\n ' # mis-deliver -> re-deliver).',\n ' # Worst case is a re-delivery, never silent loss (the marker is preserved).',\n ' # The sole-discretionary case (a genuine composed-but-unsent reply to an',\n ' # auto-followed thread, no competing engaged conversation) is unaffected and',\n ' # still recovers, so ENG-6319 is preserved. Slack-only: telegram/teams have',\n ' # no discretionary-marker class (no auto-follow variant).',\n ' local target_disc engaged_other=0 om_disc om_undel',\n \" target_disc=$(jq -r '.discretionary // false' \\\"$marker_path\\\" 2>/dev/null || echo false)\",\n ' if [ \"$target_disc\" = \"true\" ]; then',\n ' shopt -s nullglob',\n ' for om in \"$SL_MARKER_DIR\"/*.json; do',\n ' if [ \"$om\" != \"$marker_path\" ]; then',\n \" om_disc=$(jq -r '.discretionary // false' \\\"$om\\\" 2>/dev/null || echo false)\",\n \" om_undel=$(jq -r '.undeliverable // false' \\\"$om\\\" 2>/dev/null || echo false)\",\n ' # An engaged competitor is a marker the agent actually OWES a reply to:',\n ' # neither discretionary (auto-followed, skippable) nor undeliverable',\n ' # (ENG-5846: x-acked, could not reply at arrival) - the same owed/engaged',\n ' # predicate the block-turn-end paths use. Only such a marker outranks a',\n ' # discretionary target and suppresses recovery.',\n ' if [ \"$om_disc\" != \"true\" ] && [ \"$om_undel\" != \"true\" ]; then',\n ' engaged_other=1',\n ' break',\n ' fi',\n ' fi',\n ' done',\n ' if [ \"$engaged_other\" = \"1\" ]; then',\n ' log_ghost \"slack skip=discretionary_target_engaged_elsewhere channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\") marker=$(basename \"$marker_path\") text_len=${#TEXT}\"',\n ' return',\n ' fi',\n ' fi',\n ' local marker_name; marker_name=\"$(basename \"$marker_path\")\"',\n ' # ENG-7805 (fm2): confirm-before-clear. Cap re-fire with a per-marker ledger and',\n ' # LEAVE the marker for the consumer to clear on confirmed send (ledger-first fail-safe).',\n ' if ! mkdir -p \"$SL_RECOVERY_LEDGER_DIR\" 2>/dev/null; then log_ghost \"slack skip=recovery_ledger_unwritable channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\")\"; return; fi',\n ' # ENG-7805 (CodeRabbit): atomic acquire - noclobber create fails if the entry',\n ' # already exists (recovery in flight) OR the write faults; the follow-up [ -f ]',\n ' # disambiguates. Closes the \"[ -f ] then : >\" TOCTOU between concurrent Stops.',\n ' if ! ( set -o noclobber; : > \"${SL_RECOVERY_LEDGER_DIR}/${marker_name}\" ) 2>/dev/null; then',\n ' if [ -f \"${SL_RECOVERY_LEDGER_DIR}/${marker_name}\" ]; then log_ghost \"slack skip=recovery_in_flight channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\")\"; else log_ghost \"slack skip=recovery_ledger_unwritable channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\")\"; fi',\n ' return',\n ' fi',\n ' local TS',\n ' TS=$(date -u +%Y%m%dT%H%M%S%N)',\n ' if ! atomic_write_payload \"${AGENT_DIR}/slack-recovery-outbox\" \"${TS}.json\" \\\\',\n ' \\'{channel:$c, thread_ts:$th, text:$t, marker_name:$mn, source:\"ghost-reply-recovery\"}\\' \\\\',\n ' --arg c \"$channel\" --arg th \"$thread_ts\" --arg t \"$TEXT\" --arg mn \"$marker_name\"; then',\n ' # ENG-7805 (CodeRabbit): write faulted after the ledger was acquired - drop',\n ' # the ledger entry so replay + the next Stop are not suppressed until GC.',\n ' rm -f \"${SL_RECOVERY_LEDGER_DIR}/${marker_name}\" 2>/dev/null || true',\n ' log_ghost \"slack skip=recovery_payload_write_failed channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\")\"',\n ' return',\n ' fi',\n ' log_ghost \"slack RECOVERED channel=$(safe_id \"$channel\") thread=$(safe_id \"$thread_ts\") marker=${marker_name} text_len=${#TEXT}\"',\n '}',\n 'recover_teams_for() {',\n ' local conversation_id=\"$1\" reply_to_id=\"$2\" service_url=\"$3\"',\n ' if [ -z \"$conversation_id\" ] || [ -z \"$service_url\" ]; then return; fi',\n ' # ENG-6727 (mode 1): scoped same-turn guard. Suppress only if the agent',\n ' # replied to THIS conversation in the final turn. A reply to a DIFFERENT',\n ' # conversation no longer suppresses — marker existence + the recency guard',\n ' # below decide that. Was a global grep over TOOL_NAMES.',\n ' if replied_this_conv_teams \"$conversation_id\"; then log_ghost \"teams skip=replied_this_turn conv=$(safe_id \"$conversation_id\")\"; return; fi',\n ' # ENG-6405 / ENG-6467 — recency-aware koda suppression (KEPT). If the agent has',\n ' # teams.replied to a DIFFERENT conversation_id AFTER this inbound tag, TEXT is',\n ' # not a reliably-correlated ghost of THIS inbound — stay silent rather than post',\n ' # mis-correlated text (silence the operator re-pings beats wrong content).',\n ' # ENG-7806 (fm1): delivery-ledger read replaces the transcript scan (conv_key',\n ' # = conversation_id). no_ledger falls back to the scan. The teams marker file',\n ' # is hex(conversation_id)[..64]--... so locate it the same way to read its id.',\n ' # CodeRabbit: select the SAME marker recovery will process ONCE (hex(conversation_id)',\n ' # prefix; the activity_id is not available here), so the self-check reads the exact',\n ' # inbound being recovered. Reused by the recovery block below.',\n ' local hex_conv',\n ' hex_conv=$(printf %s \"$conversation_id\" | od -An -tx1 | tr -d \" \\\\n\" | cut -c1-64)',\n ' local marker_path=\"\"',\n ' shopt -s nullglob',\n ' for f in \"$MS_MARKER_DIR\"/${hex_conv}*.json; do if [ -z \"$marker_path\" ]; then marker_path=\"$f\"; fi; done',\n ' if [ -z \"$marker_path\" ]; then log_ghost \"teams skip=no_pending_marker conv=$(safe_id \"$conversation_id\")\"; return; fi',\n ' local _ms_iid=\"\" _ms_recv=\"\"',\n ' _ms_iid=$(jq -r \\'.inbound_id // \"\"\\' \"$marker_path\" 2>/dev/null || echo \"\"); _ms_recv=$(jq -r \\'.received_at // \"\"\\' \"$marker_path\" 2>/dev/null || echo \"\")',\n ' case \"$(delivery_ledger_verdict \"$_ms_iid\" \"$conversation_id\" \"$_ms_recv\")\" in',\n ' self) log_ghost \"teams skip=already_delivered conv=$(safe_id \"$conversation_id\")\"; return ;;',\n ' moved_on) log_ghost \"teams skip=moved_on_after_tag_ledger conv=$(safe_id \"$conversation_id\")\"; return ;;',\n ' no_ledger)',\n ' local replied_other',\n ' replied_other=$(jq -s --arg cid \"$conversation_id\" \\'def ctext: (.message.content // .content // []) | if type==\"string\" then . elif type==\"array\" then (map(select(type==\"object\" and .type==\"text\")|.text)|join(\" \")) else \"\" end; . as $all | ([ range(0; ($all|length)) | select((($all[.]|ctext)|test(\"<channel \")) and (($all[.]|(.type // .role // \"\")) != \"assistant\")) ] | last) as $idx | [ $all[(($idx // -1)+1):][] | select((.type // .role)==\"assistant\") | (.message.content // .content // []) | (if type==\"array\" then . else [] end) | .[] | select((.type==\"tool_use\") and ((.name|tostring)|test(\"teams[._]reply$\")) and (((.input.conversation_id // \"\")) as $c | ($c != $cid and $c != \"\"))) ] | length\\' \"$RECENT_FILE\" 2>/dev/null || true)',\n ' if [ \"${replied_other:-0}\" -gt 0 ] 2>/dev/null; then return; fi',\n ' ;;',\n ' esac',\n ' # marker_path (hex(conversation_id) prefix match, the marker the agent saw whose',\n ' # activity_id we lack) was selected above and is reused here.',\n ' local marker_name; marker_name=\"$(basename \"$marker_path\")\"',\n ' # ENG-7805 (fm2): confirm-before-clear. Cap re-fire with a per-marker ledger and',\n ' # LEAVE the marker for the consumer to clear on confirmed send (ledger-first fail-safe).',\n ' if ! mkdir -p \"$MS_RECOVERY_LEDGER_DIR\" 2>/dev/null; then log_ghost \"teams skip=recovery_ledger_unwritable conv=$(safe_id \"$conversation_id\")\"; return; fi',\n ' # ENG-7805 (CodeRabbit): atomic acquire - noclobber create fails if the entry',\n ' # already exists (recovery in flight) OR the write faults; the follow-up [ -f ]',\n ' # disambiguates. Closes the \"[ -f ] then : >\" TOCTOU between concurrent Stops.',\n ' if ! ( set -o noclobber; : > \"${MS_RECOVERY_LEDGER_DIR}/${marker_name}\" ) 2>/dev/null; then',\n ' if [ -f \"${MS_RECOVERY_LEDGER_DIR}/${marker_name}\" ]; then log_ghost \"teams skip=recovery_in_flight conv=$(safe_id \"$conversation_id\")\"; else log_ghost \"teams skip=recovery_ledger_unwritable conv=$(safe_id \"$conversation_id\")\"; fi',\n ' return',\n ' fi',\n ' local TS',\n ' TS=$(date -u +%Y%m%dT%H%M%S%N)',\n ' if ! atomic_write_payload \"${AGENT_DIR}/msteams-recovery-outbox\" \"${TS}.json\" \\\\',\n ' \\'{conversation_id:$c, reply_to_id:$r, service_url:$s, text:$t, marker_name:$mn, source:\"ghost-reply-recovery\"}\\' \\\\',\n ' --arg c \"$conversation_id\" --arg r \"$reply_to_id\" --arg s \"$service_url\" --arg t \"$TEXT\" --arg mn \"$marker_name\"; then',\n ' # ENG-7805 (CodeRabbit): write faulted after the ledger was acquired - drop',\n ' # the ledger entry so replay + the next Stop are not suppressed until GC.',\n ' rm -f \"${MS_RECOVERY_LEDGER_DIR}/${marker_name}\" 2>/dev/null || true',\n ' log_ghost \"teams skip=recovery_payload_write_failed conv=$(safe_id \"$conversation_id\")\"',\n ' return',\n ' fi',\n ' log_ghost \"teams RECOVERED conv=$(safe_id \"$conversation_id\") text_len=${#TEXT}\"',\n '}',\n '# ENG-7814 (ENG-6722 slice E): always-on direct-chat recovery. Re-send the last',\n '# assistant text to the (unambiguous) session via a direct-chat-recovery-outbox the',\n '# direct-chat MCP consumes. NO recency/replied_other guard: session_id is unambiguous',\n '# (one session per marker), so there is no cross-thread mis-correlation risk that the',\n '# recover_slack_for recency guard exists to prevent. Confirm-before-clear: unlike the',\n '# slack/telegram/teams recover_*_for above (which rm the marker here), this leaves the',\n '# marker armed and caps re-fire with a per-marker ledger; the MCP consumer clears the',\n '# marker only after /host/direct-chat/reply confirms, and removes the ledger entry on',\n '# success OR failure (re-arm), so a failed re-send retries on a later Stop.',\n 'recover_directchat_for() {',\n ' local session_id=\"$1\" hex_session=\"$2\"',\n ' if [ -z \"$session_id\" ] || [ -z \"$hex_session\" ]; then return; fi',\n ' local marker_path=\"\"',\n ' shopt -s nullglob',\n ' for f in \"$DC_MARKER_DIR\"/${hex_session}__*.json; do',\n ' if [ -z \"$marker_path\" ]; then marker_path=\"$f\"; fi',\n ' done',\n ' if [ -z \"$marker_path\" ]; then log_ghost \"direct-chat skip=no_pending_marker session=$(safe_id \"$session_id\")\"; return; fi',\n ' local marker_name; marker_name=\"$(basename \"$marker_path\")\"',\n ' # Persist the per-marker cap BEFORE the side effect (same fail-safe as',\n ' # block-turn-end): if the ledger dir/file write fails we could re-recover every',\n ' # Stop, so degrade to a logged skip and write no payload.',\n ' if ! mkdir -p \"$DC_RECOVERY_LEDGER_DIR\" 2>/dev/null; then log_ghost \"direct-chat skip=recovery_ledger_unwritable session=$(safe_id \"$session_id\")\"; return; fi',\n ' # ENG-7944 (CodeRabbit): atomic acquire - noclobber create fails if the entry',\n ' # already exists (recovery in flight) OR the write faults; the follow-up [ -f ]',\n ' # disambiguates. Closes the \"[ -f ] then : >\" TOCTOU where two concurrent Stops',\n ' # both observe no entry and both enqueue a duplicate payload (matches the',\n ' # telegram/slack recovery paths).',\n ' if ! ( set -o noclobber; : > \"${DC_RECOVERY_LEDGER_DIR}/${marker_name}\" ) 2>/dev/null; then',\n ' if [ -f \"${DC_RECOVERY_LEDGER_DIR}/${marker_name}\" ]; then log_ghost \"direct-chat skip=recovery_in_flight session=$(safe_id \"$session_id\")\"; else log_ghost \"direct-chat skip=recovery_ledger_unwritable session=$(safe_id \"$session_id\")\"; fi',\n ' return',\n ' fi',\n ' local TS',\n ' TS=$(date -u +%Y%m%dT%H%M%S%N)',\n ' if ! atomic_write_payload \"$DC_RECOVERY_OUTBOX_DIR\" \"${TS}.json\" \\\\',\n ' \\'{session_id:$s, text:$t, marker_name:$mn, source:\"ghost-reply-recovery\"}\\' \\\\',\n ' --arg s \"$session_id\" --arg t \"$TEXT\" --arg mn \"$marker_name\"; then',\n ' # ENG-7944 (CodeRabbit): the write faulted after the ledger was acquired -',\n ' # drop the ledger entry so replay + the next Stop are not suppressed until GC.',\n ' rm -f \"${DC_RECOVERY_LEDGER_DIR}/${marker_name}\" 2>/dev/null || true',\n ' log_ghost \"direct-chat skip=recovery_payload_write_failed session=$(safe_id \"$session_id\")\"',\n ' return',\n ' fi',\n ' log_ghost \"direct-chat RECOVERED session=$(safe_id \"$session_id\") text_len=${#TEXT}\"',\n '}',\n '# ENG-6467 / ADR-0024 Slice 2.5 — block-turn-end (the D1 \"composed-but-unsent\"',\n '# fix), gated dark behind AGT_CHANNEL_BLOCK_TURN_END_ENABLED (registry flag',\n '# channel-block-turn-end; the env var is the operator/canary override the bash',\n '# hook reads directly). When ON: if the agent owes a reply to the last-tagged',\n '# inbound (a pending marker that is NOT discretionary and NOT undeliverable) and',\n '# did NOT call the matching reply tool this turn, return {\"decision\":\"block\"} so',\n '# the MODEL sends the reply itself — right thread, right content, NO recovery',\n '# mis-correlation (the D2/koda failure the recover_* paths guard against).',\n '# Capped to one block per inbound via a per-marker ledger; stop_hook_active is',\n '# an ADDITIONAL belt suppressor, never the sole cap (it is unverified across the',\n \"# fleet's --resume sessions). After one block we fall through to the existing\",\n \"# recovery-outbox. OFF (default / unset) ⇒ exactly today's behavior.\",\n 'BLOCK_TURN_END_ON=0',\n 'case \"${AGT_CHANNEL_BLOCK_TURN_END_ENABLED:-}\" in true|1|TRUE|True) BLOCK_TURN_END_ON=1;; esac',\n '# WS3 (ENG-7397): block-turn-end-all-markers. When ON (and channel-block-turn-end',\n '# is also ON), the block scan covers EVERY pending marker across all sources, not',\n '# just the last-tagged conversation, so an agent that owes replies to several',\n '# threads at once is blocked until it answers all of them (with WS2 it answers',\n '# each by inbound_id). Registry flag block-turn-end-all-markers; the env var is',\n '# the operator/canary override the bash reads directly. OFF (default) leaves the',\n '# single-marker last-tag behavior exactly as-is.',\n 'BLOCK_ALL_MARKERS_ON=0',\n 'case \"${AGT_BLOCK_TURN_END_ALL_MARKERS_ENABLED:-}\" in true|1|TRUE|True) BLOCK_ALL_MARKERS_ON=1;; esac',\n \"STOP_ACTIVE=$(echo \\\"$INPUT\\\" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)\",\n 'BLOCK_LEDGER_DIR=\"${AGENT_DIR}/.agt-block-turn-end-ledger\"',\n '# GC stale ledger entries (>1 day) so the per-marker cap dir cannot grow unbounded.',\n 'if [ -d \"$BLOCK_LEDGER_DIR\" ]; then find \"$BLOCK_LEDGER_DIR\" -type f -mtime +1 -delete 2>/dev/null || true; fi',\n '# ENG-7814 (ENG-6722 slice E): always-on direct-chat recovery. When ON, an owed',\n '# and unanswered direct-chat inbound that block-turn-end did NOT handle gets its',\n '# last assistant text written to a direct-chat-recovery-outbox that the direct-chat',\n '# MCP re-sends via /host/direct-chat/reply. ENG-7944: recovery is now always-on',\n '# (matching Slack/Telegram/Teams). The kill-switch is the MCP consumer flag',\n '# (direct-chat-recovery / AGT_DIRECT_CHAT_RECOVERY_ENABLED), which can be set',\n '# to false to disable delivery without a deploy. DC_RECOVERY_ON removed.',\n 'DC_RECOVERY_OUTBOX_DIR=\"${AGENT_DIR}/direct-chat-recovery-outbox\"',\n '# Per-marker recovery ledger: caps ONE in-flight recovery per inbound (so a marker',\n '# left pending for confirm-before-clear is not re-recovered every Stop). The MCP',\n '# consumer removes the entry on delivery success OR failure (re-arm); we GC stale',\n '# entries here as a backstop, same as the block ledger.',\n 'DC_RECOVERY_LEDGER_DIR=\"${AGENT_DIR}/.agt-direct-chat-recovery-ledger\"',\n 'if [ -d \"$DC_RECOVERY_LEDGER_DIR\" ]; then find \"$DC_RECOVERY_LEDGER_DIR\" -type f -mtime +1 -delete 2>/dev/null || true; fi',\n '# ENG-7805 (ENG-6722 slice B): per-marker recovery ledgers for slack/telegram/teams.',\n '# recover_*_for now LEAVES the pending marker in place (confirm-before-clear, fm2)',\n '# and writes a ledger entry here to cap ONE in-flight recovery per inbound; the',\n '# channel MCP consumer clears the marker on confirmed send and removes the ledger',\n '# entry when the recovery resolves (delivered / suppressed / poisoned = re-arm).',\n '# GC stale entries (>1 day) as a backstop, same as the block + direct-chat ledgers.',\n 'SL_RECOVERY_LEDGER_DIR=\"${AGENT_DIR}/.agt-slack-recovery-ledger\"',\n 'TG_RECOVERY_LEDGER_DIR=\"${AGENT_DIR}/.agt-telegram-recovery-ledger\"',\n 'MS_RECOVERY_LEDGER_DIR=\"${AGENT_DIR}/.agt-msteams-recovery-ledger\"',\n 'for _rl in \"$SL_RECOVERY_LEDGER_DIR\" \"$TG_RECOVERY_LEDGER_DIR\" \"$MS_RECOVERY_LEDGER_DIR\"; do',\n ' if [ -d \"$_rl\" ]; then find \"$_rl\" -type f -mtime +1 -delete 2>/dev/null || true; fi',\n 'done',\n '# ENG-7806 (ENG-6722 slice C): the inbound-delivery ledger - a durable, POSITIVE',\n '# record (\"inbound X was confirmed-delivered at time T\") the channel MCPs write on',\n '# each confirmed reply, keyed by inbound_id. The recover_*_for path reads it instead',\n '# of scraping the transcript for the replied_other/moved-on decision (fm1). Disjoint',\n '# from the recovery-ledgers above (those are marker-filename-keyed, negative/in-flight).',\n '# GC at 1 day like the others (a delivery record only needs to outlive the pending',\n '# marker + a restart within the recovery window; the MCP never needs it after the',\n '# marker is gone).',\n 'DELIVERY_LEDGER_DIR=\"${AGENT_DIR}/.agt-inbound-delivery-ledger\"',\n 'if [ -d \"$DELIVERY_LEDGER_DIR\" ]; then find \"$DELIVERY_LEDGER_DIR\" -type f -mtime +1 -delete 2>/dev/null || true; fi',\n '# ENG-7806 telemetry: count each ledger verdict into a counter file the manager',\n '# already reads+resets (the *-reply-binding-classifications.json suffix), so it',\n '# reaches CloudWatch as Classification dims with no new pipeline. Best-effort +',\n '# fully silent (no stdout) - delivery_ledger_verdict runs in $(...), so a stray',\n '# byte would corrupt the captured verdict.',\n 'LEDGER_METRIC_FILE=\"${AGENT_DIR}/ghost-ledger-reply-binding-classifications.json\"',\n 'bump_ledger_metric() {',\n ' local key=\"$1\" tmp=\"${LEDGER_METRIC_FILE}.$$.tmp\"',\n ' if [ -f \"$LEDGER_METRIC_FILE\" ]; then',\n ' jq --arg k \"$key\" \\'.[$k] = ((.[$k] // 0) + 1)\\' \"$LEDGER_METRIC_FILE\" > \"$tmp\" 2>/dev/null && mv -f \"$tmp\" \"$LEDGER_METRIC_FILE\" 2>/dev/null || rm -f \"$tmp\" 2>/dev/null || true',\n ' else',\n ' jq -n --arg k \"$key\" \\'{($k): 1}\\' > \"$tmp\" 2>/dev/null && mv -f \"$tmp\" \"$LEDGER_METRIC_FILE\" 2>/dev/null || rm -f \"$tmp\" 2>/dev/null || true',\n ' fi',\n '}',\n '# Deterministic replacement for the recover_*_for replied_other transcript scan.',\n '# Echoes exactly one verdict for (inbound_id, conv_key, received_at):',\n '# self - a delivery record for THIS inbound_id exists (already answered)',\n '# moved_on - a record for a DIFFERENT conv_key was delivered AFTER this inbound',\n '# arrived (the agent answered elsewhere; trailing text is ambiguous)',\n '# clear - the ledger has data but neither of the above (safe to recover)',\n '# no_ledger - the ledger is empty/absent (pre-C session) -> caller falls back to',\n '# the transcript scan, so the rollout is safe.',\n '# ISO-8601 Z timestamps sort lexicographically == chronologically, so a string',\n '# compare is a correct time compare here (all delivered_at/received_at are UTC Z).',\n 'delivery_ledger_verdict() {',\n ' local iid=\"$1\" convk=\"$2\" recv=\"$3\"',\n ' if [ ! -d \"$DELIVERY_LEDGER_DIR\" ]; then bump_ledger_metric delivery_ledger_fallback; echo \"no_ledger\"; return; fi',\n ' shopt -s nullglob',\n ' local _dl_files=(\"$DELIVERY_LEDGER_DIR\"/*.json)',\n ' if [ \"${#_dl_files[@]}\" = \"0\" ]; then bump_ledger_metric delivery_ledger_fallback; echo \"no_ledger\"; return; fi',\n ' # Self-check first (needs only inbound_id): a delivery record for THIS inbound_id',\n ' # means it was already answered, so suppress even if the marker somehow lacks a',\n ' # received_at.',\n ' if [ -n \"$iid\" ] && [ -f \"${DELIVERY_LEDGER_DIR}/$(safe_id \"$iid\").json\" ]; then bump_ledger_metric delivery_ledger_self; echo \"self\"; return; fi',\n ' # ENG-7806 (CodeRabbit): after the self-check, without THIS marker\\'s own inbound_id',\n ' # AND received_at we cannot make a trustworthy moved-on decision for it (a pre-C',\n ' # marker that predates the ledger, or a torn read). Fall back to the transcript',\n ' # scan rather than defaulting to clear - otherwise, once ANY ledger file exists, an',\n ' # id-less marker would bypass the rollout fallback and recover text the old scan',\n ' # would suppress.',\n ' if [ -z \"$iid\" ] || [ -z \"$recv\" ]; then bump_ledger_metric delivery_ledger_fallback; echo \"no_ledger\"; return; fi',\n ' local _dl_f _dl_ck _dl_da',\n ' for _dl_f in \"${_dl_files[@]}\"; do',\n \" _dl_ck=$(jq -r '.conv_key // \\\"\\\"' \\\"$_dl_f\\\" 2>/dev/null || echo \\\"\\\")\",\n \" _dl_da=$(jq -r '.delivered_at // \\\"\\\"' \\\"$_dl_f\\\" 2>/dev/null || echo \\\"\\\")\",\n ' if [ -n \"$_dl_ck\" ] && [ \"$_dl_ck\" != \"$convk\" ] && [ -n \"$_dl_da\" ] && [ -n \"$recv\" ] && [[ \"$_dl_da\" > \"$recv\" ]]; then',\n ' bump_ledger_metric delivery_ledger_moved_on; echo \"moved_on\"; return',\n ' fi',\n ' done',\n ' bump_ledger_metric delivery_ledger_clear; echo \"clear\"',\n '}',\n '# Returns 0 (after printing the block JSON to stdout) when it blocked; 1 otherwise.',\n '# $1 = \"yes\"/\"no\" — did the agent reply to THIS conversation in the final turn?',\n '# (ENG-6727: per-conversation, computed by the caller via replied_this_conv_*)',\n '# $2 = newline-separated candidate marker paths for the tagged conversation',\n '# $3 = reply-tool hint shown to the model in the block reason',\n 'emit_block_if_obligated() {',\n ' local replied_this_conv=\"$1\" candidates=\"$2\" tool_hint=\"$3\"',\n ' if [ \"$BLOCK_TURN_END_ON\" != \"1\" ]; then return 1; fi',\n ' # Belt: never block during a continuation Claude Code already flagged.',\n ' if [ \"$STOP_ACTIVE\" = \"true\" ]; then return 1; fi',\n ' # ENG-6727 (mode 1): already replied to THIS conversation this turn ⇒ nothing',\n ' # owed. Was a global grep over TOOL_NAMES for ANY reply tool, so a reply to',\n ' # thread B let an owed block for thread A fall through to recovery, which the',\n ' # recency guard then suppressed → silent loss (the sherlock symptom, since',\n ' # sherlock runs with block-turn-end ON). Now scoped to the tagged conversation.',\n ' if [ \"$replied_this_conv\" = \"yes\" ]; then return 1; fi',\n ' local m d u obligated=\"\"',\n ' while IFS= read -r m; do',\n ' if [ -z \"$m\" ] || [ ! -f \"$m\" ]; then continue; fi',\n \" # Fail-safe: a parse error defaults to 'true' (treat as discretionary /\",\n ' # undeliverable ⇒ NOT owed ⇒ do not block) so a partial/corrupt marker can',\n ' # never manufacture a spurious block (the ENG-6288 swallowed-jq-error class).',\n \" d=$(jq -r '.discretionary // false' \\\"$m\\\" 2>/dev/null || echo true)\",\n \" u=$(jq -r '.undeliverable // false' \\\"$m\\\" 2>/dev/null || echo true)\",\n ' if [ \"$d\" != \"true\" ] && [ \"$u\" != \"true\" ]; then obligated=\"$m\"; break; fi',\n ' done <<MARKERS',\n '$candidates',\n 'MARKERS',\n ' if [ -z \"$obligated\" ]; then return 1; fi',\n ' local led_name; led_name=\"$(basename \"$obligated\")\"',\n ' # Only block when the per-marker cap can be PERSISTED. If the ledger dir or',\n ' # file write fails we cannot record \"blocked once\", so blocking would risk a',\n ' # re-block loop on the next Stop — degrade to recovery instead (CodeRabbit).',\n ' if ! mkdir -p \"$BLOCK_LEDGER_DIR\" 2>/dev/null; then echo \"agt-ghost-reply-hook: block-turn-end degraded (ledger dir create failed) — falling through to recovery\" >&2; return 1; fi',\n ' local led=\"${BLOCK_LEDGER_DIR}/${led_name}\"',\n ' # Per-marker cap: block at most once per inbound. Second Stop with the same',\n ' # marker still pending ⇒ the model ignored the block ⇒ fall through to recovery.',\n ' if [ -f \"$led\" ]; then echo \"agt-ghost-reply-hook: block-turn-end degraded (already blocked ${led_name}) — falling through to recovery\" >&2; return 1; fi',\n ' if ! : > \"$led\" 2>/dev/null; then echo \"agt-ghost-reply-hook: block-turn-end degraded (ledger write failed ${led_name}) — falling through to recovery\" >&2; return 1; fi',\n ' echo \"agt-ghost-reply-hook: block-turn-end FIRED marker=${led_name} tool=$tool_hint\" >&2',\n ' local reason=\"You composed a reply but did not send it. The person is still waiting and will see only silence. Call ${tool_hint} now for the message you were answering: send your answer, or a brief status update if you are still working. Do not just summarize what you intended to say; actually send it via the tool.\"',\n \" jq -cn --arg r \\\"$reason\\\" '{decision:\\\"block\\\", reason:$r}'\",\n ' return 0',\n '}',\n '# WS3 (ENG-7397): the multi-marker generalization of emit_block_if_obligated.',\n '# Enumerates EVERY pending marker across all three source dirs, drops the',\n '# discretionary/undeliverable ones (same fail-safe defaults) and the ones already',\n '# answered THIS turn (per-marker replied_this_conv_* on the marker\\'s own coords),',\n '# and - when 1+ obligated-and-unanswered markers survive and NONE was already',\n '# blocked once - writes a ledger entry for each and emits ONE block enumerating',\n '# every owed conversation by inbound_id + source + safe key (never content).',\n '# Marker-semantics-neutral: arms/clears no markers, only decides WHEN to block.',\n '# Returns 0 (after printing the block JSON) when it blocked; 1 otherwise.',\n 'emit_block_all_obligated() {',\n ' if [ \"$BLOCK_TURN_END_ON\" != \"1\" ] || [ \"$BLOCK_ALL_MARKERS_ON\" != \"1\" ]; then return 1; fi',\n ' if [ \"$STOP_ACTIVE\" = \"true\" ]; then return 1; fi',\n ' local -a owed_paths=()',\n ' local owed_lines=\"\" owed_count=0 any_capped=0 entry src dir m d u replied ch th cid conv sess iid key led_name',\n ' for entry in \"slack:$SL_MARKER_DIR\" \"telegram:$TG_MARKER_DIR\" \"msteams:$MS_MARKER_DIR\" \"direct-chat:$DC_MARKER_DIR\"; do',\n ' src=\"${entry%%:*}\"; dir=\"${entry#*:}\"',\n ' if [ ! -d \"$dir\" ]; then continue; fi',\n ' shopt -s nullglob',\n ' for m in \"$dir\"/*.json; do',\n ' if [ ! -f \"$m\" ]; then continue; fi',\n \" d=$(jq -r '.discretionary // false' \\\"$m\\\" 2>/dev/null || echo true)\",\n \" u=$(jq -r '.undeliverable // false' \\\"$m\\\" 2>/dev/null || echo true)\",\n ' if [ \"$d\" = \"true\" ] || [ \"$u\" = \"true\" ]; then continue; fi',\n ' replied=no; ch=\"\"; th=\"\"; cid=\"\"; conv=\"\"; sess=\"\"',\n ' case \"$src\" in',\n \" slack) ch=$(jq -r '.channel // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\"); th=$(jq -r '.thread_ts // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\"); if replied_this_conv_slack \\\"$ch\\\" \\\"$th\\\"; then replied=yes; fi ;;\",\n \" telegram) cid=$(jq -r '.chat_id // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\"); if replied_this_conv_telegram \\\"$cid\\\"; then replied=yes; fi ;;\",\n \" msteams) conv=$(jq -r '.conversation_id // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\"); if replied_this_conv_teams \\\"$conv\\\"; then replied=yes; fi ;;\",\n \" direct-chat) sess=$(jq -r '.session_id // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\"); if replied_this_conv_directchat \\\"$sess\\\"; then replied=yes; fi ;;\",\n ' esac',\n ' if [ \"$replied\" = \"yes\" ]; then continue; fi',\n ' led_name=\"$(basename \"$m\")\"',\n ' if [ -f \"${BLOCK_LEDGER_DIR}/${led_name}\" ]; then any_capped=1; fi',\n \" iid=$(jq -r '.inbound_id // \\\"\\\"' \\\"$m\\\" 2>/dev/null || echo \\\"\\\")\",\n ' case \"$src\" in',\n ' slack) key=\"thread=$(safe_id \"$th\")\" ;;',\n ' telegram) key=\"chat=$(safe_id \"$cid\")\" ;;',\n ' msteams) key=\"conv=$(safe_id \"$conv\")\" ;;',\n ' direct-chat) key=\"session=$(safe_id \"$sess\")\" ;;',\n ' *) key=\"\" ;;',\n ' esac',\n ' owed_count=$((owed_count + 1))',\n ' if [ -n \"$iid\" ]; then owed_lines=\"${owed_lines}${owed_count}) source=${src} inbound_id=${iid} ${key}; \"; else owed_lines=\"${owed_lines}${owed_count}) source=${src} ${key}; \"; fi',\n ' owed_paths+=(\"$m\")',\n ' done',\n ' done',\n ' if [ \"$owed_count\" = \"0\" ]; then return 1; fi',\n ' # Block only when NONE of the owed markers was already blocked once - a capped',\n ' # marker means the model already ignored a block for it, so fall through to the',\n ' # per-source recovery instead of re-blocking.',\n ' if [ \"$any_capped\" = \"1\" ]; then echo \"agt-ghost-reply-hook: block-turn-end-all degraded (an owed marker already blocked) - falling through to recovery\" >&2; return 1; fi',\n ' if ! mkdir -p \"$BLOCK_LEDGER_DIR\" 2>/dev/null; then echo \"agt-ghost-reply-hook: block-turn-end-all degraded (ledger dir create failed) - falling through\" >&2; return 1; fi',\n ' local p',\n ' for p in \"${owed_paths[@]}\"; do',\n ' if ! : > \"${BLOCK_LEDGER_DIR}/$(basename \"$p\")\" 2>/dev/null; then echo \"agt-ghost-reply-hook: block-turn-end-all degraded (ledger write failed) - falling through\" >&2; return 1; fi',\n ' done',\n ' echo \"agt-ghost-reply-hook: block-turn-end-all FIRED count=${owed_count}\" >&2',\n ' local reason=\"You have ${owed_count} conversation(s) you did not answer this turn; the people are waiting and will see only silence. Reply to EACH now with its reply tool, passing its inbound_id so your answer lands in the right thread (never answer one conversation in a different thread). Owed: ${owed_lines}Actually send each answer via the tool; do not just summarize what you intended to say.\"',\n \" jq -cn --arg r \\\"$reason\\\" '{decision:\\\"block\\\", reason:$r}'\",\n ' return 0',\n '}',\n '# Strict correlation: only recover if the last channel tag in the',\n '# transcript points at a channel/key that has an exact-match pending',\n '# marker. No tag found → skip; let timeout handle it. Block-turn-end (when',\n '# armed) runs FIRST per source; if it blocks we exit before recovery so the',\n '# two mechanisms never both fire on one Stop.',\n '# WS3: when block-turn-end-all-markers is armed, try the multi-marker block',\n '# FIRST - it covers every owed conversation, not just the last tag. It self-gates',\n '# on both flags (returns 1 when off), so this call is a no-op unless armed. If it',\n '# blocks we exit; otherwise fall through to the per-source single-marker path,',\n '# which still handles the last-tagged conversation.',\n 'if emit_block_all_obligated; then exit 0; fi',\n 'if [ \"$TAG_SOURCE\" = \"telegram\" ]; then',\n ' CHAT_ID=$(extract_attr \"$CHANNEL_TAG\" \"chat_id\")',\n ' MSG_ID=$(extract_attr \"$CHANNEL_TAG\" \"message_id\")',\n ' if [ -n \"$CHAT_ID\" ] && [ -n \"$MSG_ID\" ]; then',\n ' TG_CAND=\"${TG_MARKER_DIR}/$(safe_id \"$CHAT_ID\")__$(safe_id \"$MSG_ID\").json\"',\n ' TG_REPLIED=no; if replied_this_conv_telegram \"$CHAT_ID\"; then TG_REPLIED=yes; fi',\n \" if emit_block_if_obligated \\\"$TG_REPLIED\\\" \\\"$TG_CAND\\\" 'telegram.reply'; then exit 0; fi\",\n ' fi',\n ' recover_telegram_for \"$CHAT_ID\" \"$MSG_ID\"',\n 'elif [ \"$TAG_SOURCE\" = \"slack\" ]; then',\n ' CHANNEL=$(extract_attr \"$CHANNEL_TAG\" \"channel\")',\n ' THREAD_TS=$(extract_attr \"$CHANNEL_TAG\" \"thread_ts\")',\n ' if [ -n \"$CHANNEL\" ]; then',\n ' SL_PREFIX=\"${SL_MARKER_DIR}/$(safe_id \"$CHANNEL\")__$(safe_id \"$THREAD_TS\")__\"',\n ' shopt -s nullglob',\n ' SL_CAND=$(printf \\'%s\\\\n\\' \"$SL_PREFIX\"*.json)',\n ' SL_REPLIED=no; if replied_this_conv_slack \"$CHANNEL\" \"$THREAD_TS\"; then SL_REPLIED=yes; fi',\n \" if emit_block_if_obligated \\\"$SL_REPLIED\\\" \\\"$SL_CAND\\\" 'slack.reply'; then exit 0; fi\",\n ' fi',\n ' recover_slack_for \"$CHANNEL\" \"$THREAD_TS\"',\n 'elif [ \"$TAG_SOURCE\" = \"msteams\" ]; then',\n ' CONVERSATION_ID=$(extract_attr \"$CHANNEL_TAG\" \"conversation_id\")',\n ' REPLY_TO_ID=$(extract_attr \"$CHANNEL_TAG\" \"reply_to_id\")',\n ' SERVICE_URL=$(extract_attr \"$CHANNEL_TAG\" \"service_url\")',\n ' if [ -n \"$CONVERSATION_ID\" ]; then',\n ' HEX_CONV=$(printf %s \"$CONVERSATION_ID\" | od -An -tx1 | tr -d \" \\\\n\" | cut -c1-64)',\n ' shopt -s nullglob',\n ' MS_CAND=$(printf \\'%s\\\\n\\' \"${MS_MARKER_DIR}/${HEX_CONV}\"*.json)',\n ' MS_REPLIED=no; if replied_this_conv_teams \"$CONVERSATION_ID\"; then MS_REPLIED=yes; fi',\n \" if emit_block_if_obligated \\\"$MS_REPLIED\\\" \\\"$MS_CAND\\\" 'teams.reply'; then exit 0; fi\",\n ' fi',\n ' recover_teams_for \"$CONVERSATION_ID\" \"$REPLY_TO_ID\" \"$SERVICE_URL\"',\n 'elif [ \"$TAG_SOURCE\" = \"direct-chat\" ]; then',\n ' # ENG-7808 (ENG-6722 fm4) + ENG-7814 (slice E): direct-chat backstop. First try',\n ' # block-turn-end (make the MODEL re-send). If that does not fire and the flag',\n ' # direct-chat-recovery is ON, re-send the composed text via the recovery-outbox',\n ' # (ENG-7814). session_id is unambiguous (one session per marker), so there is no',\n ' # cross-thread class and no recency guard is needed. With both off we just log the',\n ' # miss and stand down.',\n ' SESSION_ID=$(extract_attr \"$CHANNEL_TAG\" \"session_id\")',\n ' if [ -n \"$SESSION_ID\" ]; then',\n ' shopt -s nullglob',\n ' # Marker filenames hex-encode the session (injective; matches the MCP',\n ' # writer + the msteams pattern). Reproduce that hex here for the glob.',\n ' HEX_SESSION=$(printf %s \"$SESSION_ID\" | od -An -tx1 | tr -d \" \\\\n\")',\n ' DC_CAND=$(printf \\'%s\\\\n\\' \"${DC_MARKER_DIR}/${HEX_SESSION}__\"*.json)',\n ' DC_REPLIED=no; if replied_this_conv_directchat \"$SESSION_ID\"; then DC_REPLIED=yes; fi',\n \" if emit_block_if_obligated \\\"$DC_REPLIED\\\" \\\"$DC_CAND\\\" 'direct_chat.reply'; then exit 0; fi\",\n ' if [ \"$DC_REPLIED\" = \"no\" ]; then',\n ' # ENG-7944: call unconditionally — mirrors the always-on Slack/Telegram/Teams',\n ' # recovery paths. The MCP consumer (processDirectChatRecoveryOutboxFile) still',\n ' # gates on the direct-chat-recovery feature flag before delivering, so the hook',\n ' # just writes the outbox payload and lets the consumer decide. DC_RECOVERY_ON',\n ' # is no longer needed here.',\n ' recover_directchat_for \"$SESSION_ID\" \"$HEX_SESSION\"',\n ' fi',\n ' fi',\n 'elif [ -z \"$TAG_SOURCE\" ]; then',\n ' # ENG-6467: TAG_SOURCE=none fallback (AC1). Guarded on an EMPTY source, not',\n ' # a catch-all else: a parsed-but-unsupported source must NOT fall through here',\n ' # or its trailing text could be misdelivered to a pending marker on the wrong',\n ' # channel. The last <channel> tag could not be parsed (a malformed/partial',\n ' # preamble, or any residual extraction miss) yet a reply is owed. When EXACTLY',\n ' # ONE channel marker is pending agent-wide AND the final turn produced text',\n ' # with NO reply tool call, there is a single unambiguous candidate, so recover',\n ' # it. Cross-thread mis-correlation is impossible with one candidate, and the',\n ' # recover_*_for recency guard still suppresses if the agent moved on.',\n ' # ENG-7944: DC_PENDING is now included so a lone direct-chat pending marker',\n ' # with an unparsed tag can also be recovered via the single-candidate path.',\n ' TOTAL_PENDING=$(( TG_PENDING + SL_PENDING + MS_PENDING + DC_PENDING ))',\n ' if [ \"$TOTAL_PENDING\" = \"1\" ] && [ \"$REPLY_IN_LAST\" = \"no\" ]; then',\n ' shopt -s nullglob',\n ' if [ \"$SL_PENDING\" = \"1\" ]; then',\n ' for f in \"$SL_MARKER_DIR\"/*.json; do',\n ' FB_CH=$(jq -r \\'.channel // empty\\' \"$f\" 2>/dev/null || true)',\n ' FB_TH=$(jq -r \\'.thread_ts // empty\\' \"$f\" 2>/dev/null || true)',\n ' log_ghost \"fallback=tag_source_none channel=slack marker=$(basename \"$f\")\"',\n ' recover_slack_for \"$FB_CH\" \"$FB_TH\"',\n ' done',\n ' elif [ \"$TG_PENDING\" = \"1\" ]; then',\n ' for f in \"$TG_MARKER_DIR\"/*.json; do',\n ' FB_CID=$(jq -r \\'.chat_id // empty\\' \"$f\" 2>/dev/null || true)',\n ' FB_MID=$(jq -r \\'.message_id // empty\\' \"$f\" 2>/dev/null || true)',\n ' log_ghost \"fallback=tag_source_none channel=telegram marker=$(basename \"$f\")\"',\n ' recover_telegram_for \"$FB_CID\" \"$FB_MID\"',\n ' done',\n ' elif [ \"$MS_PENDING\" = \"1\" ]; then',\n ' for f in \"$MS_MARKER_DIR\"/*.json; do',\n ' FB_CONV=$(jq -r \\'.conversation_id // empty\\' \"$f\" 2>/dev/null || true)',\n ' FB_RID=$(jq -r \\'.reply_to_id // .activity_id // empty\\' \"$f\" 2>/dev/null || true)',\n ' FB_SVC=$(jq -r \\'.service_url // empty\\' \"$f\" 2>/dev/null || true)',\n ' log_ghost \"fallback=tag_source_none channel=msteams marker=$(basename \"$f\")\"',\n ' recover_teams_for \"$FB_CONV\" \"$FB_RID\" \"$FB_SVC\"',\n ' done',\n ' elif [ \"$DC_PENDING\" = \"1\" ]; then',\n ' # ENG-7944: single-candidate direct-chat recovery for unparsed TAG_SOURCE.',\n ' for f in \"$DC_MARKER_DIR\"/*.json; do',\n ' FB_SID=$(jq -r \\'.session_id // empty\\' \"$f\" 2>/dev/null || true)',\n ' FB_HSID=$(printf %s \"$FB_SID\" | od -An -tx1 | tr -d \" \\\\n\")',\n ' log_ghost \"fallback=tag_source_none channel=direct-chat marker=$(basename \"$f\")\"',\n ' recover_directchat_for \"$FB_SID\" \"$FB_HSID\"',\n ' done',\n ' fi',\n ' else',\n ' log_ghost \"skip=tag_source_none pending_total=$TOTAL_PENDING reply_in_last=$REPLY_IN_LAST\"',\n ' fi',\n 'fi',\n 'exit 0',\n ].join('\\n') + '\\n';\n\n writeFileSync(ghostHookPath, ghostHookScript, { mode: 0o755 });\n\n // CS-1549: the backlog-pull hook. Third Stop hook, written here so all three\n // are provisioned together and registered in one settings write below.\n // Take the path it actually wrote rather than re-deriving the filename here:\n // two spellings that drift leave settings.local.json pointing at a missing\n // file, and Claude Code reports a hook error on every Stop event.\n const backlogHookPath = provisionBacklogPullHook(codeName);\n\n // Write or update .claude/settings.local.json with all three Stop hooks\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n // Order matters. The ghost-reply hook must get first refusal on blocking the\n // turn: an unanswered person is a worse outcome than an unpulled backlog item,\n // and Claude Code surfaces one block reason. The backlog hook self-gates on\n // `stop_hook_active`, so once ghost-reply blocks, this one stands down for the\n // replayed turn rather than stacking a second demand on top.\n hooks['Stop'] = [\n {\n hooks: [\n { type: 'command', command: hookScriptPath },\n { type: 'command', command: ghostHookPath },\n { type: 'command', command: backlogHookPath },\n ],\n },\n ];\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n// ---------------------------------------------------------------------------\n// PreToolUse isolation hook — blocks cross-agent file access on shared hosts.\n//\n// Isolation model:\n// - Each agent runs in its own project dir: ~/.augmented/{codeName}/project/\n// - Agents must NOT access sibling dirs: ~/.augmented/{otherAgent}/\n// - The hook intercepts Read, Edit, Write, Bash, and Glob tool calls,\n// resolves file paths, and blocks any that target another agent's directory.\n// - Blocked attempts are logged to ~/.augmented/{codeName}/isolation.log\n// ---------------------------------------------------------------------------\nexport function provisionIsolationHook(codeName: string, agentId?: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // ENG-7891 / ADR-0049: under the id-keyed layout an agent's own files live\n // under ~/.augmented/{agent_id}/, so the \"own directory\" segment the deny\n // check compares against is the agent_id, not the codename. Accept BOTH: the\n // codename (legacy agents, and the compatibility symlink alias) and the\n // agent_id (id-keyed agents). agentId is UUID-validated before it reaches the\n // baked shell, so interpolating it is injection-safe. Distinguish \"omitted\"\n // (undefined - legacy provisioning, clause empty, behavior unchanged) from\n // \"provided but empty/invalid\" - the latter is a caller bug we fail loud on\n // via assertValidAgentId rather than silently downgrading to legacy and\n // dropping the id segment from the deny check.\n const hasAgentId = agentId !== undefined;\n if (hasAgentId) assertValidAgentId(agentId);\n const homeDir = getHomeDir();\n const augmentedBase = join(homeDir, '.augmented');\n const ownAgentDir = getAgentDir(codeName);\n const logFile = join(ownAgentDir, 'isolation.log');\n const idAllowClause = hasAgentId ? ` && [ \"$AGENT_DIR\" != \"${agentId}\" ]` : '';\n\n const hookScriptPath = join(claudeDir, 'agt-isolation-hook.sh');\n const hookScript = [\n '#!/bin/bash',\n '# Auto-generated by Augmented — prevents cross-agent file access.',\n '# Exit 0 = allow, Exit 2 = block (with stderr message shown to agent)',\n 'set -euo pipefail',\n 'INPUT=$(cat)',\n 'TOOL=$(echo \"$INPUT\" | jq -r \\'.tool_name // empty\\')',\n '',\n '# Only check file-access tools',\n 'case \"$TOOL\" in',\n ' Read|Edit|Write|Glob|Grep|MultiEdit) ;;',\n ' Bash)',\n ' # For Bash, we can\\'t reliably parse arbitrary commands — rely on allowedDirectories.',\n ' # But block obvious attempts to read other agent dirs.',\n ' CMD=$(echo \"$INPUT\" | jq -r \\'.tool_input.command // empty\\')',\n ` if echo \"$CMD\" | grep -qE '${augmentedBase}/[^/]+/' 2>/dev/null; then`,\n ` MATCH=$(echo \"$CMD\" | grep -oE '${augmentedBase}/[^/]+' | head -1)`,\n ` AGENT_DIR=$(basename \"$MATCH\")`,\n ` if [ \"$AGENT_DIR\" != \"${codeName}\" ]${idAllowClause} && [ \"$AGENT_DIR\" != \"_mcp\" ]; then`,\n ` echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) BLOCKED bash targeting $MATCH\" >> \"${logFile}\"`,\n ' echo \"Access denied: you cannot access other agents\\' directories.\" >&2',\n ' exit 2',\n ' fi',\n ' fi',\n ' exit 0 ;;',\n ' *) exit 0 ;;',\n 'esac',\n '',\n '# Extract file_path from tool input',\n 'FILE_PATH=$(echo \"$INPUT\" | jq -r \\'.tool_input.file_path // .tool_input.path // empty\\')',\n '[ -z \"$FILE_PATH\" ] && exit 0',\n '',\n '# Resolve to absolute path',\n 'if [[ \"$FILE_PATH\" != /* ]]; then',\n ' CWD=$(echo \"$INPUT\" | jq -r \\'.cwd // empty\\')',\n ' [ -n \"$CWD\" ] && FILE_PATH=\"$CWD/$FILE_PATH\"',\n 'fi',\n '',\n '# Check if path targets another agent\\'s directory',\n `AUGMENTED_BASE=\"${augmentedBase}\"`,\n 'case \"$FILE_PATH\" in',\n ' \"$AUGMENTED_BASE\"/*/*) ',\n ' AGENT_DIR=$(echo \"$FILE_PATH\" | sed \"s|$AUGMENTED_BASE/||\" | cut -d/ -f1)',\n ` if [ \"$AGENT_DIR\" != \"${codeName}\" ]${idAllowClause} && [ \"$AGENT_DIR\" != \"_mcp\" ]; then`,\n ` echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) BLOCKED $TOOL on $FILE_PATH\" >> \"${logFile}\"`,\n ' echo \"Access denied: you cannot access other agents\\' directories.\" >&2',\n ' exit 2',\n ' fi ;;',\n 'esac',\n '',\n 'exit 0',\n ].join('\\n') + '\\n';\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n // Add PreToolUse hook to .claude/settings.local.json\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n hooks['PreToolUse'] = [\n {\n hooks: [\n {\n type: 'command',\n command: hookScriptPath,\n },\n ],\n },\n ];\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n// ---------------------------------------------------------------------------\n// PostToolUse auto-progress hook (ENG-6179 / ENG-6241).\n//\n// Mirrors the agent's latest tool action onto its active in-thread kanban\n// progress card, so the card's Progress row tracks real work even when the\n// model never calls `kanban_progress` inline. Best-effort, silent, throttled\n// (~1 push / 15s); every path degrades to exit 0 and the push is\n// fire-and-forget so it can never block or fail a tool call.\n//\n// ENG-6241: the canonical copy of this script lives at\n// packages/claudecode-plugin-augmented/hooks/auto-kanban-progress.sh, but the\n// managed-agent fleet never installs that plugin (no marketplace entry, no CLI\n// bundle path), so the ENG-6179 hook was inert in prod — cards stayed frozen\n// on \"Waiting for the agent's first update…\". We inline it here and register\n// it in settings.local.json during provisioning, exactly like provisionStopHook\n// / provisionIsolationHook above. KEEP THE INLINED BODY BELOW IN SYNC WITH THE\n// PLUGIN COPY.\n// ---------------------------------------------------------------------------\n// Backlog-pull hook (CS-1549) — Stop (turn-end).\n//\n// THE FAILURE. An agent with a non-empty backlog sits idle while sincerely\n// reporting itself busy. Two mechanisms combine, and neither is the agent being\n// lazy:\n//\n// 1. The old Kanban Work Policy told agents, verbatim, \"if todo +\n// in_progress are empty but backlog has items, don't self-assign - ask\n// your manager once which to pick up, then stand down.\" That made\n// passivity CORRECT, so the backlog drained only as fast as a human\n// remembered to push work. (That text is rewritten in the same change as\n// this hook; while it stood, every skill and hook argued with the agent's\n// own instructions.)\n// 2. `waiting` reads as busy. An agent that parks three cards on human\n// review sees \"3 Waiting\" and concludes it is occupied. Every card is\n// correctly classified and the agent is still doing nothing.\n//\n// WHY A HOOK AND NOT A GUARDRAIL. CS-1549 asked for a guardrail. Outside\n// `email.domain_restrict` and `calendar.confidentiality`, the guardrail system\n// has NO runtime evaluator: `GuardrailViolation` and `GuardrailEvaluationResult`\n// are declared and never constructed, and `resolveGuardrailsForAgent`'s only\n// non-test callers are those two guards, one read-only GET, and the code that\n// renders guardrails into CLAUDE.md. A guardrail row set to `enforce` therefore\n// produces prose and enforces nothing - which is the exact failure this ticket\n// was filed about. The ticket's own words: \"an agent believing it is already\n// complying. A turn-end check does not require the agent to notice.\"\n//\n// SAFETY, copied deliberately from the ghost-reply hook rather than reinvented:\n//\n// * LEDGER. Each distinct top-of-backlog item can block AT MOST ONCE. An\n// agent that declines cannot be wedged in an infinite turn-end loop - it\n// states its reason, the turn ends, and the reason is in the transcript.\n// That IS the override Brad asked for: not a new approval rail, just a\n// block that cannot repeat itself.\n// * FAIL OPEN EVERYWHERE. Missing jq, missing curl, no agent id, API down,\n// unwritable ledger - every one exits 0 and lets the turn end. A hook that\n// traps an agent because the network blipped is far worse than a backlog\n// that drains a day slower.\n// ---------------------------------------------------------------------------\nexport function provisionBacklogPullHook(codeName: string): string {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n const hookScriptPath = join(claudeDir, 'agt-backlog-pull-hook.sh');\n const hookScript = `#!/usr/bin/env bash\n# Auto-generated by Augmented (CS-1549) — Stop hook: don't end a turn idle\n# beside a non-empty backlog. Fail-open on every path; blocks at most once per\n# distinct backlog item so it can never wedge a session.\nset -uo pipefail\ntrap 'exit 0' ERR\n\nINPUT=\"$(cat 2>/dev/null || true)\"\n[ -n \"$INPUT\" ] || INPUT=\"\\${CLAUDE_HOOK_INPUT:-}\"\n[ -n \"$INPUT\" ] || exit 0\ncommand -v jq >/dev/null 2>&1 || exit 0\ncommand -v curl >/dev/null 2>&1 || exit 0\n\n# Never re-block a turn that Claude Code is already replaying because a hook\n# blocked it. Without this two Stop hooks can ping-pong.\nSTOP_ACTIVE=\"$(printf '%s' \"$INPUT\" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)\"\n# ENG-4660 form. \\`[ test ] && exit 0\\` is safe here only because this script omits\n# \\`set -e\\` and the list is not last; adding either re-creates the silent non-zero\n# hook exit. Use the explicit form both other Stop hooks were converted to.\nif [ \"$STOP_ACTIVE\" = \"true\" ]; then exit 0; fi\n\n# CodeRabbit on #4134, and the finding that most undermines this hook:\n# stop_hook_active alone is NOT enough. On the FIRST Stop event Claude Code runs\n# all three commands in the same hook group, with stop_hook_active=false for\n# every one of them. So if ghost-reply blocks, this hook still runs, still burns\n# its one-shot ledger entry, and its reason is never the one shown - the backlog\n# nudge is consumed unseen, and that item can never nudge again.\n#\n# Standing down whenever a channel reply is OWED fixes it, and matches the\n# ordering documented at the Stop registration: an unanswered person beats an\n# unpulled card. Cheap - a directory test, and it runs before either curl.\nCWD_IN=\"$(printf '%s' \"$INPUT\" | jq -r '.cwd // empty' 2>/dev/null || true)\"\nif [ -n \"$CWD_IN\" ]; then\n OWED_BASE=\"$(dirname \"$CWD_IN\")\"\n for d in telegram-pending-inbound slack-pending-inbound direct-chat-pending-inbound msteams-pending-inbound/.markers; do\n if [ -d \"$OWED_BASE/$d\" ] && [ -n \"$(ls -A \"$OWED_BASE/$d\" 2>/dev/null || true)\" ]; then\n echo \"agt-backlog-pull-hook: standing down, a channel reply is owed\" >&2\n exit 0\n fi\n done\nfi\n\n# Identity. AGT_AGENT_ID is NOT exported into the parent claude's env (see\n# ENG-4823), so .mcp.json is the reliable source — the manager writes it\n# literally into every augmented MCP server's env block.\nPROJECT_DIR=\"\\${CLAUDE_PROJECT_DIR:-$PWD}\"\nAGENT_ID=\"\\${AGT_AGENT_ID:-}\"\nif [ -z \"$AGENT_ID\" ] && [ -f \"$PROJECT_DIR/.mcp.json\" ]; then\n AGENT_ID=\"$(jq -r '[.mcpServers[]?.env?.AGT_AGENT_ID // empty] | map(select(. != \"\")) | .[0] // empty' \"$PROJECT_DIR/.mcp.json\" 2>/dev/null || true)\"\nfi\nHOST=\"\\${AGT_HOST:-}\"\nKEY=\"\\${AGT_API_KEY:-}\"\n[ -n \"$AGENT_ID\" ] && [ -n \"$HOST\" ] && [ -n \"$KEY\" ] || exit 0\n\nJWT=\"$(curl -sf --connect-timeout 2 --max-time 5 -X POST \"\\${HOST}/host/exchange\" -H 'Content-Type: application/json' -d \"$(jq -n --arg k \"$KEY\" '{api_key:$k}')\" 2>/dev/null | jq -r '.token // empty' 2>/dev/null || true)\"\n[ -n \"$JWT\" ] || exit 0\n\nBOARD=\"$(curl -sf --connect-timeout 2 --max-time 5 -X POST \"\\${HOST}/host/my-kanban\" -H 'Content-Type: application/json' -H \"Authorization: Bearer $JWT\" -d \"$(jq -n --arg a \"$AGENT_ID\" '{agent_id:$a}')\" 2>/dev/null || true)\"\n[ -n \"$BOARD\" ] || exit 0\n\n# The counts that matter. \\`waiting\\` is deliberately NOT counted as work: a card\n# parked on a PR review or a human is someone else's work in progress, and\n# treating it as load is the exact loophole CS-1549 names. \\`needs_attention\\` is\n# excluded from the pullable set because the reaper parks stalled cards there\n# for a human — pulling one would just re-stall it.\nCOUNTS=\"$(printf '%s' \"$BOARD\" | jq -c '\n (.items // []) as $i\n | {\n in_progress: ([$i[] | select(.status == \"in_progress\")] | length),\n todo: ([$i[] | select(.status == \"todo\")] | length),\n pullable: [$i[] | select(.status == \"backlog\")]\n }\n | {in_progress, todo, n: ($i | length), top: (.pullable | first), count: (.pullable | length)}\n' 2>/dev/null || true)\"\n[ -n \"$COUNTS\" ] || exit 0\n\nIN_PROGRESS=\"$(printf '%s' \"$COUNTS\" | jq -r '.in_progress // 0')\"\nTODO=\"$(printf '%s' \"$COUNTS\" | jq -r '.todo // 0')\"\nBACKLOG_N=\"$(printf '%s' \"$COUNTS\" | jq -r '.count // 0')\"\nTOP_ID=\"$(printf '%s' \"$COUNTS\" | jq -r '.top.id // empty')\"\nTOP_TITLE=\"$(printf '%s' \"$COUNTS\" | jq -r '.top.title // empty')\"\n\n# Only fire when the agent is genuinely idle beside pullable work.\n[ \"$IN_PROGRESS\" = \"0\" ] || exit 0\n[ \"$TODO\" = \"0\" ] || exit 0\n[ \"$BACKLOG_N\" -gt 0 ] 2>/dev/null || exit 0\n[ -n \"$TOP_ID\" ] || exit 0\n\n# Ledger: at most one block per distinct top-of-backlog item. If the agent\n# declines and states a reason, the turn ends and this item never blocks again —\n# a NEW top item re-arms it. This is the override: it needs no approval rail,\n# and the stated reason lands in the transcript where it is reviewable.\nLEDGER_DIR=\"\\${HOME:-/tmp}/.augmented/.backlog-pull/\\${AGENT_ID}\"\nmkdir -p \"$LEDGER_DIR\" 2>/dev/null || exit 0\nSAFE_ID=\"$(printf '%s' \"$TOP_ID\" | tr -c 'A-Za-z0-9_.-' '_')\"\nLEDGER=\"$LEDGER_DIR/$SAFE_ID\"\n# ATOMIC acquire (CodeRabbit on #4134). A [ -e ] test then a : > write is a TOCTOU:\n# two concurrent Stop events can both observe no entry and both block. noclobber\n# makes create-if-absent one atomic operation - what every other ledger in this\n# file already uses. A failure means either \"already claimed\" or \"unwritable\";\n# both must NOT block, so either way exit 0.\nif ! ( set -o noclobber; : > \"$LEDGER\" ) 2>/dev/null; then exit 0; fi\n\n# Bound the ledger: one file per backlog item forever grows without limit on a\n# long-lived agent. Keep the newest 200. A pruned entry can at worst re-nudge\n# about an item that is still genuinely unpulled - the safe direction.\nLEDGER_N=\"$(ls -1 \"$LEDGER_DIR\" 2>/dev/null | wc -l | tr -d ' ')\"\nif [ \"\\${LEDGER_N:-0}\" -gt 200 ] 2>/dev/null; then\n ls -1t \"$LEDGER_DIR\" 2>/dev/null | tail -n +201 | while IFS= read -r stale; do\n rm -f \"$LEDGER_DIR/$stale\" 2>/dev/null || true\n done\nfi\n\necho \"agt-backlog-pull-hook: FIRED backlog=\\${BACKLOG_N} top=\\${TOP_ID}\" >&2\n\nREASON=\"You are ending this turn with nothing in progress and nothing in todo, while \\${BACKLOG_N} item(s) sit in your backlog. That is idle, not busy — cards in \\\\\\`waiting\\\\\\` are parked on someone else, so they are not your work in progress.\n\nPull the top item now: kanban_move(\\\\\\\"\\${TOP_ID}\\\\\\\", \\\\\\\"in_progress\\\\\\\") — \\\\\\\"\\${TOP_TITLE}\\\\\\\" — and start it.\n\nIf you genuinely should not pull it, say so in one line and why (it is deliberately parked, it is ambiguous and you have asked a specific question, or you are mid-flight on long-running external work). Then end the turn — this will not ask you again about this item.\"\n\njq -cn --arg r \"$REASON\" '{decision:\"block\", reason:$r}'\nexit 0\n`;\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n return hookScriptPath;\n}\n\n// ---------------------------------------------------------------------------\nexport function provisionAutoKanbanProgressHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n const hookScriptPath = join(claudeDir, 'agt-auto-kanban-progress-hook.sh');\n const hookScript = `#!/usr/bin/env bash\n# Auto-generated by Augmented (ENG-6179 / ENG-6241) — PostToolUse auto-progress.\n# Maps the agent's latest tool action onto its active in-thread kanban progress\n# card. Best-effort, silent, throttled; every path degrades to exit 0 and the\n# push is fire-and-forget so it never blocks or fails a tool call.\n# Canonical source: packages/claudecode-plugin-augmented/hooks/auto-kanban-progress.sh\n\n# Best-effort: any unexpected error just exits clean.\ntrap 'exit 0' ERR\n\n# Hook input (stdin canonical; CLAUDE_HOOK_INPUT kept as a fallback).\nINPUT=\"$(cat 2>/dev/null || true)\"\n[ -n \"$INPUT\" ] || INPUT=\"\\${CLAUDE_HOOK_INPUT:-}\"\n[ -n \"$INPUT\" ] || exit 0\ncommand -v jq >/dev/null 2>&1 || exit 0\ncommand -v curl >/dev/null 2>&1 || exit 0\n\nTOOL=\"$(printf '%s' \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null || true)\"\n[ -n \"$TOOL\" ] || exit 0\n\n# Skip tools that aren't \"work the requester wants narrated\". kanban_* (incl.\n# the explicit kanban_progress) and channel/status tools would only produce\n# noise — and a kanban_done clears the step anyway.\ncase \"$TOOL\" in\n *kanban*|*reply*|*ask_user*|*send_message*|*direct_chat*|status_*|TodoWrite|ExitPlanMode) exit 0 ;;\nesac\n\n# Identity + endpoint.\nPROJECT_DIR=\"\\${CLAUDE_PROJECT_DIR:-$PWD}\"\nAGENT_ID=\"\\${AGT_AGENT_ID:-}\"\nif [ -z \"$AGENT_ID\" ] && [ -f \"$PROJECT_DIR/.mcp.json\" ]; then\n # The manager writes AGT_AGENT_ID literally into every augmented MCP server's\n # env block; take the first non-empty one.\n AGENT_ID=\"$(jq -r '[.mcpServers[]?.env?.AGT_AGENT_ID // empty] | map(select(. != \"\")) | .[0] // empty' \"$PROJECT_DIR/.mcp.json\" 2>/dev/null || true)\"\nfi\nHOST=\"\\${AGT_HOST:-}\"\nKEY=\"\\${AGT_API_KEY:-}\"\n[ -n \"$AGENT_ID\" ] && [ -n \"$HOST\" ] && [ -n \"$KEY\" ] || exit 0\n\n# Throttle: at most one push per ~15s per agent.\nSTATE_DIR=\"\\${HOME:-/tmp}/.augmented/.auto-progress/\\${AGENT_ID}\"\nmkdir -p \"$STATE_DIR\" 2>/dev/null || exit 0\nSTAMP=\"$STATE_DIR/last-push\"\nNOW=\"$(date +%s)\"\nif [ -f \"$STAMP\" ]; then\n LAST=\"$(cat \"$STAMP\" 2>/dev/null || echo 0)\"\n case \"$LAST\" in ''|*[!0-9]*) LAST=0 ;; esac\n [ $(( NOW - LAST )) -ge 15 ] || exit 0\nfi\n\n# Derive the one-line \"what I'm doing right now\" step.\nSTEP=\"$(printf '%s' \"$INPUT\" | jq -r '\n def base(p): (p | tostring | ltrimstr(\"./\") | split(\"/\") | last);\n .tool_name as $t | (.tool_input // {}) as $i |\n if $t == \"Bash\" then ($i.description // (\"Running: \" + (($i.command // \"\") | tostring | .[0:80])))\n elif $t == \"Edit\" then (\"Editing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"MultiEdit\" then (\"Editing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"Write\" then (\"Writing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"NotebookEdit\" then (\"Editing \" + base($i.notebook_path // \"a notebook\"))\n elif $t == \"Read\" then (\"Reading \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"Grep\" then (\"Searching for \" + (($i.pattern // \"\") | tostring | .[0:60]))\n elif $t == \"Glob\" then (\"Finding files: \" + (($i.pattern // \"\") | tostring | .[0:60]))\n elif $t == \"WebSearch\" then (\"Searching the web: \" + (($i.query // \"\") | tostring | .[0:60]))\n elif $t == \"WebFetch\" then (\"Fetching \" + (($i.url // \"\") | tostring | .[0:80]))\n elif ($t == \"Task\" or $t == \"Agent\") then (\"Working on: \" + (($i.description // \"a subtask\") | tostring | .[0:80]))\n elif ($t | startswith(\"mcp__\")) then (\"Using \" + ($t | sub(\"^mcp__\"; \"\") | gsub(\"__\"; \" / \")))\n else (\"Working (\" + $t + \")\")\n end\n' 2>/dev/null || true)\"\nSTEP=\"$(printf '%s' \"$STEP\" | tr '\\\\n\\\\t' ' ' | sed 's/ */ /g' | cut -c1-240)\"\n[ -n \"$STEP\" ] || exit 0\n\n# Stamp BEFORE the network call so a slow API can't unthrottle us.\nprintf '%s' \"$NOW\" > \"$STAMP\" 2>/dev/null || true\n\n# Bearer JWT (exchange tlk_ -> JWT, cached with expiry).\nTOKEN_FILE=\"$STATE_DIR/token\"\nTOKEN=\"\"\nif [ -f \"$TOKEN_FILE\" ]; then\n T_EXP=\"$(jq -r '.exp // 0' \"$TOKEN_FILE\" 2>/dev/null || echo 0)\"\n case \"$T_EXP\" in ''|*[!0-9]*) T_EXP=0 ;; esac\n [ $(( T_EXP - NOW )) -gt 300 ] && TOKEN=\"$(jq -r '.token // empty' \"$TOKEN_FILE\" 2>/dev/null || true)\"\nfi\nif [ -z \"$TOKEN\" ]; then\n EX=\"$(curl -fsS --max-time 5 -X POST \"$HOST/host/exchange\" \\\\\n -H 'Content-Type: application/json' \\\\\n -d \"{\\\\\"host_key\\\\\":\\\\\"$KEY\\\\\"}\" 2>/dev/null || true)\"\n TOKEN=\"$(printf '%s' \"$EX\" | jq -r '.token // empty' 2>/dev/null || true)\"\n [ -n \"$TOKEN\" ] || exit 0\n EXP_ISO=\"$(printf '%s' \"$EX\" | jq -r '.expires_at // empty' 2>/dev/null || true)\"\n # GNU date (Linux hosts) first, then BSD date (macOS dev machines), then a\n # safe 30-min fallback so a parse miss doesn't poison the token cache.\n EXP_EPOCH=\"$(date -d \"$EXP_ISO\" +%s 2>/dev/null \\\\\n || date -j -f '%Y-%m-%dT%H:%M:%SZ' \"$EXP_ISO\" +%s 2>/dev/null \\\\\n || echo $(( NOW + 1800 )))\"\n ( umask 177; printf '{\"token\":\"%s\",\"exp\":%s}' \"$TOKEN\" \"$EXP_EPOCH\" > \"$TOKEN_FILE\" 2>/dev/null || true )\nfi\n\n# Fire-and-forget the push (never block the agent's tool flow).\nBODY=\"$(jq -nc --arg a \"$AGENT_ID\" --arg s \"$STEP\" '{agent_id:$a, step:$s}' 2>/dev/null || printf '{\"agent_id\":\"%s\",\"step\":\"%s\"}' \"$AGENT_ID\" \"$STEP\")\"\n( curl -fsS --max-time 4 -X POST \"$HOST/host/kanban/auto-progress\" \\\\\n -H 'Content-Type: application/json' \\\\\n -H \"Authorization: Bearer $TOKEN\" \\\\\n -d \"$BODY\" >/dev/null 2>&1 || true ) &\n\nexit 0\n`;\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n // Register the hook in .claude/settings.local.json under PostToolUse. Read-\n // modify-write preserves the Stop / PreToolUse / SessionStart keys written by\n // the sibling provision*Hook functions earlier in the same provisioning pass.\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n hooks['PostToolUse'] = [\n {\n hooks: [\n { type: 'command', command: hookScriptPath },\n ],\n },\n ];\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n// ---------------------------------------------------------------------------\n// PostToolUse channel-progress heartbeat hook (ENG-6567 Phase 2).\n//\n// Writes a throttled, local-only heartbeat — {step, updated_at_ms} — to\n// ~/.augmented/{codeName}/channel-progress-heartbeat.json describing the\n// agent's latest work step. The channel MCP server reads it on a timer and,\n// while a non-discretionary inbound is pending and the flag `channel-live-progress`\n// is on, maintains a slimline \"⏳ working…\" Block Kit message on the thread,\n// clearing it the moment the final reply lands. The heartbeat is cheap and\n// purely local; rendering is what's flag-gated, so the feature ships dark even\n// though the heartbeat is always written. Best-effort: every path degrades to\n// exit 0; never blocks or fails a tool call.\n//\n// Registered by APPENDING into the PostToolUse group the auto-kanban-progress\n// hook owns (it runs first in the provisioning sequence) — idempotent, so a\n// re-provision pass never duplicates or drops either command.\n// ---------------------------------------------------------------------------\nexport function provisionChannelProgressHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n const hookScriptPath = join(claudeDir, 'agt-channel-progress-hook.sh');\n const hookScript = `#!/usr/bin/env bash\n# Auto-generated by Augmented (ENG-6567 Phase 2) — PostToolUse channel-progress\n# heartbeat. Writes a throttled local {step, updated_at_ms} the channel MCP reads\n# to drive a live in-thread progress indicator. Best-effort, silent, local-only.\ntrap 'exit 0' ERR\nINPUT=\"$(cat 2>/dev/null || true)\"\n[ -n \"$INPUT\" ] || INPUT=\"\\${CLAUDE_HOOK_INPUT:-}\"\n[ -n \"$INPUT\" ] || exit 0\ncommand -v jq >/dev/null 2>&1 || exit 0\n\nTOOL=\"$(printf '%s' \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null || true)\"\n[ -n \"$TOOL\" ] || exit 0\n# Skip tools that aren't narratable work (same set as auto-kanban-progress).\ncase \"$TOOL\" in\n *kanban*|*reply*|*ask_user*|*send_message*|*direct_chat*|status_*|TodoWrite|ExitPlanMode) exit 0 ;;\nesac\n\n# AGENT_DIR = ~/.augmented/<codeName> (parent of the project dir).\nPROJECT_DIR=\"\\${CLAUDE_PROJECT_DIR:-$PWD}\"\nAGENT_DIR=\"$(dirname \"$PROJECT_DIR\")\"\n[ -d \"$AGENT_DIR\" ] || exit 0\n\n# Throttle: at most one heartbeat per ~10s (the renderer polls slower).\nSTAMP=\"$AGENT_DIR/.channel-progress-last\"\nNOW=\"$(date +%s)\"\nif [ -f \"$STAMP\" ]; then\n LAST=\"$(cat \"$STAMP\" 2>/dev/null || echo 0)\"\n case \"$LAST\" in ''|*[!0-9]*) LAST=0 ;; esac\n [ $(( NOW - LAST )) -ge 10 ] || exit 0\nfi\n\n# Derive the one-line \"what I'm doing right now\" step (mirrors auto-kanban-progress).\nSTEP=\"$(printf '%s' \"$INPUT\" | jq -r '\n def base(p): (p | tostring | ltrimstr(\"./\") | split(\"/\") | last);\n .tool_name as $t | (.tool_input // {}) as $i |\n if $t == \"Bash\" then ($i.description // (\"Running: \" + (($i.command // \"\") | tostring | .[0:80])))\n elif $t == \"Edit\" then (\"Editing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"MultiEdit\" then (\"Editing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"Write\" then (\"Writing \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"Read\" then (\"Reading \" + base($i.file_path // $i.filePath // \"a file\"))\n elif $t == \"Grep\" then (\"Searching for \" + (($i.pattern // \"\") | tostring | .[0:60]))\n elif $t == \"Glob\" then (\"Finding files: \" + (($i.pattern // \"\") | tostring | .[0:60]))\n elif $t == \"WebSearch\" then (\"Searching the web: \" + (($i.query // \"\") | tostring | .[0:60]))\n elif $t == \"WebFetch\" then (\"Fetching \" + (($i.url // \"\") | tostring | .[0:80]))\n elif ($t == \"Task\" or $t == \"Agent\") then (\"Working on: \" + (($i.description // \"a subtask\") | tostring | .[0:80]))\n elif ($t | startswith(\"mcp__\")) then (\"Using \" + ($t | sub(\"^mcp__\"; \"\") | gsub(\"__\"; \" / \")))\n else (\"Working (\" + $t + \")\")\n end\n' 2>/dev/null || true)\"\nSTEP=\"$(printf '%s' \"$STEP\" | tr '\\\\n\\\\t' ' ' | sed 's/ */ /g' | cut -c1-160)\"\n[ -n \"$STEP\" ] || exit 0\n\nprintf '%s' \"$NOW\" > \"$STAMP\" 2>/dev/null || true\n# Atomic write: per-process tmp + rename so the renderer never reads a\n# half-written file AND overlapping PostToolUse hook processes can't clobber\n# each other's tmp (CodeRabbit, ENG-6567). $$ + $RANDOM keeps the temp unique;\n# clean it up if the rename never happens.\nHB=\"$AGENT_DIR/channel-progress-heartbeat.json\"\nTMP=\"$AGENT_DIR/.channel-progress-heartbeat.$$.\\${RANDOM}.tmp\"\nif jq -nc --arg s \"$STEP\" --argjson t \"$(( NOW * 1000 ))\" '{step:$s, updated_at_ms:$t}' > \"$TMP\" 2>/dev/null; then\n mv -f \"$TMP\" \"$HB\" 2>/dev/null || rm -f \"$TMP\" 2>/dev/null || true\nelse\n rm -f \"$TMP\" 2>/dev/null || true\nfi\nexit 0\n`;\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n // Idempotent append into the PostToolUse group. The auto-kanban-progress hook\n // (which runs earlier in the provisioning sequence) owns/overwrites the key to\n // a single-command group; we add ours as a second command without dropping it.\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n const groups = Array.isArray(hooks['PostToolUse'])\n ? (hooks['PostToolUse'] as Array<{ hooks?: Array<{ type: string; command: string }> }>)\n : [];\n const cmd = { type: 'command', command: hookScriptPath };\n const already = groups.some((g) => (g.hooks ?? []).some((h) => h.command === hookScriptPath));\n if (!already) {\n if (groups.length > 0) {\n groups[0]!.hooks = [...(groups[0]!.hooks ?? []), cmd];\n } else {\n groups.push({ hooks: [cmd] });\n }\n }\n hooks['PostToolUse'] = groups;\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n// ---------------------------------------------------------------------------\n// SessionStart orient hook (ENG-5397).\n//\n// Every spawn starts a brand-new Claude Code conversation (--session-id with\n// a fresh UUID, never --resume — see persistent-session.ts ENG-5397). On\n// boot, this hook fires and prints structured orientation context to stdout,\n// which Claude Code injects into the new session before the agent's first\n// turn. The agent reads it as part of its initial context: \"today's date is\n// X, you have N unread Slack threads in pending-inbound, etc.\"\n//\n// This is the structural replacement for --resume's transcript-replay\n// continuity. Instead of leaning on Anthropic's request shape (which broke\n// fleet-wide as recently as ENG-5353 / Claude Code 2.1.139), we lean on\n// surfaces we own: filesystem pending-inbound, and — as follow-ups — the\n// augmented API (kanban, audit_log, memory).\n//\n// First-cut data sources: agent codename, current local time, pending-inbound\n// counts per channel. API-backed sources (kanban, audit_log, memory) are\n// scaffolded as TODOs and will land in follow-up tickets.\n// ---------------------------------------------------------------------------\nexport function provisionOrientHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // ENG-7891 / ADR-0049: bake the resolved agent dir (id-keyed under the new\n // layout, the codename dir under legacy) so the path stays valid after a\n // rename and matches the id-keyed spawn cwd. getAgentDir resolves the\n // codename compatibility symlink; for legacy agents it is the codename dir.\n const agentDir = getAgentDir(codeName);\n\n const hookScriptPath = join(claudeDir, 'agt-orient-hook.sh');\n const hookScript = [\n '#!/bin/bash',\n '# Auto-generated by Augmented (ENG-5397) — SessionStart orientation hook.',\n '# stdout is injected into the new Claude Code session as additional context.',\n 'set -uo pipefail',\n '# Soft-fail: any error must not block session boot. Print whatever we have.',\n 'trap \\'ec=$?; echo \"(orient hook hit error code $ec at line $LINENO — partial context above)\" >&2; exit 0\\' ERR',\n '',\n `CODE_NAME=\"${codeName}\"`,\n `AGENT_DIR=\"${agentDir}\"`,\n 'NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)',\n 'NOW_LOCAL=$(date \"+%Y-%m-%d %H:%M %Z\")',\n '',\n '# ENG-6703: the SessionStart \"source\" tells us whether this boot RESUMED the',\n '# prior conversation context (--resume) or is a FRESH respawn with no',\n '# transcript (wedge / new day / rotated session). Branch the orientation',\n '# framing on it so a resumed agent continues its in-flight work instead of',\n '# being told (wrongly) that its context is gone. Read stdin once; prefer',\n '# .source, fall back to .matcher (test harness), then \"startup\" if absent or',\n '# no jq on the host.',\n 'HOOK_INPUT=$(cat 2>/dev/null || true)',\n '# Prefer .source, fall back to .matcher (test harness), then \"startup\".',\n '# Skip null AND empty-string values - jq\\'s `//` only falls back on null/false,',\n '# so an explicit empty .source would otherwise win over a real .matcher',\n '# (CodeRabbit, PR #2385).',\n 'SOURCE=$(printf \"%s\" \"$HOOK_INPUT\" | jq -r \\'[.source, .matcher] | map(select(. != null and . != \"\")) | (.[0] // \"startup\")\\' 2>/dev/null || echo startup)',\n '# jq on empty/invalid input exits 0 with no output, so coerce a blank result',\n '# (and a literal jq \"null\") back to the fresh-boot default.',\n 'if [ -z \"$SOURCE\" ] || [ \"$SOURCE\" = \"null\" ]; then SOURCE=startup; fi',\n '',\n 'echo \"# Orientation\"',\n 'echo',\n 'if [ \"$SOURCE\" = \"resume\" ]; then',\n ' echo \"You are **${CODE_NAME}**. You were just restarted, but this session RESUMED your prior conversation context (your earlier transcript is available above). You may have been interrupted mid-task. Re-read your last few messages, reconcile them with the signals below (recent kanban work and any queued messages), and CONTINUE whatever was in flight. Before repeating an action, check what you already did (your transcript plus the kanban card state) so you never duplicate a reply or a side effect.\"',\n 'else',\n ' echo \"You are **${CODE_NAME}**. This is a fresh session: your previous in-conversation context is NOT available. Reconstruct what you were doing from the signals below (recent kanban work and any queued messages) before responding, and continue any task that was in flight.\"',\n 'fi',\n 'echo',\n 'echo \"- Current time: ${NOW_LOCAL} (${NOW_ISO})\"',\n 'echo \"- Session source: ${SOURCE}\"',\n '',\n '# --- Pending-inbound counts + details -----------------------------------',\n '# Channel MCPs queue inbound messages here while the claude process is',\n '# starting / down — and ENG-6289 wedge respawns PARK undrained markers',\n '# here for exactly this hook to surface. A non-empty queue is real user',\n '# traffic this fresh session must deal with. Markers already flagged',\n '# undeliverable were ⏳-noticed at arrival and are excluded.',\n '# Per-marker details (sender, age, addressing ids, content snippet) give',\n '# the agent enough to reply via the channel tools when the channel-side',\n '# replay (AGT_CHANNEL_REPLAY_ENABLED) is off — without replay, nothing',\n '# else ever re-delivers a parked message. msteams is count-only: its',\n '# queue files are raw activities the channel server re-delivers itself',\n '# on boot. Detail lines cap at 5 per channel to bound context size.',\n 'echo',\n 'echo \"## Pending inbound\"',\n 'pending_total=0',\n 'shopt -s nullglob',\n 'for channel in slack telegram direct-chat msteams; do',\n ' dir=\"${AGENT_DIR}/${channel}-pending-inbound\"',\n ' if [ ! -d \"$dir\" ]; then continue; fi',\n ' count=0',\n ' details=\"\"',\n ' for marker in \"$dir\"/*; do',\n ' if [ ! -f \"$marker\" ]; then continue; fi',\n ' case \"$(basename \"$marker\")\" in .*) continue ;; esac',\n ' # One jq per marker: emits the sentinel for undeliverable markers, the',\n ' # detail line otherwise. jq failure (malformed / drained mid-scan / no',\n ' # jq on host) → empty line → counted live with no detail, matching the',\n ' # live-scan philosophy (corrupt must never hide a message).',\n ' line=$(jq -r \\'def mins: (((now - ((.received_at // \"\" | sub(\"\\\\\\\\.[0-9]+Z$\"; \"Z\")) | fromdateiso8601? // now)) / 60) | floor | tostring) + \"m ago\"; if (.undeliverable // false) == true then \"__UNDELIVERABLE__\" else (. as $m | ([(\"chat_id\",\"message_id\",\"channel\",\"thread_ts\",\"session_id\") | select($m[.] != null) | \"\\\\(.)=\\\\($m[.])\"] | join(\" \")) as $addr | (($m.payload.content // \"\") | gsub(\"[\\\\\\\\n\\\\\\\\r\\\\\\\\t]+\"; \" \") | .[0:140]) as $snippet | \"from \" + ($m.payload.meta.user_name // $m.meta.user_name // \"unknown sender\") + \", \" + mins + (if ($addr | length) > 0 then \" [\" + $addr + \"]\" else \"\" end) + (if ($snippet | length) > 0 then \" — \" + $snippet else \"\" end)) end\\' \"$marker\" 2>/dev/null || true)',\n ' if [ \"$line\" = \"__UNDELIVERABLE__\" ]; then continue; fi',\n ' count=$((count + 1))',\n ' if [ \"$channel\" != \"msteams\" ] && [ \"$count\" -le 5 ] && [ -n \"$line\" ]; then',\n ' details=\"${details} - ${line}\"$\\'\\\\n\\'',\n ' fi',\n ' done',\n ' if [ \"$count\" -gt 0 ]; then',\n ' echo \"- ${channel}: ${count} queued message(s)\"',\n ' if [ -n \"$details\" ]; then printf \"%s\" \"$details\"; fi',\n ' pending_total=$((pending_total + count))',\n ' fi',\n 'done',\n 'if [ \"$pending_total\" -eq 0 ]; then',\n ' echo \"- No queued messages on any channel.\"',\n 'fi',\n '',\n '# --- Recent work (kanban) - ENG-6703 ----------------------------------',\n '# The kanban board is the DURABLE record of what this agent was doing - it',\n '# survives a restart even when the in-conversation transcript does not, so',\n '# it is the primary way a fresh respawn reconstructs in-flight work (and a',\n '# cross-check for a resumed one). We instruct the agent to query it via its',\n '# own kanban tools rather than fetching here: the agent already has',\n '# kanban_list (active + last 24h done) and kanban_search (older/closed),',\n '# scoped to its identity, so there is no auth-aware HTTP path to build in',\n '# the hook and no risk of embedding stale board state.',\n '# (audit_log + memory remain ENG-5397 follow-ups.)',\n 'echo',\n 'echo \"## Recent work (kanban)\"',\n 'echo',\n 'echo \"Before you respond, call **kanban_list** to see your in-progress and recently-completed cards and reconstruct what you were working on. If a card is still \\\\\"in_progress\\\\\" (or a queued message above maps to one), that is almost certainly the work you were interrupted on - pick it back up. Use **kanban_search** for older or already-closed cards if needed. As you resume, update the card (kanban_progress / kanban_log) so the next restart has an even clearer trail. Do NOT re-do a card already marked done.\"',\n '',\n 'echo',\n 'echo \"## Next step\"',\n 'echo',\n '# ENG-6289: with queued messages present the old \"do not act until a',\n '# message arrives\" instruction guaranteed a parked message was never',\n '# answered. Replay-enabled hosts get re-delivery (answering from the',\n '# details too would double-reply); replay-off hosts must act on the',\n '# details above — nothing else will ever re-deliver.',\n '# ENG-6540: resolve EFFECTIVE channel-replay the same way',\n '# resolveHostBooleanFlag does post-ENG-6503 (env override > manager flags',\n '# cache > compiled default), so a CENTRAL flag flip (no env var set) still',\n '# picks the right copy - otherwise a flag-only flip leaves this reading the',\n '# unset env, prints the wrong instruction, and the agent double-replies',\n '# (answers from the details AND replay re-delivers) or drops a message.',\n '# ENG-6683: the compiled default is true (replay is permanently enabled',\n '# fleet-wide); it must match channelReplayEnabled()\\'s default so the two',\n '# never disagree. Only an explicit cache \"false\" (or env off) turns it off.',\n 'REPLAY_ON=true',\n 'case \"${AGT_CHANNEL_REPLAY_ENABLED:-}\" in',\n ' true|1|yes|on) REPLAY_ON=true ;;',\n ' false|0|no|off) REPLAY_ON=false ;;',\n ' *)',\n ' # No env override: the heartbeat-cache value wins if present, else the',\n ' # compiled default (true). Read WITHOUT jq\\'s // operator - `false // x`',\n ' # collapses a boolean false to the alternative, so an explicit cache',\n ' # \"false\" (the kill switch) would be indistinguishable from \"absent\".',\n ' FLAGS_CACHE=\"$(dirname \"$AGENT_DIR\")/flags-cache.json\"',\n ' if [ -f \"$FLAGS_CACHE\" ] && [ \"$(jq -r \\'.flags[\"channel-replay\"]\\' \"$FLAGS_CACHE\" 2>/dev/null || echo null)\" = \"false\" ]; then',\n ' REPLAY_ON=false',\n ' fi',\n ' ;;',\n 'esac',\n 'if [ \"$pending_total\" -gt 0 ]; then',\n ' if [ \"$REPLAY_ON\" = \"true\" ]; then',\n ' echo \"Respond \\\\\"Ready.\\\\\" once. The queued messages above will be re-delivered to you by the channel server within a few minutes — answer each as it arrives (acknowledge before tool use). Do not reply from the queue details directly; wait for the re-delivery.\"',\n ' else',\n ' echo \"Respond \\\\\"Ready.\\\\\" once, then immediately work through the queued messages above, oldest first — they are real user messages from before this session started and will NOT be re-delivered. Use the addressing ids with the matching channel reply tool (slack.reply with channel + thread_ts, telegram.reply with chat_id, etc.). If a queue detail lacks the message content, first try the channel\\'s thread/history tools to read the conversation (Slack threads can be re-read); only if the content is truly unrecoverable, say so honestly in your reply and ask the user to resend.\"',\n ' fi',\n 'elif [ \"$SOURCE\" = \"resume\" ]; then',\n ' # ENG-6703: a resumed session with no queued message may still have been',\n ' # interrupted mid-task - re-read context + kanban and continue rather than',\n ' # idle-waiting (which would strand the in-flight work).',\n ' echo \"Respond \\\\\"Ready.\\\\\" once. Re-read your recent transcript and your kanban (above): if you were mid-task, continue it now. Otherwise wait for the next user or channel message before running tools.\"',\n 'else',\n ' echo \"Respond \\\\\"Ready.\\\\\" once. Do not run any tools or load any data until a real user or channel message arrives. When you do receive a message, acknowledge it before tool use.\"',\n 'fi',\n 'exit 0',\n ].join('\\n') + '\\n';\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n // ENG-6703: register for BOTH `startup` (fresh respawn) AND `resume`\n // (--resume, context restored). The manager now resumes sessions with their\n // transcript (ENG-6088/6375), and a `startup`-only matcher would skip the\n // orient hook on exactly those resumes - so a restarted-but-resumed agent\n // never got the \"you were interrupted, check kanban, continue\" nudge. The\n // hook branches its copy on the SessionStart `source`. `clear`/`compact` stay\n // unmatched: those are operator/compaction flows, not restarts.\n //\n // Upsert rather than overwrite: plugin-managed agents may have registered\n // their own SessionStart entries (e.g. claudecode-plugin-augmented).\n // Clobbering the array would silently strip them. We add our orient entry per\n // matcher only if an equivalent one (same matcher + same command path) isn't\n // already present - keeps reprovisioning idempotent and co-exists with future\n // plugin-registered startup hooks.\n const existingSessionStart = Array.isArray(hooks['SessionStart'])\n ? [...(hooks['SessionStart'] as Array<Record<string, unknown>>)]\n : [];\n\n for (const matcher of ['startup', 'resume'] as const) {\n const alreadyRegistered = existingSessionStart.some((entry) => {\n const entryMatcher = (entry as { matcher?: unknown }).matcher;\n const entryHooks = (entry as { hooks?: unknown }).hooks;\n return (\n entryMatcher === matcher &&\n Array.isArray(entryHooks) &&\n entryHooks.some(\n (h) =>\n typeof h === 'object' &&\n h !== null &&\n (h as { type?: unknown }).type === 'command' &&\n (h as { command?: unknown }).command === hookScriptPath,\n )\n );\n });\n if (!alreadyRegistered) {\n existingSessionStart.push({\n matcher,\n hooks: [{ type: 'command', command: hookScriptPath }],\n });\n }\n }\n hooks['SessionStart'] = existingSessionStart;\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n/**\n * ENG-7339: provision the PreCompact courtesy-notice hook.\n *\n * When a persistent Claude Code session compacts its context, the session\n * pauses and stops replying for a stretch, which from the channel side looks\n * identical to a dead agent. This hook fires just before compaction and, if a\n * conversation is live (the last `<channel ...>` tag in the transcript), drops\n * a short \"reorganizing my memory, back shortly\" notice into the matching\n * `<channel>-notice-outbox`. The channel MCP server (a separate process that\n * stays alive while the Claude process compacts) posts it. The agent itself\n * cannot send during compaction, which is exactly why an out-of-band notice is\n * needed.\n *\n * Design mirrors the ghost-reply Stop hook (same transcript `<channel>`-tag\n * scan for the active conversation) but is much simpler: it only writes a\n * notice file. It NEVER touches the pending-inbound marker, so the genuine\n * reply the agent still owes after compaction is untouched. No active channel\n * tag ⇒ silent, so idle agents (and non-channel work like kanban) never\n * broadcast. Gated dark behind the `compaction-notice` flag (env override\n * AGT_COMPACTION_NOTICE_ENABLED > manager flags-cache > compiled default off),\n * resolved in bash the same way the orient hook resolves channel-replay.\n */\nexport function provisionPreCompactHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // ENG-7891 / ADR-0049: bake the resolved agent dir (id-keyed under the new\n // layout, the codename dir under legacy) so the path stays valid after a\n // rename and matches the id-keyed spawn cwd. getAgentDir resolves the\n // codename compatibility symlink; for legacy agents it is the codename dir.\n const agentDir = getAgentDir(codeName);\n\n // Total-normalize the transcript record's content the same way the\n // ghost-reply hook does (ENG-6288): a jq type error on an unexpected shape is\n // swallowed by `2>/dev/null` and would silently no-op the tag scan, so map\n // string → one text block, array → as-is, anything else → [].\n const jqNormalizeContent =\n '(.message.content // .content // []) | if type == \"string\" then [{type: \"text\", text: .}] elif type == \"array\" then . else [] end';\n\n const hookScriptPath = join(claudeDir, 'agt-pre-compact-hook.sh');\n const hookScript = [\n '#!/bin/bash',\n '# Auto-generated by Augmented (ENG-7339) - PreCompact courtesy notice.',\n '# Fires just before the session compacts its context. If a channel',\n '# conversation is live, drops a notice into <channel>-notice-outbox so the',\n '# channel MCP server can tell the user the agent is briefly reorganizing',\n \"# memory (the agent can't speak during compaction). Must never block\",\n '# compaction: every failure path exits 0.',\n 'set -uo pipefail',\n 'trap \\'ec=$?; echo \"agt-pre-compact-hook failed (exit $ec) at line $LINENO: $BASH_COMMAND\" >&2; exit 0\\' ERR',\n '',\n `AGENT_DIR=\"${agentDir}\"`,\n 'INPUT=$(cat 2>/dev/null || true)',\n '',\n '# --- Gate: compaction-notice flag (env override > flags-cache > on) ------',\n '# Default ON (ENG-8040: promoted from dark to on-by-default). Mirrors the',\n '# orient hook\\'s flag resolution; the cache lives one dir above AGENT_DIR at',\n '# ~/.augmented/flags-cache.json. Explicit false (env or cache) disables.',\n 'NOTICE_ON=1',\n 'case \"${AGT_COMPACTION_NOTICE_ENABLED:-}\" in',\n ' true|1|yes|on) NOTICE_ON=1 ;;',\n ' false|0|no|off) NOTICE_ON=0 ;;',\n ' *)',\n ' FLAGS_CACHE=\"$(dirname \"$AGENT_DIR\")/flags-cache.json\"',\n ' if [ -f \"$FLAGS_CACHE\" ] && [ \"$(jq -r \\'.flags[\"compaction-notice\"]\\' \"$FLAGS_CACHE\" 2>/dev/null || echo null)\" = \"false\" ]; then',\n ' NOTICE_ON=0',\n ' fi',\n ' ;;',\n 'esac',\n 'if [ \"$NOTICE_ON\" != \"1\" ]; then exit 0; fi',\n '',\n '# --- Debounce: at most one notice per agent per 60s ----------------------',\n '# Auto-compaction is a whole-session event; a single window is enough and',\n '# guards against a manual+auto double-fire spamming the user.',\n 'NOTICE_MARKER=\"${AGENT_DIR}/.compaction-notice-last\"',\n 'if [ -f \"$NOTICE_MARKER\" ]; then',\n ' LAST=$(cat \"$NOTICE_MARKER\" 2>/dev/null || echo 0)',\n ' NOW=$(date +%s 2>/dev/null || echo 0)',\n ' if [ \"${LAST:-0}\" -gt 0 ] 2>/dev/null && [ $((NOW - LAST)) -lt 60 ] 2>/dev/null; then exit 0; fi',\n 'fi',\n '',\n '# --- Find the live conversation (last <channel ...> tag) -----------------',\n 'TRANSCRIPT_PATH=$(printf \"%s\" \"$INPUT\" | jq -r \\'.transcript_path // empty\\' 2>/dev/null || true)',\n 'if [ -z \"$TRANSCRIPT_PATH\" ] || [ ! -f \"$TRANSCRIPT_PATH\" ]; then exit 0; fi',\n '# Scan only the tail, skip assistant-authored records (an agent quoting a',\n '# <channel> tag in its own text must not become the target), flatten',\n '# newlines per text block so a multi-line thread_context preamble cannot',\n '# split the opening tag across lines (ENG-6467). Take the LAST tag = the',\n '# conversation the user is currently on.',\n `CHANNEL_TAG=$(tail -400 \"$TRANSCRIPT_PATH\" | jq -r 'select((.type // \"\") != \"assistant\" and (.role // \"\") != \"assistant\") | ${jqNormalizeContent} | map(select(type == \"object\" and .type == \"text\") | .text | gsub(\"[\\\\n\\\\r]+\"; \" \")) | join(\" \")' 2>/dev/null | grep -oE '<channel [^>]+>' | tail -1 || true)`,\n 'if [ -z \"$CHANNEL_TAG\" ]; then exit 0; fi',\n 'TAG_SOURCE=$(printf \"%s\" \"$CHANNEL_TAG\" | grep -oE \\'source=\"[^\"]+\"\\' | head -1 | sed \\'s/source=\"\\\\(.*\\\\)\"/\\\\1/\\' || true)',\n 'if [ -z \"$TAG_SOURCE\" ]; then exit 0; fi',\n 'extract_attr() { echo \"$1\" | grep -oE \"$2=\\\\\"[^\\\\\\\"]+\\\\\"\" | head -1 | sed -E \"s/$2=\\\\\\\"(.*)\\\\\\\"/\\\\\\\\1/\"; }',\n '',\n '# The notice. No em-dash (house style); no 👋 wave (reserved for the',\n '# back-online greeting) and no 🟢.',\n 'NOTICE=\"Give me a moment while I reorganize my memory so I can keep our conversation going. I will pick this back up in a few seconds.\"',\n '',\n '# Atomic write: jq → tmp in the same dir, then rename. The channel-side',\n '# fs.watch only fires on rename, so the file is never observed mid-write.',\n 'atomic_write_payload() {',\n ' local out_dir=\"$1\" final=\"$2\" jq_expr=\"$3\"; shift 3',\n ' mkdir -p \"$out_dir\"',\n ' local tmp=\"$out_dir/.${final##*/}.tmp\"',\n ' jq -n \"$jq_expr\" \"$@\" > \"$tmp\"',\n ' chmod 600 \"$tmp\" 2>/dev/null || true',\n ' mv -f \"$tmp\" \"$out_dir/$final\"',\n '}',\n 'TS=$(date -u +%Y%m%dT%H%M%S%N)',\n 'WROTE=0',\n 'if [ \"$TAG_SOURCE\" = \"slack\" ]; then',\n ' CHANNEL=$(extract_attr \"$CHANNEL_TAG\" \"channel\")',\n ' THREAD_TS=$(extract_attr \"$CHANNEL_TAG\" \"thread_ts\")',\n ' if [ -n \"$CHANNEL\" ]; then',\n ' atomic_write_payload \"${AGENT_DIR}/slack-notice-outbox\" \"${TS}.json\" \\\\',\n ' \\'{channel:$c, thread_ts:$th, text:$t, source:\"compaction-notice\"}\\' \\\\',\n ' --arg c \"$CHANNEL\" --arg th \"$THREAD_TS\" --arg t \"$NOTICE\"',\n ' WROTE=1',\n ' fi',\n 'elif [ \"$TAG_SOURCE\" = \"telegram\" ]; then',\n ' CHAT_ID=$(extract_attr \"$CHANNEL_TAG\" \"chat_id\")',\n ' if [ -n \"$CHAT_ID\" ]; then',\n ' atomic_write_payload \"${AGENT_DIR}/telegram-notice-outbox\" \"${TS}.json\" \\\\',\n ' \\'{chat_id:$c, text:$t, source:\"compaction-notice\"}\\' \\\\',\n ' --arg c \"$CHAT_ID\" --arg t \"$NOTICE\"',\n ' WROTE=1',\n ' fi',\n 'elif [ \"$TAG_SOURCE\" = \"msteams\" ]; then',\n ' CONVERSATION_ID=$(extract_attr \"$CHANNEL_TAG\" \"conversation_id\")',\n ' SERVICE_URL=$(extract_attr \"$CHANNEL_TAG\" \"service_url\")',\n ' REPLY_TO_ID=$(extract_attr \"$CHANNEL_TAG\" \"reply_to_id\")',\n ' if [ -n \"$CONVERSATION_ID\" ] && [ -n \"$SERVICE_URL\" ]; then',\n ' atomic_write_payload \"${AGENT_DIR}/msteams-notice-outbox\" \"${TS}.json\" \\\\',\n ' \\'{conversation_id:$c, service_url:$s, reply_to_id:$r, text:$t, source:\"compaction-notice\"}\\' \\\\',\n ' --arg c \"$CONVERSATION_ID\" --arg s \"$SERVICE_URL\" --arg r \"$REPLY_TO_ID\" --arg t \"$NOTICE\"',\n ' WROTE=1',\n ' fi',\n 'fi',\n '# direct-chat and any other source: no notice-outbox consumer, stay silent.',\n 'if [ \"$WROTE\" = \"1\" ]; then date +%s > \"$NOTICE_MARKER\" 2>/dev/null || true; fi',\n 'exit 0',\n ].join('\\n') + '\\n';\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n // writeFileSync's `mode` only applies when creating the file; on a\n // reprovision overwrite it is ignored, so a hook that ever lost its execute\n // bit would stay broken. Force the mode explicitly.\n chmodSync(hookScriptPath, 0o755);\n\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n // Register for both PreCompact triggers: `auto` (context-full, the silent\n // surprising case this hook exists for) and `manual` (operator /compact).\n // Upsert per matcher so reprovisioning is idempotent and any plugin-owned\n // PreCompact entries survive.\n const existingPreCompact = Array.isArray(hooks['PreCompact'])\n ? [...(hooks['PreCompact'] as Array<Record<string, unknown>>)]\n : [];\n for (const matcher of ['auto', 'manual'] as const) {\n const alreadyRegistered = existingPreCompact.some((entry) => {\n const entryMatcher = (entry as { matcher?: unknown }).matcher;\n const entryHooks = (entry as { hooks?: unknown }).hooks;\n return (\n entryMatcher === matcher &&\n Array.isArray(entryHooks) &&\n entryHooks.some(\n (h) =>\n typeof h === 'object' &&\n h !== null &&\n (h as { type?: unknown }).type === 'command' &&\n (h as { command?: unknown }).command === hookScriptPath,\n )\n );\n });\n if (!alreadyRegistered) {\n existingPreCompact.push({\n matcher,\n hooks: [{ type: 'command', command: hookScriptPath }],\n });\n }\n }\n hooks['PreCompact'] = existingPreCompact;\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n// ---------------------------------------------------------------------------\n// SessionStart session-state recorder (ENG-6233 / ENG-6268).\n//\n// Captures the facts only the agent's own Claude Code session knows — which\n// model is running and whether this session is fresh / resumed / compacted —\n// and writes them to ~/.augmented/{codeName}/session-state.json. The channel\n// MCP servers (slack-channel / telegram-channel) run as SEPARATE processes and\n// can't see the model or session origin, so they read this file back to answer\n// the Slack `/status-<code>` and Telegram `/status` commands. SessionStart is\n// the ONLY hook event whose payload carries `model`, so this is the one place\n// that fact can be recorded.\n//\n// ENG-6268: the canonical copy of this script lives at\n// packages/claudecode-plugin-augmented/hooks/session-state.sh, but the managed\n// fleet never installs that plugin (no marketplace entry, no CLI bundle path),\n// so ENG-6233 was inert in prod — `/status` replied \"session state not recorded\n// yet\" on every agent. We inline it here and register it in settings.local.json\n// during provisioning, exactly like provisionOrientHook above. Unlike the orient\n// hook (matcher: startup), this registers with NO matcher so it also fires on\n// /resume, /clear and /compact — the very transitions whose origin `/status`\n// reports. KEEP THE INLINED BODY BELOW IN SYNC WITH THE PLUGIN COPY.\n// ---------------------------------------------------------------------------\nexport function provisionSessionStateHook(codeName: string): void {\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // ENG-7891 / ADR-0049: bake the resolved agent dir (id-keyed under the new\n // layout, the codename dir under legacy) so the path stays valid after a\n // rename and matches the id-keyed spawn cwd. getAgentDir resolves the\n // codename compatibility symlink; for legacy agents it is the codename dir.\n const agentDir = getAgentDir(codeName);\n\n const hookScriptPath = join(claudeDir, 'agt-session-state-hook.sh');\n // Code name + state dir are baked in (not read from $AGT_AGENT_CODE_NAME) so\n // the hook works regardless of the session's env, matching how the orient /\n // stop / isolation hooks are provisioned with absolute, agent-specific paths.\n const hookScript = `#!/usr/bin/env bash\n# Auto-generated by Augmented (ENG-6233 / ENG-6268) — SessionStart session-state\n# recorder. Writes the model + session origin (which only the agent's own\n# Claude Code session knows) to session-state.json, which the channel servers\n# read back for the /status command. Best-effort, silent; every path exits 0 so\n# it can NEVER block or fail session start.\n# Canonical source: packages/claudecode-plugin-augmented/hooks/session-state.sh\n\n# Best-effort: any unexpected error just exits clean.\ntrap 'exit 0' ERR\n\n# Hook input (stdin canonical; CLAUDE_HOOK_INPUT kept as a fallback).\nINPUT=\"$(cat 2>/dev/null || true)\"\n[ -n \"$INPUT\" ] || INPUT=\"\\${CLAUDE_HOOK_INPUT:-}\"\n[ -n \"$INPUT\" ] || exit 0\ncommand -v jq >/dev/null 2>&1 || exit 0\n\n# Identity / state dir are baked at provision time.\nCODE_NAME=\"${codeName}\"\nSTATE_DIR=\"${agentDir}\"\nmkdir -p \"$STATE_DIR\" 2>/dev/null || exit 0\n\n# Pull the fields we surface from the SessionStart payload.\nSESSION_ID=\"$(printf '%s' \"$INPUT\" | jq -r '.session_id // empty' 2>/dev/null || true)\"\nSOURCE=\"$(printf '%s' \"$INPUT\" | jq -r '.source // empty' 2>/dev/null || true)\"\nMODEL=\"$(printf '%s' \"$INPUT\" | jq -r '.model // empty' 2>/dev/null || true)\"\nCWD=\"$(printf '%s' \"$INPUT\" | jq -r '.cwd // empty' 2>/dev/null || true)\"\n[ -n \"$CWD\" ] || CWD=\"\\${CLAUDE_PROJECT_DIR:-$PWD}\"\n\n# Channels: the channel MCP servers wired in the project .mcp.json. The adapter\n# writes each channel as a bare server id — slack / telegram / msteams (the\n# DEV_CHANNEL_SERVER_IDS set) plus direct-chat — NOT a \"<name>-channel\" key, so\n# match that allowlist and exclude the non-channel servers (augmented,\n# cloud-broker, composio_*). The \"-channel\" suffix only names the bundled .js\n# asset, never the .mcp.json key (verified against a live host, ENG-6268).\nCHANNELS_JSON='[]'\nif [ -f \"$CWD/.mcp.json\" ]; then\n CHANNELS_JSON=\"$(jq -c '\n [\"slack\",\"telegram\",\"msteams\",\"direct-chat\"] as $ch\n | [ (.mcpServers // {} | keys[]) | select(. as $k | $ch | index($k)) ]\n ' \"$CWD/.mcp.json\" 2>/dev/null || echo '[]')\"\n [ -n \"$CHANNELS_JSON\" ] || CHANNELS_JSON='[]'\nfi\n\n# Environment: best-effort from the agent's CLAUDE.md frontmatter (CHARTER.md\n# maps to CLAUDE.md for the Claude Code adapter; the frontmatter carries\n# environment: dev|stage|prod). Missing / unreadable -> omitted.\nENVIRONMENT=\"\"\nif [ -f \"$CWD/CLAUDE.md\" ]; then\n ENVIRONMENT=\"$(grep -m1 -E '^environment:[[:space:]]*' \"$CWD/CLAUDE.md\" 2>/dev/null \\\n | sed -E 's/^environment:[[:space:]]*//; s/[[:space:]]*$//' || true)\"\nfi\n\n# Seconds->millis keeps this portable across GNU (Linux hosts) and BSD (macOS\n# dev) date; second granularity is plenty for \"started 12m ago\".\nRECORDED_AT=\"$(date +%s)000\"\n\n# Write atomically (temp + rename) so a concurrent reader never sees a\n# half-written file.\nOUT=\"$STATE_DIR/session-state.json\"\nTMP=\"$OUT.$$.tmp\"\nif jq -nc \\\n --arg session_id \"$SESSION_ID\" \\\n --arg source \"$SOURCE\" \\\n --arg model \"$MODEL\" \\\n --arg cwd \"$CWD\" \\\n --arg environment \"$ENVIRONMENT\" \\\n --argjson channels \"$CHANNELS_JSON\" \\\n --argjson recorded_at \"$RECORDED_AT\" \\\n '{\n session_id: $session_id,\n source: $source,\n model: $model,\n cwd: $cwd,\n channels: $channels,\n recorded_at: $recorded_at\n }\n | if $environment == \"\" then . else . + { environment: $environment } end' \\\n > \"$TMP\" 2>/dev/null; then\n mv -f \"$TMP\" \"$OUT\" 2>/dev/null || rm -f \"$TMP\" 2>/dev/null || true\nelse\n rm -f \"$TMP\" 2>/dev/null || true\nfi\n\nexit 0\n`;\n\n writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });\n\n // Register under settings.local.json -> hooks.SessionStart. Read-modify-write\n // preserves the orient SessionStart entry (matcher: startup) written by\n // provisionOrientHook in the same provisioning pass, plus the Stop /\n // PreToolUse / PostToolUse keys from the sibling provision*Hook functions.\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n const existingSessionStart = Array.isArray(hooks['SessionStart'])\n ? [...(hooks['SessionStart'] as Array<Record<string, unknown>>)]\n : [];\n\n // Idempotent upsert: add our entry only if an equivalent one (same command\n // path) isn't already registered, so reprovisioning doesn't stack duplicates\n // and we co-exist with the orient startup entry and any plugin-registered\n // SessionStart hooks.\n const alreadyRegistered = existingSessionStart.some((entry) => {\n const entryHooks = (entry as { hooks?: unknown }).hooks;\n return (\n Array.isArray(entryHooks) &&\n entryHooks.some(\n (h) =>\n typeof h === 'object' &&\n h !== null &&\n (h as { type?: unknown }).type === 'command' &&\n (h as { command?: unknown }).command === hookScriptPath,\n )\n );\n });\n\n if (!alreadyRegistered) {\n // No matcher: fire on every SessionStart source (startup/resume/clear/\n // compact), so session-state.json tracks /compact and /clear transitions\n // — the origins `/status` reports — not just fresh boots.\n existingSessionStart.push({\n hooks: [{ type: 'command', command: hookScriptPath }],\n });\n }\n hooks['SessionStart'] = existingSessionStart;\n settings['hooks'] = hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n}\n\n/** Read, modify, and write a JSON config file. Returns true if changes were made. */\nfunction modifyJsonConfig(filePath: string, fn: (config: Record<string, unknown>) => boolean): void {\n let originalContent: string;\n let config: Record<string, unknown>;\n try {\n originalContent = readFileSync(filePath, 'utf-8');\n config = JSON.parse(originalContent);\n } catch {\n return;\n }\n\n const changed = fn(config);\n if (!changed) return;\n\n const newContent = JSON.stringify(config, null, 2);\n if (newContent === originalContent) return;\n\n writeFileSync(filePath, newContent);\n}\n\n// ---------------------------------------------------------------------------\n// Security constants\n// ---------------------------------------------------------------------------\n\n/**\n * Deny list injected into every provisioned agent's settings.json.\n * Prevents agents from reading or writing secrets, credentials, and key\n * material that should never be visible to an AI process.\n *\n * NOTE: Claude Code's deny rules have known bypass edge-cases (sub-agent\n * propagation, >50-subcommand batches). Treat this as a defence-in-depth\n * layer — it is not a substitute for OS-level secrets management.\n */\nconst SECRETS_DENY_PERMISSIONS: string[] = [\n // Read blocks\n 'Read(**/.env*)',\n 'Read(**/.dev.vars*)',\n 'Read(**/*.pem)',\n 'Read(**/*.key)',\n 'Read(**/secrets/**)',\n 'Read(**/credentials/**)',\n 'Read(**/credentials.json)',\n 'Read(**/.aws/**)',\n // gcloud stores ADC + cached access/refresh tokens under ~/.config/gcloud/\n // (application_default_credentials.json, credentials.db, access_tokens.db).\n // Brokered GCP access flows via CLOUDSDK_AUTH_ACCESS_TOKEN env, never these\n // files, so denying reads here is the GCP analogue of the .aws/ block.\n 'Read(**/.config/gcloud/**)',\n 'Read(**/.ssh/**)',\n 'Read(**/config/database.yml)',\n 'Read(**/config/credentials.json)',\n 'Read(**/.npmrc)',\n 'Read(**/.pypirc)',\n // Write blocks\n 'Write(**/.env*)',\n 'Write(**/secrets/**)',\n 'Write(**/.ssh/**)',\n];\n\n/**\n * Pre-commit hook script installed into the agent project's .git/hooks/.\n * Blocks commits that contain known secret patterns or sensitive file names.\n */\nconst PRE_COMMIT_HOOK = `#!/bin/bash\n# Augmented-managed pre-commit hook — blocks commits containing secrets.\n# Regenerated on each provision — do not edit by hand.\n\nPATTERNS=(\n 'sk-ant-' # Anthropic API keys\n 'sk-live-' # Stripe live keys\n 'sk_live_' # Stripe live keys (alt format)\n 'ghp_' # GitHub personal tokens\n 'gho_' # GitHub OAuth tokens\n 'AKIA' # AWS access key IDs\n 'xox[bpors]-' # Slack tokens\n 'SG\\\\.' # SendGrid keys\n 'eyJ' # JWTs\n 'BEGIN.*PRIVATE KEY' # Private key material\n)\n\n# Shell glob patterns matched against staged filenames (not grep regexes).\nBLOCKED_FILES=('.env' '.env.*' 'credentials.json' 'id_rsa' '*.pem' '*.key')\n\n# Only inspect ADDED lines from staged changes — \\`+\\` lines, ignoring the \\`+++\\`\n# file header. This keeps cleanup commits that REMOVE a secret from being\n# blocked, and avoids false positives on context lines.\nADDED_LINES=\\$(git diff --cached --diff-filter=ACM --unified=0 | grep -E '^\\\\+' | grep -vE '^\\\\+\\\\+\\\\+ ')\n\nfor pattern in \"\\${PATTERNS[@]}\"; do\n if echo \"\\$ADDED_LINES\" | grep -qE \"\\$pattern\"; then\n echo \"BLOCKED: Found potential secret matching '\\$pattern'\"\n echo \"Remove the secret and try again.\"\n exit 1\n fi\ndone\n\n# Iterate staged filenames and shell-glob match them against BLOCKED_FILES.\n# \\`case\\` does pathname-style globbing without forking grep and without\n# treating the glob as a regex.\nwhile IFS= read -r file; do\n [ -z \"\\$file\" ] && continue\n base=\\$(basename \"\\$file\")\n for pattern in \"\\${BLOCKED_FILES[@]}\"; do\n case \"\\$base\" in\n \\$pattern)\n echo \"BLOCKED: Attempted to commit sensitive file: \\$file\"\n exit 1\n ;;\n esac\n done\ndone < <(git diff --cached --name-only --diff-filter=ACM)\n\necho \"Pre-commit security check passed.\"\nexit 0\n`;\n\n// ---------------------------------------------------------------------------\n// Settings.json builder\n// ---------------------------------------------------------------------------\n\nfunction buildSettingsJson(input: ProvisionInput): Record<string, unknown> {\n const { agent, charterFrontmatter, toolsFrontmatter } = input;\n\n const settings: Record<string, unknown> = {\n // Agent metadata (readable by the agent at runtime)\n _augmented: {\n agent_id: agent.agent_id,\n code_name: agent.code_name,\n display_name: agent.display_name,\n environment: agent.environment,\n risk_tier: agent.risk_tier,\n framework: 'claude-code',\n charter_version: charterFrontmatter.version,\n tools_version: toolsFrontmatter.version,\n },\n };\n\n // Model configuration\n // ENG-4672: default to Opus 4.7 when the agent has no per-row preference.\n // Operators who set `primary_model` on the agent record still win — that\n // value flows through unchanged. Re-provisioning happens every supervisor\n // tick, so the default rolls out the next time the manager runs through\n // each agent.\n //\n // ENG-5631: NOTE this lands in the bare `<project>/settings.json`, which\n // Claude Code does NOT read at runtime (it reads ~/.claude/settings.json\n // and <project>/.claude/settings.json). The model that actually takes\n // effect is the session-scoped `--model <alias>` flag the launcher passes\n // (apps/cli/src/lib/persistent-session.ts, derived from this same\n // primary_model via claudeModelAlias). This field is retained as provision\n // metadata / drift-tracked artifact; redirecting it into .claude/ is tracked\n // as the deferred stretch on ENG-5631.\n settings['model'] = agent.primary_model || 'claude-opus-4-7';\n\n // Filesystem isolation: restrict file access to the agent's own directories.\n // Prevents cross-agent file reads on shared hosts (e.g., agent A reading agent B's CLAUDE.md).\n const projectDir = getProjectDir(agent.code_name);\n const agentDir = getAgentDir(agent.code_name);\n const homeDir = getHomeDir();\n // ENG-7891 / ADR-0049: projectDir/agentDir resolve to the real id-keyed dir\n // under the new layout. Also allow the codename compatibility symlink path\n // (kept indefinitely for operator reads per ADR-0049) so a path that\n // references the codename alias is not rejected by Claude Code's\n // allowedDirectories check, which may not canonicalize symlinks. For legacy\n // agents this alias equals agentDir and the Set dedupes it out.\n const codenameAliasDir = join(homeDir, '.augmented', agent.code_name); // agent-dir-allow: codename symlink alias for allowedDirectories (ADR-0049)\n settings['allowedDirectories'] = [\n ...new Set([\n projectDir, // Agent's project dir (CLAUDE.md, settings.json, etc.)\n agentDir, // Agent's config dir (.env, schedules, registration)\n codenameAliasDir, // Codename symlink alias (== agentDir for legacy agents)\n join(homeDir, '.augmented', '_mcp'), // Shared MCP binaries\n '/tmp', // Temp files\n ]),\n ];\n\n settings['permissions'] = { deny: SECRETS_DENY_PERMISSIONS };\n\n return settings;\n}\n\n// ---------------------------------------------------------------------------\n// .mcp.json builder\n// ---------------------------------------------------------------------------\n\n// ENG-4684: named subagent for the dispatcher pattern. Slow channel\n// requests (Xero pulls, multi-step skills, web research) get delegated\n// to this background subagent so the parent's listener turn returns\n// immediately and stays available for new inbound messages.\n//\n// `background: true` in the frontmatter is the load-bearing piece —\n// without it the parent blocks awaiting the subagent and we lose the\n// responsiveness win. Pre-approved tool list mirrors what the parent\n// has via buildAllowedTools, since background mode strictly auto-denies\n// anything not pre-approved.\n//\n// ENG-4793: the tools list is derived from the agent's actual `.mcp.json`\n// `mcpServers` keys — the same source of truth that buildMcpJson and the\n// incremental write paths produce. Pre-fix, every Claude Code agent\n// advertised wildcards for Xero, Granola, and a dozen Composio toolkits\n// regardless of what was wired, so the LLM would faithfully report tools\n// the agent did not have. Worse, if any of those MCP servers landed at\n// user scope later, the subagent would inherit access without the\n// per-agent allowlist gating.\n//\n// Driving from mcpServers keys (rather than `installed integrations`)\n// closes two gaps CodeRabbit flagged on PR #762: (1) env-only integrations\n// that don't emit an MCP server no longer get a wildcard for a server\n// that doesn't exist; (2) re-rendering on every `.mcp.json` mutation —\n// hooked through syncMcpToProject — keeps the subagent allowlist in sync\n// with the incremental writeMcpServer / removeMcpServer / channel-credential\n// paths, not just the initial `buildArtifacts`.\n//\n// MCP server-name patterns come from mcpWildcardsForServers (ENG-8181).\n// This file used to derive them with a local hyphen -> underscore rewrite, on\n// the assumption that Claude Code normalises the server key. It does not: tool\n// names carry the key verbatim (`mcp__direct-chat__direct_chat_reply`), so the\n// rewritten pattern matched nothing and every hyphenated server was silently\n// filtered out of the sub-agent's registry. The shared helper emits both\n// spellings; see mcp-tool-patterns.ts for the full account.\n//\n// The triage prompt in CLAUDE.md instructs the parent on when to\n// dispatch (≥60s estimated work) vs handle inline (< 60s).\n\nexport function buildChannelMessageHandlerAgent(args?: {\n /**\n * The literal keys present (or about to be present) in the agent's\n * `.mcp.json` `mcpServers` object. Each becomes a `mcp__<key>__*`\n * wildcard in the subagent's allowlist. This is the only source of truth\n * for which MCP servers the subagent may call.\n */\n mcpServerKeys?: string[];\n /**\n * ENG-4821: integration manifest mirroring CLAUDE.md's `## Integrations`\n * section. Without it the subagent has no awareness that env vars like\n * `GITHUB_ACCESS_TOKEN` or CLI binaries like `gh` exist on its environment\n * — even though it inherits both — and confabulates \"no creds\" when asked\n * about a capability the parent has. The manifest is what makes the\n * subagent answer from observation (env / CLI) instead of from prompt\n * silence.\n */\n integrations?: IntegrationSummary[];\n}): string {\n const mcpServerKeys = args?.mcpServerKeys ?? [];\n const integrations = args?.integrations ?? [];\n\n const mcpWildcards = mcpWildcardsForServers(mcpServerKeys);\n\n // Always-on basics: built-in tools the subagent uses to do work. The\n // `mcp__augmented__*` wildcard is included via mcpServerKeys (buildMcpJson\n // unconditionally emits the `augmented` server), not hardcoded here.\n // ENG-5929: ToolSearch is load-bearing for the mcp wildcards above.\n // Modern Claude Code lazy-loads MCP tool schemas via ToolSearch — the\n // `mcp__<server>__*` wildcards in this allowlist are pattern-permissions,\n // not schemas. Without ToolSearch in the sub-agent's own `tools:` line\n // (which Claude Code treats as a strict allowlist when present), the\n // sub-agent cannot resolve the wildcards into invocable tools. ENG-5926\n // added ToolSearch to the parent's `--allowedTools`, which fixed the\n // parent's own MCP binding but didn't propagate because sub-agent tool\n // sets come from THIS list, not the parent's. Don's empirical evidence\n // 2026-06-03 from sub-agent: 'ToolSearch exists but is not enabled in\n // this context'. Required everywhere the sub-agent dispatches against\n // MCP tools.\n const tools = [\n 'Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Skill', 'Agent', 'ToolSearch',\n ...mcpWildcards,\n ].join(', ');\n\n const integrationsBlock = integrations.length === 0\n ? ''\n : `\\n## Integrations available\\n\\nYou inherit the parent agent's environment, including credentials for the integrations below. Env vars follow the convention \\`<DEFINITION_ID>_ACCESS_TOKEN\\` for OAuth and \\`<DEFINITION_ID>_API_KEY\\` for API-key integrations (e.g. \\`GITHUB_ACCESS_TOKEN\\`, \\`POSTIZ_API_KEY\\`). Where a CLI is listed, prefer it over raw curl — the CLI handles auth automatically.\\n\\n${integrations\n .map((i) => {\n const cli = i.cliBinary ? ` — use the \\`${i.cliBinary}\\` CLI` : '';\n const desc = i.description ? `. ${i.description}` : '';\n return `- **${i.name}**${cli}${desc}`;\n })\n .join('\\n')}\\n\\nIf asked about your capabilities, **check first** (run the CLI, echo the env var, or invoke an MCP tool) before answering. Do not claim a capability is absent without verifying — the parent's environment is yours.\\n`;\n\n return `---\nname: channel-message-handler\ndescription: Handles a single inbound Slack/Telegram/Direct-Chat message end to end. Posts the reply itself via the matching channel tool. The parent agent dispatches to this subagent only for slow requests (≥ ~60s) so the parent's listener turn stays free for new inbound work.\nbackground: true\ntools: ${tools}\n---\n\nYou are dispatched by the parent agent to handle one channel message.\n\nThe parent passes you in the task description:\n- The full original message text\n- Channel metadata: Slack channel ID + thread_ts, Telegram chat_id + message_id, OR Direct Chat conversation_id\n- Any supporting context the parent thought relevant\n\nYour job:\n\n1. Do the work the message asks for (Xero pull, research, multi-step skill, etc.). Use whichever tools you need.\n2. Post the reply yourself via the matching channel tool — slack.reply for Slack, telegram.reply for Telegram, teams.reply for Microsoft Teams, direct_chat.reply for Direct Chat. Use the metadata the parent gave you to address the right thread/conversation.\n3. End. Do not do additional follow-up work; if more is needed, the user will send another message.\n\nDo NOT post intermediate progress updates unless the work spans 5+ minutes — keep noise low. The parent already sent a single-line acknowledgement before dispatching you.\n${integrationsBlock}`;\n}\n\n/**\n * ENG-5897 / ENG-5905: general-purpose background worker sub-agent.\n * Sibling of channel-message-handler — same dynamic-render pattern, same\n * explicit `mcp__*` wildcard allowlist built from the parent's\n * `.mcp.json`, but for tasks the parent wants done in the background\n * that DON'T require posting a channel reply (Don's Attio/Granola\n * dispatch was the triggering case).\n *\n * Why project-scope dynamic render and not a static plugin-scope file:\n * ENG-5897 originally shipped this as a plugin-scope sub-agent at\n * `packages/claudecode-plugin-augmented/agents/augmented-worker.md`,\n * relying on Anthropic's \"tools: omitted ⇒ inherit all\" rule. Don's\n * empirical re-test (2026-06-02, after the CLI auto-published) showed\n * the static plugin file never reached the runtime — there was no\n * deploy path. Worse, his probe reported even ToolSearch wasn't\n * available in his sub-agent (a different Claude Code session probe\n * showed it WAS available), pointing at plugin-scope vs project-scope\n * inheritance differences we shouldn't depend on. Mirroring the proven\n * channel-message-handler render path eliminates both gaps: same\n * deploy mechanism, same explicit `mcp__*` wildcards in `tools:` (no\n * inheritance assumption).\n */\nexport function buildAugmentedWorkerAgent(args?: {\n /**\n * The literal keys present (or about to be present) in the agent's\n * `.mcp.json` `mcpServers` object. Each becomes a `mcp__<key>__*`\n * wildcard in the subagent's allowlist. Same source-of-truth rule as\n * channel-message-handler.\n */\n mcpServerKeys?: string[];\n /**\n * Integration manifest mirroring CLAUDE.md's `## Integrations` section\n * (same shape as channel-message-handler). Without it the sub-agent\n * has no awareness that env vars like `GITHUB_ACCESS_TOKEN` or CLI\n * binaries like `gh` exist on its environment, and confabulates\n * \"no creds\" when asked about a capability the parent has.\n */\n integrations?: IntegrationSummary[];\n}): string {\n const mcpServerKeys = args?.mcpServerKeys ?? [];\n const integrations = args?.integrations ?? [];\n\n const mcpWildcards = mcpWildcardsForServers(mcpServerKeys);\n\n // Always-on basics — same set as channel-message-handler. The\n // `mcp__augmented__*` wildcard comes via mcpServerKeys (buildMcpJson\n // unconditionally emits the `augmented` server).\n // ENG-5929: ToolSearch is load-bearing for the mcp wildcards above.\n // Modern Claude Code lazy-loads MCP tool schemas via ToolSearch — the\n // `mcp__<server>__*` wildcards in this allowlist are pattern-permissions,\n // not schemas. Without ToolSearch in the sub-agent's own `tools:` line\n // (which Claude Code treats as a strict allowlist when present), the\n // sub-agent cannot resolve the wildcards into invocable tools. ENG-5926\n // added ToolSearch to the parent's `--allowedTools`, which fixed the\n // parent's own MCP binding but didn't propagate because sub-agent tool\n // sets come from THIS list, not the parent's. Don's empirical evidence\n // 2026-06-03 from sub-agent: 'ToolSearch exists but is not enabled in\n // this context'. Required everywhere the sub-agent dispatches against\n // MCP tools.\n const tools = [\n 'Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Skill', 'Agent', 'ToolSearch',\n ...mcpWildcards,\n ].join(', ');\n\n const integrationsBlock = integrations.length === 0\n ? ''\n : `\\n## Integrations available\\n\\nYou inherit the parent agent's environment, including credentials for the integrations below. Env vars follow the convention \\`<DEFINITION_ID>_ACCESS_TOKEN\\` for OAuth and \\`<DEFINITION_ID>_API_KEY\\` for API-key integrations (e.g. \\`GITHUB_ACCESS_TOKEN\\`, \\`POSTIZ_API_KEY\\`). Where a CLI is listed, prefer it over raw curl — the CLI handles auth automatically.\\n\\n${integrations\n .map((i) => {\n const cli = i.cliBinary ? ` — use the \\`${i.cliBinary}\\` CLI` : '';\n const desc = i.description ? `. ${i.description}` : '';\n return `- **${i.name}**${cli}${desc}`;\n })\n .join('\\n')}\\n\\nIf a capability seems missing, **check first** — run the CLI, list tools (\\`mcp__augmented__list_tools\\` or equivalent), or confirm the relevant env var is set **without printing its value** (e.g. \\`[ -n \"$POSTIZ_API_KEY\" ] && echo present || echo absent\\`, never \\`echo $POSTIZ_API_KEY\\`). Do not claim a capability is absent without verifying — the parent's environment is yours.\\n`;\n\n return `---\nname: augmented-worker\ndescription: Background worker for multi-step tool tasks the parent doesn't want to inline (data pulls, multi-API workflows, CRM enrichments, research that needs MCP tools). Carries an explicit \\`mcp__*\\` wildcard allowlist for every server the parent has wired, so it gets the tool surface the task needs and no more — **prefer it over \\`general-purpose\\` for MCP-tool dispatch**, and reach for \\`general-purpose\\` (the inherit-all wildcard form) only when you need something outside the allowlist. Historical, do not reintroduce — [anthropics/claude-code#64909](https://github.com/anthropics/claude-code/issues/64909) used to leave \\`tools:\\`-allowlisted sub-agents with an empty MCP registry (every \\`mcp__*\\` call returning \"No such tool available.\"), which is why this description once steered dispatch to \\`general-purpose\\`. Anthropic fixed it in v2.1.163 and it was re-verified on 2026-07-29 — see [[ENG-8219]].\nbackground: true\ntools: ${tools}\n---\n\nYou are dispatched by the parent agent to do a multi-step task in the background while the parent's listener turn stays free.\n\n## What you can do\n\nYour \\`tools:\\` allowlist (above) names every MCP server the parent has connected — Granola, Composio toolkits, Slack/Telegram/Direct-Chat channel tools, the platform \\`mcp__augmented__*\\` bridge, native integrations (Xero / Postiz / qmd / AWS), etc. — plus the built-ins (\\`Bash\\`, \\`Read\\`, \\`Write\\`, \\`Edit\\`, \\`Grep\\`, \\`Glob\\`, \\`Skill\\`, \\`Agent\\`). All environment variables the parent has are yours: OAuth access tokens (\\`GITHUB_ACCESS_TOKEN\\`), API keys (\\`POSTIZ_API_KEY\\`), native-CLI binaries (\\`gh\\`, \\`aws\\`, \\`xero\\`).\n\n## Hard rules — Credential Access Control\n\n1. **Never** read raw secrets out of \\`.mcp.json\\`, \\`~/.augmented/*/provision/.mcp.json\\`, \\`.env.integrations\\`, or any agent config file. Those files contain bot tokens, API keys, and OAuth credentials. The Credential Access Control guardrail (\\`block_read: true\\` on secrets) treats reads of those values as a violation regardless of intent. As **defence-in-depth (not structural enforcement)**, the plugin's \\`settings.json\\` also denies \\`Bash(cat:*/.mcp.json)\\`, \\`Bash(cat:*/.env.integrations)\\`, and \\`Bash(jq:*/.mcp.json)\\` (ENG-5901 / ADR-0018) — these block the obvious copy-paste paths but a determined in-process reader can still reach the values; the durable fix is Phase 2/3 of ADR-0018.\n2. **Never** post channel messages via raw API calls + bot tokens lifted from config. Use the channel MCP reply tools (\\`mcp__slack__slack_reply\\`, \\`mcp__telegram__telegram_reply\\`, \\`mcp__direct_chat__direct_chat_reply\\`, etc.) so calls go through the audited path.\n3. If an \\`mcp__*\\` tool you expect to be available returns \"No such tool available.\", **stop and surface the gap to the parent** in your summary rather than working around it. A missing MCP binding is a platform bug worth fixing — it's the exact failure shape this sub-agent was added to prevent (see ENG-5897 / ENG-5905).\n4. When verifying a capability is wired, confirm the relevant env var exists **without printing its value** — use \\`[ -n \"$POSTIZ_API_KEY\" ] && echo present || echo absent\\`, never \\`echo $POSTIZ_API_KEY\\`. The Credential Access Control guardrail covers tool-call output as well as file reads.\n\n## What to return\n\nHand the parent a tight summary of what you did, what you found, and any follow-ups it should know about. Do not echo intermediate tool transcripts — keep the parent's context window clean. If you produced an artifact (a file, a draft, a record ID), name it and where it lives.\n${integrationsBlock}`;\n}\n\n/**\n * ENG-5905: re-render `.claude/agents/augmented-worker.md` from the\n * agent's current `.mcp.json` `mcpServers` keys + persisted\n * `integrations-summary.json` manifest. Sibling of\n * `renderChannelMessageHandlerForAgent` — same render-to-both-dirs\n * pattern, same chokepoint via `syncMcpToProject`. No-op when\n * `.mcp.json` is missing or unreadable.\n */\nfunction renderAugmentedWorkerForAgent(codeName: string): void {\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n const provisionMcpPath = join(agentDir, 'provision', '.mcp.json');\n\n let mcpServerKeys: string[];\n try {\n const config = JSON.parse(readFileSync(provisionMcpPath, 'utf-8')) as {\n mcpServers?: Record<string, unknown>;\n };\n mcpServerKeys = Object.keys(config.mcpServers ?? {});\n } catch {\n return; // No `.mcp.json` yet — nothing to mirror.\n }\n\n const integrations = readIntegrationsSummaryForAgent(codeName);\n const content = buildAugmentedWorkerAgent({ mcpServerKeys, integrations });\n for (const baseDir of [agentDir, projectDir]) {\n const target = join(baseDir, '.claude', 'agents', 'augmented-worker.md');\n try {\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, content);\n } catch {\n // Non-fatal: the artifact pipeline will recreate it on next full provision.\n }\n }\n}\n\n/**\n * ENG-4594: Construct the .mcp.json entry for the Postiz integration.\n * Shared between buildMcpJson (initial provisioning) and writeIntegrations\n * (incremental sync after the agent connects Postiz post-provisioning).\n *\n * POSTIZ_BASE_URL is only emitted when the integration's config carries a\n * non-empty base_url. Without that guard, Claude Code passes the literal\n * `${POSTIZ_BASE_URL}` to the MCP child when no value is set in the spawn\n * env, which prevents the MCP server from falling back to its built-in\n * cloud default (CodeRabbit PR #659).\n */\nfunction buildPostizMcpEntry(\n integration: ResolvedIntegration,\n): { command: string; args: string[]; env: Record<string, string> } {\n const rawBaseUrl = integration.config['base_url'];\n const postizBaseUrl =\n typeof rawBaseUrl === 'string' ? rawBaseUrl.trim() : '';\n const env: Record<string, string> = {\n // Raw key, no `Bearer` prefix — Postiz's Authorization header takes\n // the API key verbatim. The MCP server handles framing.\n POSTIZ_API_KEY: '${POSTIZ_API_KEY}',\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n };\n if (postizBaseUrl.length > 0) {\n // Self-hosted: the manager writes POSTIZ_BASE_URL into the agent's\n // .env from metadata.base_url; Claude Code substitutes here at MCP\n // launch. Cloud users omit base_url → no env entry → MCP falls back\n // to its built-in https://api.postiz.com default.\n env['POSTIZ_BASE_URL'] = '${POSTIZ_BASE_URL}';\n }\n return {\n command: 'npx',\n args: ['-y', '@antoniolg/postiz-mcp'],\n env,\n };\n}\n\nexport function buildMcpJson(input: ProvisionInput): Record<string, unknown> {\n const mcpServers: Record<string, unknown> = {};\n\n // ENG-6563 (D16): host-chosen absolute path for the per-turn initiator marker.\n // The channel MCP (writer) and the broker MCPs (readers) share NO state-dir env\n // otherwise, so the manager injects this single path into both. The channel MCP\n // stamps the verified turn sender here; broker MCPs read it back and forward it\n // to the API, which validates it against the conversations table.\n const turnInitiatorFile = join(getAgentDir(input.agent.code_name), '.current-turn-initiator.json');\n\n // Always add the Augmented MCP server so the agent can manage its kanban,\n // submit standups, report drift, and refresh tokens.\n // Always use the local path — the manager's deployMcpAssets() guarantees\n // ~/.augmented/_mcp/index.js exists before any session starts.\n // The npx fallback was never published and caused \"tools unavailable\" errors.\n const localMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'index.js');\n mcpServers['augmented'] = {\n command: 'node',\n args: [localMcpPath],\n env: {\n AGT_HOST: process.env['AGT_HOST'] ?? '',\n // ENG-5901 Track D: templated — the manager exports AGT_API_KEY to\n // every spawn env (getApiKey()), and Claude Code substitutes at\n // MCP-launch (same contract as AGT_RUN_ID below). The impersonation\n // redeem path renders this server-side with no AGT_API_KEY in env;\n // impersonate-mcp-rewrite.ts treats the literal `${AGT_API_KEY}`\n // placeholder as fillable and swaps in the operator token.\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: input.agent.agent_id,\n AGT_AGENT_CODE_NAME: input.agent.code_name,\n // ENG-4561: Claude Code substitutes `${VAR}` in .mcp.json env values\n // at MCP-launch time using the spawn environment, not at file-write\n // time. Manager-worker exports AGT_RUN_ID per Claude spawn so the\n // bridge can stamp run_id onto inserted rows (kanban, knowledge).\n // If AGT_RUN_ID isn't set in the environment, Claude leaves the\n // literal `${AGT_RUN_ID}` — the bridge filters that case out.\n AGT_RUN_ID: '${AGT_RUN_ID}',\n // Console origin so the MCP bridge can build absolute URLs (e.g.\n // dashboard links) in tool replies. Falls back to NEXT_PUBLIC_APP_URL\n // / AGT_CONSOLE_URL — same chain consoleUrl uses for kanban links.\n AGT_APP_URL:\n process.env['AGT_APP_URL'] ??\n process.env['NEXT_PUBLIC_APP_URL'] ??\n process.env['AGT_CONSOLE_URL'] ??\n '',\n // ENG-6229: arms the in-session `request_restart` self-restart tool.\n // Read at provision time from the manager's env (like AGT_HOST above),\n // NOT a per-spawn `${...}` template — it's a host-level gate. Empty by\n // default ⇒ the tool isn't registered at all (ships dark). When an\n // operator sets it on the host, the next /host/refresh bakes it in.\n AGT_AGENT_SELF_RESTART_ENABLED: process.env['AGT_AGENT_SELF_RESTART_ENABLED'] ?? '',\n // Include PATH/HOME so the MCP subprocess can resolve binaries\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n };\n\n // ENG-5815: data-driven native MCP entries. Each integration whose\n // INTEGRATION_REGISTRY definition carries a `nativeMcp` spec is\n // rendered here via the shared templating renderer. qmd migrated as\n // the first user (ENG-5815); AWS shipped purely via this path with no\n // buildMcpJson edit. xero / postiz / cloud-broker still need richer\n // schema support (conditional env, broker-mode toggles) and remain\n // hardcoded below as the fallback the ADR calls for. New simple\n // native integrations land via a registry/seed update alone.\n for (const integration of input.integrations ?? []) {\n const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);\n if (!def?.nativeMcp) continue;\n const key = def.nativeMcp.key ?? integration.definition_id;\n mcpServers[key] = buildNativeMcpEntry(def.nativeMcp, {\n agentId: input.agent.agent_id,\n agentCodeName: input.agent.code_name,\n integration,\n });\n }\n\n // ENG-4679: official Xero MCP server when the agent has the Xero\n // integration. Replaces the legacy skill+bash+curl path, which is\n // incompatible with auto-mode classifier (drops broad Bash(*)).\n //\n // Token plumbing: the manager already writes `XERO_ACCESS_TOKEN` to\n // .env.integrations and refreshes it via the OAuth path. Claude Code\n // substitutes `${XERO_ACCESS_TOKEN}` here from the spawn env at MCP\n // launch time, so we map our env var name to the official server's\n // expected `XERO_CLIENT_BEARER_TOKEN` without renaming downstream.\n //\n // Scheduled-task fires (`claude -p`) re-read env on every spawn so a\n // refreshed token lands cleanly. Interactive tmux sessions inherit the\n // env at session-start; mid-day refresh staleness on the long-running\n // MCP child is a known gap (track separately if it bites).\n // ENG-4922: when the agent also has the xero-broker integration, its\n // money/ledger writes are HITL-gated through approval-core, so the vendor\n // Xero server must run read-only — XERO_WRITES_VIA_BROKER strips its\n // Create/Update/Delete tools, leaving the broker as the only write path.\n const hasXeroBroker = input.integrations?.some((i) => i.definition_id === 'xero-broker') ?? false;\n const xeroIntegration = input.integrations?.find((i) => i.definition_id === 'xero');\n if (xeroIntegration) {\n // ENG-4898: switched from upstream `@xeroapi/xero-mcp-server` to our\n // fork `@integrity-labs/xero-mcp-server`, which honours\n // XERO_TENANT_ID. Without that env var pinned, multi-tenant OAuth\n // tokens silently default to tenants[0] (cross-tenant data leak).\n //\n // ENG-4920: set AGT_INTEGRATION_ID + AGT_AGENT_ID so the MCP server\n // can fetch the freshest access_token from the credential broker on\n // every call (POST /host/agent-integrations/:id/credential) instead\n // of relying on the spawn-time XERO_CLIENT_BEARER_TOKEN — that env\n // path is what froze Sterling-on-agt-demo's token at the 4-hour-old\n // value claude was launched with.\n //\n // ENG-5318: when broker mode is engaged (integration has an id), do\n // NOT emit XERO_CLIENT_BEARER_TOKEN: '${XERO_ACCESS_TOKEN}'. That\n // reference is what trips stale-mcp-reaper into killing the xero\n // child every ~20 min when the manager rotates the token in\n // .env.integrations — which forces a full session restart even\n // though the broker-mode MCP would have picked up the new token\n // on the next call without restarting. Legacy fallback (when no\n // integration id is available) keeps the env var for back-compat\n // with xero-mcp-server versions before 0.0.19 / broker mode.\n const brokerMode = Boolean(xeroIntegration.id);\n // ENG-7579: bundled with the CLI (deployMcpAssets writes\n // ~/.augmented/_mcp/xero.js), NOT `npx @integrity-labs/xero-mcp-server@latest`.\n // The npx form required a manual npm publish on every change and silently\n // stranded the fleet on the last-published version - CS-1440's attachment\n // tool shipped in 0.0.21 but npm @latest stayed at 0.0.20. Same local-node\n // pattern as augmented-admin / origami below.\n const localXeroMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'xero.js');\n mcpServers['xero'] = {\n command: 'node',\n args: [localXeroMcpPath],\n env: {\n ...(brokerMode ? {} : { XERO_CLIENT_BEARER_TOKEN: '${XERO_ACCESS_TOKEN}' }),\n // ENG-8264: broker mode must ALSO declare which env var holds the\n // credential, or dropping the `${XERO_ACCESS_TOKEN}` reference above\n // silently re-classifies it. `findMcpServersUsingVars` only sees vars\n // referenced as `${VAR}`, so with that reference gone the manager's\n // `envOnlyRespawnVars` treats XERO_ACCESS_TOKEN as env-only and\n // schedules a FULL SESSION RESPAWN on every OAuth rotation (~30 min) -\n // trading the ~20-min child kill ENG-5318 fixed for something worse.\n // Observed live on stirling at 2026-08-02T07:29:34Z.\n //\n // This is a declaration of fact, not a carve-out: in broker mode the\n // server fetches the freshest token from the credential broker on every\n // call (that is what AGT_INTEGRATION_ID/AGT_AGENT_ID above are for), so\n // a rotation genuinely needs no restart - exactly what\n // AGT_REMOTE_MCP_TOKEN_VAR means. `liveProxyTokenVars` reads this key\n // off ANY mcpServers entry, not only remote-proxy ones, so the manager\n // filters the var out of the respawn set. The `${VAR}` reference stays\n // absent, so stale-mcp-reaper stays quiet too: both restart paths\n // satisfied at once.\n ...(brokerMode ? { AGT_REMOTE_MCP_TOKEN_VAR: 'XERO_ACCESS_TOKEN' } : {}),\n XERO_TENANT_ID: '${XERO_TENANT_ID}',\n AGT_HOST: '${AGT_HOST}',\n AGT_TOKEN: '${AGT_TOKEN}',\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: input.agent.agent_id,\n ...(brokerMode ? { AGT_INTEGRATION_ID: xeroIntegration.id } : {}),\n // ENG-4922: writes go through xero-broker → read-only vendor server.\n ...(hasXeroBroker ? { XERO_WRITES_VIA_BROKER: 'true' } : {}),\n // CS-1511: where get-invoice-pdf saves the document Xero renders. It\n // MUST land inside this agent's own project dir - that is the only\n // place its file tools can read back, so an export written anywhere\n // else is invisible to the agent that asked for it. Resolved through\n // getProjectDir (the ADR-0049 seam), not the MCP child's inherited\n // cwd, so the path stays correct if Claude Code ever spawns the\n // server from somewhere else.\n XERO_EXPORT_DIR: join(getProjectDir(input.agent.code_name), 'xero-exports'),\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n };\n }\n\n // ENG-4594: Postiz social-scheduling MCP. Wraps the community\n // antoniolg/postiz-mcp local-stdio server via npx. API-key auth + an\n // optional base_url so self-hosted Postiz instances can override the\n // cloud default. Built via buildPostizMcpEntry so the same shape is\n // used by writeIntegrations on incremental sync.\n const postizIntegration = input.integrations?.find((i) => i.definition_id === 'postiz');\n if (postizIntegration) {\n mcpServers['postiz'] = buildPostizMcpEntry(postizIntegration);\n }\n\n // ENG-4694: generic remote-MCP wiring. For any integration whose OAuth\n // provider config has an `mcpUrl`, wire it in. New OAuth-MCP integrations\n // land here with a config-only change in OAUTH_PROVIDERS, not a hand-rolled\n // block. Granola (ENG-4693) is the first user.\n //\n // ENG-6859: OAuth-wired remote MCPs now route through the stdio\n // remote-oauth-proxy, which re-reads the access token from the per-agent\n // secrets file per request - so a token rotated mid-session reaches the\n // running session without a restart. The old direct streamable-HTTP entry\n // froze the bearer at spawn (the 1h-token \"requires re-authorization\" bug).\n // Custom-header integrations (anchor-browser) keep the direct HTTP entry from\n // buildRemoteMcpEntry - their api-key doesn't rotate on the session timescale.\n const remoteOAuthProxyPaths = {\n proxyPath: join(getHomeDir(), '.augmented', '_mcp', 'remote-oauth-proxy.js'),\n tokenFile: join(getProjectDir(input.agent.code_name), '.env.integrations'),\n };\n for (const integration of input.integrations ?? []) {\n // ENG-8359: key by CONNECTION, not by definition. The default connection\n // keeps the bare `definition_id` (byte-identical for the whole fleet); a\n // named one gets `<sanitized>-<connection_key>`. Pre-fix all three branches\n // wrote `mcpServers[definition_id]`, so installing a second connection of one\n // remote MCP silently overwrote the first and the agent only ever saw one.\n const connectionKey = integration.connection_key;\n const serverKey = remoteMcpServerKey(integration.definition_id, connectionKey);\n // ENG-7748: live-header remote MCPs (anchor-browser) route through the same\n // stdio proxy, which reads the api-key + minted session-id headers live per\n // request - so a re-minted session id takes effect with no agent respawn.\n const liveEntry = buildLiveHeaderRemoteMcpProxyEntry(\n integration.definition_id,\n integration.remoteMcp,\n remoteOAuthProxyPaths,\n connectionKey,\n );\n if (liveEntry) {\n mcpServers[serverKey] = liveEntry;\n continue;\n }\n const proxyEntry = buildOAuthRemoteMcpProxyEntry(\n integration.definition_id,\n remoteOAuthProxyPaths,\n connectionKey,\n );\n if (proxyEntry) {\n mcpServers[serverKey] = proxyEntry;\n continue;\n }\n // ADR-0033 Slice 2: forward the DB catalog spec (remote_mcp column); falls\n // back to the code registry when absent.\n const entry = buildRemoteMcpEntry(integration.definition_id, integration.remoteMcp, connectionKey);\n if (entry) {\n mcpServers[serverKey] = entry;\n }\n }\n\n // ENG-4685: AWS Cloud Access Broker MCP server when the agent has the\n // cloud-broker toolkit installed (paired with aws-cli via the AWS\n // plugin's required_toolkits). The plugin wizard creates one integration\n // row per required toolkit, so we match the toolkit id here, not the\n // parent integration code_name `aws` — that row never exists at agent\n // scope. Spawned as a stdio child from the cloud-broker npm package;\n // the broker calls back to the Augmented API on every aws_request_access /\n // aws_poll_grant / aws_release_access to mint scoped, TTL-bounded STS\n // credentials. Auth back to the API uses the agent's existing host env\n // (AGT_HOST + AGT_TEAM_SLUG + AGT_TOKEN/AGT_API_KEY) — already\n // populated by the manager for every MCP it spawns.\n const hasCloudBroker = input.integrations?.some((i) => i.definition_id === 'cloud-broker');\n if (hasCloudBroker) {\n // AGT_TOKEN is intentionally OMITTED. When the manager spawn env doesn't\n // have it set (which is the normal case — only AGT_API_KEY is permanent;\n // AGT_TOKEN is the short-lived JWT we exchange for at runtime), Claude\n // Code passes the literal string \"${AGT_TOKEN}\" through to the child\n // process. The broker would then treat that placeholder as a valid\n // initial JWT and 401 every API call. Letting AGT_TOKEN be undefined\n // makes the broker fall back to AGT_API_KEY → /host/exchange, which is\n // the path that actually works.\n mcpServers['cloud-broker'] = {\n command: 'npx',\n args: ['-y', '@integrity-labs/cloud-broker@latest'],\n env: {\n AGT_HOST: '${AGT_HOST}',\n // ENG-4739: agent_id is baked into .mcp.json at provision time\n // (literal UUID, not env-substituted). The broker sends it on\n // every call; the API derives team server-side. AGT_TEAM_SLUG\n // dropped — single-team-per-host invariant gone.\n AGT_AGENT_ID: input.agent.agent_id,\n // ENG-4788: cloud-broker@0.6+ exits at startup if AGT_RUN_ID\n // is missing (packages/cloud-broker/src/index.ts:63). Manager\n // exports AGT_RUN_ID per Claude spawn; Claude substitutes the\n // placeholder at MCP-launch time. Same pattern as augmented.\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n // ENG-6586 (D16): read the verified turn initiator the channel MCP stamps\n // and forward it on each aws_request_access call (same contract as\n // xero-broker below).\n AGT_TURN_INITIATOR_FILE: turnInitiatorFile,\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n };\n }\n\n // ENG-8440: Higgsfield used to be a host-brokered remote MCP here\n // (ENG-4695, the Granola shape — operator runs `/mcp` once in tmux to\n // complete a browser OAuth). It is now a Direct HTTP api_key integration\n // against platform.higgsfield.ai, so it writes no MCP entry at all: its\n // tools reach the agent through the integration broker, and the customer\n // completes setup by pasting a key in the console.\n //\n // Vercel left the host-brokered category the same way (ENG-8421). Neither\n // needs a branch here, and with both gone this file writes no\n // host-brokered remote MCP entries.\n\n // ENG-4922: xero-broker MCP — HITL-gated Xero writes via approval-core.\n // Opt-in by having the `xero-broker` integration (same convention as\n // cloud-broker). When present the vendor Xero server runs read-only (see\n // XERO_WRITES_VIA_BROKER above), so this broker is the only write path.\n // Same env contract as cloud-broker; the broker calls back to /xero-broker\n // on the API and the team is derived server-side from agent_id.\n if (hasXeroBroker) {\n mcpServers['xero-broker'] = {\n command: 'npx',\n args: ['-y', '@integrity-labs/xero-broker@latest'],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_AGENT_ID: input.agent.agent_id,\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n // ENG-6563 (D16): read the verified turn initiator the channel MCP stamps\n // and forward it on each /xero-broker/requests call.\n AGT_TURN_INITIATOR_FILE: turnInitiatorFile,\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n };\n }\n\n // ENG-6195: augmented-admin-mcp — Integrity Labs STAFF-only cross-org agent\n // diagnostics. Opt-in by having the `augmented-admin` integration (same\n // convention as cloud-broker / xero-broker). The thin stdio broker calls back\n // to /admin/debug/* on the API; authority + the diagnostic projection live\n // server-side. The API fail-closes unless the caller's owning org is\n // is_internal — so provisioning this to a non-staff agent grants nothing.\n const hasAdminDebug =\n input.integrations?.some((i) => i.definition_id === 'augmented-admin') ?? false;\n if (hasAdminDebug) {\n // Bundled with the CLI (deployMcpAssets writes ~/.augmented/_mcp/augmented-admin.js),\n // NOT `npx @latest` — the broker is staff-only and was never published to npm, so\n // the npx form 404'd and the MCP could never start (reaper restart churn). Same\n // local-node pattern as the `augmented` server above.\n const localAdminMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'augmented-admin.js');\n mcpServers['augmented-admin'] = {\n command: 'node',\n args: [localAdminMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_AGENT_ID: input.agent.agent_id,\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n },\n };\n }\n\n // ENG-7023 (ADR-0031/0032): augmented-support - the per-org self-troubleshoot\n // broker for the system_support concierge agent. Same convention as\n // augmented-admin: opt-in by having the `augmented-support` integration\n // attached (the provision-support path attaches it to system_support agents,\n // ENG-6975). The thin stdio broker calls back to /host/support/* on the API,\n // which derives the org from the host JWT - so the agent can only ever read\n // and act on its OWN org. Bundled with the CLI (deployMcpAssets writes\n // ~/.augmented/_mcp/augmented-support.js), never `npx @latest` - it is private\n // and unpublished. Same local-node pattern as the servers above.\n const hasSupport =\n input.integrations?.some((i) => i.definition_id === 'augmented-support') ?? false;\n if (hasSupport) {\n const localSupportMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'augmented-support.js');\n mcpServers['augmented-support'] = {\n command: 'node',\n args: [localSupportMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_AGENT_ID: input.agent.agent_id,\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n },\n };\n }\n\n // ENG-7358: origami's dedicated stdio MCP server - retires the bespoke\n // Direct-HTTP broker lane (tools move from `mcp__augmented__origami_*` to\n // `mcp__origami__*`). Gated on the CATALOG cutover flag\n // (`integration_definitions.metadata.stdio_mcp` -> ResolvedIntegration.stdioMcp),\n // so merging this code changes nothing until the cutover migration flips the\n // flag; rollback is flipping it back (the ENG-5277 prune below the\n // incremental path removes the entry symmetrically). Bundled with the CLI\n // (deployMcpAssets writes ~/.augmented/_mcp/origami.js), never `npx @latest`\n // - same local-node pattern and rationale as augmented-admin above. The\n // integration row UUID is required: the server fetches the og_live key AND\n // the server-derived org/team identity per call from\n // POST /host/agent-integrations/:id/credential (ENG-4918 path, api_key\n // allow-list) - identity never enters the spawn env or the wire.\n const origamiIntegration = input.integrations?.find((i) => i.definition_id === 'origami');\n if (origamiIntegration?.stdioMcp === true && origamiIntegration.id) {\n const localOrigamiMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'origami.js');\n mcpServers['origami'] = {\n command: 'node',\n args: [localOrigamiMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_TOKEN: '${AGT_TOKEN}',\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: input.agent.agent_id,\n AGT_INTEGRATION_ID: origamiIntegration.id,\n },\n };\n }\n\n return { mcpServers };\n}\n\n/**\n * ENG-8056: reconstruct the single `.mcp.json` server entry a quarantined\n * native-MCP / remote-MCP / bundled-stdio integration WOULD have, so its\n * connectivity probe (the ENG-8049 inline-entry seam) can reach the real\n * server and a sustained recovery auto-resumes the quarantine (ENG-8036).\n *\n * A quarantined integration is dropped from `.mcp.json`, so the on-disk probe\n * lookup finds nothing. Rather than re-derive each kind's entry shape (native\n * raw-id key, remote OAuth-proxy, bundled-stdio literal key), we run the SAME\n * `buildMcpJson` the real file is generated from over a synthetic input of JUST\n * this one integration, and lift out its entry - guaranteeing byte-parity with\n * what quarantine removed.\n *\n * MUST run host-side (the manager): `buildMcpJson` bakes host-absolute paths and\n * env into stdio entries, which a control-plane caller could not reproduce.\n * Managed (Composio) rows are reconstructed separately, API-side (ENG-8055), and\n * produce no entry here.\n *\n * `buildMcpJson` always emits the `augmented` server; every other key is this\n * integration's reconstructed entry. Returns undefined when it emits none (a\n * managed / non-MCP kind) or - defensively - more than one (ambiguous; never\n * guess which is the probe target).\n */\nexport function reconstructQuarantinedMcpServerEntry(\n agent: { agent_id: string; code_name: string },\n integration: ResolvedIntegration,\n): Record<string, unknown> | undefined {\n // `buildMcpJson` reads only `input.agent.{agent_id,code_name}` + `input.integrations`;\n // the rest of ProvisionInput is irrelevant to the emitted server entries, so a\n // minimal synthetic input is faithful (asserted by the reconstruction unit tests).\n const built = buildMcpJson({\n agent: { agent_id: agent.agent_id, code_name: agent.code_name },\n integrations: [integration],\n } as unknown as ProvisionInput);\n const servers = (built.mcpServers ?? {}) as Record<string, unknown>;\n const keys = Object.keys(servers).filter((k) => k !== 'augmented');\n if (keys.length !== 1) return undefined;\n const only = keys[0];\n return only ? (servers[only] as Record<string, unknown>) : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Scheduled task mapping\n// ---------------------------------------------------------------------------\n\ninterface ClaudeCodeSchedule {\n id: string;\n name: string;\n prompt: string;\n schedule_type: 'cloud' | 'desktop' | 'loop';\n cron_expression?: string;\n interval_minutes?: number;\n}\n\nfunction parseIntervalMinutes(scheduleEvery: string | null): number {\n if (!scheduleEvery) return 60;\n const match = scheduleEvery.match(/^(\\d+)\\s*(m|min|h|hr|d)$/i);\n if (!match) return 60;\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!.toLowerCase();\n if (unit === 'h' || unit === 'hr') return value * 60;\n if (unit === 'd') return value * 1440;\n return value;\n}\n\nfunction mapScheduledTasks(tasks: ScheduledTaskRow[]): ClaudeCodeSchedule[] {\n return tasks.map((task) => {\n // Determine scheduling tier based on task properties\n // Cloud tasks: durable, min 1hr interval — mapped from cron/every schedules\n // Desktop tasks: persistent local, needs file access — isolated sessions\n // Loop: session-scoped, quick polling — main session targets\n const intervalMinutes = task.schedule_kind === 'every'\n ? parseIntervalMinutes(task.schedule_every)\n : 60;\n\n let scheduleType: 'cloud' | 'desktop' | 'loop';\n if (task.session_target === 'isolated' || intervalMinutes >= 60) {\n scheduleType = 'cloud';\n } else if (task.session_target === 'main') {\n scheduleType = 'loop';\n } else {\n scheduleType = 'desktop';\n }\n\n return {\n id: task.id ?? task.template_id,\n name: task.name,\n // ENG-5065: pass the task's timezone so the wrapped preamble can\n // anchor \"today/yesterday/tomorrow\" correctly — without it the\n // agent fell back to the model's UTC clock and a Sydney 7am brief\n // listed Sydney-yesterday's meetings.\n prompt: wrapScheduledTaskPrompt(task.prompt, { timezone: task.timezone }),\n schedule_type: scheduleType,\n cron_expression: task.schedule_expr ?? undefined,\n interval_minutes: intervalMinutes,\n };\n });\n}\n\n/**\n * Map a URL-based MCP integration to its `.mcp.json` server entry.\n *\n * Extracted from `writeMcpServer` (ENG-5545) so the per-provider transport\n * decision is a pure, unit-testable function. It survived a six-week-stale\n * branch (the Composio stdio bridge ran long after Claude Code gained native\n * remote-MCP support) precisely because nothing exercised this mapping in\n * isolation — the logic was buried inside filesystem I/O.\n *\n * Branch order is significant — earlier matches win:\n * 1. Composio (+headers) → native `{ type: 'http', url, headers }`. Composio\n * speaks Streamable HTTP directly and authenticates via the `x-api-key`\n * header; no child process means the mcp-presence-reaper skips it.\n * 2. Pipedream → `@pipedream/mcp` stdio bridge. This one genuinely\n * needs the bridge: the Authorization header is a short-lived (1h) token,\n * so the bridge does its own token exchange from the raw client creds.\n * 3. Generic (+headers) → native `{ type: 'http', url, headers }` (ENG-4694\n * / ENG-5074 — the `type` field is required by Claude Code's MCP schema).\n * 4. Generic (no headers) → `mcp-remote` stdio shim for host-brokered /\n * unauthenticated remotes.\n */\nexport function buildUrlMcpServerEntry(\n url: string,\n headers?: Record<string, string>,\n // ENG-5855: transport for the generic native branch. A `remoteMcp` spec can\n // declare 'sse' and it must survive incremental syncs — see writeMcpServer,\n // which threads the entry's `type` through here. ENG-4695: an explicit type\n // also flips the headerless branch from the legacy mcp-remote shim to a\n // native entry (host-brokered OAuth remotes like Higgsfield); left undefined\n // it defaults to 'http' for the with-headers branches and keeps the shim for\n // unauthenticated remotes on older clients.\n type?: 'http' | 'sse',\n): Record<string, unknown> {\n const hasHeaders = !!headers && Object.keys(headers).length > 0;\n\n if (url.includes('composio.dev') && hasHeaders) {\n // ENG-5545: Composio dials natively over Streamable HTTP — no stdio\n // bridge. `generateMcpUrl` returns a stable\n // `https://backend.composio.dev/v3/mcp/{serverId}/mcp?user_id={userId}` url\n // (ENG-5695: the `/mcp` sub-path is mandatory; the bare form 307-redirects\n // and Claude Code's MCP client doesn't follow POST redirects) + an\n // `x-api-key` header, which is exactly the shape Composio's own docs\n // hand to OpenAI / Anthropic / @ai-sdk MCP clients to dial the endpoint\n // directly. The previous `npx @composio/mcp start --url` bridge (ENG-4271,\n // 2026-03-31) predated this file's native remote-MCP support (ENG-5074,\n // 2026-05-15) by six weeks and was never revisited; the `@composio/mcp`\n // CLI only exists to adapt stdio-only clients, which Claude Code no longer\n // is. Going native kills the stdio child's death mode (no process to exit\n // → the mcp-presence-reaper skips url-only entries → no restart loop → no\n // ENG-5441 breaker trip / agent auto-pause). The header guard mirrors the\n // generic branch: a Composio url with no api key is not a valid native\n // http entry.\n return { type: 'http', url, headers };\n }\n\n if (url.includes('mcp.pipedream.net')) {\n // Pipedream: @pipedream/mcp stdio --app <slug> --external-user-id <id>\n // The URL contains /{externalUserId}/{appSlug}, headers contain credentials.\n // Parse the URL to extract app slug and user ID, pass credentials as env vars.\n const pdUrl = new URL(url);\n const pathParts = pdUrl.pathname.split('/').filter(Boolean);\n const externalUserId = decodeURIComponent(pathParts[0] ?? '');\n const appSlug = decodeURIComponent(pathParts[1] ?? '');\n const h = headers ?? {};\n\n // The access token is short-lived (1 hour). We pass the raw client\n // credentials so @pipedream/mcp can do its own token exchange.\n // These come through as x-pd-client-id / x-pd-client-secret if set,\n // otherwise fall back to env vars.\n return {\n command: 'npx',\n args: ['-y', '@pipedream/mcp', 'stdio', '--app', appSlug, '--external-user-id', externalUserId],\n env: {\n PIPEDREAM_PROJECT_ID: h['x-pd-project-id'] ?? process.env['PIPEDREAM_PROJECT_ID'] ?? '',\n PIPEDREAM_CLIENT_ID: h['x-pd-client-id'] ?? process.env['PIPEDREAM_CLIENT_ID'] ?? '',\n PIPEDREAM_CLIENT_SECRET: h['x-pd-client-secret'] ?? process.env['PIPEDREAM_CLIENT_SECRET'] ?? '',\n PIPEDREAM_PROJECT_ENVIRONMENT: h['x-pd-environment'] ?? process.env['PIPEDREAM_ENVIRONMENT'] ?? 'development',\n },\n };\n }\n\n if (hasHeaders) {\n // Generic remote MCP with auth headers (ENG-4694) — emit the url+headers\n // shape with an explicit `type: 'http'`. Claude Code dials the URL\n // directly with the supplied Authorization: Bearer header. mcp-remote\n // can't pass headers through, so we MUST NOT wrap when headers are\n // required for auth.\n //\n // ENG-5074: the `type` field is required by Claude Code's MCP schema.\n // Pre-fix this entry shape omitted it and claude rejected the whole\n // config at startup (\"Does not adhere to MCP server configuration\n // schema\") — the agent's tmux session exited inside a second and the\n // manager looped it forever. ENG-5855: honour the caller-supplied\n // transport (defaults to 'http') so an SSE-backed `remoteMcp` spec\n // round-trips through incremental syncs instead of being coerced to http.\n return { type: type ?? 'http', url, headers };\n }\n\n // ENG-4695: host-brokered native remote (no headers — OAuth handled by\n // Claude Code itself, e.g. Higgsfield). An explicit transport signals the\n // caller wants the native streamable-HTTP entry, matching buildMcpJson /\n // buildHostBrokeredRemoteMcpEntry, rather than the mcp-remote shim below.\n if (type) {\n return { type, url };\n }\n\n // Generic: mcp-remote stdio shim. Used only when the integration has no\n // auth headers AND no declared transport (unauthenticated remote MCPs).\n // Keeps backwards compatibility with older clients that don't speak\n // streamable-HTTP MCP natively.\n return { command: 'npx', args: ['-y', 'mcp-remote', url, '--allow-http'] };\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code Adapter\n// ---------------------------------------------------------------------------\n\nexport const claudeCodeAdapter: FrameworkAdapter = {\n id: 'claude-code',\n label: 'Claude Code',\n cliBinary: 'claude',\n\n getAgentDir(codeName: string): string {\n // Resolve the validated path first (getAgentDir asserts), then trigger\n // migration. If migration throws (corrupt .mcp.json merge, etc.), the\n // caller still gets a valid path and can decide how to proceed.\n const agentDir = getAgentDir(codeName);\n migrateLegacyClaudecodeDir(codeName);\n return agentDir;\n },\n\n buildArtifacts(input: ProvisionInput): ProvisionArtifact[] {\n // Build integration summaries for CLAUDE.md\n const integrationSummaries: IntegrationSummary[] = (input.integrations ?? []).map((i) => {\n const def = INTEGRATION_REGISTRY.find((d) => d.id === i.definition_id);\n return {\n id: i.definition_id,\n name: i.display_name || def?.name || i.definition_id,\n cliBinary: def?.cli_tool?.binary,\n description: def?.description,\n };\n });\n\n const knowledgeRefs = (input.knowledge ?? []).map((k) => ({\n title: k.title,\n slug: k.slug,\n scope: k.scope,\n }));\n\n const claudeMdInput = {\n frontmatter: input.charterFrontmatter,\n role: input.agent.role,\n description: input.agent.description,\n resolvedChannels: input.resolvedChannels,\n team: input.team,\n // ENG-5009: org context for the identity preamble.\n organization: input.organization,\n consoleUrl: process.env['NEXT_PUBLIC_APP_URL'] || process.env['AGT_CONSOLE_URL'] || 'https://app.augmented.team',\n hasQmd: input.integrations?.some((i) => i.definition_id === 'qmd') ?? false,\n // ENG-7831: the server includes `workflows` (possibly empty) only when\n // the dynamic-workflows feature is on for this agent, so field presence\n // is the render gate for the platform-storage section's workflow rows.\n hasWorkflows: input.workflows !== undefined,\n integrations: integrationSummaries,\n // ENG-8174: gate for the `## Integrations` section only. Must match\n // what the manager passes to `writeIntegrations` — the two writers\n // target the same sentinel range, and disagreeing would have one\n // re-add what the other just stripped.\n renderIntegrationsSection: input.renderIntegrationsSection === true,\n knowledge: knowledgeRefs.length > 0 ? knowledgeRefs : undefined,\n timezone: input.timezone,\n reportsTo: input.reportsTo,\n personalitySeed: input.personalitySeed,\n teamMembers: input.teamMembers,\n people: input.people,\n // ENG-4941: optional gate-path map from the manager. Passing it\n // through unconditionally — `undefined` triggers the\n // backwards-compat single-bucket rendering in identity.ts.\n peerGates: input.peerGates,\n // Effective guardrails (org → team → agent), pre-joined with\n // definitions server-side. Renders into the Guardrails section\n // right after Governance. Omit / empty array → section skipped.\n guardrails: input.guardrails,\n // ENG-5380: active kanban tasks (todo/in_progress) the manager\n // chose to inject. `undefined` when the feature flag is off; the\n // identity generator short-circuits the section in that case.\n activeTasks: input.activeTasks,\n };\n\n // ENG-4793: derive the channel-message-handler wildcard set from the\n // exact `mcpServers` keys buildMcpJson is about to emit. Same source\n // of truth as `.mcp.json` itself, so the two files cannot drift —\n // the incremental write paths re-render this artifact via\n // syncMcpToProject (which now calls renderChannelMessageHandlerForAgent).\n const mcpJson = buildMcpJson(input);\n const initialMcpServerKeys = Object.keys(\n (mcpJson as { mcpServers?: Record<string, unknown> }).mcpServers ?? {},\n );\n\n const artifacts = [\n { relativePath: 'CLAUDE.md', content: generateClaudeMd(claudeMdInput) },\n { relativePath: 'settings.json', content: JSON.stringify(buildSettingsJson(input), null, 2) },\n { relativePath: '.mcp.json', content: JSON.stringify(mcpJson, null, 2) },\n { relativePath: 'CHARTER.md', content: input.charterContent },\n { relativePath: 'TOOLS.md', content: input.toolsContent },\n // ENG-4684: named subagent the parent uses for slow channel-message\n // handling. Frontmatter `background: true` makes the parent's listener\n // turn return immediately on dispatch, so new inbound messages get a\n // fresh turn while the subagent does the work in parallel. Triggered\n // by the \"Channel message triage\" instruction in CLAUDE.md.\n // ENG-4821: integrations are rendered into the subagent body so it\n // stops claiming \"no creds\" for capabilities the parent has — and the\n // sidecar JSON keeps incremental writeIntegrations syncs in lockstep.\n {\n relativePath: '.claude/agents/channel-message-handler.md',\n content: buildChannelMessageHandlerAgent({\n mcpServerKeys: initialMcpServerKeys,\n integrations: integrationSummaries,\n }),\n },\n // ENG-5905: project-scope augmented-worker sub-agent — sibling of\n // channel-message-handler, same dynamic render shape, used by the\n // parent for general multi-step background work that doesn't\n // require a channel reply. Closes the gap ENG-5897 left open\n // (the plugin-scope static file never reached the runtime).\n {\n relativePath: '.claude/agents/augmented-worker.md',\n content: buildAugmentedWorkerAgent({\n mcpServerKeys: initialMcpServerKeys,\n integrations: integrationSummaries,\n }),\n },\n {\n relativePath: `provision/${INTEGRATIONS_SUMMARY_FILE}`,\n content: JSON.stringify(integrationSummaries, null, 2),\n },\n ];\n\n // Generate a single combined knowledge skill containing all org + team knowledge.\n // ENG-4524: gated by agents.knowledge_delivery — 'search' agents reach\n // knowledge through MCP tools only; 'files' / 'both' get the bundled file.\n const knowledgeEntries = input.knowledge ?? [];\n const delivery = input.knowledgeDelivery ?? 'both';\n const includeFiles = delivery === 'files' || delivery === 'both';\n if (knowledgeEntries.length > 0 && includeFiles) {\n const safeTitles = knowledgeEntries.map((k) => k.title.replace(/[\\n\\r\"\\\\]/g, ' ').trim().toLowerCase()).filter(Boolean);\n const sections = knowledgeEntries.map((entry) => {\n const scopeLabel = entry.scope === 'org' ? 'Organization' : entry.scope === 'global' ? 'Augmented Team' : 'Team';\n const safeTitle = entry.title.replace(/[\\n\\r]/g, ' ').trim();\n return `## ${safeTitle}\\n*${scopeLabel} knowledge*\\n\\n${entry.content}`;\n }).join('\\n\\n---\\n\\n');\n\n const description = `Use this skill when the user asks about the company, organization, team, products, or any of: ${safeTitles.join(', ')}. Provides core reference material from the knowledge base.`;\n artifacts.push({\n relativePath: '.claude/skills/core-knowledge/SKILL.md',\n content: `---\\nname: core-knowledge\\ndescription: ${JSON.stringify(description)}\\n---\\n\\n# Core Knowledge\\n\\n${sections}`,\n });\n }\n\n // ADR-0012 / ENG-6352: down-synced dynamic workflows. One artifact per\n // resolved workflow at `.claude/workflows/<name>.js`; deployArtifactsToProject\n // mirrors + prunes them into the project dir on the same drift loop. The\n // server resolves the set (team default ∪ agent override) and gates it on\n // the `workflows-down-sync` flag, so an empty/undefined array simply emits\n // nothing. `name` is kebab-case by construction (validated on create), but\n // we re-guard here against path traversal before it becomes a filename —\n // an unexpected name is skipped, never written outside the workflows dir.\n for (const workflow of input.workflows ?? []) {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(workflow.name)) continue;\n artifacts.push({\n relativePath: `.claude/workflows/${workflow.name}.js`,\n content: workflow.script,\n });\n }\n\n artifacts.push({ relativePath: '.git-hooks/pre-commit', content: PRE_COMMIT_HOOK });\n\n return artifacts;\n },\n\n driftTrackedFiles(): string[] {\n return ['CLAUDE.md', 'settings.json', '.mcp.json', 'CHARTER.md', 'TOOLS.md'];\n },\n\n deployArtifactsToProject(codeName: string, provisionDir: string): void {\n deployArtifactsToProject(codeName, provisionDir);\n },\n\n async getRegisteredAgents(_profile?: string): Promise<Set<string>> {\n // Claude Code doesn't have a central agent registry like OpenClaw.\n // We detect registered agents by scanning ~/.augmented/ for directories\n // containing `registration.json` — the canonical marker written by\n // registerAgent and removed by deregisterAgent. The manager-worker's\n // `provision/` tree persists independently (generated artifacts) so\n // it's NOT a reliable registration signal — a deregistered agent\n // would keep its provision/ around and get falsely rediscovered here.\n //\n // Before ENG-4418 we checked for a `claudecode/` subdirectory, but\n // that intermediate was collapsed away — newly provisioned agents\n // never create it and migrated agents have it removed post-migration.\n const homeDir = getHomeDir();\n const augDir = join(homeDir, '.augmented');\n const agents = new Set<string>();\n\n try {\n const entries = readdirSync(augDir);\n for (const entry of entries) {\n // Skip the shared `_mcp` assets dir and any hidden files.\n if (entry.startsWith('_') || entry.startsWith('.')) continue;\n const agentRoot = join(augDir, entry);\n // ENG-7891 / ADR-0049: skip the codename compatibility symlink so an\n // id-keyed agent is discovered once - via its real ~/.augmented/{id}\n // dir, not also via the {codeName} -> {id} link. `lstatSync` (not the\n // symlink-following `statSync`) is what makes this a dedupe: the link\n // is classified as a symlink here rather than as a directory.\n let st;\n try {\n st = lstatSync(agentRoot);\n } catch {\n continue;\n }\n if (st.isSymbolicLink() || !st.isDirectory()) continue;\n if (!existsSync(join(agentRoot, 'registration.json'))) continue;\n // Return the codename the rest of the manager keys on. For a legacy\n // agent the dir name IS the codename; for an id-keyed agent the real\n // dir is the UUID, so resolve back to the codename via the marker.\n let codeName = entry;\n try {\n const reg = JSON.parse(readFileSync(join(agentRoot, 'registration.json'), 'utf8'));\n if (reg && typeof reg.code_name === 'string' && reg.code_name) {\n codeName = reg.code_name;\n }\n } catch {\n // Marker unreadable/corrupt - fall back to the directory name.\n }\n agents.add(codeName);\n }\n } catch {\n // .augmented dir doesn't exist yet\n }\n\n return agents;\n },\n\n async registerAgent(codeName: string, teamDir: string, _model?: string | null, agentId?: string | null): Promise<boolean> {\n try {\n // ENG-7891 / ADR-0049: once armed, new agents get a real\n // ~/.augmented/{agent_id} dir plus a codename compatibility symlink.\n // Until then (and for legacy agents already on disk) this is inert and\n // everything resolves to the codename dir, unchanged.\n if (ID_KEYED_LAYOUT_ENABLED && agentId) {\n ensureIdKeyedLayout(codeName, agentId);\n }\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n mkdirSync(agentDir, { recursive: true });\n mkdirSync(projectDir, { recursive: true });\n\n // Write a registration marker with both team and project directory paths.\n // `code_name` is the identity the manager keys on; getRegisteredAgents\n // reads it back so an id-keyed real dir still reports its codename.\n writeFileSync(\n join(agentDir, 'registration.json'),\n JSON.stringify({\n code_name: codeName,\n agent_id: agentId ?? null,\n team_dir: teamDir,\n project_dir: projectDir,\n framework: 'claude-code',\n registered_at: new Date().toISOString(),\n }, null, 2),\n );\n\n // Deploy artifacts from provision dir to the isolated project dir\n // teamDir is the manager's provision dir (e.g., ~/.augmented/bob/provision)\n if (existsSync(teamDir)) {\n deployArtifactsToProject(codeName, teamDir);\n }\n\n return true;\n } catch {\n return false;\n }\n },\n\n async deregisterAgent(codeName: string): Promise<boolean> {\n try {\n const agentDir = getAgentDir(codeName);\n const regFile = join(agentDir, 'registration.json');\n if (existsSync(regFile)) {\n const { unlinkSync } = await import('node:fs');\n unlinkSync(regFile);\n }\n return true;\n } catch {\n return false;\n }\n },\n\n writeAuthProfiles(codeName: string, profiles: AuthProfileInput[]): void {\n const agentDir = getAgentDir(codeName);\n mkdirSync(agentDir, { recursive: true });\n\n // Write auth profiles as environment variables in a .env file\n // Claude Code reads env vars from .env files in the project directory\n const envLines: string[] = ['# Augmented auth profiles — auto-generated, do not edit'];\n\n for (const p of profiles) {\n if (!p.api_key) continue;\n\n // Map provider names to standard env var conventions\n const providerEnvPrefix = p.provider.toUpperCase().replace(/[^A-Z0-9]/g, '_');\n envLines.push(`${providerEnvPrefix}_API_KEY=${shellQuote(p.api_key)}`);\n // ENG-4594: integrations whose API ships in both managed-cloud and\n // self-hosted shapes (Postiz, eventually others) carry a\n // `metadata.base_url` so the operator can point the MCP server at\n // their self-hosted instance. Emit `${PROVIDER}_BASE_URL` alongside\n // the API key — generic enough that a second self-hosted-aware\n // integration can ride this without another adapter change.\n // CodeRabbit (PR #659): trim before the length check so a value\n // of \" \" doesn't pass through and break the MCP child's URL parse.\n const rawBaseUrl = p.metadata['base_url'];\n const baseUrl = typeof rawBaseUrl === 'string' ? rawBaseUrl.trim() : '';\n if (baseUrl.length > 0) {\n envLines.push(`${providerEnvPrefix}_BASE_URL=${shellQuote(baseUrl)}`);\n }\n }\n\n if (envLines.length > 1) {\n const envPath = join(agentDir, '.env');\n writeFileSync(envPath, envLines.join('\\n') + '\\n');\n chmodSync(envPath, SECRET_FILE_MODE);\n }\n },\n\n // Claude Code has no gateway process — methods intentionally omitted\n // so ensureGatewayRunning() returns early with running=false\n\n async getVersion(): Promise<string | null> {\n try {\n const { execFile } = await import('node:child_process');\n return new Promise((resolve) => {\n execFile('claude', ['--version'], { timeout: 5000 }, (err, stdout) => {\n if (err) { resolve(null); return; }\n const match = stdout.trim().match(/(\\d+\\.\\d+\\.\\d+)/);\n resolve(match?.[1] ?? (stdout.trim() || null));\n });\n });\n } catch {\n return null;\n }\n },\n\n writeChannelCredentials(codeName: string, channelId: string, config: Record<string, unknown>, options?: { addBinding?: boolean; sessionMode?: string; agentId?: string; telegramPeerDisabled?: boolean; peerDisabled?: 'off' | 'cross_team_only' | 'all'; telegramPeers?: ReadonlyArray<{ code_name: string; bot_id: number; agent_id: string; gate_path?: 'same_team' | 'intra_org_unrestricted' | `grant:${string}` | null }>; slackPeers?: ReadonlyArray<{ code_name: string; bot_user_id: string; agent_id: string; gate_path?: 'same_team' | 'intra_org_unrestricted' | `grant:${string}` | null }>; /** ENG-7349: same-team managed agents' Slack user ids → SLACK_TEAM_PEER_USER_IDS (peer-enablement hint only; never widens the classifier admit set). */ slackTeamPeerUserIds?: ReadonlyArray<string>; agentTimezone?: string; /** ENG-5841: effective sender_policy for the slack-channel / teams-channel MCP filters PR #1525 shipped. /host/refresh resolves (agent override > org default > 'all') and only sends this when the mode is restrictive — null/undefined means no env var injection. */ senderPolicy?: { mode: 'all' | 'agents_only' | 'team_only' | 'team_agents_only' | 'manager_only'; team_id?: string; /** ENG-5842: per-channel principal IDs for manager_only mode (resolved at /host/refresh via reports_to_person). */ principal?: { slack_user_id?: string; telegram_chat_id?: string; teams_aad_object_id?: string }; /** ENG-5871: per-channel team-member principal ID lists for team_only mode (resolved at /host/refresh via team_members ⋈ organization_people ⋈ contact_preferences). */ team_principals?: { slack_user_ids?: string[]; telegram_chat_ids?: string[]; teams_aad_object_ids?: string[] }; /** ENG-5843: drop external Slack Connect / Teams federated tenant senders by injecting SLACK_HOME_TEAM_ID / MSTEAMS_HOME_TENANT_ID env vars (read from the channel's bot install config). */ internal_only?: boolean; source?: 'agent' | 'org' } | null; /** ENG-6155: agent avatar URL → SLACK_AGENT_AVATAR_URL for the slack-channel MCP (sets the bot's Slack profile photo). */ agentAvatarUrl?: string }): void {\n // ENG-5363: TZ env var the spawned channel MCPs inherit so any\n // agent-local timestamps they render use the agent's configured\n // timezone rather than the host zone. Empty / 'UTC' / unset all fall\n // through to the existing UTC default — no behaviour change for teams\n // that haven't set a timezone.\n const tzEnv: Record<string, string> = options?.agentTimezone && options.agentTimezone.trim() !== ''\n ? { TZ: options.agentTimezone.trim() }\n : {};\n\n // ENG-5841: assemble the sender_policy env block once and spread into both\n // slack-channel and teams-channel env blocks below. The MCP filter at\n // packages/mcp/src/slack-channel.ts:105 and teams-channel.ts:144 reads\n // <CHANNEL>_SENDER_POLICY as the mode string and AGT_TEAM_ID (singular,\n // shared) for team_agents_only mode.\n //\n // ENG-5842: manager_only mode also injects <CHANNEL>_SENDER_POLICY_PRINCIPAL_ID\n // — channel-specific because the principal's ID format is different per\n // channel (Slack user_id \"U...\", Teams AAD object id GUID). Each channel\n // block below picks the right principal field.\n //\n // When the option isn't set (most agents, default 'all' mode), the shared\n // block stays empty and no extra env vars get injected — keeps the env\n // clean and the \"is this gated?\" check a 1-line grep on the MCP env.\n const senderPolicyMode = options?.senderPolicy?.mode;\n // ENG-5841 + ENG-5842 + ENG-5871: team_id is needed by team_agents_only,\n // manager_only, AND team_only — all three share the same-team-agent\n // label-check path on the agent axis. /host/refresh sets team_id on all\n // three modes; the adapter just propagates it.\n const senderPolicyTeamId =\n options?.senderPolicy?.mode === 'team_agents_only' ||\n options?.senderPolicy?.mode === 'manager_only' ||\n options?.senderPolicy?.mode === 'team_only'\n ? options.senderPolicy.team_id\n : undefined;\n const slackPrincipalId =\n options?.senderPolicy?.mode === 'manager_only' ? options.senderPolicy.principal?.slack_user_id : undefined;\n const teamsPrincipalId =\n options?.senderPolicy?.mode === 'manager_only' ? options.senderPolicy.principal?.teams_aad_object_id : undefined;\n // ENG-5871: team_only mode injects comma-separated lists of team-member\n // principal IDs per channel. Empty / absent list = MCP filter fails\n // closed on humans for this channel but still admits same-team Augmented\n // agents via the label path (per the migration header).\n const slackTeamPrincipalIds =\n options?.senderPolicy?.mode === 'team_only'\n ? options.senderPolicy.team_principals?.slack_user_ids?.join(',')\n : undefined;\n const teamsTeamPrincipalIds =\n options?.senderPolicy?.mode === 'team_only'\n ? options.senderPolicy.team_principals?.teams_aad_object_ids?.join(',')\n : undefined;\n // ENG-5843: org-boundary gate. When true, the per-channel block below\n // injects <CHANNEL>_HOME_TEAM_ID / <CHANNEL>_HOME_TENANT_ID env vars\n // sourced from the channel's bot install config so the MCP filter can\n // drop external Slack Connect / Teams federated tenant senders.\n const senderPolicyInternalOnly = options?.senderPolicy?.internal_only === true;\n // Shared (channel-agnostic) part — just the team_id when needed.\n const senderPolicyEnv: Record<string, string> = senderPolicyTeamId\n ? { AGT_TEAM_ID: senderPolicyTeamId }\n : {};\n const agentDir = getAgentDir(codeName);\n mkdirSync(agentDir, { recursive: true });\n\n const isPersistent = options?.sessionMode === 'persistent';\n // ENG-4940: channel-agnostic peer kill switch — resolved once at the\n // top so both the Telegram and Slack branches below can emit the\n // PEER_DISABLED env consistently. Honours the legacy boolean\n // `telegramPeerDisabled` as a fallback during the rollout.\n const peerDisabledMode: 'off' | 'cross_team_only' | 'all' =\n options?.peerDisabled ??\n (options?.telegramPeerDisabled === true ? 'all' : 'off');\n\n // ENG-4437: Telegram routes through the per-agent MCP server in BOTH\n // session modes. The old `@anthropic/claude-code-telegram` npx path is\n // gone — that package name doesn't exist on public npm, and anyway the\n // whole reason this change exists is to avoid the single-global-token\n // collision (which the plugin pattern forces). No mode-specific fork:\n // the local telegram-channel.js handles inbound + outbound + shutdown\n // identically for oneshot and persistent sessions.\n if (channelId === 'telegram') {\n const botToken = config['bot_token'] as string | undefined;\n if (!botToken) return;\n\n const allowedChats = config['allowed_chats'] as string[] | undefined;\n // deployMcpAssets() on manager startup writes this file from the\n // CLI's bundled MCP assets - that's the authoritative path on\n // prod, and now the ONLY one: @integrity-labs/augmented-mcp is no\n // longer published to npm (ENG-7197 retired the standalone publish;\n // npm `latest` is frozen at a divergent 0.8.0 with old tool names),\n // so an `npx -y @integrity-labs/augmented-mcp` fallback would pull a\n // stale, wrong build. We deliberately don't fall back: the manager-\n // deployed local copy is the single source of truth. If the local\n // file is missing the node spawn will error clearly, pointing at a\n // broken manager install, which is the right failure mode vs a\n // silently-divergent version pulled from npm.\n const localTelegramChannel = join(getHomeDir(), '.augmented', '_mcp', 'telegram-channel.js');\n // ENG-4986 + ENG-4937 + ENG-4909 follow-up: the Telegram MCP child\n // needs the AGT auth trio (AGT_HOST + AGT_API_KEY + AGT_AGENT_ID)\n // to call back into /host/*. Three runtime features silently\n // no-op without these:\n // - observed-chat client (ENG-4986) — saved_chats auto-populate\n // (so the webapp Multi-agent panel never shows group chats)\n // - cross-team peer audit (ENG-4937) — audit log emission\n // - peer rate limiter (ENG-4909) — durable budget enforcement\n // Mirrors the pattern used in Slack's blockKitEnv (line ~1869):\n // resolve AGT_HOST from process.env, fall back to the production\n // host string so default-host managers still work, and only\n // forward the API key when it's actually set.\n const resolvedAgtHostForTelegram =\n process.env['AGT_HOST']?.trim() || 'https://api.augmented.team';\n // CodeRabbit on PR #912: trim AGT_API_KEY too. A whitespace-only\n // value would otherwise forward as-is and the child would treat\n // auth as configured — callback auth then fails with a confusing\n // 401 instead of cleanly falling through to the documented\n // no-op-when-unset path.\n const resolvedAgtApiKeyForTelegram = process.env['AGT_API_KEY']?.trim();\n // ENG-5901 Track D: the raw bot token lands in .env.integrations\n // (upserted BEFORE the templated .mcp.json write so no spawn can\n // observe a template whose value isn't on disk yet); .mcp.json\n // carries the `${VAR}` placeholder that Claude Code substitutes at\n // MCP-launch from the sourced spawn env. AGT_API_KEY is templated\n // too — the manager exports it to every spawn env via getApiKey(),\n // so no .env.integrations entry is needed for it.\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'upsert',\n updates: { TELEGRAM_BOT_TOKEN: botToken },\n });\n const telegramEnv: Record<string, string> = {\n TELEGRAM_BOT_TOKEN: '${TELEGRAM_BOT_TOKEN}',\n AGT_AGENT_CODE_NAME: codeName,\n AGT_HOST: resolvedAgtHostForTelegram,\n ...(resolvedAgtApiKeyForTelegram\n ? { AGT_API_KEY: '${AGT_API_KEY}' }\n : {}),\n ...(options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}),\n ...tzEnv,\n // ENG-6582 (D16): stamp the verified turn initiator for broker MCPs.\n AGT_TURN_INITIATOR_FILE: join(getAgentDir(codeName), '.current-turn-initiator.json'),\n };\n if (allowedChats && allowedChats.length > 0) {\n telegramEnv.TELEGRAM_ALLOWED_CHATS = allowedChats.join(',');\n }\n\n // ENG-6464: ack/skip reaction emoji (unicode, from Telegram's free-tier\n // set). ack_reaction only emitted when set so unset configs keep the MCP's\n // historical '👀' fallback (no silent change); skip_reaction is gated\n // MCP-side by the channel-skip-reaction flag, so emitting it is inert\n // until the flag is flipped on.\n const telegramAckReaction = config['ack_reaction'];\n if (typeof telegramAckReaction === 'string' && telegramAckReaction.trim().length > 0) {\n telegramEnv.TELEGRAM_ACK_REACTION = telegramAckReaction.trim();\n }\n const telegramSkipReaction = config['skip_reaction'];\n if (typeof telegramSkipReaction === 'string' && telegramSkipReaction.trim().length > 0) {\n telegramEnv.TELEGRAM_SKIP_REACTION = telegramSkipReaction.trim();\n }\n\n // ENG-6059: diagnostic allowlist for /investigate-<code-name> — the\n // chat IDs of team owners/admins + the agent's reports-to person,\n // resolved server-side at /host/refresh and injected into the\n // response config (never persisted). Absent or empty → the env var\n // is omitted and the MCP's fail-closed gate keeps the command\n // disabled (it exposes the agent's raw terminal).\n const rawDiagnosticChatIds = config['diagnostic_chat_ids'];\n if (Array.isArray(rawDiagnosticChatIds)) {\n const diagnosticChatIds = rawDiagnosticChatIds\n .map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : ''))\n .filter((v) => v.length > 0);\n if (diagnosticChatIds.length > 0) {\n telegramEnv.TELEGRAM_DIAGNOSTIC_CHAT_IDS = diagnosticChatIds.join(',');\n }\n }\n\n // ENG-6931: /ping connectivity allowlist - chat IDs of the agent's team\n // members + reports-to manager, resolved server-side at /host/refresh\n // (never persisted). Absent or empty -> the env var is omitted and the\n // MCP's fail-closed /ping gate keeps the command disabled.\n const rawPingChatIds = config['ping_allowed_chat_ids'];\n if (Array.isArray(rawPingChatIds)) {\n const pingChatIds = rawPingChatIds\n .map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : ''))\n .filter((v) => v.length > 0);\n if (pingChatIds.length > 0) {\n telegramEnv.TELEGRAM_PING_ALLOWED_CHAT_IDS = pingChatIds.join(',');\n }\n }\n\n // ENG-4923: per-agent peer-collaboration env. The MCP child's\n // classifier (ENG-4902/4909) reads three env vars to decide peer\n // behaviour; without them the classifier defaults peer_agent_mode\n // to 'off' and every bot-authored message short-circuits with\n // mode_off. Read peer_agent_mode + peer_group_ids straight off\n // the per-agent TelegramChannelConfig (storage from ENG-4900);\n // assemble TELEGRAM_PEERS from the manager-supplied peer roster\n // (manager resolves CHARTER multi_agent.telegram_peers entries\n // to {code_name, bot_id, agent_id} triples by cross-referencing\n // the team's other agents).\n // `config` is Record<string, unknown> — runtime data from the API.\n // Validate shapes defensively rather than trusting casts; CodeRabbit\n // on PR #847 caught that an unexpected non-array `peer_group_ids`\n // would throw at `.join()` and abort the credentials write.\n const rawPeerAgentMode = config['peer_agent_mode'];\n if (rawPeerAgentMode === 'listen' || rawPeerAgentMode === 'respond') {\n telegramEnv.TELEGRAM_PEER_AGENT_MODE = rawPeerAgentMode;\n }\n const rawPeerGroupIds = config['peer_group_ids'];\n if (Array.isArray(rawPeerGroupIds) && rawPeerGroupIds.length > 0) {\n // Coerce to non-empty strings; tolerate Telegram's numeric chat_ids\n // returning as numbers without needing a separate type to express it.\n const peerGroupIds = rawPeerGroupIds\n .map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : ''))\n .filter((v) => v.length > 0);\n if (peerGroupIds.length > 0) {\n telegramEnv.TELEGRAM_PEER_GROUP_IDS = peerGroupIds.join(',');\n }\n }\n if (options?.telegramPeers && options.telegramPeers.length > 0) {\n // TELEGRAM_PEERS keeps its pre-ENG-4935 shape (`{code_name, bot_id,\n // agent_id}`) so older classifier env parsers stay compatible. The\n // gate_path travels in a separate TELEGRAM_PEERS_GATE env var.\n telegramEnv.TELEGRAM_PEERS = JSON.stringify(\n options.telegramPeers.map((p) => ({\n code_name: p.code_name,\n bot_id: p.bot_id,\n agent_id: p.agent_id,\n })),\n );\n // ENG-4935 / ENG-4929 §4.0: per-peer gate_path keyed by bot_id.\n // Emit only when at least one peer carries a gate_path (older\n // managers don't set it; in that case omit the var entirely and\n // the classifier falls back to its pre-ENG-4935 admit-all path).\n const gateEntries = options.telegramPeers\n .filter((p) => p.gate_path !== undefined)\n .map((p) => [String(p.bot_id), p.gate_path] as const);\n if (gateEntries.length > 0) {\n telegramEnv.TELEGRAM_PEERS_GATE = JSON.stringify(\n Object.fromEntries(gateEntries),\n );\n }\n }\n\n // ENG-4912 / ENG-4940 / spec §5.5 #3, §8, §12 #6: channel-agnostic\n // peer kill switch. The team-admin flips\n // `teams.settings.peer_disabled` to 'cross_team_only' or 'all' via\n // PATCH /teams/:slug/peer-disabled — the manager folds it into\n // `peerDisabled` here and we emit PEER_DISABLED on the MCP child.\n // For backwards compat the legacy `telegramPeerDisabled` boolean\n // and TELEGRAM_PEER_DISABLED env are still honoured during the\n // ENG-4940 rollout — MCP children built on the old shape pick up\n // the legacy env until they redeploy, and pre-ENG-4940 managers\n // can still drive the old boolean.\n if (peerDisabledMode !== 'off') {\n telegramEnv.PEER_DISABLED = peerDisabledMode;\n }\n // Legacy env mirror — only emit when the effective mode is 'all'\n // since the old shape couldn't express 'cross_team_only'. A\n // child still reading TELEGRAM_PEER_DISABLED will behave the same\n // as before for the kill-all case.\n if (peerDisabledMode === 'all') {\n telegramEnv.TELEGRAM_PEER_DISABLED = 'true';\n }\n const telegramEntry = {\n command: 'node',\n args: [localTelegramChannel],\n env: telegramEnv,\n };\n const provisionMcpPath = join(agentDir, 'provision', '.mcp.json');\n mkdirSync(dirname(provisionMcpPath), { recursive: true });\n let mcpConfig: { mcpServers: Record<string, unknown> } = { mcpServers: {} };\n try {\n mcpConfig = JSON.parse(readFileSync(provisionMcpPath, 'utf-8'));\n if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};\n } catch { /* new file */ }\n mcpConfig.mcpServers['telegram'] = telegramEntry;\n if (!writeMcpJsonGuarded(codeName, provisionMcpPath, mcpConfig)) {\n // Validation rejected the rendered config — bail without\n // syncing so we don't propagate a stale provision file.\n return;\n }\n syncMcpToProject(codeName);\n return;\n }\n\n // For persistent mode: Slack uses a per-agent MCP channel server\n // (written into the provision .mcp.json so claude-code resolves it\n // under project scope — see the Slack comment below for the scope\n // gotcha). Discord still uses the global-env plugin pattern.\n if (isPersistent && (channelId === 'discord' || channelId === 'slack')) {\n const channelDir = join(getHomeDir(), '.claude', 'channels', channelId);\n // Only create the global channel dir for the plugin-based path\n // (Discord). Slack no longer uses a global .env — each agent carries\n // its own token inside its provision .mcp.json.\n if (channelId === 'discord') mkdirSync(channelDir, { recursive: true });\n\n if (channelId === 'discord') {\n const botToken = config['bot_token'] as string | undefined;\n if (botToken) {\n writeFileSync(join(channelDir, '.env'), `DISCORD_BOT_TOKEN=${botToken}\\n`);\n }\n } else if (channelId === 'slack') {\n // Slack MCP server must go in the project-scope .mcp.json. Claude Code\n // only resolves `--dangerously-load-development-channels server:<name>`\n // against MCP servers in scopes enterprise/user/project/local — servers\n // passed via --mcp-config register as session scope and are invisible\n // to the channel name matcher (yG5 in 2.1.114).\n //\n // Write to the provision-dir .mcp.json (source of truth) and call\n // syncMcpToProject so subsequent artifact syncs don't wipe slack.\n const botToken = config['bot_token'] as string | undefined;\n const appToken = config['app_token'] as string | undefined;\n const threadAutoFollow = config['thread_auto_follow'] as string | undefined;\n const channelResponseMode = config['channel_response_mode'] as string | undefined;\n // ENG-6464: ack/skip reaction emoji. ack_reaction was previously\n // dead-wired for the Claude Code fleet (the MCP hardcoded 'eyes'); only\n // emit when set so unset configs keep the MCP's 'eyes' fallback — no\n // silent change on ship. skip_reaction is gated MCP-side by the\n // channel-skip-reaction flag, so emitting it here is inert until flipped.\n // Trim and treat whitespace-only as unset so a stray \" \" can't be\n // emitted as the env var and override the MCP's fallback reaction.\n const ackReaction = ((config['ack_reaction'] as string | undefined) ?? '').trim() || undefined;\n const skipReaction = ((config['skip_reaction'] as string | undefined) ?? '').trim() || undefined;\n // ENG-6035: per-agent diagnostic/restart allowlist. Defensive shape\n // filter — the config JSONB is operator-editable, so drop anything\n // that isn't a non-empty string before joining on the MCP's\n // `.split(',')` wire format (slack-channel.ts parses env once at\n // boot). Slack member IDs are [A-Z0-9]+ so commas can't collide.\n const allowedUsers = Array.isArray(config['allowed_users'])\n ? (config['allowed_users'] as unknown[])\n .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)\n .map((v) => v.trim())\n : [];\n // ENG-6931: /ping connectivity allowlist (team members + reports-to\n // manager), resolved server-side. Same defensive shape filter as\n // allowed_users; emitted as SLACK_PING_ALLOWED_USERS below.\n const pingAllowedUsers = Array.isArray(config['ping_allowed_users'])\n ? (config['ping_allowed_users'] as unknown[])\n .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)\n .map((v) => v.trim())\n : [];\n // ENG-6504: Block Kit + ask_user are permanently ON for every Slack\n // channel. Always emit SLACK_BLOCK_KIT_ENABLED / SLACK_BLOCK_KIT_ASK_USER_ENABLED\n // — the per-channel opt-in (block_kit_enabled / block_kit_ask_user_enabled)\n // was removed. The fleet-level SLACK_BLOCK_KIT_DISABLED brake is still\n // honoured (passed through to the MCP). The AGT_* callback trio that\n // ask_user needs (to reach /host/pending-interactions) is emitted\n // unconditionally below via slackAgtAuthEnv, so it isn't repeated here.\n // CodeRabbit (PR #535): when AGT_HOST is unset on the manager process,\n // apps/cli's getHost() defaults to the production host; we mirror that\n // default for slackAgtAuthEnv below. Kept in sync manually with\n // apps/cli/src/lib/config.ts:DEFAULT_AGT_HOST.\n const blockKitDisabled = process.env['SLACK_BLOCK_KIT_DISABLED'] === 'true';\n const resolvedAgtHost = process.env['AGT_HOST']?.trim() || 'https://api.augmented.team';\n const blockKitEnv = {\n SLACK_BLOCK_KIT_ENABLED: 'true',\n SLACK_BLOCK_KIT_ASK_USER_ENABLED: 'true',\n ...(blockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: 'true' } : {}),\n };\n if (botToken) {\n // CodeRabbit on PR #934: persistent Slack branch was missing\n // the peer/gate + AGT-auth env wiring the oneshot branch\n // below already had. Without this, agents in persistent\n // session mode (which is most of them) would never get\n // SLACK_PEERS populated → classifier stays empty → cross-team\n // Slack messages keep dropping as `unknown_peer`. Mirrors the\n // oneshot env block at line ~2050 verbatim.\n const slackPeerEnv: Record<string, string> = {};\n const rawSlackPeerAgentMode = config['peer_agent_mode'];\n if (rawSlackPeerAgentMode === 'listen' || rawSlackPeerAgentMode === 'respond') {\n slackPeerEnv.SLACK_PEER_AGENT_MODE = rawSlackPeerAgentMode;\n }\n const rawSlackPeerGroupIds = config['peer_group_ids'];\n if (Array.isArray(rawSlackPeerGroupIds) && rawSlackPeerGroupIds.length > 0) {\n const ids = rawSlackPeerGroupIds\n .map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : ''))\n .filter((v) => v.length > 0);\n if (ids.length > 0) slackPeerEnv.SLACK_PEER_GROUP_IDS = ids.join(',');\n }\n if (options?.slackPeers && options.slackPeers.length > 0) {\n slackPeerEnv.SLACK_PEERS = JSON.stringify(\n options.slackPeers.map((p) => ({\n code_name: p.code_name,\n bot_user_id: p.bot_user_id,\n agent_id: p.agent_id,\n })),\n );\n const gateEntries = options.slackPeers\n .filter((p) => p.gate_path !== undefined)\n .map((p) => [p.bot_user_id, p.gate_path] as const);\n if (gateEntries.length > 0) {\n slackPeerEnv.SLACK_PEERS_GATE = JSON.stringify(Object.fromEntries(gateEntries));\n }\n }\n // ENG-7349: same-team roster for the peer-enablement operator hint.\n // Deliberately NOT gated on peer_agent_mode / slackPeers - the hint\n // fires precisely when those are unset.\n if (options?.slackTeamPeerUserIds && options.slackTeamPeerUserIds.length > 0) {\n slackPeerEnv.SLACK_TEAM_PEER_USER_IDS = options.slackTeamPeerUserIds.join(',');\n }\n // AGT auth trio — full trio, not just the block-kit-conditional\n // subset above. The Slack MCP child needs these to call\n // /host/cross-team-peer-event (ENG-4937), and any future\n // self-heal route (e.g. ENG-4986-equivalent for slack\n // bot_user_id backfill).\n const slackResolvedAgtApiKey = process.env['AGT_API_KEY']?.trim();\n const slackAgtAuthEnv: Record<string, string> = {\n AGT_HOST: resolvedAgtHost,\n // ENG-5901 Track D: template — manager exports AGT_API_KEY to\n // every spawn env (getApiKey()); the gate still keys off the\n // manager actually having one.\n ...(slackResolvedAgtApiKey ? { AGT_API_KEY: '${AGT_API_KEY}' } : {}),\n ...(options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}),\n };\n\n // ENG-5901 Track D: raw Slack tokens to .env.integrations first,\n // `${VAR}` templates in .mcp.json (Claude Code substitutes at\n // MCP-launch from the sourced spawn env).\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'upsert',\n updates: {\n SLACK_BOT_TOKEN: botToken,\n ...(appToken ? { SLACK_APP_TOKEN: appToken } : {}),\n },\n });\n const localSlackChannel = join(getHomeDir(), '.augmented', '_mcp', 'slack-channel.js');\n // ENG-6245: refuse to inject a data-URI / oversized avatar URL —\n // posix_spawn caps an env entry at MAX_ARG_STRLEN (128 KiB), so a\n // ~1.5 MB base64 data-URI E2BIGs the slack MCP and bricks the channel.\n // null ⇒ env omitted, bot keeps its current photo (graceful degrade).\n const slackAvatarEnvUrl = resolveAvatarEnvUrl(options?.agentAvatarUrl).url;\n const slackEntry = {\n command: existsSync(localSlackChannel) ? 'node' : 'npx',\n args: existsSync(localSlackChannel) ? [localSlackChannel] : ['-y', '@augmented/claude-code-channel-slack'],\n env: {\n SLACK_BOT_TOKEN: '${SLACK_BOT_TOKEN}',\n ...(appToken ? { SLACK_APP_TOKEN: '${SLACK_APP_TOKEN}' } : {}),\n ...(threadAutoFollow && threadAutoFollow !== 'off' ? { SLACK_THREAD_AUTO_FOLLOW: threadAutoFollow } : {}),\n // ENG-4464: only emit when non-default — `mention_only` is the\n // default in slack-response-mode.ts, so omitting keeps the env\n // block tidy for the common case.\n ...(channelResponseMode && channelResponseMode !== 'mention_only' ? { SLACK_CHANNEL_RESPONSE_MODE: channelResponseMode } : {}),\n // ENG-6464: ack/skip reaction emoji (only when set — see above).\n ...(ackReaction ? { SLACK_ACK_REACTION: ackReaction } : {}),\n ...(skipReaction ? { SLACK_SKIP_REACTION: skipReaction } : {}),\n // Scopes slack.upload_file uploads to the agent's project dir.\n AGT_AGENT_CODE_NAME: codeName,\n ...blockKitEnv,\n ...slackPeerEnv,\n ...slackAgtAuthEnv,\n ...tzEnv,\n // ENG-6155: the agent's avatar URL (public 512×512 JPG). The\n // slack-channel MCP sets it as the bot's Slack profile photo on\n // first connect (users.setPhoto). Omitted when absent so the bot\n // keeps its current photo; the URL's cache-bust param changes on\n // re-generation, so the MCP re-applies a new avatar.\n // ENG-6245: slackAvatarEnvUrl is null for data-URI / oversized values.\n ...(slackAvatarEnvUrl\n ? { SLACK_AGENT_AVATAR_URL: slackAvatarEnvUrl }\n : {}),\n // ENG-4940: channel-agnostic peer kill switch — same enum\n // as the Telegram path emits above. The Slack classifier\n // (ENG-4936) honours PEER_DISABLED with identical\n // semantics. Only emit when non-default so the env block\n // stays clean for the common case.\n ...(peerDisabledMode !== 'off' ? { PEER_DISABLED: peerDisabledMode } : {}),\n // ENG-5841: SLACK_SENDER_POLICY drives slack-inbound-filter.ts.\n // Only emitted when the effective mode is restrictive (default\n // 'all' is the absence of the env var — same convention the\n // MCP filter uses on its own end).\n ...(senderPolicyMode ? { SLACK_SENDER_POLICY: senderPolicyMode } : {}),\n ...senderPolicyEnv, // AGT_TEAM_ID when team_agents_only\n // ENG-5842: principal ID for manager_only — Slack user_id from\n // people.contact_preferences.slack_user_id. Omitted when the\n // principal has no Slack ID; MCP filter fails closed on the\n // missing env var by dropping all human inbound.\n ...(slackPrincipalId ? { SLACK_SENDER_POLICY_PRINCIPAL_ID: slackPrincipalId } : {}),\n // ENG-5871: team_only mode injects a comma-separated list of\n // team-member Slack user_ids. Absent/empty = MCP filter drops\n // humans on Slack but still admits same-team agents via label.\n // No additional shape parsing on the MCP side — `.split(',')`\n // works because Slack user_ids are `[A-Z0-9]+` (no commas).\n ...(slackTeamPrincipalIds\n ? { SLACK_SENDER_POLICY_TEAM_PRINCIPAL_IDS: slackTeamPrincipalIds }\n : {}),\n // ENG-5843: org-boundary gate. SLACK_INTERNAL_ONLY signals the\n // filter to check sender's workspace against SLACK_HOME_TEAM_ID\n // (sourced from the bot install's team_id, populated by\n // auth.test at install / first-run). Omitted unless explicitly\n // enabled — the consumer's env-absent default is \"no gate\".\n // When INTERNAL_ONLY is true but home team_id can't be\n // resolved (config['team_id'] not set on the install), the\n // MCP boot guard fails closed at startup rather than admitting\n // every sender as \"internal\".\n ...(senderPolicyInternalOnly ? { SLACK_INTERNAL_ONLY: 'true' } : {}),\n ...(senderPolicyInternalOnly && typeof config['team_id'] === 'string' && (config['team_id'] as string).length > 0\n ? { SLACK_HOME_TEAM_ID: config['team_id'] as string }\n : {}),\n // ENG-7919: inject the bot's own Slack user ID so it is available at\n // MCP startup before auth.test resolves. Populated by the slack-bot-user-id\n // client on first startup and stored in agent_channel_configs.config.bot_user_id.\n // On first provision bot_user_id is absent; the MCP resolves it via auth.test\n // as before and falls back gracefully. On all subsequent provisions this env\n // var eliminates the ~10 s null window that caused self-mention misses.\n ...(typeof config['bot_user_id'] === 'string' && (config['bot_user_id'] as string).length > 0\n ? { SLACK_OWN_BOT_USER_ID: config['bot_user_id'] as string }\n : {}),\n // ENG-6035: per-agent diagnostic/restart allowlist. Gates\n // /investigate-<code-name> (fail-closed: command disabled when\n // unset) and /restart-<code-name> (open when unset). An explicit\n // env entry here overrides any host-level systemd value, which\n // is the point — the host-wide drop-in pattern wrongly scoped\n // the allowlist to every agent on the host. Omitted when empty\n // so the host fallback (and the fail-closed /investigate\n // default) still apply to unconfigured agents.\n ...(allowedUsers.length > 0\n ? { SLACK_ALLOWED_USERS: allowedUsers.join(',') }\n : {}),\n // ENG-6931: /ping connectivity allowlist (team + manager).\n // Omitted when empty so the MCP's fail-closed /ping gate keeps\n // the command disabled for unconfigured agents.\n ...(pingAllowedUsers.length > 0\n ? { SLACK_PING_ALLOWED_USERS: pingAllowedUsers.join(',') }\n : {}),\n // ENG-6563 (D16): stamp the verified turn initiator so broker MCPs\n // can forward it when the agent files an approval mid-turn.\n AGT_TURN_INITIATOR_FILE: join(agentDir, '.current-turn-initiator.json'),\n },\n };\n const provisionMcpPath = join(agentDir, 'provision', '.mcp.json');\n mkdirSync(dirname(provisionMcpPath), { recursive: true });\n let mcpConfig: { mcpServers: Record<string, unknown> } = { mcpServers: {} };\n try {\n mcpConfig = JSON.parse(readFileSync(provisionMcpPath, 'utf-8'));\n if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};\n } catch { /* new file */ }\n mcpConfig.mcpServers['slack'] = slackEntry;\n if (!writeMcpJsonGuarded(codeName, provisionMcpPath, mcpConfig)) {\n return;\n }\n syncMcpToProject(codeName);\n\n // Remove stale .mcp-channels.json left over from the pre-fix layout.\n const staleChannelsPath = join(getProjectDir(codeName), '.mcp-channels.json');\n if (existsSync(staleChannelsPath)) {\n try { rmSync(staleChannelsPath, { force: true }); } catch { /* non-fatal */ }\n }\n }\n }\n\n return;\n }\n\n // For oneshot mode (or non-channel plugins like Slack): add to .mcp.json\n const mcpJsonPath = join(agentDir, 'provision', '.mcp.json');\n // ENG-4970 / ENG-4974: ensure the provision dir exists before\n // safeWriteJsonAtomic tries to write its `.new` temp file. The\n // persistent-telegram branch above does this at line ~1838; without\n // the same guard here, a first-touch Slack write (e.g. an agent\n // that hasn't been through buildArtifacts yet) ENOENTs trying to\n // create the temp file.\n mkdirSync(dirname(mcpJsonPath), { recursive: true });\n\n let mcpConfig: Record<string, { mcpServers: Record<string, unknown> }>;\n try {\n mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));\n } catch {\n mcpConfig = { mcpServers: {} } as any;\n }\n\n const mcpServers = (mcpConfig as any).mcpServers as Record<string, unknown>;\n\n // Telegram is handled unconditionally above — intentionally not in this\n // oneshot/fall-through block. See the ENG-4437 comment at the top of\n // this method.\n if (channelId === 'discord') {\n const botToken = config['bot_token'] as string | undefined;\n if (!botToken) return;\n\n mcpServers['discord'] = {\n command: 'npx',\n args: ['-y', '@anthropic/claude-code-discord'],\n env: { DISCORD_BOT_TOKEN: botToken },\n };\n } else if (channelId === 'slack') {\n const botToken = config['bot_token'] as string | undefined;\n const appToken = config['app_token'] as string | undefined;\n if (!botToken) return;\n\n // For persistent mode: use the custom channel server with claude/channel capability\n // For oneshot mode: use the basic Slack MCP server (outbound only)\n const localSlackChannel = join(getHomeDir(), '.augmented', '_mcp', 'slack-channel.js');\n const slackThreadAutoFollow = config['thread_auto_follow'] as string | undefined;\n const slackAutoFollowEnv = slackThreadAutoFollow && slackThreadAutoFollow !== 'off'\n ? { SLACK_THREAD_AUTO_FOLLOW: slackThreadAutoFollow } : {};\n // ENG-4464: channel response mode (default mention_only is omitted).\n const slackChannelResponseMode = config['channel_response_mode'] as string | undefined;\n const slackResponseModeEnv = slackChannelResponseMode && slackChannelResponseMode !== 'mention_only'\n ? { SLACK_CHANNEL_RESPONSE_MODE: slackChannelResponseMode } : {};\n // ENG-6464: ack/skip reaction emoji (mirror of the persistent surface\n // above — only emit when set; skip is gated MCP-side by the flag).\n // Trim + treat whitespace-only as unset (see persistent surface above).\n const slackAckReaction = ((config['ack_reaction'] as string | undefined) ?? '').trim() || undefined;\n const slackAckReactionEnv = slackAckReaction ? { SLACK_ACK_REACTION: slackAckReaction } : {};\n const slackSkipReaction = ((config['skip_reaction'] as string | undefined) ?? '').trim() || undefined;\n const slackSkipReactionEnv = slackSkipReaction ? { SLACK_SKIP_REACTION: slackSkipReaction } : {};\n // ENG-6035: per-agent diagnostic/restart allowlist — same defensive\n // shape filter and omit-when-empty semantics as the persistent slack\n // branch above (the two slack-spawn surfaces are kept in sync\n // manually; see the mirroring notes throughout this method).\n const slackAllowedUsersList = Array.isArray(config['allowed_users'])\n ? (config['allowed_users'] as unknown[])\n .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)\n .map((v) => v.trim())\n : [];\n const slackAllowedUsersEnv = slackAllowedUsersList.length > 0\n ? { SLACK_ALLOWED_USERS: slackAllowedUsersList.join(',') }\n : {};\n // ENG-6931: /ping connectivity allowlist (team members + reports-to\n // manager) - same defensive filter + omit-when-empty semantics as the\n // persistent slack branch above (kept in sync manually).\n const slackPingAllowedUsersList = Array.isArray(config['ping_allowed_users'])\n ? (config['ping_allowed_users'] as unknown[])\n .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)\n .map((v) => v.trim())\n : [];\n const slackPingAllowedUsersEnv = slackPingAllowedUsersList.length > 0\n ? { SLACK_PING_ALLOWED_USERS: slackPingAllowedUsersList.join(',') }\n : {};\n\n // ENG-6504: same permanent Block Kit + ask_user wiring as the persistent\n // path above. Both tools are always ON for every Slack channel — always\n // emit SLACK_BLOCK_KIT_ENABLED / SLACK_BLOCK_KIT_ASK_USER_ENABLED and the\n // AGT_* callback trio ask_user needs (the one-shot slack entry has no\n // separate always-trio block, so it's included here). The fleet-level\n // SLACK_BLOCK_KIT_DISABLED brake is still passed through.\n // Same default-host fallback as the persistent path above\n // (CodeRabbit, PR #535) — kept in sync manually with\n // apps/cli/src/lib/config.ts:DEFAULT_AGT_HOST.\n const oneshotBlockKitDisabled = process.env['SLACK_BLOCK_KIT_DISABLED'] === 'true';\n const oneshotResolvedAgtHost = process.env['AGT_HOST']?.trim() || 'https://api.augmented.team';\n // Trim before the presence check (mirrors the persistent path's\n // slackResolvedAgtApiKey) so a whitespace-only value doesn't emit a\n // bogus AGT_API_KEY template.\n const oneshotResolvedAgtApiKey = process.env['AGT_API_KEY']?.trim();\n const oneshotBlockKitEnv = {\n SLACK_BLOCK_KIT_ENABLED: 'true',\n SLACK_BLOCK_KIT_ASK_USER_ENABLED: 'true',\n ...(oneshotBlockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: 'true' } : {}),\n AGT_HOST: oneshotResolvedAgtHost,\n // ENG-5901 Track D: template; manager spawn env carries the value.\n ...(oneshotResolvedAgtApiKey ? { AGT_API_KEY: '${AGT_API_KEY}' } : {}),\n ...(options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}),\n };\n\n // ENG-4970 / ENG-4974: per-agent Slack peer-collaboration env.\n // Mirrors the Telegram branch above. The classifier\n // (slack-peer-classifier.ts, ENG-4936) reads four env vars:\n // SLACK_PEER_AGENT_MODE — this agent's mode (off/listen/respond)\n // SLACK_PEER_GROUP_IDS — comma-list of allowed channel ids\n // SLACK_PEERS — JSON [{code_name, bot_user_id, agent_id}]\n // SLACK_PEERS_GATE — JSON {bot_user_id: gate_path}\n // Until this PR, only the classifier code existed — these env vars\n // were never populated by the adapter, so every bot-authored Slack\n // message dropped with `unknown_peer` regardless of grant state.\n const slackPeerEnv: Record<string, string> = {};\n const rawSlackPeerAgentMode = config['peer_agent_mode'];\n if (rawSlackPeerAgentMode === 'listen' || rawSlackPeerAgentMode === 'respond') {\n slackPeerEnv.SLACK_PEER_AGENT_MODE = rawSlackPeerAgentMode;\n }\n const rawSlackPeerGroupIds = config['peer_group_ids'];\n if (Array.isArray(rawSlackPeerGroupIds) && rawSlackPeerGroupIds.length > 0) {\n // Same defensive shape-coerce as the Telegram branch (CR PR #847):\n // an unexpected non-array would otherwise throw at `.join()`.\n const ids = rawSlackPeerGroupIds\n .map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : ''))\n .filter((v) => v.length > 0);\n if (ids.length > 0) {\n slackPeerEnv.SLACK_PEER_GROUP_IDS = ids.join(',');\n }\n }\n if (options?.slackPeers && options.slackPeers.length > 0) {\n slackPeerEnv.SLACK_PEERS = JSON.stringify(\n options.slackPeers.map((p) => ({\n code_name: p.code_name,\n bot_user_id: p.bot_user_id,\n agent_id: p.agent_id,\n })),\n );\n const gateEntries = options.slackPeers\n .filter((p) => p.gate_path !== undefined)\n .map((p) => [p.bot_user_id, p.gate_path] as const);\n if (gateEntries.length > 0) {\n slackPeerEnv.SLACK_PEERS_GATE = JSON.stringify(Object.fromEntries(gateEntries));\n }\n }\n // ENG-7349: same-team roster for the peer-enablement operator hint.\n // Deliberately NOT gated on peer_agent_mode / slackPeers - the hint\n // fires precisely when those are unset. Mirrors the persistent branch.\n if (options?.slackTeamPeerUserIds && options.slackTeamPeerUserIds.length > 0) {\n slackPeerEnv.SLACK_TEAM_PEER_USER_IDS = options.slackTeamPeerUserIds.join(',');\n }\n // Channel-agnostic kill switch (ENG-4940). The Slack classifier\n // reads PEER_DISABLED — same env var the Telegram MCP reads.\n if (peerDisabledMode !== 'off') {\n slackPeerEnv.PEER_DISABLED = peerDisabledMode;\n }\n\n // AGT auth trio so the MCP child can call back into /host/* for\n // the same reasons as the Telegram side (audit, rate limiting,\n // future Slack equivalent of observed-chat). Mirrors PR #912 +\n // CodeRabbit on #918 (trim whitespace before forwarding).\n const slackResolvedAgtApiKey = process.env['AGT_API_KEY']?.trim();\n const slackAgtAuthEnv: Record<string, string> = {\n AGT_HOST: process.env['AGT_HOST']?.trim() || 'https://api.augmented.team',\n AGT_AGENT_CODE_NAME: codeName,\n // ENG-5901 Track D: template; manager spawn env carries the value.\n ...(slackResolvedAgtApiKey ? { AGT_API_KEY: '${AGT_API_KEY}' } : {}),\n ...(options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}),\n };\n\n // ENG-5901 Track D: raw Slack tokens to .env.integrations first,\n // `${VAR}` templates in .mcp.json (covers both branches below).\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'upsert',\n updates: {\n SLACK_BOT_TOKEN: botToken,\n ...(appToken ? { SLACK_APP_TOKEN: appToken } : {}),\n },\n });\n\n // ENG-6245: guard the avatar env once for both branches below — a\n // data-URI / oversized value is dropped (null) so it can't E2BIG the\n // slack MCP spawn. See the persistent-config branch above for the why.\n const slackAvatarEnvUrl = resolveAvatarEnvUrl(options?.agentAvatarUrl).url;\n if (isPersistent && existsSync(localSlackChannel)) {\n mcpServers['slack'] = {\n command: 'node',\n args: [localSlackChannel],\n env: {\n SLACK_BOT_TOKEN: '${SLACK_BOT_TOKEN}',\n ...(appToken ? { SLACK_APP_TOKEN: '${SLACK_APP_TOKEN}' } : {}),\n ...slackAutoFollowEnv,\n ...slackResponseModeEnv,\n ...slackAckReactionEnv,\n ...slackSkipReactionEnv,\n ...slackAllowedUsersEnv,\n ...slackPingAllowedUsersEnv,\n ...oneshotBlockKitEnv,\n ...slackPeerEnv,\n ...slackAgtAuthEnv,\n ...tzEnv,\n // ENG-6563 (D16): stamp the verified turn initiator for broker MCPs.\n AGT_TURN_INITIATOR_FILE: join(getAgentDir(codeName), '.current-turn-initiator.json'),\n // ENG-6155: avatar URL → bot Slack profile photo (see persistent\n // branch above). Mirrored here so oneshot-mode agents get it too.\n // ENG-6245: slackAvatarEnvUrl is null for data-URI / oversized values.\n ...(slackAvatarEnvUrl\n ? { SLACK_AGENT_AVATAR_URL: slackAvatarEnvUrl }\n : {}),\n },\n };\n } else {\n mcpServers['slack'] = {\n command: 'npx',\n args: ['-y', '@augmented/claude-code-channel-slack'],\n env: {\n SLACK_BOT_TOKEN: '${SLACK_BOT_TOKEN}',\n ...(appToken ? { SLACK_APP_TOKEN: '${SLACK_APP_TOKEN}' } : {}),\n ...slackAutoFollowEnv,\n ...slackResponseModeEnv,\n ...slackAckReactionEnv,\n ...slackSkipReactionEnv,\n ...slackAllowedUsersEnv,\n ...slackPingAllowedUsersEnv,\n ...oneshotBlockKitEnv,\n ...slackPeerEnv,\n ...slackAgtAuthEnv,\n ...tzEnv,\n // ENG-6563 (D16): stamp the verified turn initiator for broker MCPs.\n AGT_TURN_INITIATOR_FILE: join(getAgentDir(codeName), '.current-turn-initiator.json'),\n // ENG-6155: avatar URL → bot Slack profile photo (see persistent\n // branch above). Mirrored here so oneshot-mode agents get it too.\n // ENG-6245: slackAvatarEnvUrl is null for data-URI / oversized values.\n ...(slackAvatarEnvUrl\n ? { SLACK_AGENT_AVATAR_URL: slackAvatarEnvUrl }\n : {}),\n },\n };\n }\n } else if (channelId === 'msteams') {\n // ENG-5509: wire the Teams MCP channel server. Mirrors the Slack\n // branch above — same shape (local CLI-bundled JS for persistent\n // sessions; behaviour gates passed through as env). The\n // teams-channel.js path is the manager-deployed file from the CLI\n // bundle (see packages/mcp/src/teams-channel.ts → `tsup` →\n // dist/teams-channel.js → manager deploys to ~/.augmented/_mcp/).\n const appId = config['app_id'] as string | undefined;\n const clientSecret = config['client_secret'] as string | undefined;\n if (!appId || !clientSecret) return;\n\n const localTeamsChannel = join(getHomeDir(), '.augmented', '_mcp', 'teams-channel.js');\n\n // Ensure the per-agent inbound + interaction + recovery dirs\n // exist. teams-channel.ts also ensures these on boot, but doing\n // it at provision time avoids a first-message race where Azure\n // Bot Service POSTs the activity before the MCP server's first\n // mkdir lands.\n // ENG-7891 / ADR-0049: create these under the resolved (id-keyed) agent\n // dir so they match the id-keyed spawn cwd the Stop/ghost-reply hooks\n // read markers from. getAgentDir resolves the codename compatibility\n // symlink; for legacy agents it is the codename dir (unchanged).\n const agentDirMs = getAgentDir(codeName);\n try {\n mkdirSync(join(agentDirMs, 'msteams-pending-inbound', '.markers'), { recursive: true });\n mkdirSync(join(agentDirMs, 'msteams-pending-interactions'), { recursive: true });\n mkdirSync(join(agentDirMs, 'msteams-recovery-outbox'), { recursive: true });\n } catch {\n /* non-fatal — teams-channel.ts will retry */\n }\n\n const tenantId = (config['tenant_id'] as string | undefined) ?? 'common';\n const botObjectId = config['bot_object_id'] as string | undefined;\n const allowedTeamIds = (config['allowed_team_ids'] as string[] | undefined) ?? [];\n const threadAutoFollow = (config['thread_auto_follow'] as string | undefined) ?? 'off';\n const channelResponseMode = (config['channel_response_mode'] as string | undefined) ?? 'mention_only';\n const adaptiveCardsEnabled = config['adaptive_cards_enabled'] === true;\n const adaptiveCardsAskUserEnabled = config['adaptive_cards_ask_user_enabled'] === true;\n const peerAgentMode = (config['peer_agent_mode'] as string | undefined) ?? 'off';\n const peerTeamIds = (config['peer_team_ids'] as string[] | undefined) ?? [];\n const knownPeerBotIds = (config['known_peer_bot_ids'] as string[] | undefined) ?? [];\n\n // AGT auth trio so the MCP child can call back into /host/* for\n // the same audit / rate-limiting / observed-chat needs as Slack.\n const msResolvedAgtApiKey = process.env['AGT_API_KEY']?.trim();\n const msteamsAgtAuthEnv: Record<string, string> = {\n AGT_HOST: process.env['AGT_HOST']?.trim() || 'https://api.augmented.team',\n AGT_AGENT_CODE_NAME: codeName,\n // ENG-5901 Track D: template; manager spawn env carries the value.\n ...(msResolvedAgtApiKey ? { AGT_API_KEY: '${AGT_API_KEY}' } : {}),\n ...(options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}),\n };\n\n // ENG-5901 Track D: the client secret is the one Teams credential\n // that must never sit literal in .mcp.json — raw value to\n // .env.integrations, `${VAR}` template below. APP_ID/TENANT_ID are\n // identifiers, not secrets, and stay literal.\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'upsert',\n updates: { MSTEAMS_CLIENT_SECRET: clientSecret },\n });\n const teamsEnv: Record<string, string> = {\n MSTEAMS_APP_ID: appId,\n MSTEAMS_CLIENT_SECRET: '${MSTEAMS_CLIENT_SECRET}',\n MSTEAMS_TENANT_ID: tenantId,\n ...(botObjectId ? { MSTEAMS_BOT_OBJECT_ID: botObjectId } : {}),\n ...(allowedTeamIds.length > 0 ? { MSTEAMS_ALLOWED_TEAMS: allowedTeamIds.join(',') } : {}),\n ...(threadAutoFollow !== 'off' ? { MSTEAMS_THREAD_AUTO_FOLLOW: threadAutoFollow } : {}),\n ...(channelResponseMode !== 'mention_only'\n ? { MSTEAMS_CHANNEL_RESPONSE_MODE: channelResponseMode }\n : {}),\n ...(adaptiveCardsEnabled ? { MSTEAMS_ADAPTIVE_CARDS_ENABLED: 'true' } : {}),\n ...(adaptiveCardsEnabled && adaptiveCardsAskUserEnabled\n ? { MSTEAMS_ADAPTIVE_CARDS_ASK_USER_ENABLED: 'true' }\n : {}),\n ...(peerAgentMode !== 'off' ? { MSTEAMS_PEER_AGENT_MODE: peerAgentMode } : {}),\n ...(peerTeamIds.length > 0 ? { MSTEAMS_PEER_TEAM_IDS: peerTeamIds.join(',') } : {}),\n ...(knownPeerBotIds.length > 0\n ? { MSTEAMS_KNOWN_PEER_BOT_IDS: knownPeerBotIds.join(',') }\n : {}),\n ...msteamsAgtAuthEnv,\n ...tzEnv,\n // ENG-5841: MSTEAMS_SENDER_POLICY drives teams-inbound-filter.ts.\n // Mirrors the Slack branch above — only emitted when restrictive.\n ...(senderPolicyMode ? { MSTEAMS_SENDER_POLICY: senderPolicyMode } : {}),\n ...senderPolicyEnv, // AGT_TEAM_ID when team_agents_only\n // ENG-5842: principal ID for manager_only — Teams AAD object id from\n // people.contact_preferences.teams_aad_object_id. Same fail-closed\n // contract as the Slack branch above.\n ...(teamsPrincipalId ? { MSTEAMS_SENDER_POLICY_PRINCIPAL_ID: teamsPrincipalId } : {}),\n // ENG-5871: team_only mode injects a comma-separated list of team-\n // member Teams AAD object IDs. Same fail-closed contract as Slack.\n // AAD IDs are UUIDs (no commas), so .split(',') is safe.\n ...(teamsTeamPrincipalIds\n ? { MSTEAMS_SENDER_POLICY_TEAM_PRINCIPAL_IDS: teamsTeamPrincipalIds }\n : {}),\n // ENG-5843: org-boundary gate. MSTEAMS_INTERNAL_ONLY + MSTEAMS_HOME_TENANT_ID\n // mirror the Slack pair. Source is the same tenantId the existing\n // MSTEAMS_TENANT_ID env var already carries — defaulting to \"common\"\n // would be wrong here (it'd admit any tenant), so we skip the\n // SLACK_HOME_TEAM_ID-equivalent emission when the install hasn't\n // pinned a real tenant. MCP boot guard fails closed.\n ...(senderPolicyInternalOnly ? { MSTEAMS_INTERNAL_ONLY: 'true' } : {}),\n ...(senderPolicyInternalOnly && tenantId !== 'common'\n ? { MSTEAMS_HOME_TENANT_ID: tenantId }\n : {}),\n };\n\n if (isPersistent && existsSync(localTeamsChannel)) {\n mcpServers['msteams'] = {\n command: 'node',\n args: [localTeamsChannel],\n env: teamsEnv,\n };\n } else {\n // No published npm package for the Teams channel server yet —\n // ENG-5511 will decide whether to publish under\n // @integrity-labs/augmented-mcp or bundle inline. Until then,\n // a non-persistent agent without the local bundle is a no-op\n // for the channel (no inbound forwarding); operators provision\n // via persistent sessions only.\n mcpServers['msteams'] = {\n command: 'node',\n args: [localTeamsChannel],\n env: teamsEnv,\n };\n }\n }\n\n if (channelId === 'whatsapp') {\n const provider = (config['provider'] as string | undefined) ?? 'kapso';\n const localWhatsappChannel = join(getHomeDir(), '.augmented', '_mcp', 'whatsapp-channel.js');\n\n if (provider === 'baileys') {\n // ENG-6827: unofficial WhatsApp Web (QR-link). No Meta credentials —\n // the link auth lives on the host at\n // ~/.augmented/{codeName}/whatsapp-baileys-auth (written once by the\n // whatsapp-link tool). The MCP reconnects headlessly with WHATSAPP_PROVIDER\n // = baileys and pushes inbound / sends over the persistent socket.\n mcpServers['whatsapp'] = {\n command: 'node',\n args: [localWhatsappChannel],\n env: {\n AGT_AGENT_CODE_NAME: codeName,\n WHATSAPP_PROVIDER: 'baileys',\n },\n };\n if (writeMcpJsonGuarded(codeName, mcpJsonPath, mcpConfig as { mcpServers?: Record<string, unknown> })) {\n syncMcpToProject(codeName);\n }\n return;\n }\n\n // ENG-6812: wire the WhatsApp (Kapso) MCP channel server. Like the Teams\n // branch above this is host-resident: inbound arrives via the API webhook\n // route writing pending-inbound files, and the CLI-bundled\n // whatsapp-channel.js (manager-deployed to ~/.augmented/_mcp/) watches\n // that dir + sends outbound through the Kapso REST client.\n const projectApiKey = config['project_api_key'] as string | undefined;\n const phoneNumberId = config['phone_number_id'] as string | undefined;\n if (projectApiKey && phoneNumberId) {\n // Pre-create the pending-inbound dir so the webhook route can write\n // before the MCP server's first boot mkdir (same race guard as Teams).\n try {\n mkdirSync(\n join(getAgentDir(codeName), 'whatsapp-pending-inbound'),\n { recursive: true },\n );\n } catch {\n /* non-fatal — whatsapp-channel.ts retries on boot */\n }\n\n // ENG-5901 Track D: the project API key is a secret — raw value to\n // .env.integrations, `${VAR}` template in .mcp.json. phone_number_id\n // and the optional base/version overrides are identifiers, not\n // secrets, so they stay literal.\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'upsert',\n updates: { WHATSAPP_PROJECT_API_KEY: projectApiKey },\n });\n\n const kapsoBaseUrl = config['kapso_base_url'] as string | undefined;\n const kapsoGraphVersion = config['kapso_graph_version'] as string | undefined;\n const whatsappEnv: Record<string, string> = {\n AGT_AGENT_CODE_NAME: codeName,\n WHATSAPP_PROJECT_API_KEY: '${WHATSAPP_PROJECT_API_KEY}',\n WHATSAPP_PHONE_NUMBER_ID: phoneNumberId,\n ...(kapsoBaseUrl ? { WHATSAPP_KAPSO_BASE_URL: kapsoBaseUrl } : {}),\n ...(kapsoGraphVersion ? { WHATSAPP_KAPSO_GRAPH_VERSION: kapsoGraphVersion } : {}),\n };\n\n mcpServers['whatsapp'] = {\n command: 'node',\n args: [localWhatsappChannel],\n env: whatsappEnv,\n };\n }\n }\n\n if (writeMcpJsonGuarded(codeName, mcpJsonPath, mcpConfig as { mcpServers?: Record<string, unknown> })) {\n syncMcpToProject(codeName);\n }\n },\n\n hasChannelCredentials(codeName: string, channelId: string): boolean {\n // Read the provision .mcp.json (source of truth — syncMcpToProject\n // mirrors this to the project dir after every write). Returns true only\n // when the file exists AND has a truthy entry under mcpServers[channelId].\n // Missing file, missing mcpServers map, or missing channel key all count\n // as \"no credentials\", which tells the caller to fall through and\n // re-invoke writeChannelCredentials — see ENG-4439.\n const provisionMcpPath = join(getAgentDir(codeName), 'provision', '.mcp.json');\n if (!existsSync(provisionMcpPath)) return false;\n try {\n const parsed = JSON.parse(readFileSync(provisionMcpPath, 'utf-8')) as {\n mcpServers?: Record<string, unknown>;\n };\n return Boolean(parsed.mcpServers?.[channelId]);\n } catch {\n // Malformed JSON — treat as missing so writeChannelCredentials repairs it\n return false;\n }\n },\n\n removeChannelCredentials(codeName: string, channelId: string): void {\n const agentDir = getAgentDir(codeName);\n const mcpJsonPath = join(agentDir, 'provision', '.mcp.json');\n\n modifyJsonConfig(mcpJsonPath, (config) => {\n const mcpServers = config['mcpServers'] as Record<string, unknown> | undefined;\n if (!mcpServers || !(channelId in mcpServers)) return false;\n delete mcpServers[channelId];\n return true;\n });\n\n syncMcpToProject(codeName);\n },\n\n async updateAgentModel(codeName: string, model: string): Promise<boolean> {\n const agentDir = getAgentDir(codeName);\n const settingsPath = join(agentDir, 'provision', 'settings.json');\n\n let changed = false;\n modifyJsonConfig(settingsPath, (config) => {\n config['model'] = model;\n changed = true;\n return true;\n });\n return changed;\n },\n\n // ENG-5901 PR 3: hoist pre-Track-D literal secrets out of the on-disk\n // .mcp.json so the armed lint stops rejecting every incremental write.\n // See migrateExistingLiteralSecrets for the full story.\n migrateSecretStorage(codeName: string): void {\n migrateExistingLiteralSecrets(codeName);\n },\n\n seedProfileConfig(codeName: string): void {\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n mkdirSync(join(agentDir, 'provision'), { recursive: true });\n mkdirSync(projectDir, { recursive: true });\n },\n\n syncScheduledTasks(codeName: string, tasks: ScheduledTaskRow[]): Promise<void> {\n const agentDir = getAgentDir(codeName);\n const schedulesPath = join(agentDir, 'schedules.json');\n\n const mapped = mapScheduledTasks(tasks);\n\n mkdirSync(agentDir, { recursive: true });\n writeFileSync(schedulesPath, JSON.stringify({ schedules: mapped }, null, 2));\n\n return Promise.resolve();\n },\n\n writeIntegrations(\n codeName: string,\n integrations: ResolvedIntegration[],\n agentId?: string,\n options?: { renderClaudeMdSection?: boolean },\n ): void {\n const agentDir = getAgentDir(codeName);\n mkdirSync(agentDir, { recursive: true });\n\n // ENG-4821: persist the integration manifest sidecar BEFORE the\n // writeMcpServer calls below. Each writeMcpServer call funnels through\n // syncMcpToProject → renderChannelMessageHandlerForAgent, which reads\n // this file. Writing it first means the very first sync after a new\n // integration lands renders the subagent with the correct integration\n // block, rather than waiting a tick. The end-of-method re-render is\n // belt-and-braces for the no-MCP case (e.g. a GitHub-only integration\n // that exposes only env vars).\n const summariesForSidecar: IntegrationSummary[] = integrations.map((i) => {\n const def = INTEGRATION_REGISTRY.find((d) => d.id === i.definition_id);\n return {\n id: i.definition_id,\n name: def?.name || i.display_name || i.definition_id,\n cliBinary: def?.cli_tool?.binary,\n description: def?.description || (i.auth_type === 'managed' ? 'Managed integration via Composio' : undefined),\n };\n });\n writeIntegrationsSummaryForAgent(codeName, summariesForSidecar);\n\n // Decrypt once so every downstream writer (env file, xurl store, etc.)\n // sees plaintext credentials. Forwarding the original `integrations`\n // array would leak `enc:...` ciphertext into xurl-config, which reads\n // `integration.credentials` directly.\n const decryptedIntegrations: ResolvedIntegration[] = integrations.map((integration) => ({\n ...integration,\n credentials: decryptIntegrationCredentials(\n integration.credentials as Record<string, unknown>,\n ) as ResolvedIntegration['credentials'],\n }));\n\n // Write integration credentials as env vars for Claude Code connectors.\n // ENG-5901 Track D: built as a key→RAW-value map and written through the\n // shared merge model in 'replace-preserving' mode — integration keys are\n // rebuilt from scratch each tick (stale-key pruning on disconnect still\n // works) while channel-owned keys (CHANNEL_SECRET_ENV_KEYS, upserted by\n // writeChannelCredentials) are carried over instead of clobbered.\n const envUpdates: Record<string, string> = {};\n\n for (const integration of decryptedIntegrations) {\n // ENG-8359: the prefix carries the CONNECTION, so two connections of one\n // definition publish two credentials instead of the second overwriting the\n // first in this very map. Without it, giving them distinct `.mcp.json`\n // keys would be worse than the clobber it replaces: two servers that both\n // report healthy while authenticating as one account. Identity for the\n // default connection, so no existing agent's env var name changes.\n const prefix = remoteMcpEnvPrefix(integration.definition_id, integration.connection_key);\n // The same prefix WITHOUT the connection infix — what a definition-prefixed\n // config key actually carries. Used only to strip that prefix before\n // re-applying the scoped one (see the config loop below).\n const unscopedPrefix = remoteMcpEnvPrefix(integration.definition_id, null);\n const creds = integration.credentials;\n const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);\n\n // Resolve the single token this integration authenticates with, under\n // the generic <PREFIX>_ACCESS_TOKEN / <PREFIX>_API_KEY convention.\n //\n // ENG-7506: `github_app` normalizes to the oauth2 shape server-side. The\n // host-runtime payload (POST /host/agent-integrations, CS-1441) mints a\n // short-lived installation token from the App creds and hands it down as\n // `credentials.access_token` while leaving auth_type as 'github_app'. It\n // must therefore materialize identically to oauth2. Without this branch\n // the minted token falls through both cases, `token` stays undefined, and\n // neither GITHUB_ACCESS_TOKEN (curl-based skills) nor the cli_tool env_key\n // GITHUB_TOKEN (`gh`) is ever written, leaving a \"connected\" GitHub App\n // integration with no usable runtime access.\n // ENG-8344: a broker-delivered row carries no token by construction, and\n // must not be given one. `continue` before the resolution below so the\n // generic `<PREFIX>_ACCESS_TOKEN` and the `cli_tool.env_key` alias\n // (GITHUB_TOKEN) are both skipped — writing either from a stale local\n // value would reintroduce exactly the spawn-frozen credential this\n // removes. Config-derived env vars are skipped with them: `github` has\n // none today, and a config value is not what broker delivery is about.\n if (integration.credentialDelivery === 'broker') continue;\n\n let token: string | undefined;\n if (integration.auth_type === 'oauth2' || integration.auth_type === 'github_app') {\n token = creds.access_token as string | undefined;\n if (token) {\n envUpdates[`${prefix}_ACCESS_TOKEN`] = token;\n }\n } else if (integration.auth_type === 'api_key') {\n token = creds.api_key as string | undefined;\n if (token) {\n envUpdates[`${prefix}_API_KEY`] = token;\n }\n }\n\n // ENG-6206: native CLI tools authenticate from a binary-specific env var\n // (`gh` reads GH_TOKEN/GITHUB_TOKEN, NOT GITHUB_ACCESS_TOKEN). The\n // generic convention above is invisible to those binaries, so ALSO\n // publish the token under `cli_tool.env_key`. Mirrors the OpenClaw mapper\n // (openclaw/mapper.ts). Without this the `gh` CLI is permanently\n // unauthenticated on the Claude Code fleet despite a connected\n // integration. extra_env (e.g. linear's LINEAR_ISSUE_SORT) is seeded\n // too, for full parity — config/credential-derived values always win.\n if (def?.cli_tool) {\n const { env_key, extra_env } = def.cli_tool;\n if (env_key && token && !(env_key in envUpdates)) {\n envUpdates[env_key] = token;\n }\n if (extra_env) {\n for (const [k, v] of Object.entries(extra_env)) {\n if (typeof v === 'string' && v && !(k in envUpdates)) {\n envUpdates[k] = v;\n }\n }\n }\n }\n\n // Write extra config fields as env vars (e.g., xero_tenant_id → XERO_TENANT_ID)\n if (integration.config) {\n const config = integration.config;\n for (const [key, value] of Object.entries(config)) {\n if (typeof value === 'string' && value) {\n // Avoid double-prefixing: a config key may already carry the\n // definition prefix (`xero_tenant_id` → XERO_TENANT_ID), so strip it\n // before re-applying.\n //\n // ENG-8359: the strip MUST test the UNSCOPED prefix. `prefix` now\n // carries the connection infix, so for a named connection\n // `ANCHOR_BROWSER_SESSION_ID` doesn't start with\n // `ANCHOR_BROWSER__SECOND_` and would be prefixed a second time —\n // published as ANCHOR_BROWSER__SECOND_ANCHOR_BROWSER_SESSION_ID\n // while the rendered header references\n // ANCHOR_BROWSER__SECOND_SESSION_ID. That named connection's header\n // would then resolve to nothing while the default one stayed\n // healthy: the same silent split this change removes elsewhere.\n const upperKey = key.toUpperCase();\n const stripped = upperKey.startsWith(`${unscopedPrefix}_`)\n ? upperKey.slice(unscopedPrefix.length + 1)\n : upperKey;\n envUpdates[`${prefix}_${stripped}`] = value;\n }\n }\n }\n }\n\n // ENG-5855: seed declared env defaults for data-driven remote MCPs. A\n // `remoteMcp` header may reference an env var that no credential/config\n // value populates yet (e.g. Anchor's `ANCHOR_BROWSER_SESSION_ID`, minted\n // later by ENG-5857). Seed it empty so the header resolves cleanly\n // instead of shipping a literal `${...}` placeholder. Gap-fill only — a\n // real value written above always wins.\n for (const integration of decryptedIntegrations) {\n // ADR-0033 Slice 2: prefer the DB catalog spec's envDefaults; fall back\n // to the code registry for unmigrated callers.\n const defaults =\n integration.remoteMcp?.envDefaults ??\n INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id)?.remoteMcp?.envDefaults;\n if (!defaults) continue;\n for (const [key, value] of Object.entries(defaults)) {\n // ENG-8359: seed the key under THIS connection's name. The spec declares\n // the default by its unscoped var name, but a named connection's\n // rendered header references the scoped one — seeding the unscoped name\n // would leave the scoped reference unseeded and ship the literal\n // `${...}` placeholder this block exists to prevent. Identity for the\n // default connection.\n const scoped = remoteMcpConnectionScopedEnvVar(\n integration.definition_id,\n key,\n integration.connection_key,\n );\n if (scoped in envUpdates) continue;\n envUpdates[scoped] = value;\n }\n }\n\n // ENG-8344: install or remove the use-time GitHub credential programs, and\n // fold the resulting GIT_CONFIG_* pointers into the same env write. Keyed\n // off the row the API marked, not off `auth_type` — the API owns the flag\n // decision, the host just honours it, so a host can never enable broker\n // delivery on its own.\n Object.assign(\n envUpdates,\n syncGitHubBrokerCredentialTooling(\n getProjectDir(codeName),\n decryptedIntegrations.some(\n (i) => i.definition_id === 'github' && i.credentialDelivery === 'broker',\n ),\n ),\n );\n\n writeEnvIntegrationsForAgent(codeName, {\n mode: 'replace-preserving',\n updates: envUpdates,\n });\n\n // xurl reads credentials from $HOME/.xurl (no env-var override exists).\n // Merge agt-managed apps in, preserving any apps the user added manually.\n // Pass the already-decrypted integrations so xurl-config never sees enc:.\n writeXurlStoreForIntegrations(decryptedIntegrations);\n\n // Ensure integrations that require MCP servers have their entries in .mcp.json.\n // This handles integrations added after initial provisioning (buildArtifacts).\n //\n // ENG-5815: data-driven native MCP entries. Mirrors the loop in\n // buildMcpJson — any integration whose definition has a `nativeMcp`\n // spec is emitted via the shared templating renderer. qmd was the\n // first migration; AWS shipped purely via this path. When agentId\n // is unset (legacy callers / tests), `{{agent_id}}` resolves to ''\n // — only specs that don't use the agent_id template stay safe in\n // that case (qmd and aws are both agent-id-free).\n for (const integration of decryptedIntegrations) {\n const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);\n if (!def?.nativeMcp) continue;\n const key = def.nativeMcp.key ?? integration.definition_id;\n this.writeMcpServer!(codeName, key, buildNativeMcpEntry(def.nativeMcp, {\n agentId: agentId ?? '',\n agentCodeName: codeName,\n integration,\n }));\n }\n\n // ENG-4679: mirror buildMcpJson's Xero entry on incremental sync, so\n // an agent that connects Xero after initial provisioning gets the MCP\n // server in .mcp.json without waiting for a full reprovision.\n // ENG-4898: switched from upstream to @integrity-labs fork +\n // XERO_TENANT_ID env (see buildMcpJson above for rationale).\n // ENG-4922: read-only vendor Xero when xero-broker gates writes (mirrors\n // buildMcpJson). Computed once for both the vendor entry and the broker\n // entry below.\n const hasXeroBroker = integrations.some((i) => i.definition_id === 'xero-broker');\n const xeroIntegration = integrations.find((i) => i.definition_id === 'xero');\n if (xeroIntegration) {\n // ENG-4920: pass AGT_INTEGRATION_ID + AGT_AGENT_ID so the MCP\n // server can fetch the freshest token per-call. Mirrors the\n // buildMcpJson path above — see that comment for full rationale.\n // agentId is threaded from manager-worker's agent.agent_id; if it's\n // unset (older callers / no-op test paths) the broker-mode env\n // entries are omitted and the MCP falls back to legacy\n // XERO_CLIENT_BEARER_TOKEN env mode without breaking.\n //\n // ENG-5318: when broker mode is engaged, skip XERO_CLIENT_BEARER_TOKEN\n // so token rotation in .env.integrations doesn't trip stale-mcp-reaper\n // into restarting the xero child. See the buildMcpJson block above\n // for the full rationale.\n const brokerMode = Boolean(agentId && xeroIntegration.id);\n const xeroEnv: Record<string, string> = {\n ...(brokerMode ? {} : { XERO_CLIENT_BEARER_TOKEN: '${XERO_ACCESS_TOKEN}' }),\n // ENG-8264: declare the credential var in broker mode so the manager\n // does not re-classify it as env-only and respawn the whole session on\n // every rotation. Mirrors the buildMcpJson path above - see that block\n // for the full rationale. Kept in both renderers deliberately: they\n // produce the same entry by two routes, and a fix in only one of them\n // is a fix that holds until whichever path the agent happens to take.\n ...(brokerMode ? { AGT_REMOTE_MCP_TOKEN_VAR: 'XERO_ACCESS_TOKEN' } : {}),\n XERO_TENANT_ID: '${XERO_TENANT_ID}',\n // ENG-4922: writes go through xero-broker → read-only vendor server.\n ...(hasXeroBroker ? { XERO_WRITES_VIA_BROKER: 'true' } : {}),\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n };\n if (brokerMode) {\n xeroEnv.AGT_HOST = '${AGT_HOST}';\n xeroEnv.AGT_TOKEN = '${AGT_TOKEN}';\n xeroEnv.AGT_API_KEY = '${AGT_API_KEY}';\n xeroEnv.AGT_AGENT_ID = agentId!;\n xeroEnv.AGT_INTEGRATION_ID = xeroIntegration.id!;\n }\n // ENG-7579: bundled with the CLI (~/.augmented/_mcp/xero.js), NOT\n // `npx @latest`. Mirrors the buildMcpJson path above - see that block\n // for the full delivery-change rationale.\n const localXeroMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'xero.js');\n this.writeMcpServer!(codeName, 'xero', {\n command: 'node',\n args: [localXeroMcpPath],\n env: xeroEnv,\n });\n }\n\n // ENG-4594: mirror buildMcpJson's Postiz entry on incremental sync,\n // so an agent that connects Postiz after initial provisioning gets\n // the MCP server in .mcp.json without waiting for a full reprovision.\n const postizIntegration = integrations.find((i) => i.definition_id === 'postiz');\n if (postizIntegration) {\n this.writeMcpServer!(codeName, 'postiz', buildPostizMcpEntry(postizIntegration));\n }\n\n // ENG-4694: generic remote-MCP wiring for incremental sync. Mirrors the\n // buildMcpJson loop. New OAuth-MCP integrations land here automatically\n // by virtue of having an `mcpUrl` in OAUTH_PROVIDERS. Granola (ENG-4693)\n // is the first user.\n //\n // ENG-6859: OAuth-wired remote MCPs route through the stdio\n // remote-oauth-proxy (re-reads the token per request); custom-header\n // integrations keep the direct HTTP entry. Mirror of the buildMcpJson loop.\n const remoteOAuthProxyPaths = {\n proxyPath: join(getHomeDir(), '.augmented', '_mcp', 'remote-oauth-proxy.js'),\n tokenFile: join(getProjectDir(codeName), '.env.integrations'),\n };\n for (const integration of integrations) {\n // ENG-8359: key by CONNECTION. Mirror of the buildMcpJson loop — this\n // incremental path writes the SAME keys, or a named connection would be\n // written under one name on full provision and another on hot-reload.\n const connectionKey = integration.connection_key;\n const serverKey = remoteMcpServerKey(integration.definition_id, connectionKey);\n // ENG-7748: live-header remote MCPs (anchor-browser) route through the\n // stdio proxy for live per-request header reads. Mirror of buildMcpJson.\n const liveEntry = buildLiveHeaderRemoteMcpProxyEntry(\n integration.definition_id,\n integration.remoteMcp,\n remoteOAuthProxyPaths,\n connectionKey,\n );\n if (liveEntry) {\n this.writeMcpServer!(codeName, serverKey, liveEntry);\n continue;\n }\n const proxyEntry = buildOAuthRemoteMcpProxyEntry(\n integration.definition_id,\n remoteOAuthProxyPaths,\n connectionKey,\n );\n if (proxyEntry) {\n this.writeMcpServer!(codeName, serverKey, proxyEntry);\n continue;\n }\n // ADR-0033 Slice 2: forward the DB catalog spec; registry fallback when absent.\n const entry = buildRemoteMcpEntry(integration.definition_id, integration.remoteMcp, connectionKey);\n if (entry) {\n this.writeMcpServer!(codeName, serverKey, entry);\n }\n }\n\n // ENG-4685: mirror buildMcpJson's cloud-broker entry on incremental sync,\n // so an agent that connects AWS after initial provisioning gets the\n // broker MCP without waiting for a full reprovision. Match by toolkit\n // id (cloud-broker), not parent integration code_name (aws) — the\n // wizard creates per-toolkit integration rows.\n const hasCloudBroker = integrations.some((i) => i.definition_id === 'cloud-broker');\n if (hasCloudBroker) {\n // AGT_TOKEN omitted — see buildMcpJson comment. Manager spawn env has\n // AGT_API_KEY (permanent); AGT_TOKEN is exchanged at runtime. Passing\n // a literal \"${AGT_TOKEN}\" placeholder would 401 every call.\n //\n // ENG-4739 / ENG-4823: cloud-broker exits on cold start unless\n // AGT_AGENT_ID is a real UUID — Claude Code only substitutes ${VAR}\n // values from the parent claude's spawn env at MCP-launch time, and\n // AGT_AGENT_ID is NOT exported there (the augmented server entry\n // bakes it as a literal instead). Pre-fix this code wrote\n // `existingAgentId ?? '${AGT_AGENT_ID}'` as the fallback, which\n // poisoned older agents whose first writeIntegrations ran before\n // any cloud-broker block existed (Vigil sat broken for 3 hours).\n // resolveBrokerAgentId walks: existing broker entry → augmented\n // entry (always baked) → undefined (skip the write rather than\n // write a poisonous placeholder).\n const brokerAgentId = resolveBrokerAgentId(codeName);\n if (!brokerAgentId) {\n // Skip ONLY the broker write — let the rest of writeIntegrations\n // (other servers, CLAUDE.md regen, env file, etc.) continue. The\n // full-provision artifact pipeline will recreate the broker entry\n // on the next cycle with input.agent.agent_id baked in directly.\n process.stderr.write(\n `[manager-worker] [cloud-broker] skipping write for '${codeName}': no real AGT_AGENT_ID available (no existing broker entry, no augmented entry to copy from). The full-provision artifact pipeline will recreate this on the next cycle.\\n`,\n );\n } else {\n this.writeMcpServer!(codeName, 'cloud-broker', {\n command: 'npx',\n args: ['-y', '@integrity-labs/cloud-broker@latest'],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: brokerAgentId,\n // ENG-4788: cloud-broker@0.6+ exits at startup if AGT_RUN_ID\n // is missing. Mirror buildMcpJson's entry on incremental sync\n // so an agent that connects AWS post-provision boots cleanly.\n AGT_RUN_ID: '${AGT_RUN_ID}',\n // ENG-6586 (D16): keep incremental cloud-broker wiring in sync with\n // buildMcpJson so agents that add cloud-broker post-provision still\n // forward the per-turn initiator.\n AGT_TURN_INITIATOR_FILE: join(getAgentDir(codeName), '.current-turn-initiator.json'),\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n });\n }\n }\n\n // ENG-8440: Higgsfield no longer writes an MCP entry here. It moved to a\n // Direct HTTP api_key integration against platform.higgsfield.ai, so its\n // tools reach the agent through the integration broker like Vercel's and\n // v0's — nothing agent-local to wire, and no `/mcp` Authenticate step.\n // Vercel (ENG-8421) left the same way; neither needs a branch here.\n\n // ENG-4922: mirror buildMcpJson's xero-broker entry on incremental sync,\n // so an agent that adds the xero-broker integration post-provision gets\n // the broker MCP without waiting for a full reprovision. Same\n // resolveBrokerAgentId guard as cloud-broker (skip rather than write a\n // poisonous ${AGT_AGENT_ID} placeholder).\n if (hasXeroBroker) {\n const brokerAgentId = resolveBrokerAgentId(codeName);\n if (!brokerAgentId) {\n process.stderr.write(\n `[manager-worker] [xero-broker] skipping write for '${codeName}': no real AGT_AGENT_ID available. The full-provision artifact pipeline will recreate this on the next cycle.\\n`,\n );\n } else {\n this.writeMcpServer!(codeName, 'xero-broker', {\n command: 'npx',\n args: ['-y', '@integrity-labs/xero-broker@latest'],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: brokerAgentId,\n AGT_RUN_ID: '${AGT_RUN_ID}',\n // ENG-6563 (D16): keep incremental xero-broker wiring in sync with\n // buildMcpJson so agents that add xero-broker post-provision still\n // forward the per-turn initiator.\n AGT_TURN_INITIATOR_FILE: join(getAgentDir(codeName), '.current-turn-initiator.json'),\n PATH: process.env['PATH'] ?? '',\n HOME: process.env['HOME'] ?? '',\n },\n });\n }\n }\n\n // ENG-6195: mirror buildMcpJson's augmented-admin entry on incremental\n // sync, so a staff agent that adds the augmented-admin integration\n // post-provision gets the debug MCP without waiting for a full reprovision.\n // Unlike cloud/xero-broker, the API derives the caller from the host JWT\n // org_id claim, so AGT_AGENT_ID is informational here — write best-effort\n // (no skip-on-unresolvable guard); the MCP boots fine without a real id.\n const hasAdminDebug = integrations.some((i) => i.definition_id === 'augmented-admin');\n if (hasAdminDebug) {\n // Bundled local-node path (see buildMcpJson) — not the unpublished npx form.\n const localAdminMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'augmented-admin.js');\n this.writeMcpServer!(codeName, 'augmented-admin', {\n command: 'node',\n args: [localAdminMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_AGENT_ID: resolveBrokerAgentId(codeName) ?? agentId ?? '',\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n },\n });\n }\n\n // ENG-7023: mirror buildMcpJson's augmented-support entry on incremental\n // sync, so a system_support agent that gets the augmented-support\n // integration attached post-provision (the provision-support path, ENG-6975)\n // picks up the self-troubleshoot MCP without a full reprovision. Same as\n // augmented-admin: the API derives the org from the host JWT, so\n // AGT_AGENT_ID is informational - write best-effort.\n const hasSupport = integrations.some((i) => i.definition_id === 'augmented-support');\n if (hasSupport) {\n const localSupportMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'augmented-support.js');\n this.writeMcpServer!(codeName, 'augmented-support', {\n command: 'node',\n args: [localSupportMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_AGENT_ID: resolveBrokerAgentId(codeName) ?? agentId ?? '',\n AGT_RUN_ID: '${AGT_RUN_ID}',\n AGT_API_KEY: '${AGT_API_KEY}',\n },\n });\n }\n\n // ENG-7358: mirror buildMcpJson's origami entry on incremental sync, so an\n // agent whose origami install gains the catalog stdio_mcp flag (the PR-3\n // cutover) picks up the dedicated server without a full reprovision. Same\n // flag + row-UUID gate as buildMcpJson; AGT_AGENT_ID must be real here\n // (the credential fetch posts it), so fall back through the broker\n // resolution chain like xero's broker mode does.\n const origamiStdio = integrations.find((i) => i.definition_id === 'origami');\n const origamiStdioAgentId = resolveBrokerAgentId(codeName) ?? agentId;\n // Durable expectation (integration present + catalog flag + row UUID) is\n // tracked separately from the write gate: a transiently unresolvable\n // AGT_AGENT_ID skips the write this tick but must NOT prune an existing\n // entry (see the ENG-5277 prune contract below).\n const origamiStdioExpected = Boolean(origamiStdio?.stdioMcp === true && origamiStdio.id);\n if (origamiStdioExpected && origamiStdioAgentId) {\n const localOrigamiMcpPath = join(getHomeDir(), '.augmented', '_mcp', 'origami.js');\n this.writeMcpServer!(codeName, 'origami', {\n command: 'node',\n args: [localOrigamiMcpPath],\n env: {\n AGT_HOST: '${AGT_HOST}',\n AGT_TOKEN: '${AGT_TOKEN}',\n AGT_API_KEY: '${AGT_API_KEY}',\n AGT_AGENT_ID: origamiStdioAgentId!,\n AGT_INTEGRATION_ID: origamiStdio!.id!,\n },\n });\n }\n\n // ENG-5277: prune orphaned integration-derived MCP entries. Each\n // writeMcpServer block above writes a key when the integration is\n // present, but pre-ENG-5277 there was no symmetric remove path when\n // the integration disappeared. The orphan stayed in `.mcp.json`,\n // mcp-presence-reaper saw a declared stdio child with no live\n // process, and looped the session forever (Don, 2026-05, postiz).\n //\n // Universe: every key this function could ever produce — the\n // hardcoded stdio integrations plus every OAUTH_PROVIDERS entry\n // with an `mcpUrl` (the generic remote-MCP loop above). Channel\n // servers, `augmented`, and managed-toolkit entries are owned by\n // other code paths and must not be touched here.\n //\n // Expected: derived from the *current* integration list, not from\n // \"did we successfully write the key this tick\" — a transient skip\n // (e.g. cloud-broker with no resolvable AGT_AGENT_ID at line 2412)\n // must NOT cause the existing entry to be pruned; the next full-\n // provision cycle will recreate it.\n if (this.removeMcpServer) {\n // ENG-5815: data-driven native MCP keys join the reap universe\n // automatically — every INTEGRATION_REGISTRY entry with a\n // `nativeMcp` spec contributes its key (`def.nativeMcp.key ??\n // def.id`). Old hardcoded keys stay in the literal set for\n // back-compat until each migrates.\n const nativeMcpKeys = INTEGRATION_REGISTRY\n .filter((d) => d.nativeMcp !== undefined)\n .map((d) => d.nativeMcp!.key ?? d.id);\n // ENG-8035: INTEGRATION_REGISTRY entries with a remoteMcp spec (e.g.\n // anchor-browser) must be in the prune universe so that removing one\n // clears its MCP entry. Previously only nativeMcp and OAUTH_PROVIDERS\n // entries were enrolled; remoteMcp entries were silently skipped and\n // their servers kept running after removal.\n const registryRemoteMcpKeys = INTEGRATION_REGISTRY\n .filter((d) => d.remoteMcp !== undefined)\n .map((d) => d.id);\n const integrationDerivedKeys = new Set<string>([\n 'xero',\n 'postiz',\n 'cloud-broker',\n 'xero-broker',\n 'augmented-admin',\n 'augmented-support',\n // ENG-7358: the dedicated origami stdio server. In the universe so\n // that removing the integration OR flipping the catalog stdio_mcp\n // flag back (rollback) prunes the entry symmetrically.\n 'origami',\n // ENG-8421 retirement tombstone. Vercel moved from a remote MCP\n // (`https://mcp.vercel.com`) to an api_key Direct HTTP integration, so\n // its registry entry no longer carries `remoteMcp` — which means it no\n // longer contributes to `registryRemoteMcpKeys` above. Without this\n // literal, a host that installed the MCP variant would keep its\n // `vercel` entry in `.mcp.json` FOREVER: nothing declares it any more,\n // and the prune can only remove keys it knows about. The stale server\n // would keep resolving against Claude Code's still-valid stored OAuth\n // grant, so the agent would see the retired MCP tools AND the new\n // Direct HTTP ones at once. Inert on a host that never had the MCP\n // variant; keep until the fleet has refreshed past the retirement.\n 'vercel',\n // ENG-8440 retirement tombstone — identical reasoning to 'vercel'\n // above. Higgsfield moved from a host-brokered remote MCP\n // (`https://mcp.higgsfield.ai/mcp`) to an api_key Direct HTTP\n // integration, so it no longer writes an entry and no longer appears in\n // `registryRemoteMcpKeys`. It was never in that set to begin with — its\n // entry came from a hardcoded branch in buildMcpJson/writeIntegrations\n // rather than a registry `remoteMcp` spec — so removing those branches\n // alone leaves every host that installed it holding a `higgsfield`\n // server in `.mcp.json` that nothing declares and nothing can prune.\n // Worse than dead weight: the stored Claude Code OAuth grant is still\n // valid, so those agents would keep the retired MCP tools alongside the\n // new Direct HTTP ones and could call either. Inert on a host that\n // never had the MCP variant; keep until the fleet has refreshed past\n // the retirement.\n 'higgsfield',\n ...nativeMcpKeys,\n ...registryRemoteMcpKeys,\n ...Object.entries(OAUTH_PROVIDERS)\n .filter(([, provider]) => Boolean(provider.mcpUrl))\n .map(([id]) => id),\n ]);\n // ENG-8359: named-connection keys join the prune universe, so REMOVING a\n // named connection drops its entry exactly as removing the default one\n // does. They can't come from a static list (the universe cannot know which\n // connection slugs ever existed), so they're recovered from the keys\n // actually DECLARED on disk — a declared `<base>-<slug>` whose `<base>` is\n // the sanitized form of a universe member is a connection of that member.\n //\n // Scoped to `remoteCapableBases` (registry remoteMcp + OAuth mcpUrl), NOT\n // the whole universe: the hardcoded stdio ids are ordinary server names, so\n // pattern-matching against them could enrol an unrelated `<id>-<something>`\n // server into a set whose members get deleted when unexpected.\n const remoteCapableBases = new Set(\n [\n ...registryRemoteMcpKeys,\n ...Object.entries(OAUTH_PROVIDERS)\n .filter(([, provider]) => Boolean(provider.mcpUrl))\n .map(([id]) => id),\n ].map((id) => id.replace(/[^a-z0-9]/gi, '_').toLowerCase()),\n );\n try {\n for (const declared of Object.keys(this.readMcpServers?.(codeName) ?? {})) {\n const hyphen = declared.indexOf('-');\n // A named key is `<sanitized-base>-<connection_key>`; the sanitized\n // base contains no hyphen, so the FIRST hyphen is always the joint.\n if (hyphen <= 0) continue;\n if (remoteCapableBases.has(declared.slice(0, hyphen))) {\n integrationDerivedKeys.add(declared);\n }\n }\n } catch {\n // Unreadable config: leave the universe as-is. Failing to enrol a named\n // key only leaves a stale entry behind; guessing could delete a live one.\n }\n const expectedKeys = new Set<string>();\n if (xeroIntegration) expectedKeys.add('xero');\n if (postizIntegration) expectedKeys.add('postiz');\n if (hasCloudBroker) expectedKeys.add('cloud-broker');\n if (hasXeroBroker) expectedKeys.add('xero-broker');\n if (hasAdminDebug) expectedKeys.add('augmented-admin');\n if (hasSupport) expectedKeys.add('augmented-support');\n if (origamiStdioExpected) expectedKeys.add('origami');\n for (const integration of integrations) {\n const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);\n if (def?.nativeMcp) {\n expectedKeys.add(def.nativeMcp.key ?? integration.definition_id);\n }\n // ENG-8359: expect this CONNECTION's key, not the definition's. Without\n // this a named connection is written by the loop above and then deleted\n // by the reap below on the very same pass — the writer and the reaper\n // disagreeing about one integration's name, with no error either side.\n //\n // The single `buildRemoteMcpEntry` probe covers all three writer\n // branches: an OAuth provider with an `mcpUrl` and a spec-bearing\n // live-header integration both return non-null here, so anything the\n // loop above wrote is expected.\n //\n // Wrapped because it CAN throw: it delegates to `renderRemoteMcpSpec`,\n // which runs `assertSafeRemoteMcpUrl` and rejects a header-scheme `auth`\n // with no `header_name`. That spec comes from the\n // `integration_definitions.remote_mcp` catalog column, so a malformed\n // one is data, not code — and an uncaught throw here would abort not\n // just the reap but the rest of writeIntegrations (CLAUDE.md\n // regeneration, subagent renders). Treat an unrenderable row as \"not\n // expected\"; the worst case is a stale entry left behind, never a\n // half-finished provision.\n let remoteEntry: unknown = null;\n try {\n remoteEntry = buildRemoteMcpEntry(\n integration.definition_id,\n integration.remoteMcp,\n integration.connection_key,\n );\n } catch {\n remoteEntry = null;\n }\n if (remoteEntry) {\n expectedKeys.add(remoteMcpServerKey(integration.definition_id, integration.connection_key));\n }\n }\n for (const key of integrationDerivedKeys) {\n if (!expectedKeys.has(key)) {\n this.removeMcpServer(codeName, key);\n }\n }\n }\n\n // Regenerate CLAUDE.md so the integrations section stays current.\n // Read existing CLAUDE.md to preserve frontmatter/identity, then patch the integrations section.\n const projectDir = getProjectDir(codeName);\n const claudeMdPath = join(projectDir, 'CLAUDE.md');\n try {\n const existing = readFileSync(claudeMdPath, 'utf-8');\n // ENG-8174: `## Integrations` is gated on the\n // `claude-md-integrations-section` registry flag (default OFF), resolved\n // by the manager and passed in. Suppressed means BOTH \"don't write it\"\n // and \"strip whatever is already on disk\" — this method runs on every\n // integration sync, so a gate without the strip would simply freeze the\n // last-synced block in place forever.\n const renderSection = options?.renderClaudeMdSection === true;\n // Reuse the summaries computed above for the sidecar — same shape,\n // same source of truth, no chance of drift between CLAUDE.md and\n // integrations-summary.json.\n const newSection = renderSection ? buildIntegrationsSection(summariesForSidecar) : '';\n\n // ENG-5794: prefer the sentinel-bracketed range when present — it\n // pins the side-effect-managed section to a precise span so the\n // manager's diff-then-write strip can match exactly the same range\n // without swallowing every other section between `## Integrations`\n // and `## Rules`. Fall back to the legacy regex for existing on-disk\n // CLAUDE.md files written before sentinels existed; the next render\n // brings them up to date with the new shape.\n const sentinelStart = INTEGRATIONS_SECTION_START.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const sentinelEnd = INTEGRATIONS_SECTION_END.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const sentinelPattern = new RegExp(`${sentinelStart}[\\\\s\\\\S]*?${sentinelEnd}\\\\n*`);\n\n let updated: string;\n if (sentinelPattern.test(existing)) {\n updated = existing.replace(sentinelPattern, newSection);\n } else if (existing.includes('## Integrations')) {\n // Legacy pre-sentinel fallback (CR on PR #1570): replace only\n // the Integrations H2 block — stop at the next H2 heading or\n // EOF, not through `## Rules`. Pre-ENG-5794 this fallback ran\n // the broad `(?=## Rules)` sweep, which would clobber every\n // section between `## Integrations` and `## Rules` (capability\n // prompt, knowledge, kanban work policy, dashboards) during\n // the one-cycle window between manager upgrade and the first\n // `/host/refresh`. The next render still brings them back, but\n // the transient broken state is avoidable.\n updated = existing.replace(\n /## Integrations[\\s\\S]*?(?=\\n##\\s|$)/,\n newSection ? newSection.trimEnd() + '\\n\\n' : '',\n );\n } else if (newSection) {\n updated = existing.replace('## Rules', `${newSection}## Rules`);\n } else {\n // Suppressed, and nothing on disk to strip — the ENG-8174 steady\n // state. Fall through to the no-op write guard below rather than\n // rewriting identical bytes on every sync.\n updated = existing;\n }\n // Only touch the file when the content actually changed. Every\n // integration sync reaches this point, and an unconditional write\n // churns mtime against the manager's own diff-then-write pass\n // (the ENG-8170 destroy/re-add cycle) for no benefit.\n if (updated !== existing) writeFileSync(claudeMdPath, updated);\n\n // Mirror .env.integrations into project dir so Claude Code picks up\n // credentials. writeEnvIntegrationsForAgent mirrors on every write\n // (ENG-5901 Track D); this path covers project-dir rebuilds that\n // happen without an env write. Keep the mirror owner-only — it's a\n // raw-secrets file (mode applies at creation; chmod covers legacy).\n const agentDir = getAgentDir(codeName);\n const envSrc = join(agentDir, '.env.integrations');\n try {\n const envContent = readFileSync(envSrc, 'utf-8');\n const envDest = join(projectDir, '.env.integrations');\n writeFileSync(envDest, envContent, { mode: SECRET_FILE_MODE });\n try { chmodSync(envDest, SECRET_FILE_MODE); } catch { /* best-effort */ }\n } catch {\n // Source file doesn't exist — no credentials to mirror\n }\n } catch {\n // CLAUDE.md doesn't exist yet — will be created on next full provision\n }\n\n // ENG-4821: belt-and-braces re-render for integrations that don't write\n // an MCP server (the only path that otherwise refreshes the subagent\n // file). Without this, an env-var-only integration like GitHub\n // (`GITHUB_ACCESS_TOKEN` + `gh` CLI, no MCP server) would land the\n // sidecar but leave the subagent's system prompt stale until the next\n // unrelated MCP mutation. The render is idempotent — if a writeMcpServer\n // above already triggered it, the second pass is a no-op rewrite.\n renderChannelMessageHandlerForAgent(codeName);\n // ENG-5905: same belt-and-braces guarantee for augmented-worker.\n renderAugmentedWorkerForAgent(codeName);\n },\n\n writeMcpServer(codeName: string, serverId: string, config: { command: string; args?: string[]; env?: Record<string, string> } | { url: string; headers?: Record<string, string>; type?: 'http' | 'sse' }): void {\n const agentDir = getAgentDir(codeName);\n const mcpJsonPath = join(agentDir, 'provision', '.mcp.json');\n mkdirSync(join(agentDir, 'provision'), { recursive: true });\n\n let mcpConfig: Record<string, unknown>;\n try {\n mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));\n } catch {\n mcpConfig = { mcpServers: {} };\n }\n\n if (!mcpConfig['mcpServers'] || typeof mcpConfig['mcpServers'] !== 'object') {\n mcpConfig['mcpServers'] = {};\n }\n const mcpServers = mcpConfig['mcpServers'] as Record<string, unknown>;\n\n let serverEntry: Record<string, unknown>;\n if ('url' in config) {\n // URL-based MCP server — the per-provider transport decision lives in\n // the pure, unit-tested buildUrlMcpServerEntry (ENG-5545). ENG-5855:\n // forward the entry's `type` so an SSE-backed remoteMcp survives the\n // incremental-sync round-trip instead of being coerced to http.\n serverEntry = buildUrlMcpServerEntry(config.url, config.headers, config.type);\n\n // ENG-5901 Track D: hoist literal secrets out of the rendered entry.\n // Raw values land in .env.integrations (upserted BEFORE the .mcp.json\n // write below); the entry carries `${VAR}` templates that Claude Code\n // substitutes at MCP-launch — docs-confirmed for http `headers` as\n // well as `env`. Already-templated values (Granola/Anchor remoteMcp\n // specs) pass through untouched.\n const hoisted: Record<string, string> = {};\n const entryHeaders = (serverEntry as { headers?: Record<string, string> }).headers;\n if (entryHeaders) {\n const composioKey = entryHeaders['x-api-key'];\n if (composioKey && !composioKey.includes('${')) {\n hoisted['COMPOSIO_API_KEY'] = composioKey;\n entryHeaders['x-api-key'] = '${COMPOSIO_API_KEY}';\n }\n }\n const entryEnv = (serverEntry as { env?: Record<string, string> }).env;\n if (entryEnv) {\n const pdSecret = entryEnv['PIPEDREAM_CLIENT_SECRET'];\n if (pdSecret && !pdSecret.includes('${')) {\n hoisted['PIPEDREAM_CLIENT_SECRET'] = pdSecret;\n entryEnv['PIPEDREAM_CLIENT_SECRET'] = '${PIPEDREAM_CLIENT_SECRET}';\n }\n }\n if (Object.keys(hoisted).length > 0) {\n writeEnvIntegrationsForAgent(codeName, { mode: 'upsert', updates: hoisted });\n }\n } else {\n // Command-based MCP server\n serverEntry = { command: config.command };\n if (config.args?.length) serverEntry['args'] = config.args;\n if (config.env && Object.keys(config.env).length) serverEntry['env'] = config.env;\n }\n\n mcpServers[serverId] = serverEntry;\n\n if (writeMcpJsonGuarded(codeName, mcpJsonPath, mcpConfig as { mcpServers?: Record<string, unknown> })) {\n // Sync to project dir\n syncMcpToProject(codeName);\n }\n },\n\n getMcpPath(codeName: string): string {\n return join(getAgentDir(codeName), 'provision', '.mcp.json');\n },\n\n /**\n * ENG-7994: the declared servers from `provision/.mcp.json`. Mirrors what the\n * manager's prunes used to do inline; owning it here is what lets those call\n * sites stay shape-agnostic across frameworks. A missing or unparseable file\n * means \"nothing declared\", matching `removeMcpServer`'s fail-soft read.\n */\n readMcpServers(codeName: string): Record<string, unknown> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(join(getAgentDir(codeName), 'provision', '.mcp.json'), 'utf-8'));\n } catch {\n return {};\n }\n // A file holding `null`, `[]`, or a bare scalar parses without throwing, so\n // the catch above doesn't cover it and `parsed['mcpServers']` would TypeError\n // on the null — which the prune's own try/catch would then swallow, silently\n // disabling it. Anything that isn't a JSON object reads as \"no servers\", and\n // an array-valued map is rejected rather than yielding numeric index keys\n // (CodeRabbit, PR #3712).\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};\n const mcpServers = (parsed as Record<string, unknown>)['mcpServers'];\n if (!mcpServers || typeof mcpServers !== 'object' || Array.isArray(mcpServers)) return {};\n return mcpServers as Record<string, unknown>;\n },\n\n removeMcpServer(codeName: string, serverId: string): void {\n const agentDir = getAgentDir(codeName);\n const mcpJsonPath = join(agentDir, 'provision', '.mcp.json');\n\n let mcpConfig: Record<string, unknown>;\n try {\n mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));\n } catch {\n return; // No .mcp.json — nothing to remove\n }\n\n const mcpServers = mcpConfig['mcpServers'] as Record<string, unknown> | undefined;\n if (!mcpServers || !(serverId in mcpServers)) return;\n\n delete mcpServers[serverId];\n if (writeMcpJsonGuarded(codeName, mcpJsonPath, mcpConfig as { mcpServers?: Record<string, unknown> })) {\n // Sync to project dir\n syncMcpToProject(codeName);\n }\n },\n\n installSkillFiles(codeName: string, skillId: string, files: CapabilitySkillFile[]): void {\n assertValidCodeName(skillId);\n\n // ENG-4346: plugin skills are derived from the platform DB and should\n // not be edited in place. Write them as 0o444 so the agent's Edit tool\n // fails clearly instead of silently mutating a file that will be\n // overwritten on the next manager refresh. Non-plugin skills (e.g.\n // kanban) stay writable because the agent is allowed to inspect and\n // tweak them locally for ad-hoc workflows.\n const isPluginManaged = skillId.startsWith('plugin-');\n const READ_ONLY_MODE = 0o444;\n const READ_WRITE_MODE = 0o644;\n\n // Install to both the agent config dir and the project dir's .claude/skills/\n const agentDir = getAgentDir(codeName);\n const projectDir = getProjectDir(codeName);\n\n for (const baseDir of [join(agentDir, 'skills'), join(projectDir, '.claude', 'skills')]) {\n const skillDir = join(baseDir, skillId);\n mkdirSync(skillDir, { recursive: true });\n\n for (const file of files) {\n assertSafeRelativePath(file.relativePath);\n const filePath = join(skillDir, file.relativePath);\n // Verify resolved path stays within the skill directory\n const rel = relative(skillDir, filePath);\n if (rel.startsWith('..') || rel === '') {\n throw new Error(`Path traversal detected: ${file.relativePath} resolves outside ${skillDir}`);\n }\n mkdirSync(join(filePath, '..'), { recursive: true });\n\n // If the file already exists with read-only mode (from a previous\n // plugin-skill write), flip it back to writable so writeFileSync\n // can replace it. Otherwise we get EACCES on the rewrite path.\n if (isPluginManaged && existsSync(filePath)) {\n try { chmodSync(filePath, READ_WRITE_MODE); } catch { /* ignore */ }\n }\n\n writeFileSync(filePath, file.content);\n\n if (isPluginManaged) {\n try { chmodSync(filePath, READ_ONLY_MODE); } catch { /* ignore */ }\n }\n }\n }\n },\n\n installPlugin(codeName: string, pluginId: string, pluginPath: string, pluginConfig?: Record<string, unknown>): void {\n const agentDir = getAgentDir(codeName);\n const pluginsJsonPath = join(agentDir, 'plugins.json');\n mkdirSync(agentDir, { recursive: true });\n\n // Track installed plugins in a local registry\n let pluginsConfig: Record<string, unknown>;\n try {\n pluginsConfig = JSON.parse(readFileSync(pluginsJsonPath, 'utf-8'));\n } catch {\n pluginsConfig = { plugins: {} };\n }\n\n if (!pluginsConfig['plugins'] || typeof pluginsConfig['plugins'] !== 'object') {\n pluginsConfig['plugins'] = {};\n }\n const plugins = pluginsConfig['plugins'] as Record<string, unknown>;\n\n plugins[pluginId] = {\n path: pluginPath,\n installed_at: new Date().toISOString(),\n ...(pluginConfig ? { config: pluginConfig } : {}),\n };\n\n writeFileSync(pluginsJsonPath, JSON.stringify(pluginsConfig, null, 2));\n },\n\n /**\n * Full plugin provisioning: install scripts, register hooks, apply permissions,\n * generate config, and write skill files. Called by the manager when a plugin\n * like Ultimate Coder is installed for an agent.\n *\n * @param codeName Agent code_name\n * @param plugin Integration definition from the integration_definitions table\n * @param contextValues Resolved context values from plugin_context\n * @param options.scriptSource How to install scripts: 'git-clone' (path) or 'npm' (package name)\n */\n provisionPluginFull(\n codeName: string,\n plugin: {\n id: string;\n slug: string;\n skills: Array<{ id: string; name: string; content: string; references?: unknown[] }>;\n allowed_tools: string[];\n scripts: Record<string, unknown>;\n },\n contextValues?: Record<string, unknown>,\n options?: { scriptSource?: string },\n ): void {\n assertValidCodeName(codeName);\n assertValidCodeName(plugin.slug);\n const projectDir = getProjectDir(codeName);\n const claudeDir = join(projectDir, '.claude');\n mkdirSync(claudeDir, { recursive: true });\n\n // 1. Register plugin in plugins.json (reuse existing installPlugin logic)\n const sourceSpec = options?.scriptSource ?? `augmented-plugin:${plugin.slug}`;\n this.installPlugin!(codeName, plugin.slug, sourceSpec, contextValues);\n\n // Resolve the actual on-disk plugin directory for hook commands.\n // sourceSpec is registry metadata; hooks need real filesystem paths.\n const installedDir = join(projectDir, '.claude', 'plugins', plugin.slug);\n\n // 2. Install skill files per scope\n for (const skill of plugin.skills) {\n const skillId = skill.id;\n assertValidCodeName(skillId);\n\n const files: CapabilitySkillFile[] = [{\n relativePath: 'SKILL.md',\n content: skill.content,\n }];\n\n this.installSkillFiles!(codeName, `plugin-${skillId}`, files);\n }\n\n // 3. Write hooks to .claude/settings.local.json\n const scriptsConfig = plugin.scripts as {\n hooks?: Record<string, unknown>;\n } | undefined;\n\n if (scriptsConfig?.hooks) {\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const existingHooks = (settings['hooks'] ?? {}) as Record<string, unknown>;\n\n for (const [hookType, hookConfig] of Object.entries(scriptsConfig.hooks)) {\n // hookConfig can be a string (script path) or object (with matcher)\n if (typeof hookConfig === 'string') {\n // Simple hook: script path relative to plugin\n const existing = (existingHooks[hookType] ?? []) as Array<Record<string, unknown>>;\n const alreadyRegistered = existing.some(\n (entry) => JSON.stringify(entry).includes(plugin.slug),\n );\n if (!alreadyRegistered) {\n const scriptPath = hookConfig.startsWith('hooks/') ? hookConfig : `hooks/${hookConfig}`;\n existing.push({\n hooks: [{ type: 'command', command: `${installedDir}/${scriptPath}` }],\n });\n }\n existingHooks[hookType] = existing;\n } else if (typeof hookConfig === 'object' && hookConfig !== null) {\n // Complex hook with matcher (e.g. PostToolUse with TaskUpdate matcher)\n const config = hookConfig as { matcher?: string; script?: string };\n const existing = (existingHooks[hookType] ?? []) as Array<Record<string, unknown>>;\n const alreadyRegistered = existing.some(\n (entry) => JSON.stringify(entry).includes(plugin.slug),\n );\n if (!alreadyRegistered) {\n const rawScript = config.script ?? '';\n const scriptPath = rawScript.startsWith('hooks/') ? rawScript : `hooks/${rawScript}`;\n const hookEntry: Record<string, unknown> = {\n hooks: [{ type: 'command', command: `${installedDir}/${scriptPath}` }],\n };\n if (config.matcher) {\n hookEntry['matcher'] = config.matcher;\n }\n existing.push(hookEntry);\n }\n existingHooks[hookType] = existing;\n }\n }\n\n settings['hooks'] = existingHooks;\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n }\n\n // 4. Write allowed_tools as permissions to .claude/settings.local.json\n if (plugin.allowed_tools.length > 0) {\n const settingsPath = join(claudeDir, 'settings.local.json');\n let settings: Record<string, unknown> = {};\n try {\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n } catch { /* doesn't exist yet */ }\n\n const existingPerms = (settings['permissions'] ?? {}) as Record<string, unknown>;\n const allowList = (existingPerms['allow'] ?? []) as string[];\n\n for (const tool of plugin.allowed_tools) {\n if (!allowList.includes(tool)) {\n allowList.push(tool);\n }\n }\n\n existingPerms['allow'] = allowList;\n settings['permissions'] = existingPerms;\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2));\n }\n\n // 5. Write config file from context values\n if (contextValues && Object.keys(contextValues).length > 0) {\n const configDir = join(projectDir, `.${plugin.slug}`);\n mkdirSync(configDir, { recursive: true });\n writeFileSync(\n join(configDir, 'config.json'),\n JSON.stringify(contextValues, null, 2),\n );\n }\n },\n\n executePluginHook(ctx: PluginHookContext): Promise<PluginHookResult> {\n assertValidCodeName(ctx.codeName);\n // Run hooks from the agent ROOT directory (~/.augmented/<code>), not the\n // claudecode subdir, so relative paths in hook scripts (e.g. `qmd collection\n // add \"$AGENT_CODE_NAME\" project`) resolve to ~/.augmented/<code>/project.\n const agentRootDir = join(getHomeDir(), '.augmented', ctx.codeName);\n const projectDir = getProjectDir(ctx.codeName);\n mkdirSync(agentRootDir, { recursive: true });\n mkdirSync(projectDir, { recursive: true });\n\n // SECURITY: scripts inherit the manager process env, which may include\n // sensitive secrets. This is acceptable while plugins are seed-migrated only;\n // when user-authored plugins land, hook execution must be sandboxed and the\n // env scrubbed to a minimal allowlist.\n const startedAt = Date.now();\n return new Promise<PluginHookResult>((resolve) => {\n const child = execFile(\n 'bash',\n ['-c', ctx.script],\n {\n cwd: agentRootDir,\n timeout: 60_000,\n maxBuffer: 1024 * 1024,\n env: {\n ...process.env,\n // ENG-4510: prepend canonical brew + system bin dirs so hooks\n // resolve binaries (npm, npx, qmd, xurl, …) when the manager is\n // spawned from cloud-init with a minimal PATH. Otherwise hooks\n // exit 127 (\"command not found\") on fresh prod EC2 hosts.\n PATH: augmentedHookPath(process.env.PATH),\n AGENT_CODE_NAME: ctx.codeName,\n AGENT_DIR: agentRootDir,\n AGENT_PROJECT_DIR: projectDir,\n AGENT_FRAMEWORK: 'claude-code',\n },\n },\n (error, stdout, stderr) => {\n const durationMs = Date.now() - startedAt;\n const timedOut = !!error && (error as NodeJS.ErrnoException).code === 'ETIMEDOUT';\n resolve({\n exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0,\n stdout: stdout?.toString() ?? '',\n stderr: stderr?.toString() ?? '',\n durationMs,\n timedOut,\n });\n },\n );\n // Ensure the child is killed if the parent decides to bail out\n child.on('error', () => { /* handled in callback */ });\n });\n },\n\n writeTokenFile(codeName: string, integrations: ResolvedIntegration[]): void {\n // For Claude Code, we write a .tokens.json similar to OpenClaw for live token refresh\n const agentDir = getAgentDir(codeName);\n mkdirSync(agentDir, { recursive: true });\n\n const tokens: Record<string, { access_token: string; config?: Record<string, unknown>; expires_at?: string }> = {};\n\n for (const integration of integrations) {\n // ENG-7506: include `github_app`, which is normalized to the oauth2\n // credential shape server-side (installation token in `access_token`).\n if (integration.auth_type !== 'oauth2' && integration.auth_type !== 'github_app') continue;\n const creds = decryptIntegrationCredentials(\n integration.credentials as Record<string, unknown>,\n );\n const accessToken = creds.access_token as string | undefined;\n if (!accessToken) continue;\n\n tokens[integration.definition_id] = {\n access_token: accessToken,\n ...(Object.keys(integration.config).length > 0 ? { config: integration.config } : {}),\n ...(creds.token_expires_at ? { expires_at: creds.token_expires_at as string } : {}),\n };\n }\n\n if (Object.keys(tokens).length === 0) return;\n\n const tokenPath = join(agentDir, '.tokens.json');\n writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));\n chmodSync(tokenPath, SECRET_FILE_MODE);\n },\n};\n\n// Self-register on import\nregisterFramework(claudeCodeAdapter);\n","// ---------------------------------------------------------------------------\n// xurl credential writer\n//\n// Builds and merges the YAML store at ~/.xurl that the official xurl CLI\n// (https://github.com/xdevplatform/xurl) reads at startup. The on-disk schema\n// is mirrored from xurl's `store/tokens.go`:\n//\n// apps:\n// <app-name>:\n// client_id: \"...\"\n// client_secret: \"...\"\n// default_user: \"\"\n// oauth2_tokens:\n// <username>:\n// type: oauth2\n// oauth2: { access_token, refresh_token, expiration_time }\n// oauth1_token:\n// type: oauth1\n// oauth1: { access_token, token_secret, consumer_key, consumer_secret }\n// bearer_token:\n// type: bearer\n// bearer: \"...\"\n// default_app: <app-name>\n//\n// agt writes one app per xurl integration, prefixed `agt-`. Apps the user\n// configured manually with `xurl auth apps add` are preserved untouched.\n// ---------------------------------------------------------------------------\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ResolvedIntegration } from '../types/integration.js';\n\nconst XURL_FILE_MODE = 0o600;\n\nexport interface XurlOAuth1Payload {\n access_token: string;\n token_secret: string;\n consumer_key: string;\n consumer_secret: string;\n}\n\nexport interface XurlOAuth2Payload {\n access_token: string;\n refresh_token: string;\n expiration_time: number;\n}\n\nexport interface XurlToken {\n type: 'bearer' | 'oauth2' | 'oauth1';\n bearer?: string;\n oauth2?: XurlOAuth2Payload;\n oauth1?: XurlOAuth1Payload;\n}\n\nexport interface XurlApp {\n client_id: string;\n client_secret: string;\n default_user?: string;\n oauth2_tokens?: Record<string, XurlToken>;\n oauth1_token?: XurlToken;\n bearer_token?: XurlToken;\n}\n\nexport interface XurlStore {\n apps: Record<string, XurlApp>;\n default_app?: string;\n}\n\n/** Prefix for apps written by the agt manager — distinguishes them from\n * user-managed apps so cleanup/upgrade logic can act safely. */\nexport const XURL_AGT_APP_PREFIX = 'agt-';\n\nfunction asString(v: unknown): string | undefined {\n return typeof v === 'string' && v.length > 0 ? v : undefined;\n}\n\n/**\n * Returns the canonical xurl app name for an integration.\n *\n * Defaults to `agt-xurl`. When `config.x_username` is supplied, the username\n * is appended (`agt-xurl-<username>`) so multiple xurl integrations targeting\n * different X accounts land in distinct `apps[...]` entries instead of the\n * last-write-wins collapse that `definition_id` alone would cause.\n */\nexport function xurlAppNameFor(integration: ResolvedIntegration): string {\n const username = asString(integration.config?.['x_username']);\n const base = `${XURL_AGT_APP_PREFIX}${integration.definition_id}`;\n return username ? `${base}-${username.toLowerCase()}` : base;\n}\n\n/**\n * Build an xurl App entry from a ResolvedIntegration.\n *\n * Returns `null` when the integration has no usable credentials. Otherwise\n * fills bearer_token (api_key auth), oauth2_tokens (oauth2 auth), and\n * oauth1_token (when all four oauth1_* config fields are present).\n */\nexport function buildXurlAppFromIntegration(integration: ResolvedIntegration): XurlApp | null {\n const cfg = integration.config ?? {};\n const creds = integration.credentials ?? {};\n\n const app: XurlApp = {\n client_id: asString(cfg['client_id']) ?? '',\n client_secret: asString(cfg['client_secret']) ?? '',\n };\n\n const apiKey = asString(creds['api_key']);\n if (integration.auth_type === 'api_key' && apiKey) {\n app.bearer_token = { type: 'bearer', bearer: apiKey };\n }\n\n // ENG-7506: `github_app` is normalized to the oauth2 credential shape\n // server-side (host-runtime mints a short-lived installation token into\n // `credentials.access_token`), so it authenticates xurl as a bearer/oauth2\n // token identically to oauth2.\n const accessToken = asString(creds['access_token']);\n if (\n (integration.auth_type === 'oauth2' || integration.auth_type === 'github_app') &&\n accessToken\n ) {\n const username = asString(cfg['x_username']) ?? 'default';\n const expiresAt = asString(creds['token_expires_at']);\n const expirationTime = expiresAt ? Math.floor(Date.parse(expiresAt) / 1000) : 0;\n app.oauth2_tokens = {\n [username]: {\n type: 'oauth2',\n oauth2: {\n access_token: accessToken,\n refresh_token: asString(creds['refresh_token']) ?? '',\n expiration_time: Number.isFinite(expirationTime) ? expirationTime : 0,\n },\n },\n };\n app.default_user = username;\n }\n\n const ck = asString(cfg['oauth1_consumer_key']);\n const cs = asString(cfg['oauth1_consumer_secret']);\n const at = asString(cfg['oauth1_access_token']);\n const ts = asString(cfg['oauth1_token_secret']);\n if (ck && cs && at && ts) {\n app.oauth1_token = {\n type: 'oauth1',\n oauth1: { access_token: at, token_secret: ts, consumer_key: ck, consumer_secret: cs },\n };\n }\n\n if (!app.bearer_token && !app.oauth2_tokens && !app.oauth1_token) {\n return null;\n }\n return app;\n}\n\n/**\n * Merge agt-managed xurl apps into an existing store, preserving every\n * non-agt-prefixed app the user added manually with `xurl auth apps add`.\n *\n * Critically, existing `agt-*` entries that are NOT present in the new\n * `agtApps` map are dropped — the current set of integrations is the\n * source of truth, so stale tokens for removed integrations must not\n * linger on disk.\n *\n * `default_app` is left untouched if it still resolves to an app that\n * exists post-merge; otherwise it falls back to the first agt-managed\n * app so the CLI has a sensible default.\n */\nexport function mergeXurlStore(existing: XurlStore | null, agtApps: Record<string, XurlApp>): XurlStore {\n const apps: Record<string, XurlApp> = {};\n\n // 1. Keep every user-managed (non-agt-prefixed) app from the existing store.\n for (const [name, app] of Object.entries(existing?.apps ?? {})) {\n if (!name.startsWith(XURL_AGT_APP_PREFIX)) apps[name] = app;\n }\n // 2. Overlay the current set of agt-managed apps. Any previous agt-* entry\n // that isn't in `agtApps` is intentionally dropped here — stale creds.\n for (const [name, app] of Object.entries(agtApps)) {\n apps[name] = app;\n }\n\n let default_app = existing?.default_app;\n if (default_app && !apps[default_app]) {\n // The previous default_app pointed at an app that no longer exists\n // (e.g., the user deleted it, or an agt app was removed).\n default_app = undefined;\n }\n if (!default_app) {\n default_app = Object.keys(apps).find((n) => n.startsWith(XURL_AGT_APP_PREFIX));\n }\n\n const result: XurlStore = { apps };\n if (default_app) result.default_app = default_app;\n return result;\n}\n\n/**\n * Parse an existing ~/.xurl YAML payload, tolerant of empty input but\n * strict about structure — a parsed YAML object only qualifies as a\n * XurlStore when it actually carries xurl-specific fields (`apps` and/or\n * `default_app`). This prevents unrelated YAML (e.g. `foo: bar`) from\n * being treated as a valid empty store and then overwritten.\n */\nexport function parseXurlStore(yaml: string | null | undefined): XurlStore | null {\n if (!yaml) return null;\n try {\n const parsed = parseYaml(yaml) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;\n\n const obj = parsed as Record<string, unknown>;\n const hasApps = 'apps' in obj;\n const hasDefault = 'default_app' in obj;\n\n // Neither xurl-specific field present → treat as foreign YAML, fail closed.\n if (!hasApps && !hasDefault) return null;\n\n // When `apps` is present it must be a plain object (not null, not array).\n if (hasApps && obj['apps'] != null && (typeof obj['apps'] !== 'object' || Array.isArray(obj['apps']))) {\n return null;\n }\n if (hasDefault && obj['default_app'] != null && typeof obj['default_app'] !== 'string') {\n return null;\n }\n\n return {\n apps: (obj['apps'] as Record<string, XurlApp> | undefined) ?? {},\n ...(typeof obj['default_app'] === 'string' && obj['default_app'] ? { default_app: obj['default_app'] } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/** Serialize a store to YAML — suitable for writing to ~/.xurl. */\nexport function serializeXurlStore(store: XurlStore): string {\n return stringifyYaml(store);\n}\n\n/**\n * Build the agt-managed apps map for a set of resolved integrations.\n * Filters out non-xurl integrations and integrations with no usable credentials.\n */\nexport function buildAgtXurlApps(integrations: ResolvedIntegration[]): Record<string, XurlApp> {\n const result: Record<string, XurlApp> = {};\n for (const integration of integrations) {\n if (integration.definition_id !== 'xurl') continue;\n const app = buildXurlAppFromIntegration(integration);\n if (app) {\n result[xurlAppNameFor(integration)] = app;\n }\n }\n return result;\n}\n\n/**\n * Resolve the on-disk path to the xurl store. Mirrors xurl's own logic\n * (`os.UserHomeDir() / .xurl`) and respects $HOME / $USERPROFILE so tests\n * can target a temp dir.\n */\nexport function getXurlStorePath(): string {\n const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? homedir();\n return join(home, '.xurl');\n}\n\n/**\n * Persist agt-managed xurl apps into the user's `~/.xurl`, merging with any\n * apps the user added manually with `xurl auth apps add`. No-ops when the\n * caller provides no xurl integrations with usable credentials, so calling\n * it on every refresh is safe.\n *\n * Returns the absolute path written to, or `null` if nothing was written.\n */\nexport function writeXurlStoreForIntegrations(\n integrations: ResolvedIntegration[],\n filePath: string = getXurlStorePath(),\n): string | null {\n const agtApps = buildAgtXurlApps(integrations);\n if (Object.keys(agtApps).length === 0) return null;\n\n let existing: XurlStore | null = null;\n if (existsSync(filePath)) {\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf-8');\n } catch {\n // Fail closed: an unreadable existing file must not be clobbered,\n // or we could wipe out user-managed apps.\n return null;\n }\n const parsed = parseXurlStore(raw);\n if (!parsed && raw.trim().length > 0) {\n // Fail closed: the file has content but parseXurlStore could not\n // recognise it as a valid xurl store. Proceeding would overwrite\n // user-managed apps, so abort the merge.\n return null;\n }\n existing = parsed;\n }\n\n const merged = mergeXurlStore(existing, agtApps);\n\n mkdirSync(dirname(filePath), { recursive: true });\n\n // Atomic write: stage to a sibling temp file with the restrictive mode\n // applied from creation, then rename over the target so an interrupted\n // process can never leave ~/.xurl partially written or corrupted.\n const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;\n writeFileSync(tmpPath, serializeXurlStore(merged), { mode: XURL_FILE_MODE });\n try {\n renameSync(tmpPath, filePath);\n } catch (err) {\n try { unlinkSync(tmpPath); } catch { /* ignore */ }\n throw err;\n }\n try {\n chmodSync(filePath, XURL_FILE_MODE);\n } catch {\n // Best-effort: chmod fails on some platforms (e.g. Windows). The\n // temp file was created with the restrictive mode, so the renamed\n // target should already have it.\n }\n\n return filePath;\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\n\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12;\nconst AUTH_TAG_LENGTH = 16;\nconst PREFIX = 'enc:';\n\nfunction getKey(): Buffer {\n const hex = process.env['AUTH_ENCRYPTION_KEY'];\n if (!hex || hex.length !== 64) {\n throw new Error('AUTH_ENCRYPTION_KEY must be a 64-char hex string (32 bytes)');\n }\n return Buffer.from(hex, 'hex');\n}\n\n/** Encrypt a plaintext string. Returns \"enc:base64(iv):base64(ciphertext+tag)\". */\nexport function encryptSecret(plaintext: string): string {\n const key = getKey();\n const iv = randomBytes(IV_LENGTH);\n const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `${PREFIX}${iv.toString('base64')}:${Buffer.concat([encrypted, tag]).toString('base64')}`;\n}\n\n/** Decrypt a value produced by encryptSecret(). Returns plaintext. */\nexport function decryptSecret(encoded: string): string {\n if (!encoded.startsWith(PREFIX)) {\n // Not encrypted — return as-is (backward compat for pre-encryption values)\n return encoded;\n }\n const key = getKey();\n const parts = encoded.slice(PREFIX.length).split(':');\n if (parts.length !== 2) throw new Error('Invalid encrypted secret format');\n\n const iv = Buffer.from(parts[0]!, 'base64');\n const data = Buffer.from(parts[1]!, 'base64');\n\n // Last 16 bytes are the auth tag\n const ciphertext = data.subarray(0, data.length - AUTH_TAG_LENGTH);\n const tag = data.subarray(data.length - AUTH_TAG_LENGTH);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });\n decipher.setAuthTag(tag);\n return decipher.update(ciphertext) + decipher.final('utf8');\n}\n\n/** Check if a value is already encrypted. */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n","import { decryptSecret, encryptSecret, isEncrypted } from './secret.js';\n\n/**\n * Field names treated as secret-bearing across the three install tables\n * (organization_integrations / team_integrations / agent_integrations).\n *\n * Three categories:\n * - OAuth2 tokens (access_token, refresh_token)\n * - API-key auth (api_key)\n * - Webhook / HMAC-signed auth (webhook_secret, signing_secret, client_secret)\n * - GitHub App auth (private_key — the RS256 key that signs the App JWT, CS-1441)\n *\n * Provider-specific secret names not in this list will be persisted in\n * plaintext — keep this synchronised with the route-side assertion in\n * `packages/api/src/routes/integrations.ts`.\n */\nconst SENSITIVE_INTEGRATION_FIELDS = [\n 'access_token',\n 'refresh_token',\n 'api_key',\n 'webhook_secret',\n 'signing_secret',\n 'client_secret',\n 'private_key',\n] as const;\n\n/**\n * Encrypt sensitive token fields inside an `integrations.credentials` JSONB.\n * Non-sensitive fields are left untouched. Safe to call on a credentials blob\n * that already contains encrypted values — isEncrypted() gates re-encryption.\n *\n * If AUTH_ENCRYPTION_KEY is not set (dev/local without secrets), values are\n * left as plaintext so the system stays functional; this matches the existing\n * channel-config encryption behaviour.\n */\nexport function encryptIntegrationCredentials(\n credentials: Record<string, unknown>,\n): Record<string, unknown> {\n const out = { ...credentials };\n for (const field of SENSITIVE_INTEGRATION_FIELDS) {\n const value = out[field];\n if (typeof value === 'string' && value && !isEncrypted(value)) {\n try {\n out[field] = encryptSecret(value);\n } catch {\n // Encryption key not available — leave as plaintext.\n }\n }\n }\n return out;\n}\n\n/**\n * Decrypt sensitive token fields inside an `integrations.credentials` JSONB.\n * Plaintext values are returned as-is (backward compatible with rows that\n * predate encryption — they migrate on next write).\n *\n * Fails loudly: if a value is wrapped with the `enc:` prefix but cannot be\n * decrypted (missing key, wrong key, corruption), we throw rather than hand\n * ciphertext back to the caller. Returning ciphertext silently would bleed\n * `enc:...` blobs into `.env.integrations` files, `.tokens.json` artifacts,\n * and outbound requests to providers — all of which look healthy to the\n * calling code until auth fails with a misleading `invalid_grant`.\n */\nexport function decryptIntegrationCredentials(\n credentials: Record<string, unknown>,\n): Record<string, unknown> {\n const out = { ...credentials };\n for (const field of SENSITIVE_INTEGRATION_FIELDS) {\n const value = out[field];\n if (typeof value === 'string' && value && isEncrypted(value)) {\n out[field] = decryptSecret(value);\n }\n }\n return out;\n}\n","/**\n * ENG-8344 — broker-delivered GitHub App credentials for `git` and `gh`.\n *\n * The problem\n * -----------\n * A GitHub App installation token has a ~1h TTL. `POST /host/agent-integrations`\n * mints one per poll and the claudecode adapter materializes it into\n * `.env.integrations` as `GITHUB_ACCESS_TOKEN` + `GITHUB_TOKEN` (ENG-7506).\n * The persistent wrapper sources that file EXACTLY ONCE at spawn, so the only\n * way a running agent ever sees a rotated token is a full session respawn —\n * a guaranteed hourly session kill for every agent holding a GitHub App\n * install (measured: 6 agents, ~144 kills/day). ENG-8343 stopped those kills\n * landing mid-turn; this module removes them.\n *\n * The shape of the fix\n * --------------------\n * Two small programs written into the agent's own bin dir:\n *\n * * `git-credential-agt-github` — a git credential helper. git runs it at\n * USE time (per fetch/push), it fetches the current token from the broker\n * and writes it back over git's credential protocol on stdout.\n * * `gh` — a PATH shim. Resolves the same token, then runs the REAL `gh`\n * with it in that child's environment only.\n *\n * Neither ever writes the token to disk. A design review explicitly rejected\n * the token-file variant: it moves a live bearer token from RAM onto disk\n * where any process running as that user — and any backup or log snapshotting\n * the directory — can read it. Fetch at use time, hand it over a pipe, forget it.\n *\n * What this does and does NOT buy\n * -------------------------------\n * It removes the token from the agent process ENVIRONMENT, which is the thing\n * that forces the respawns. It does NOT make the token unreachable by a\n * determined agent: the agent can run the helper itself, exactly as git does.\n * That is still strictly better than today (no rotation-driven session kill, no\n * hour-old token in every child process's environ) but it is not a sandbox and\n * must not be described as one.\n *\n * Addressing\n * ----------\n * The broker credential endpoint is reached by `definition_id` (`github`)\n * rather than the integration uuid — ENG-8264 / PR #3949. That matters: the\n * uuid is only ever set inside `.mcp.json` MCP env blocks and never reaches the\n * agent process env, whereas `AGT_HOST` / `AGT_API_KEY` / `AGT_AGENT_ID` are\n * already there. Without name addressing these programs could not call the\n * endpoint at all.\n */\n\n/** Directory (under the agent's project dir) holding the helper + shim. */\nexport const GITHUB_BROKER_BIN_DIR = '.claude/agt-bin';\n\n/** Basename of the git credential helper. */\nexport const GIT_CREDENTIAL_HELPER_BASENAME = 'git-credential-agt-github';\n\n/**\n * Basename of the `gh` shim. It MUST be exactly `gh`: the shim works by\n * shadowing the real binary on PATH, and the wrapper prepends this directory.\n */\nexport const GH_SHIM_BASENAME = 'gh';\n\n/** The broker credential endpoint, addressed by definition_id (ENG-8264). */\nexport const GITHUB_CREDENTIAL_PATH = '/host/agent-integrations/github/credential';\n\n/** Mode for both programs: owner read/write/execute only. */\nexport const BROKER_SCRIPT_MODE = 0o700;\n\n/**\n * The env vars that point `git` at the credential helper.\n *\n * `GIT_CONFIG_COUNT`/`_KEY_n`/`_VALUE_n` (git >= 2.31) inject config into a\n * single process's environment rather than mutating `~/.gitconfig`. That keeps\n * the change per-agent, invisible to anything else running as the same user,\n * and removed the moment the flag goes off — no file to clean up, nothing left\n * behind on a rollback.\n *\n * Two entries for one key, deliberately:\n * * index 0 sets `credential.https://github.com.helper` to the EMPTY string,\n * which is git's documented idiom for \"reset the helper list for this URL\".\n * Without it a helper already configured in `~/.gitconfig` (a `store` file,\n * a stale `gh auth git-credential`) would be consulted first and could win\n * with an expired token — the exact failure we are removing.\n * * index 1 installs ours as the only helper for github.com.\n *\n * `GIT_TERMINAL_PROMPT=0` is set with them. Under tmux the agent HAS a tty, so\n * a credential miss would otherwise leave `git push` blocked on a Username:\n * prompt forever. Failing immediately is the legible outcome (constraint D).\n *\n * Known limitation, stated rather than hidden: an agent command that sets its\n * own `GIT_CONFIG_COUNT` replaces these. Nothing in the fleet does today.\n */\nexport function buildGitCredentialEnv(helperPath: string): Record<string, string> {\n return {\n GIT_CONFIG_COUNT: '2',\n GIT_CONFIG_KEY_0: 'credential.https://github.com.helper',\n GIT_CONFIG_VALUE_0: '',\n GIT_CONFIG_KEY_1: 'credential.https://github.com.helper',\n GIT_CONFIG_VALUE_1: helperPath,\n GIT_TERMINAL_PROMPT: '0',\n };\n}\n\n/** Every env key this module owns — used to prune them when the flag is off. */\nexport const GITHUB_BROKER_ENV_KEYS: readonly string[] = Object.keys(\n buildGitCredentialEnv(''),\n);\n\n/**\n * Shared prelude: resolve a fresh installation token from the broker.\n *\n * Written as Node rather than shell on purpose. The shell version needs `jq`\n * (not guaranteed in every agent image) or hand-rolled JSON scraping that\n * silently mis-parses a `null` token into the literal string \"null\" — which\n * would present as an authentication failure against GitHub instead of a\n * credential-fetch failure, sending whoever debugs it at the wrong system.\n * Node is present wherever Claude Code runs.\n *\n * Two calls: `POST /host/exchange` (api key -> short JWT) then the credential\n * endpoint. The JWT is deliberately NOT cached to disk — it is a bearer token,\n * and the whole point of this change is to stop parking bearer tokens where a\n * later reader can find them. The cost is one extra round trip per `git push`.\n */\nconst TOKEN_RESOLVER_PRELUDE = `\nconst AGT_HOST = (process.env.AGT_HOST || '').replace(/\\\\/+$/, '');\nconst AGT_API_KEY = process.env.AGT_API_KEY || '';\nconst AGT_AGENT_ID = process.env.AGT_AGENT_ID || '';\n\n/**\n * Every failure exits through here, so an operator always gets a sentence that\n * names the credential fetch as the failing step. A bare 401 from GitHub sends\n * people auditing App permissions; \"broker credential fetch failed: 503\" sends\n * them at the API. Constraint D of ENG-8344.\n */\nfunction fail(reason) {\n process.stderr.write(\n 'augmented: GitHub credential fetch failed: ' + reason + '\\\\n' +\n 'augmented: the token is fetched per use from ' + (AGT_HOST || '<AGT_HOST unset>') +\n '${GITHUB_CREDENTIAL_PATH}' + ' (ENG-8344). This is NOT a GitHub permissions problem.\\\\n',\n );\n process.exit(1);\n}\n\nasync function postJson(url, body, headers) {\n let res;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, headers || {}),\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(15000),\n });\n } catch (err) {\n throw new Error('cannot reach ' + url + ' (' + (err && err.message ? err.message : String(err)) + ')');\n }\n const text = await res.text().catch(() => '');\n if (!res.ok) {\n let detail = text.slice(0, 300);\n try {\n const parsed = JSON.parse(text);\n if (parsed && parsed.error) detail = String(parsed.error);\n } catch { /* keep the raw body */ }\n throw new Error(url + ' returned HTTP ' + res.status + (detail ? ': ' + detail : ''));\n }\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(url + ' returned a non-JSON body');\n }\n}\n\nasync function resolveToken() {\n if (!AGT_HOST) fail('AGT_HOST is not set in this process environment');\n if (!AGT_API_KEY) fail('AGT_API_KEY is not set in this process environment');\n if (!AGT_AGENT_ID) fail('AGT_AGENT_ID is not set in this process environment');\n\n let jwt;\n try {\n const exchanged = await postJson(AGT_HOST + '/host/exchange', { host_key: AGT_API_KEY });\n jwt = exchanged && exchanged.token;\n } catch (err) {\n fail('host key exchange failed - ' + err.message);\n }\n if (!jwt) fail('host key exchange returned no token');\n\n let credential;\n try {\n credential = await postJson(\n AGT_HOST + '${GITHUB_CREDENTIAL_PATH}',\n { agent_id: AGT_AGENT_ID },\n { Authorization: 'Bearer ' + jwt },\n );\n } catch (err) {\n fail('broker credential fetch failed - ' + err.message);\n }\n const token = credential && credential.access_token;\n if (typeof token !== 'string' || token === '') {\n fail('the broker returned no access_token for the github integration (is it still connected?)');\n }\n return token;\n}\n`;\n\nconst GENERATED_BANNER = `#!/usr/bin/env node\n// Auto-generated by Augmented (ENG-8344) — do not edit.\n// Canonical source: packages/core/src/provisioning/github-broker-credentials.ts\n`;\n\n/**\n * Render the git credential helper.\n *\n * Speaks git's credential protocol: key=value lines on stdin terminated by a\n * blank line, the same shape back on stdout. Only `get` does anything —\n * `store` and `erase` exit 0 silently because there is nothing persisted to\n * store or erase, and a non-zero exit there would make every successful push\n * print a spurious helper error.\n *\n * The host guard is not decoration. A credential helper is asked for whatever\n * host git is talking to, so without it a request for an unrelated private\n * registry would be answered with a GitHub App token — handing a live\n * credential to a third party. Anything that is not github.com exits 0 with no\n * output, which git reads as \"this helper has nothing\" and moves on.\n */\nexport function renderGitCredentialHelper(): string {\n return `${GENERATED_BANNER}${TOKEN_RESOLVER_PRELUDE}\nfunction readStdin() {\n return new Promise((resolve) => {\n let buf = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => { buf += chunk; });\n process.stdin.on('end', () => resolve(buf));\n process.stdin.on('error', () => resolve(buf));\n });\n}\n\nfunction parseCredentialRequest(input) {\n const out = {};\n for (const line of String(input).split('\\\\n')) {\n if (line === '') continue;\n const eq = line.indexOf('=');\n if (eq <= 0) continue;\n out[line.slice(0, eq)] = line.slice(eq + 1);\n }\n return out;\n}\n\nasync function main() {\n const operation = process.argv[2] || '';\n // Nothing is persisted, so there is nothing to store or erase. Exit clean so\n // a successful push does not print a helper error on the way out.\n if (operation !== 'get') process.exit(0);\n\n const request = parseCredentialRequest(await readStdin());\n\n // Only ever answer for github.com over https. Any other host gets silence,\n // which git treats as \"this helper has no credential\" and falls through to\n // the next one - never a GitHub token handed to an unrelated server.\n const host = request.host || '';\n const protocol = request.protocol || '';\n if (host !== 'github.com' || (protocol && protocol !== 'https')) process.exit(0);\n\n const token = await resolveToken();\n // x-access-token is the username GitHub expects for an App installation token.\n process.stdout.write(\n 'protocol=https\\\\n' +\n 'host=github.com\\\\n' +\n 'username=x-access-token\\\\n' +\n 'password=' + token + '\\\\n' +\n '\\\\n',\n );\n}\n\nmain().catch((err) => fail(err && err.message ? err.message : String(err)));\n`;\n}\n\n/**\n * Render the `gh` PATH shim.\n *\n * `gh` has no credential-helper concept: it reads `GH_TOKEN` / `GITHUB_TOKEN`\n * from its environment, or a token persisted in `hosts.yml`. The second is the\n * on-disk variant the design review rejected, so the shim takes the first —\n * but only in the CHILD's environment, for the lifetime of that one command.\n * The agent's own environment never holds the token.\n *\n * Finding the real binary is the fiddly part: this file is itself named `gh`\n * and sits first on PATH, so a naive lookup re-executes the shim forever. The\n * search skips any candidate that resolves to this file.\n */\nexport function renderGhShim(): string {\n return `${GENERATED_BANNER}${TOKEN_RESOLVER_PRELUDE}\nconst { spawnSync } = require('node:child_process');\nconst { accessSync, constants, realpathSync } = require('node:fs');\nconst { join, delimiter } = require('node:path');\nconst { constants: osConstants } = require('node:os');\nconst signals = osConstants.signals;\n\nconst SELF = (() => {\n try { return realpathSync(__filename); } catch { return __filename; }\n})();\n\n/**\n * First executable named 'gh' on PATH that is NOT this shim. Skipping by\n * resolved path (not by directory string) means a symlinked or relocated bin\n * dir cannot trick the search into re-entering the shim.\n */\nfunction findRealGh() {\n for (const dir of (process.env.PATH || '').split(delimiter)) {\n if (!dir) continue;\n const candidate = join(dir, 'gh');\n let resolved;\n try {\n accessSync(candidate, constants.X_OK);\n resolved = realpathSync(candidate);\n } catch { continue; }\n if (resolved === SELF) continue;\n return candidate;\n }\n return null;\n}\n\nasync function main() {\n const realGh = findRealGh();\n if (!realGh) {\n process.stderr.write(\n 'augmented: the real gh binary is not on PATH (only the Augmented shim is). ' +\n 'Install gh, or unset the broker credential path.\\\\n',\n );\n process.exit(127);\n }\n\n const token = await resolveToken();\n // Token lives ONLY in this child's environment, for this one command.\n const env = Object.assign({}, process.env, {\n GH_TOKEN: token,\n GITHUB_TOKEN: token,\n });\n const result = spawnSync(realGh, process.argv.slice(2), { stdio: 'inherit', env });\n if (result.error) {\n process.stderr.write('augmented: failed to run ' + realGh + ': ' + result.error.message + '\\\\n');\n process.exit(126);\n }\n // Report a signal death as the shell convention 128+n, with n the REAL signal\n // number from os.constants.signals. \\`result.signal\\` is a name ('SIGINT'), not\n // a number, so a truthiness check would collapse every signal to 129 and a\n // Ctrl-C would report 129 instead of 130 - wrong in exactly the case a caller\n // inspects the code to tell \"interrupted\" from \"failed\". Unknown name falls\n // back to 128, which reads as \"died on a signal we could not name\".\n if (result.status === null) {\n const number = signals[result.signal];\n process.exit(128 + (typeof number === 'number' ? number : 0));\n }\n process.exit(result.status);\n}\n\nmain().catch((err) => fail(err && err.message ? err.message : String(err)));\n`;\n}\n","import chalk from 'chalk';\n\nlet _jsonMode = false;\n\nexport function setJsonMode(enabled: boolean): void {\n _jsonMode = enabled;\n if (enabled) {\n chalk.level = 0;\n }\n}\n\nexport function isJsonMode(): boolean {\n return _jsonMode;\n}\n\n/**\n * Emit a JSON object to stdout and exit cleanly.\n * In JSON mode, this is the only output function that should be used.\n */\nexport function jsonOutput(data: Record<string, unknown>): void {\n console.log(JSON.stringify(data, null, 2));\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\nconst AUGMENTED_DIR = join(homedir(), '.augmented');\nconst CONFIG_PATH = join(AUGMENTED_DIR, 'config.json');\n\nfunction ensureAugmentedDir(): void {\n if (!existsSync(AUGMENTED_DIR)) {\n mkdirSync(AUGMENTED_DIR, { recursive: true });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shell profile env loader — reads AGT_* vars from shell profile if not in env\n// ---------------------------------------------------------------------------\n\n/**\n * If AGT_HOST or AGT_API_KEY are missing from the environment, try to\n * extract them from the user's shell profile. This handles the case where\n * `agt setup` wrote the vars but the user hasn't sourced the profile yet.\n */\nexport function reloadFromShellProfile(): void {\n return loadFromShellProfile(true);\n}\n\nfunction loadFromShellProfile(force = false): void {\n if (!force && process.env['AGT_HOST'] && process.env['AGT_API_KEY']) return;\n\n const shell = process.env['SHELL'] ?? '';\n const home = homedir();\n const candidates = shell.includes('zsh')\n ? [join(home, '.zshrc'), join(home, '.zprofile')]\n : shell.includes('fish')\n ? [join(home, '.config', 'fish', 'config.fish')]\n : [join(home, '.bashrc'), join(home, '.bash_profile')];\n\n for (const profile of candidates) {\n try {\n const content = readFileSync(profile, 'utf-8');\n for (const key of ['AGT_HOST', 'AGT_API_KEY', 'AGT_TEAM'] as const) {\n if (!force && process.env[key]) continue;\n // Match active (non-comment) lines only.\n const match = content\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith('#'))\n .map((line) =>\n line.match(\n new RegExp(\n `^(?:export\\\\s+${key}\\\\s*=\\\\s*[\"']([^\"']+)[\"']|set\\\\s+-gx\\\\s+${key}\\\\s+[\"']([^\"']+)[\"'])$`,\n ),\n ),\n )\n .find(Boolean);\n if (match) {\n process.env[key] = match[1] ?? match[2];\n }\n }\n } catch {\n // Profile doesn't exist\n }\n if (process.env['AGT_HOST'] && process.env['AGT_API_KEY']) break;\n }\n}\n\n// Auto-load on module import\nloadFromShellProfile();\n\n// ---------------------------------------------------------------------------\n// API key\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the host API key (`tlk_...`) from `AGT_API_KEY` env var, or null.\n */\nexport function getApiKey(): string | null {\n return process.env['AGT_API_KEY'] ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface AugmentedConfig {\n active_team?: string; // team slug\n}\n\nexport function getConfig(): AugmentedConfig {\n try {\n const raw = readFileSync(CONFIG_PATH, 'utf-8');\n return JSON.parse(raw) as AugmentedConfig;\n } catch {\n return {};\n }\n}\n\nexport function saveConfig(config: AugmentedConfig): void {\n ensureAugmentedDir();\n writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));\n}\n\nexport function getActiveTeam(): string | undefined {\n // Check env var first (headless / agent mode)\n const envTeam = process.env['AGT_TEAM'];\n if (envTeam) return envTeam;\n\n return getConfig().active_team;\n}\n\nexport function setActiveTeam(slug: string): void {\n const config = getConfig();\n config.active_team = slug;\n saveConfig(config);\n}\n\n// ---------------------------------------------------------------------------\n// API host\n// ---------------------------------------------------------------------------\n\n/**\n * The Production API URL. Use this when a call site needs to *explicitly*\n * target prod regardless of the operator's `AGT_HOST` (impersonate's redeem\n * exchange, ENG-5773). NEVER use as a silent fallback for unset AGT_HOST —\n * that's the ENG-5831 bug: a non-prod-stage operator who hasn't sourced their\n * profile yet would silently exchange a stage token against prod.\n */\nexport const PROD_AGT_HOST = 'https://api.augmented.team';\n\n/**\n * Actionable error message thrown by `getHost()` / `requireHost()` when\n * `AGT_HOST` is not in the environment (and the shell-profile loader didn't\n * find it either). Lists every legitimate way to set it, plus a pointer to\n * the `--api-host` flag for first-time setup (where AGT_HOST is by\n * definition not yet on disk).\n */\nexport const AGT_HOST_UNSET_MESSAGE =\n 'AGT_HOST is not set. Set it to the API URL for your stage before running this command:\\n' +\n ' Production: export AGT_HOST=https://api.augmented.team\\n' +\n ' Non-prod stage: export AGT_HOST=https://<stage>.api.staging.augmented.team\\n' +\n ' Local development: export AGT_HOST=http://api.agt.localhost:1355\\n' +\n '\\n' +\n 'For first-time host setup, pass --api-host <url> to `agt setup` instead — the ' +\n 'setup command does NOT silently default to prod (ENG-5831).';\n\n/**\n * Augmented API server URL.\n *\n * Reads `AGT_HOST` from the environment. Throws if unset — ENG-5831\n * established that a silent prod default routes non-prod-stage callers to\n * prod and looks like an opaque connectivity failure. Treats blank /\n * whitespace values as unset so a stray `export AGT_HOST=` can't propagate\n * an invalid host.\n */\nexport function getHost(): string {\n const envHost = process.env['AGT_HOST']?.trim();\n if (!envHost) {\n throw new Error(AGT_HOST_UNSET_MESSAGE);\n }\n return envHost;\n}\n\nexport function requireHost(): string {\n return getHost();\n}\n","// Auth key-exchange failure classification (ENG-8260).\n//\n// A leaf module — no imports — so both the API client (which throws these) and\n// the manager's persistent-session decision path (which reads them) can depend\n// on it without a cycle, and so the classifier is cheap to unit-test.\n//\n// WHY THIS EXISTS\n// ---------------\n// `/host/exchange` failures used to surface to the manager as a bare `Error`\n// carrying only a message string. That left the caller unable to tell\n// \"the platform API is unwell right now\" (HTTP 5xx, socket reset) from\n// \"this credential is not valid\" (401/403, revoked key) — so it treated both\n// as an auth verdict and tore down the agent's running tmux session. A ~2s API\n// blip on `alyve-host-1` on 2026-07-29 was therefore enough to kill a healthy,\n// mid-turn session for `dwight` and cancel its runs. See ENG-8260.\n//\n// The distinction is the whole fix, so it is modelled in the type system rather\n// than recovered by matching on message text.\n\n/**\n * A failure of the `tlk_` host-key → JWT exchange.\n *\n * `transient` is the load-bearing field: true means \"retry, this says nothing\n * about the credential\", false means \"this is a verdict on the credential and\n * callers may act on it\".\n *\n * The `message` format is deliberately unchanged from the pre-ENG-8260 bare\n * `Error` so operator-facing manager.log lines and any log greps keep working.\n */\nexport class ApiKeyExchangeError extends Error {\n constructor(\n message: string,\n /** HTTP status of the failed exchange, or null for a transport-level failure. */\n public readonly status: number | null,\n /**\n * True when the failure carries no information about the credential's\n * validity (5xx, 429, or a transport error) and is therefore worth\n * retrying. False for a genuine auth verdict (401/403/revoked).\n */\n public readonly transient: boolean,\n ) {\n super(message);\n this.name = 'ApiKeyExchangeError';\n }\n}\n\n/**\n * Classify an HTTP status from the exchange endpoint as transient or a verdict.\n *\n * - `>= 500` — the API is unwell (500 Internal Server Error, 502/503/504 from a\n * proxy or a restarting server). Says nothing about the key.\n * - `429` — rate limited. Also says nothing about the key.\n * - everything else (notably 400/401/403/404) — a verdict. 401/403 mean the\n * credential was rejected; those must keep their current fail-closed\n * behaviour.\n */\nexport function isTransientExchangeStatus(status: number): boolean {\n return status >= 500 || status === 429;\n}\n\n/**\n * True when an error thrown out of the auth-exchange path is a transient\n * platform fault rather than a verdict on the credential.\n *\n * Deliberately conservative: an error shape we do not recognise returns\n * `false`. The caller's `false` branch is the pre-ENG-8260 fail-closed\n * behaviour (refuse to spawn, stop a running session), so defaulting to\n * `false` means the new session-preserving branch only ever fires on positive\n * evidence of a transient fault — never on the absence of evidence. An\n * unclassifiable auth error must not be able to talk us into keeping a session\n * alive under credentials we cannot vouch for.\n */\nexport function isTransientAuthFailure(err: unknown): boolean {\n return err instanceof ApiKeyExchangeError && err.transient;\n}\n\n/**\n * Short, non-secret tag for the decision log line. Never includes the message\n * body (which carries the API host and an obfuscated key).\n */\nexport function exchangeFailureKind(err: unknown): string {\n if (!(err instanceof ApiKeyExchangeError)) return 'unclassified';\n const status = err.status === null ? 'transport' : String(err.status);\n return err.transient ? `transient-${status}` : `verdict-${status}`;\n}\n","import { requireHost, getApiKey, getActiveTeam, reloadFromShellProfile } from './config.js';\nimport { ApiKeyExchangeError, isTransientExchangeStatus } from './auth-exchange-error.js';\n\n// ENG-8260: re-exported so existing importers of the API client can reach the\n// exchange-failure classifier without also importing the leaf module.\nexport { ApiKeyExchangeError, isTransientAuthFailure, exchangeFailureKind } from './auth-exchange-error.js';\n\n// ENG-6410: stamped into the X-Agt-Cli-Version header on every authenticated\n// host-runtime request so the API can record which agt-cli version produced a\n// given audit_log row (per-version bug-rate monitoring). Resolved from the same\n// tsup `define` build global the rest of the CLI uses; 'dev' in local/unbundled\n// runs.\ndeclare const __CLI_VERSION__: string;\nconst agtCliVersion = typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : 'dev';\n\n// ENG-6412: the authoritative host config_hash is computed API-side (it includes\n// server-resolved flags), returned on each /host/heartbeat, and cached here so\n// every subsequent authenticated request carries X-Config-Hash. That lets the\n// API stamp audit_log.config_hash for host-runtime actions — the same rail as\n// X-Agt-Cli-Version above. Null until the first heartbeat returns one (an older\n// API omits it ⇒ header absent ⇒ audit config_hash NULL, null-safe).\nlet lastConfigHash: string | null = null;\nexport function setConfigHash(hash: string | null): void {\n lastConfigHash = hash && hash.length > 0 ? hash : null;\n}\n\n/** Cached exchange result for API key -> JWT. */\nlet cachedExchange: {\n token: string;\n hostId: string;\n teamId: string;\n teamSlug: string | null;\n framework: string | null;\n hostKind: string | null;\n claudeAuthMode: 'subscription' | 'api_key' | 'openrouter';\n anthropicApiKeyFingerprint: string | null;\n anthropicApiKey: string | null;\n userEmail: string | null;\n supabaseUrl: string | null;\n supabaseAnonKey: string | null;\n expiresAt: number;\n} | null = null;\n\n/** Mutex: in-flight exchange promise to prevent concurrent re-exchanges. */\nlet exchangeInFlight: Promise<ExchangeResult> | null = null;\n\nexport interface ExchangeResult {\n token: string;\n hostId: string;\n teamId: string;\n teamSlug: string | null;\n framework: string | null;\n /**\n * ENG-7587 (ADR-0042 Option-1 pt3a): the host's tenancy model ('dedicated' |\n * 'pool'), from the exchange response. null against an older API that doesn't\n * return it — callers must treat null conservatively as 'dedicated' (the\n * shared-host-key behaviour), never as pool.\n */\n hostKind: string | null;\n /**\n * Operator-configured Claude Code auth mode. 'subscription' (default) uses\n * OAuth creds from `claude /login`; 'api_key' uses anthropicApiKey below.\n */\n claudeAuthMode: 'subscription' | 'api_key' | 'openrouter';\n /**\n * First 8 hex chars of sha256(anthropicApiKey). Always returned when an\n * api_key is stored for this host — the manager uses it to detect key\n * rotation without re-decrypting every poll.\n */\n anthropicApiKeyFingerprint: string | null;\n /**\n * Decrypted Anthropic API key. Populated ONLY when claudeAuthMode=api_key\n * AND decrypt succeeded server-side. NEVER log this — the manager should\n * pass it directly to claude's env and nothing else.\n */\n anthropicApiKey: string | null;\n userEmail: string | null;\n supabaseUrl: string | null;\n supabaseAnonKey: string | null;\n}\n\n/**\n * ENG-7235: cheap synchronous read of the last-exchanged host Claude auth mode,\n * for host-side gates that must not pay a network round-trip per call (e.g. the\n * per-poll conversation-eval / memory-extraction cost gate). Returns null only\n * before the first /host/exchange of the process; the manager exchanges very\n * early (heartbeat, realtime subscriptions, spawn) so steady state is always\n * populated. claude_auth_mode is a host-level identity (every agent on the host\n * shares it), so a single cached value is authoritative for the whole fleet on\n * this host. Callers should treat null conservatively as \"mode unknown\" and not\n * change behaviour until it is known.\n */\nexport function getCachedClaudeAuthMode(): 'subscription' | 'api_key' | 'openrouter' | null {\n return cachedExchange?.claudeAuthMode ?? null;\n}\n\n/**\n * Invalidate the cached exchange JWT so the next call re-exchanges.\n */\nexport function invalidateExchange(): void {\n cachedExchange = null;\n // ENG-6412: the cached config_hash is bound to the host/auth context the last\n // heartbeat ran under. Drop it when the exchange is invalidated (token expiry,\n // key rotation, host re-exchange) so a stale hash can't ride a new auth\n // context until the next heartbeat returns a fresh one.\n lastConfigHash = null;\n}\n\n/**\n * Exchange a `tlk_` API key for a short-lived JWT via the Hono API.\n * Concurrent callers share a single in-flight request to avoid races.\n *\n * `forceRefresh: true` bypasses the JWT cache — callers that need to detect\n * server-side state changes (e.g. claude_auth_mode rotation, ENG-4417) must\n * pass this, otherwise they'll read stale values for up to ~50 minutes.\n * Concurrent in-flight requests are still coalesced either way.\n */\nexport async function exchangeApiKey(\n rawKey: string,\n retried = false,\n opts: { forceRefresh?: boolean } = {},\n): Promise<ExchangeResult> {\n // Return cached result if still valid (with 60s buffer) and caller didn't\n // explicitly request a refresh.\n if (!opts.forceRefresh && cachedExchange && Date.now() < cachedExchange.expiresAt - 60_000) {\n return {\n token: cachedExchange.token,\n hostId: cachedExchange.hostId,\n teamId: cachedExchange.teamId,\n teamSlug: cachedExchange.teamSlug,\n framework: cachedExchange.framework,\n hostKind: cachedExchange.hostKind,\n claudeAuthMode: cachedExchange.claudeAuthMode,\n anthropicApiKeyFingerprint: cachedExchange.anthropicApiKeyFingerprint,\n anthropicApiKey: cachedExchange.anthropicApiKey,\n userEmail: cachedExchange.userEmail,\n supabaseUrl: cachedExchange.supabaseUrl,\n supabaseAnonKey: cachedExchange.supabaseAnonKey,\n };\n }\n\n // Coalesce concurrent exchange calls into a single request — covers both\n // the natural-expiry refresh and the forceRefresh path.\n if (exchangeInFlight) {\n return exchangeInFlight;\n }\n\n exchangeInFlight = doExchange(rawKey, retried);\n try {\n return await exchangeInFlight;\n } finally {\n exchangeInFlight = null;\n }\n}\n\n// ENG-8260: a 5xx on /host/exchange is \"the API is unwell\", not \"this key is\n// invalid\" — but the manager's persistent-session path treated any thrown error\n// as an auth verdict and tore down running sessions. Retry transient failures\n// here so a short blip never reaches a session-affecting decision at all.\n//\n// Bounded deliberately: the manager polls on a ~30s cadence and awaits this on\n// the spawn path, so the retry budget must stay well inside a tick. Defaults\n// give ~0.5s + ~1.0s of sleep across 2 retries (~1.5s worst case, 3 requests\n// total), which covers the ~2s alyve-host-1 blip that motivated the ticket\n// without risking a tick overrun. Both are tunables, not gates.\nconst DEFAULT_EXCHANGE_RETRY_ATTEMPTS = 2;\nconst DEFAULT_EXCHANGE_RETRY_BASE_MS = 500;\n\nfunction exchangeRetryAttempts(): number {\n const raw = process.env['AGT_AUTH_EXCHANGE_RETRY_ATTEMPTS'];\n const parsed = raw ? parseInt(raw, 10) : NaN;\n // Clamp to [0, 5]: 0 disables retries (restores pre-ENG-8260 timing for a\n // host that needs it), 5 caps the worst case at ~15s.\n if (Number.isFinite(parsed) && parsed >= 0) return Math.min(parsed, 5);\n return DEFAULT_EXCHANGE_RETRY_ATTEMPTS;\n}\n\nfunction exchangeRetryBaseMs(): number {\n const raw = process.env['AGT_AUTH_EXCHANGE_RETRY_BASE_MS'];\n const parsed = raw ? parseInt(raw, 10) : NaN;\n if (Number.isFinite(parsed) && parsed > 0) return Math.min(parsed, 5_000);\n return DEFAULT_EXCHANGE_RETRY_BASE_MS;\n}\n\n/**\n * Normalise anything thrown out of `attemptExchange` into an\n * `ApiKeyExchangeError`. A raw `fetch` rejection (DNS failure, socket reset,\n * TLS error) has no HTTP status and is transient by definition — the request\n * never reached the API, so it says nothing about the credential.\n */\nfunction asExchangeError(err: unknown): ApiKeyExchangeError {\n if (err instanceof ApiKeyExchangeError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new ApiKeyExchangeError(message, null, true);\n}\n\n/**\n * ENG-8260: retry wrapper around a single exchange attempt. Transient failures\n * (5xx / 429 / transport) are retried with exponential backoff; a verdict\n * (401/403/revoked/400) is rethrown on the first attempt so genuine auth\n * failures are still acted on immediately, exactly as before.\n */\nasync function doExchange(rawKey: string, retried: boolean): Promise<ExchangeResult> {\n const maxRetries = exchangeRetryAttempts();\n const baseMs = exchangeRetryBaseMs();\n let lastErr: ApiKeyExchangeError | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await attemptExchange(rawKey, retried);\n } catch (err) {\n const exchangeErr = asExchangeError(err);\n if (!exchangeErr.transient || attempt === maxRetries) throw exchangeErr;\n lastErr = exchangeErr;\n await new Promise((resolve) => setTimeout(resolve, baseMs * 2 ** attempt));\n }\n }\n\n // Unreachable — the loop either returns or throws. Kept so the function is\n // total for the type checker without an `as` cast.\n throw lastErr ?? new ApiKeyExchangeError('API key exchange failed', null, true);\n}\n\nasync function attemptExchange(rawKey: string, retried: boolean): Promise<ExchangeResult> {\n const res = await fetch(`${requireHost()}/host/exchange`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ host_key: rawKey }),\n });\n\n if (!res.ok) {\n const body = await res.json().catch(() => ({})) as Record<string, unknown>;\n const errorMsg = String(body['error'] ?? res.statusText);\n const host = requireHost();\n const obfuscated = rawKey.length > 12\n ? `${rawKey.slice(0, 8)}${'*'.repeat(rawKey.length - 12)}${rawKey.slice(-4)}`\n : rawKey.slice(0, 4) + '****';\n\n // 502/503/504 = API unreachable (proxy error, server restarting, etc.)\n // ENG-8260: message text unchanged; now carries the status + transient flag\n // so the manager can tell this apart from a credential verdict.\n if (res.status >= 502 && res.status <= 504) {\n throw new ApiKeyExchangeError(\n `API unreachable (${res.status}): ${host} — is the API server running?`,\n res.status,\n true,\n );\n }\n\n // If key was revoked, try reloading from shell profile (agt setup may have\n // written a new key that the current process hasn't picked up yet).\n if (errorMsg.includes('revoked') && !retried) {\n reloadFromShellProfile();\n const freshKey = getApiKey();\n if (freshKey && freshKey !== rawKey) {\n // ENG-8260 (CodeRabbit, PR #3900): call `attemptExchange`, NOT\n // `doExchange`. Re-entering the retry wrapper from inside one of its own\n // attempts nests the budgets — a transient failure on the fresh key\n // would return to the outer loop, which retries with the STALE key and\n // re-enters here, making the worst case (maxRetries+1)^2 requests (9 at\n // the defaults) and blowing well past the ~1.5s the spawn path can\n // afford. The outer loop owns retries for this exchange; this hop just\n // swaps the key inside the current attempt.\n return attemptExchange(freshKey, true);\n }\n }\n\n // ENG-8260: message text unchanged (the operator-facing manager.log line and\n // any log greps depend on it) — the classification rides alongside it.\n // `isTransientExchangeStatus` puts 500 and 429 on the transient side and\n // leaves 400/401/403/404 as verdicts, preserving today's fail-closed\n // behaviour for a genuinely rejected credential.\n throw new ApiKeyExchangeError(\n `API key exchange failed: ${errorMsg} (host=${host}, key=${obfuscated})`,\n res.status,\n isTransientExchangeStatus(res.status),\n );\n }\n\n const data = await res.json() as {\n token: string;\n expires_at: string;\n host_id: string;\n team_id: string;\n team_slug: string | null;\n framework?: string | null;\n host_kind?: string | null;\n claude_auth_mode?: string | null;\n anthropic_api_key_fingerprint?: string | null;\n anthropic_api_key?: string | null;\n user_email: string | null;\n supabase_url: string | null;\n supabase_anon_key: string | null;\n };\n\n if (!data.token) {\n // ENG-8260: a 200 with no token is a server-side fault, not a verdict on the\n // credential — classify transient so it is retried rather than treated as an\n // auth failure that stops a running session.\n throw new ApiKeyExchangeError('API key exchange returned no token', null, true);\n }\n\n // ENG-7152: preserve the host-level 'openrouter' mode — collapsing it to\n // 'subscription' would make the manager run the agent on the operator\n // subscription instead of OpenRouter (or, on a host with no `claude /login`,\n // skip the spawn for \"not authenticated\").\n const claudeAuthMode: 'subscription' | 'api_key' | 'openrouter' =\n data.claude_auth_mode === 'api_key'\n ? 'api_key'\n : data.claude_auth_mode === 'openrouter'\n ? 'openrouter'\n : 'subscription';\n\n cachedExchange = {\n token: data.token,\n hostId: data.host_id,\n teamId: data.team_id,\n teamSlug: data.team_slug,\n framework: data.framework ?? null,\n hostKind: data.host_kind ?? null,\n claudeAuthMode,\n anthropicApiKeyFingerprint: data.anthropic_api_key_fingerprint ?? null,\n anthropicApiKey: data.anthropic_api_key ?? null,\n userEmail: data.user_email,\n supabaseUrl: data.supabase_url,\n supabaseAnonKey: data.supabase_anon_key,\n expiresAt: new Date(data.expires_at).getTime(),\n };\n\n return {\n token: data.token,\n hostId: data.host_id,\n teamId: data.team_id,\n teamSlug: data.team_slug,\n framework: data.framework ?? null,\n hostKind: data.host_kind ?? null,\n claudeAuthMode,\n anthropicApiKeyFingerprint: data.anthropic_api_key_fingerprint ?? null,\n anthropicApiKey: data.anthropic_api_key ?? null,\n userEmail: data.user_email,\n supabaseUrl: data.supabase_url,\n supabaseAnonKey: data.supabase_anon_key,\n };\n}\n\n/**\n * ENG-7587 (ADR-0042 Option-1 pt3a): mint a PER-AGENT host key for one bound\n * agent on a POOL host, via the pt2a `POST /host/agent-key` endpoint.\n *\n * Auth is the RAW host-wide key in the body (the same primitive `/host/exchange`\n * uses) — a pool host cannot obtain a claim-less host JWT, and only a host-wide\n * key may mint (the endpoint's escalation guard). The manager's pool spawn\n * branch (pt3b) calls this and injects the returned per-agent key into the\n * agent's container as `AGT_API_KEY` instead of forwarding the shared host key,\n * so a credential reachable from tenant A's container cannot mint tenant B's\n * token. No caller yet — this is the tool pt3b wires in.\n *\n * The endpoint is gated on `host_kind='pool'` server-side, so calling it against\n * a dedicated host returns 403 (the manager only calls it when hostKind==='pool').\n * Throws on any non-2xx so the caller can fail closed rather than fall back to\n * the shared key.\n */\nexport async function mintAgentKey(rawHostKey: string, agentId: string): Promise<string> {\n const res = await fetch(`${requireHost()}/host/agent-key`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ host_key: rawHostKey, agent_id: agentId }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;\n const errorMsg = String(body['error'] ?? res.statusText);\n throw new Error(`Per-agent key mint failed (${res.status}) for agent=${agentId}: ${errorMsg}`);\n }\n const data = (await res.json()) as { agent_key?: string; agent_id?: string; host_id?: string };\n if (!data.agent_key) {\n throw new Error(`Per-agent key mint returned no key for agent=${agentId}`);\n }\n return data.agent_key;\n}\n\n/**\n * Resolve the Bearer token from AGT_API_KEY via exchange.\n */\nasync function resolveAuth(): Promise<{ token: string; hostId: string }> {\n const apiKey = getApiKey();\n if (!apiKey) {\n throw new Error('AGT_API_KEY is not set. Export it with your host API key (tlk_...)');\n }\n\n const exchange = await exchangeApiKey(apiKey);\n return { token: exchange.token, hostId: exchange.hostId };\n}\n\n/**\n * Build standard request headers for authenticated API calls.\n * Team slug auto-resolves from the exchange, with AGT_TEAM\n * or config as an override.\n */\nasync function buildHeaders(): Promise<Record<string, string>> {\n const apiKey = getApiKey();\n if (!apiKey) {\n throw new Error('AGT_API_KEY is not set. Export it with your host API key (tlk_...)');\n }\n\n const exchange = await exchangeApiKey(apiKey);\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${exchange.token}`,\n 'Content-Type': 'application/json',\n // ENG-6410: lets the API stamp audit_log.agt_cli_version for host-runtime\n // actions so bug rates can be tracked per CLI version.\n 'X-Agt-Cli-Version': agtCliVersion,\n };\n\n // ENG-6412: carry the last heartbeat-returned config_hash so the API can stamp\n // audit_log.config_hash for host-runtime actions (config-correlation join).\n if (lastConfigHash) {\n headers['X-Config-Hash'] = lastConfigHash;\n }\n\n // Explicit team override takes precedence, then exchange auto-resolve\n const team = getActiveTeam() ?? exchange.teamSlug;\n if (team) {\n headers['X-Team-Slug'] = team;\n }\n\n return headers;\n}\n\nexport class ApiError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: Record<string, unknown>,\n ) {\n super((body['error'] as string) ?? `HTTP ${status}`);\n this.name = 'ApiError';\n }\n}\n\n/**\n * Execute a fetch request with automatic retry on 401 (expired token).\n * Invalidates the cached exchange and rebuilds headers on retry.\n */\nasync function fetchWithRetry(\n path: string,\n method: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<Response> {\n const baseHeaders = await buildHeaders();\n const headers: Record<string, string> = extraHeaders\n ? { ...baseHeaders, ...extraHeaders }\n : baseHeaders;\n const url = `${requireHost()}${path}`;\n const init: RequestInit = {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n };\n\n const res = await fetch(url, init);\n\n if (res.status === 401) {\n // Token may have expired between cache check and server verification.\n // Invalidate and retry once with a fresh token.\n invalidateExchange();\n const freshBase = await buildHeaders();\n const freshHeaders: Record<string, string> = extraHeaders\n ? { ...freshBase, ...extraHeaders }\n : freshBase;\n return fetch(url, { ...init, headers: freshHeaders });\n }\n\n return res;\n}\n\nasync function handleResponse<T>(res: Response): Promise<T> {\n const body = await res.json().catch(() => ({})) as Record<string, unknown>;\n\n if (!res.ok) {\n throw new ApiError(res.status, body);\n }\n\n return body as T;\n}\n\n/**\n * Typed HTTP client for the Augmented API.\n */\nexport const api = {\n async get<T = Record<string, unknown>>(path: string): Promise<T> {\n const res = await fetchWithRetry(path, 'GET');\n return handleResponse<T>(res);\n },\n\n async post<T = Record<string, unknown>>(\n path: string,\n body?: unknown,\n /**\n * ENG-5688: optional headers merged on top of the standard\n * Authorization + Content-Type + X-Team-Slug set. Used by the\n * impersonation flow to send `X-Agent-Impersonation` on /stop.\n * Extra headers win on key collision, by design.\n */\n extraHeaders?: Record<string, string>,\n ): Promise<T> {\n const res = await fetchWithRetry(path, 'POST', body, extraHeaders);\n return handleResponse<T>(res);\n },\n\n async patch<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T> {\n const res = await fetchWithRetry(path, 'PATCH', body);\n return handleResponse<T>(res);\n },\n\n async put<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T> {\n const res = await fetchWithRetry(path, 'PUT', body);\n return handleResponse<T>(res);\n },\n\n async del<T = Record<string, unknown>>(path: string): Promise<T> {\n const res = await fetchWithRetry(path, 'DELETE');\n return handleResponse<T>(res);\n },\n};\n\n/**\n * Resolve auth and return the host ID.\n */\nexport async function getHostId(): Promise<string | null> {\n const { hostId } = await resolveAuth();\n return hostId;\n}\n","/**\n * ENG-5865 — atomic state-file writes.\n *\n * Background. `manager-state.json` is consumed by at least two external\n * readers — the synthetic-probe and `agt status` — and is written by the\n * manager on every poll completion + on dashboard restart-acks. Today the\n * write is a plain `writeFileSync(path, JSON.stringify(state))`, which on\n * POSIX filesystems means `open(O_TRUNC) → write → close`. Between truncate\n * and the synchronous write completing, any concurrent reader sees an\n * empty or torn file; `JSON.parse` throws; the synthetic-probe reports the\n * agent as down; an alarm fires for what was actually a quarter-millisecond\n * write window. We've not seen this fire frequently in production but it\n * IS the silent class of false-positive that's worst to debug after the\n * fact (\"the agent was clearly alive — why did the probe alarm?\").\n *\n * The fix is the textbook one: write to a tmp file on the same filesystem,\n * fsync the tmp file's data + the directory entry, then `rename(tmp, target)`.\n * rename(2) is atomic on POSIX (APFS, ext4, xfs all guarantee this) — a\n * reader either sees the previous version or the new one, never partial.\n *\n * Kept as a small pure module so the swap site stays a one-liner change at\n * each writeFileSync call, and so the helper is unit-testable without\n * mocking the manager.\n */\n\nimport { closeSync, fsyncSync, openSync, writeSync, renameSync, mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\n/**\n * Atomically write `data` to `path`. Creates parent directories if missing.\n *\n * Guarantees (on POSIX filesystems): a concurrent reader of `path` sees\n * either the previous content of `path` or `data` in full — never an empty\n * file, never a torn write. On Windows the rename(2) atomicity guarantee is\n * weaker but the practical behaviour is the same as POSIX for non-shared\n * files (which this is — the manager has a PID-lock).\n *\n * Best-effort fsync: a failing fsync (e.g., EIO during a transient disk\n * fault) is logged-and-continued rather than thrown, because the durability\n * guarantee is nice-to-have and the atomicity guarantee (via rename) is\n * already met by the time we reach the fsync call. Throwing here would\n * regress callers that today succeed with plain writeFileSync.\n */\nexport function atomicWriteFileSync(path: string, data: string): void {\n // Same directory as the target so rename is a within-filesystem move and\n // therefore atomic. A `.tmp.<pid>.<rand>` suffix avoids collision when\n // two processes race the same target (the loser's rename overwrites the\n // winner's content, but each individual reader still sees a consistent\n // snapshot — which is what atomicity buys us).\n const dirPath = dirname(path);\n const tmpPath = `${path}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;\n\n try { mkdirSync(dirPath, { recursive: true }); } catch { /* parent exists or unwritable */ }\n\n const fd = openSync(tmpPath, 'w', 0o644);\n try {\n writeSync(fd, data);\n // fsync persists the data so even a power loss preserves it. Skip on\n // failure — the rename below still gives atomicity for live readers.\n try { fsyncSync(fd); } catch { /* non-fatal */ }\n } finally {\n closeSync(fd);\n }\n renameSync(tmpPath, path);\n\n // CodeRabbit (PR #1631): rename(2) is atomic but the directory entry for\n // `path` is not necessarily durable across crash/power loss until the\n // PARENT directory is fsync'd — fsyncSync on the file fd alone covers\n // the data, not the rename. Open the dir RDONLY and fsync. Best-effort:\n // Windows + some macOS fs setups don't support directory fsync; we'd\n // rather degrade silently than refuse to write.\n try {\n const dirFd = openSync(dirPath, 'r');\n try { fsyncSync(dirFd); } finally { closeSync(dirFd); }\n } catch { /* non-fatal */ }\n}\n","// Host-side feature-flag consumption (ENG-6251, ADR-0022 slice 3).\n//\n// The customer-host trust boundary (ADR-0022) keeps the vendor OpenFeature SDK\n// and any targeting rules OFF the host — the API evaluates flags centrally and\n// the heartbeat delivers a pre-resolved value map. This module turns that map\n// into a typed accessor with operator-override precedence:\n//\n// env var (operator override) -> heartbeat value -> compiled-in default\n//\n// Values are re-read every poll, so a DB flip reaches a running manager within\n// one heartbeat cycle (~5m) with no `systemctl restart`. The last-known-good\n// map is cached to `~/.augmented/flags-cache.json`; on API unreachability the\n// cache is used, then the per-flag declared safe default (the registry default)\n// as the floor. Unknown keys in the map are stored verbatim and ignored at\n// resolution time, so registry skew between a newer API and an older CLI never\n// drops or throws.\n//\n// tsup bundles `@augmented/core` inline, so the imports below resolve to the\n// compiled registry shipped in the CLI binary — core changes need both a core\n// and a CLI rebuild to reach hosts.\n\nimport { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport {\n coerceEnvValue,\n getFlagDefinition,\n listFlagDefinitions,\n normalizeFlagValue,\n type FlagDefinition,\n type FlagValue,\n} from '@augmented/core';\nimport { atomicWriteFileSync } from './atomic-write.js';\n\n/** Which layer a resolved value came from (for `agt flags resolve` + logs). */\nexport type FlagSource = 'env' | 'heartbeat-cache' | 'default';\n\nexport interface ResolvedFlag {\n key: string;\n value: FlagValue;\n source: FlagSource;\n /** The legacy env var that can override this flag, if any. */\n envVar?: string;\n /**\n * True when an env override is in force AND a differing heartbeat value is\n * present — i.e. the operator override is masking a central flip. Surfaced\n * so `agt flags resolve` and the boot logs make a \"DB change that isn't\n * taking effect\" diagnosable (ADR-0022 §5).\n */\n envMasksHeartbeat?: boolean;\n}\n\n/** On-disk shape of `~/.augmented/flags-cache.json`. */\nexport interface FlagsCacheFile {\n /** `flags_schema_version` reported by the API on the cached heartbeat. */\n schema_version: string;\n /** ISO timestamp the cache was last written. */\n updated_at: string;\n /**\n * The heartbeat map verbatim — full map, including keys this CLI doesn't\n * recognise. Never pruned, so an older CLI round-trips a newer API's flags.\n */\n flags: Record<string, FlagValue>;\n /**\n * ENG-7682 follow-up: the last-known-good per-agent notify-dispatch map,\n * keyed by agent_id, persisted alongside the host-wide `flags` map so a\n * manager restart hydrates BOTH from the same snapshot in init(). Without\n * this, init() would restore the host-wide value but reset the per-agent map\n * to {}, and an agent session spawned before the next new-schema heartbeat\n * would receive the host-wide value instead of its last-known per-agent one.\n * Optional so a cache written by an older CLI (no field) reads back as {}.\n */\n notify_dispatch_by_agent?: Record<string, FlagValue>;\n}\n\nexport function defaultFlagsCachePath(configDir: string): string {\n return join(configDir, 'flags-cache.json');\n}\n\n/**\n * Read the flags cache tolerantly: a missing file, malformed JSON, or a\n * non-object payload all collapse to `null` (caller falls back to compiled\n * defaults). Never throws.\n */\nexport function readFlagsCache(path: string): FlagsCacheFile | null {\n try {\n if (!existsSync(path)) return null;\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n const obj = parsed as Record<string, unknown>;\n const flags = obj['flags'];\n if (!flags || typeof flags !== 'object') return null;\n // ENG-7682 follow-up: restore the per-agent notify-dispatch map tolerantly.\n // A missing field (older CLI wrote the cache) or a non-object payload reads\n // back as {} so init() hydrates an empty map rather than throwing.\n const perAgent = obj['notify_dispatch_by_agent'];\n return {\n schema_version: typeof obj['schema_version'] === 'string' ? obj['schema_version'] : '',\n updated_at: typeof obj['updated_at'] === 'string' ? obj['updated_at'] : '',\n flags: { ...(flags as Record<string, FlagValue>) },\n notify_dispatch_by_agent:\n perAgent && typeof perAgent === 'object'\n ? { ...(perAgent as Record<string, FlagValue>) }\n : {},\n };\n } catch {\n return null;\n }\n}\n\nexport function writeFlagsCache(path: string, file: FlagsCacheFile): void {\n atomicWriteFileSync(path, `${JSON.stringify(file, null, 2)}\\n`);\n}\n\n/**\n * Age of a cache file in seconds, preferring its recorded `updated_at` and\n * falling back to the file mtime. `null` when neither is available — caller\n * renders \"unknown\" rather than a misleading 0.\n */\nexport function flagsCacheAgeSeconds(\n cache: FlagsCacheFile,\n path: string,\n now: Date = new Date(),\n): number | null {\n const fromRecorded = Date.parse(cache.updated_at);\n if (!Number.isNaN(fromRecorded)) {\n return Math.max(0, (now.getTime() - fromRecorded) / 1000);\n }\n try {\n return Math.max(0, (now.getTime() - statSync(path).mtimeMs) / 1000);\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve one flag across the layers. Pure — every input is explicit so the\n * `agt flags resolve` CLI (a separate process from the manager) and the manager\n * itself share identical precedence.\n */\nexport function resolveFlagFromLayers(\n definition: FlagDefinition,\n heartbeatFlags: Record<string, FlagValue> | undefined,\n env: NodeJS.ProcessEnv,\n): ResolvedFlag {\n const envValue = definition.envVar\n ? coerceEnvValue(definition, env[definition.envVar])\n : undefined;\n const hbValue = heartbeatFlags\n ? normalizeFlagValue(definition, heartbeatFlags[definition.key])\n : undefined;\n\n if (envValue !== undefined) {\n return {\n key: definition.key,\n value: envValue,\n source: 'env',\n envVar: definition.envVar,\n envMasksHeartbeat: hbValue !== undefined && hbValue !== envValue,\n };\n }\n if (hbValue !== undefined) {\n return { key: definition.key, value: hbValue, source: 'heartbeat-cache' };\n }\n return { key: definition.key, value: definition.defaultValue, source: 'default' };\n}\n\n/** Resolve every registered flag against the given heartbeat map + env. */\nexport function resolveAllFlags(\n heartbeatFlags: Record<string, FlagValue> | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): ResolvedFlag[] {\n return listFlagDefinitions().map((definition) =>\n resolveFlagFromLayers(definition, heartbeatFlags, env),\n );\n}\n\ntype LogFn = (msg: string) => void;\n\n/**\n * The manager's live flag store. Holds the last heartbeat map in memory, mirrors\n * it to disk for offline/boot use, logs every value transition, and warns when\n * an env override masks a differing heartbeat value.\n */\nexport class HostFlagStore {\n private heartbeatFlags: Record<string, FlagValue> = {};\n /**\n * ENG-7682 follow-up: per-agent notify-dispatch values from the heartbeat,\n * keyed by agent_id. Only agents whose resolved value DIFFERS from the\n * host-wide value appear here; everyone else falls back to the host-wide\n * `notify-dispatch` entry. This is what stops one agent's opt-in from fanning\n * out to every agent the host runs. Held in memory only (re-sent every\n * heartbeat), not persisted to the flags cache, which is a host-wide map.\n */\n private notifyDispatchByAgent: Record<string, FlagValue> = {};\n /**\n * ENG-7891 / ADR-0049 slice 3: per-agent boolean overrides from the heartbeat,\n * keyed `agent_id -> flag_key -> bool`. This is the generic per-agent seam\n * `getBooleanForAgent` reads so a boolean gate (today only\n * `id-keyed-layout-migration`) can be armed one agent at a time. Held in\n * memory only and re-sent every heartbeat - deliberately NOT persisted to the\n * flags cache (which is a host-wide map), so a restart cleanly falls back to\n * the host-wide value until the next heartbeat rather than pinning a stale\n * per-agent opt-in. Empty until the API sends `feature_flags_by_agent` (the\n * flag ships dark, so it stays empty and every agent resolves host-wide).\n */\n private booleanFlagsByAgent: Record<string, Record<string, boolean>> = {};\n /**\n * The `flags_schema_version` from the most recent heartbeat, retained so a\n * per-agent-map write (which carries no schema version of its own) persists\n * the same schema string the host-wide map was last written with rather than\n * clobbering it to empty. Hydrated from the cache on init().\n */\n private lastSchemaVersion = '';\n private readonly cachePath: string;\n private readonly log: LogFn;\n private readonly env: NodeJS.ProcessEnv;\n private initialised = false;\n /**\n * Per-flag record of the heartbeat value we last warned about being masked by\n * an env override. Used to log the env-masking WARN only on *transition* (when\n * masking newly begins or the masked-over heartbeat value changes) rather than\n * on every heartbeat — a stable, intentional operator override (e.g. a canary\n * pin in `agt-manager.env`) otherwise re-logs the same WARN every ~90s forever\n * (ENG-6478). A key is deleted when masking stops, so a later re-mask warns again.\n */\n private readonly lastMaskWarn = new Map<string, FlagValue>();\n\n constructor(opts: { cachePath: string; log?: LogFn; env?: NodeJS.ProcessEnv }) {\n this.cachePath = opts.cachePath;\n this.log = opts.log ?? (() => {});\n this.env = opts.env ?? process.env;\n }\n\n /**\n * Boot-time hydrate from the last-known-good cache. Cached values are used\n * only until the first successful heartbeat; the cache age is logged so a\n * stale `flags-cache.json` is visible (ADR-0022 §5 restart-with-stale-cache).\n */\n init(now: Date = new Date()): void {\n if (this.initialised) return;\n this.initialised = true;\n const cached = readFlagsCache(this.cachePath);\n if (!cached) {\n this.log('[flags] no last-known-good cache on disk; using compiled defaults until first heartbeat');\n return;\n }\n this.heartbeatFlags = { ...cached.flags };\n // ENG-7682 follow-up: hydrate the per-agent notify-dispatch map from the\n // SAME snapshot atomically, so a restart restores each agent's last-known\n // per-agent value rather than dropping it to the host-wide one until the\n // next new-schema heartbeat.\n this.notifyDispatchByAgent = { ...(cached.notify_dispatch_by_agent ?? {}) };\n this.lastSchemaVersion = cached.schema_version || '';\n const ageSeconds = flagsCacheAgeSeconds(cached, this.cachePath, now);\n const age = ageSeconds === null ? 'unknown' : `${Math.round(ageSeconds)}s`;\n const schema = cached.schema_version || 'unknown';\n this.log(\n `[flags] loaded last-known-good cache (age: ${age}, schema: ${schema}); using cached values until first heartbeat`,\n );\n }\n\n /**\n * Merge a heartbeat flag map: log per-flag transitions and env-masking warns,\n * replace the in-memory map (full map; unknown keys kept), and persist it.\n * A missing/non-object map is a no-op so an older API that omits the field\n * doesn't blow away a good cache.\n */\n applyHeartbeat(map: Record<string, FlagValue> | undefined, schemaVersion?: string): void {\n if (!map || typeof map !== 'object') return;\n const previous = this.heartbeatFlags;\n\n for (const definition of listFlagDefinitions()) {\n const before = normalizeFlagValue(definition, previous[definition.key]);\n const after = normalizeFlagValue(definition, map[definition.key]);\n if (after !== undefined && after !== before) {\n this.log(\n `[flags] ${definition.key}: ${before ?? '(default)'} -> ${after} (source: heartbeat)`,\n );\n }\n if (definition.envVar) {\n const envValue = coerceEnvValue(definition, this.env[definition.envVar]);\n const masking = envValue !== undefined && after !== undefined && envValue !== after;\n if (masking) {\n // Log only on transition: when masking newly begins, or when the\n // heartbeat value being masked changes. A stable override would\n // otherwise spam this WARN every heartbeat (ENG-6478).\n if (this.lastMaskWarn.get(definition.key) !== after) {\n this.log(\n `[flags] WARN env override ${definition.envVar}=${envValue} is masking heartbeat value ${after} for '${definition.key}' (env wins until unset)`,\n );\n this.lastMaskWarn.set(definition.key, after);\n }\n } else {\n // Masking stopped (override cleared, or heartbeat now agrees) — re-arm\n // so a future re-mask logs once more.\n this.lastMaskWarn.delete(definition.key);\n }\n }\n }\n\n this.heartbeatFlags = { ...map };\n this.lastSchemaVersion = schemaVersion ?? '';\n this.persistCache();\n }\n\n /**\n * Persist the host-wide flag map AND the per-agent notify-dispatch map to the\n * last-known-good cache in a single atomic write, so a restart hydrates both\n * from the same snapshot (ENG-7682 follow-up). Called by every mutator that\n * changes either map. A write failure is logged and swallowed: the in-memory\n * state stays authoritative and the next successful heartbeat re-persists.\n */\n private persistCache(): void {\n try {\n writeFlagsCache(this.cachePath, {\n schema_version: this.lastSchemaVersion,\n updated_at: new Date().toISOString(),\n flags: this.heartbeatFlags,\n notify_dispatch_by_agent: this.notifyDispatchByAgent,\n });\n } catch (err) {\n this.log(`[flags] cache write failed: ${(err as Error).message}`);\n }\n }\n\n /** Resolve one flag, or `undefined` if the key isn't registered. */\n resolve(key: string): ResolvedFlag | undefined {\n const definition = getFlagDefinition(key);\n if (!definition) return undefined;\n return resolveFlagFromLayers(definition, this.heartbeatFlags, this.env);\n }\n\n resolveAll(): ResolvedFlag[] {\n return resolveAllFlags(this.heartbeatFlags, this.env);\n }\n\n /**\n * Typed boolean accessor. Falls back to the registry default for the key\n * (and to `false` for an unknown/non-boolean key) so a caller can read a\n * gate without null-checking.\n */\n getBoolean(key: string): boolean {\n const definition = getFlagDefinition(key);\n const resolved = this.resolve(key);\n if (resolved && typeof resolved.value === 'boolean') return resolved.value;\n return definition?.flagType === 'boolean' ? definition.defaultValue : false;\n }\n\n /**\n * ENG-7891 / ADR-0049 slice 3: resolve a BOOLEAN flag FOR A SPECIFIC AGENT.\n * Precedence mirrors {@link getStringForAgent}:\n *\n * env override -> per-agent heartbeat value -> host-wide value (getBoolean)\n *\n * The env override (if the flag declares one) stays highest precedence and\n * host-wide; otherwise a per-agent heartbeat override beats the host-wide\n * value, and an agent with no per-agent entry inherits the host-wide value.\n * A non-boolean/unknown key delegates to getBoolean. This lets a one-way-door\n * gate be armed one agent at a time (id-keyed-layout-migration) without an\n * opt-in fanning out to every agent on the host.\n */\n getBooleanForAgent(key: string, agentId: string | undefined): boolean {\n const definition = getFlagDefinition(key);\n if (!definition || definition.flagType !== 'boolean') return this.getBoolean(key);\n const envValue = definition.envVar\n ? coerceEnvValue(definition, this.env[definition.envVar])\n : undefined;\n if (typeof envValue === 'boolean') return envValue;\n if (agentId) {\n const perAgent = this.booleanFlagsByAgent[agentId]?.[key];\n if (typeof perAgent === 'boolean') return perAgent;\n }\n return this.getBoolean(key);\n }\n\n /**\n * Typed string accessor for enum flags. Falls back to the registry default\n * (and to `''` for an unknown/non-enum key).\n */\n getString(key: string): string {\n const definition = getFlagDefinition(key);\n const resolved = this.resolve(key);\n if (resolved && typeof resolved.value === 'string') return resolved.value;\n return definition?.flagType === 'enum' ? definition.defaultValue : '';\n }\n\n /**\n * ENG-7682 follow-up: merge the heartbeat's per-agent notify-dispatch map.\n * A missing/non-object map REPLACES with an empty map so a clear (agent\n * override removed → API stops sending the entry) takes effect within one\n * poll rather than pinning a stale opt-in. Values that aren't valid\n * notify-dispatch members are dropped (registry skew / malformed payload).\n */\n applyNotifyDispatchByAgent(map: Record<string, FlagValue> | undefined): void {\n const definition = getFlagDefinition('notify-dispatch');\n if (!map || typeof map !== 'object' || !definition) {\n this.notifyDispatchByAgent = {};\n // Persist the cleared map so a restart doesn't rehydrate a stale opt-in\n // from a previous snapshot (ENG-7682 follow-up).\n this.persistCache();\n return;\n }\n const next: Record<string, FlagValue> = {};\n for (const [agentId, raw] of Object.entries(map)) {\n const value = normalizeFlagValue(definition, raw);\n if (value !== undefined) next[agentId] = value;\n }\n this.notifyDispatchByAgent = next;\n // Persist alongside the host-wide map as last-known-good so a manager\n // restart restores each agent's per-agent value (ENG-7682 follow-up).\n this.persistCache();\n }\n\n /**\n * ENG-7891 / ADR-0049 slice 3: merge the heartbeat's per-agent BOOLEAN\n * override map (`agent_id -> flag_key -> bool`), read by getBooleanForAgent.\n * A missing/non-object map REPLACES with an empty map so a cleared per-agent\n * override takes effect within one poll rather than pinning a stale opt-in.\n * Only registered boolean keys with an actual boolean value are kept (registry\n * skew / malformed payloads are dropped). In-memory only - deliberately not\n * persisted (unlike notify-dispatch), so a restart falls back to the host-wide\n * value until the next heartbeat rather than rehydrating a stale opt-in for a\n * one-way-door migration.\n */\n applyBooleanFlagsByAgent(map: Record<string, Record<string, FlagValue>> | undefined): void {\n if (!map || typeof map !== 'object') {\n this.booleanFlagsByAgent = {};\n return;\n }\n const next: Record<string, Record<string, boolean>> = {};\n for (const [agentId, perAgent] of Object.entries(map)) {\n if (!perAgent || typeof perAgent !== 'object') continue;\n for (const [key, raw] of Object.entries(perAgent)) {\n const definition = getFlagDefinition(key);\n if (!definition || definition.flagType !== 'boolean') continue;\n if (typeof raw !== 'boolean') continue;\n (next[agentId] ??= {})[key] = raw;\n }\n }\n this.booleanFlagsByAgent = next;\n }\n\n /**\n * ENG-7682 follow-up: resolve an enum flag FOR A SPECIFIC AGENT. Precedence:\n *\n * env override -> per-agent heartbeat value -> host-wide value (getString)\n *\n * The env override still wins (AGT_NOTIFY_DISPATCH stays the per-process\n * operator escape hatch); otherwise a per-agent heartbeat value beats the\n * host-wide one, and an agent with no per-agent entry inherits the host-wide\n * value unchanged. Only notify-dispatch is threaded per-agent today; other\n * enum flags stay host-wide, so a non-notify key just delegates to getString.\n */\n getStringForAgent(key: string, agentId: string | undefined): string {\n const definition = getFlagDefinition(key);\n if (!definition || definition.flagType !== 'enum') return this.getString(key);\n // Env override is highest precedence and is host-wide (per-process); if it\n // is set it masks any per-agent heartbeat value, same as the host-wide path.\n const envValue = definition.envVar\n ? coerceEnvValue(definition, this.env[definition.envVar])\n : undefined;\n if (envValue !== undefined && typeof envValue === 'string') return envValue;\n // Only notify-dispatch carries a per-agent heartbeat map today; any other\n // enum flag resolves host-wide (falls through to getString below).\n if (agentId && key === 'notify-dispatch') {\n const perAgent = this.notifyDispatchByAgent[agentId];\n if (typeof perAgent === 'string') {\n const normalized = normalizeFlagValue(definition, perAgent);\n if (typeof normalized === 'string') return normalized;\n }\n }\n return this.getString(key);\n }\n}\n","// ENG-4832: reap MCP child processes whose env references a credential\n// that's just been rotated, so the next call respawns them with the\n// fresh value.\n//\n// Why this exists: the manager polls /host/refresh, gets new OAuth\n// tokens, writes them into the agent's `.env.integrations`. But the\n// MCP children (xero-mcp-server, gmail-mcp, etc.) were spawned by\n// Claude Code at session start and hold their **spawn-time** env. They\n// never re-read .env.integrations, so once the access_token in their\n// env block expires (typically 30 min for Xero), every upstream call\n// 401s — and the agent reports \"refresh failed\" because its view of\n// the token is stale, regardless of what the DB row says. See\n// ENG-4832 for the full triage (Stirling, Day 3).\n//\n// The fix: when integration credentials rotate, identify which MCP\n// children reference the rotated env var(s) AND belong to this agent,\n// SIGTERM them. Claude Code's MCP transport detects the dead child\n// and respawns it on next request — fresh env, fresh token, agent\n// keeps working. Conversation context is preserved.\n//\n// Companion to orphan-channel-mcp-reaper.ts (ENG-4808). Same shape\n// (parse ps, walk ppid chain, kill with grace) but a different\n// targeting rule: that one finds children whose parent claude is\n// dead; this one finds children whose env is stale.\n//\n// Pure helpers exposed for tests:\n// • parseEnvIntegrationsVars() — extract var names from a\n// .env.integrations file\n// • findMcpServersUsingVars() — given the parsed .mcp.json and\n// a set of changed var names, which server keys are affected?\n// • findMcpChildrenForAgent() — walk ps rows, return PIDs\n// belonging to <codeName>'s claude AND matching one of the\n// server-key argv signatures\n//\n// Side-effecting:\n// • reapStaleMcpChildren() — orchestrates the above + SIGTERM\n// / grace / SIGKILL. Idempotent — calling repeatedly is safe.\n\nimport { execFileSync } from 'node:child_process';\nimport { parsePsRows, type PsRow } from './orphan-channel-mcp-reaper.js';\n\n// ENG-5344: rotation grace window — when reapStaleMcpChildren SIGTERMs\n// an MCP child, Claude Code's MCP transport only respawns on the next\n// tool call. There is therefore a window (≥ SIGTERM-grace + respawn\n// latency) where the child is dead and presence-reaper would see zero\n// live children for the affected server keys, even though nothing is\n// actually broken. Without coordination, the next manager tick after a\n// rotation kills the whole tmux session — observed on Stirling at ~20\n// min intervals, 54 false-positive restarts/day.\n//\n// We record (codeName, serverKey) → SIGTERM timestamp here and let\n// presence-reaper consult it to skip keys that are still in their\n// post-rotation respawn window.\nconst rotationTimestamps = new Map<string, number>();\n\n/**\n * Default rotation grace window. Sized to cover:\n * - the SIGTERM → SIGKILL grace in reapStaleMcpChildren (5s default)\n * - Claude Code's lazy respawn latency on next tool call\n * - a healthy margin so a slow respawn doesn't trip presence-reaper\n *\n * 15s is comfortably above the observed ~2s race window between\n * stale-reaper and the next manager tick.\n */\nexport const DEFAULT_ROTATION_GRACE_MS = 15_000;\n\nfunction rotationKey(codeName: string, serverKey: string): string {\n return `${codeName}\\x00${serverKey}`;\n}\n\n/**\n * Record that we just SIGTERM'd one or more MCP children for these\n * server keys belonging to <codeName>. Callers should invoke this after\n * SIGTERM (not before): no kill = no in-flight rotation to grace.\n *\n * Idempotent — the latest call wins so back-to-back rotations extend\n * the grace window rather than expire it early.\n */\nexport function recordStaleRotation(\n codeName: string,\n serverKeys: Iterable<string>,\n now: () => number = Date.now,\n): void {\n const ts = now();\n for (const key of serverKeys) {\n rotationTimestamps.set(rotationKey(codeName, key), ts);\n }\n}\n\n/**\n * Was <codeName>:<serverKey> rotated within the last `withinMs`?\n * Returns false if no rotation has been recorded for the pair.\n *\n * Self-cleaning: expired entries are deleted on read so the map stays\n * bounded under steady-state load.\n */\nexport function wasRecentlyRotated(\n codeName: string,\n serverKey: string,\n withinMs: number,\n now: () => number = Date.now,\n): boolean {\n const k = rotationKey(codeName, serverKey);\n const ts = rotationTimestamps.get(k);\n if (ts === undefined) return false;\n if (now() - ts < withinMs) return true;\n rotationTimestamps.delete(k);\n return false;\n}\n\n/**\n * ENG-8225: how a (codeName, serverKey) pair stands relative to its last\n * recorded rotation.\n *\n * 'none' — no rotation on record (or the record aged past retention).\n * 'within-grace' — rotated less than `graceMs` ago; a respawn is legitimately\n * in flight and \"no live children\" is not a fault.\n * 'expired' — rotated longer than `graceMs` ago. Combined with \"still no\n * live children\" this is the ENG-8225 hole: the grace window\n * banked on a tool call that never came.\n */\nexport type RotationStatus = 'none' | 'within-grace' | 'expired';\n\n/**\n * ENG-8225: how long a rotation record is retained AFTER its grace window\n * expires, so a caller can tell \"the rotation-driven respawn never happened\"\n * (→ 'expired') from \"this key was never rotated\" (→ 'none').\n *\n * {@link wasRecentlyRotated} deletes on an expired read, which collapses those\n * two cases into one `false` and is exactly why the expiry was unobservable\n * before this ticket. Retention has to outlast the whole prompt-probe +\n * escalation cycle in mcp-presence-reaper (3 probes spaced 30s, then escalate)\n * with generous headroom, while staying bounded so the map can't grow without\n * limit on a host that rotates credentials all day.\n */\nexport const ROTATION_TRACKING_RETENTION_MS = 15 * 60 * 1000;\n\n/**\n * ENG-8225: non-destructive tri-state read of the rotation tracker.\n *\n * Unlike {@link wasRecentlyRotated} this does NOT delete on grace expiry — the\n * record survives until `retentionMs`, which is what lets mcp-presence-reaper\n * distinguish \"rotated, grace expired, child still absent\" (arm a prompt probe)\n * from \"plain missing\" (restart as before). Past `retentionMs` the entry is\n * deleted and 'none' is returned, keeping the map bounded.\n */\nexport function classifyRotation(\n codeName: string,\n serverKey: string,\n graceMs: number,\n now: () => number = Date.now,\n retentionMs: number = ROTATION_TRACKING_RETENTION_MS,\n): RotationStatus {\n const k = rotationKey(codeName, serverKey);\n const ts = rotationTimestamps.get(k);\n if (ts === undefined) return 'none';\n const age = now() - ts;\n // Grace wins even if a caller passes a graceMs above the retention window —\n // an in-flight respawn must never be reported as a stalled one.\n if (age < graceMs) return 'within-grace';\n if (age < retentionMs) return 'expired';\n rotationTimestamps.delete(k);\n return 'none';\n}\n\n/**\n * ENG-8225: the SIGTERM timestamp of the rotation currently on record for\n * (codeName, serverKey), or `undefined` when there is none.\n *\n * This timestamp IS the rotation's identity, not merely a property of it: both\n * the grace window and the retention window in {@link classifyRotation} are\n * measured from it, and {@link recordStaleRotation} overwrites it on every new\n * SIGTERM. So a caller that keeps per-rotation state alongside the tracker (the\n * prompt-probe budget in mcp-presence-reaper) can compare its stored stamp\n * against this value to tell \"my state belongs to the rotation I'm looking at\"\n * from \"my state is a leftover describing a rotation that is over\". Without\n * that, key-keyed state outlives the rotation that created it and silently\n * governs the next, unrelated one.\n *\n * Non-destructive: reading never evicts (contrast {@link wasRecentlyRotated}),\n * so the identity is stable across the retention window. Read\n * {@link classifyRotation} for the STATUS; use this only for identity.\n *\n * Note two rotations recorded in the same millisecond are indistinguishable\n * here. That is deliberate and matches `recordStaleRotation`'s documented\n * \"latest call wins\" semantics — same-millisecond kills share one grace window,\n * so they are one rotation as far as every other consumer is concerned too.\n */\nexport function getRotationRecordedAt(codeName: string, serverKey: string): number | undefined {\n return rotationTimestamps.get(rotationKey(codeName, serverKey));\n}\n\n/**\n * ENG-8225: forget the rotation record(s) for <codeName>. Called once the\n * rotated child is observed LIVE again — the rotation is complete, so a later\n * disappearance is a fresh fault and must not be attributed to this rotation.\n */\nexport function clearStaleRotation(codeName: string, serverKeys: Iterable<string>): void {\n for (const key of serverKeys) {\n rotationTimestamps.delete(rotationKey(codeName, key));\n }\n}\n\n/**\n * Test-only seam: clear the rotation tracker between tests so a recorded\n * rotation in one case doesn't bleed into another.\n */\nexport function __resetStaleRotationTrackerForTests(): void {\n rotationTimestamps.clear();\n}\n\n/**\n * Parse a `.env.integrations` file body into a Map of var name → raw\n * value (the value is captured verbatim, including any shell-quote\n * wrapping the writer added). Pure — operate on file contents, not\n * paths. Robust to:\n * - leading/trailing whitespace\n * - shell-quote forms (`X='value'`, `X=\"value\"`, `X=raw`)\n * - comments (lines starting with `#`)\n * - blank lines\n * - duplicates (later wins, matching shell-source semantics)\n *\n * Values are NOT dequoted on purpose — for the diff use case we just\n * need byte-for-byte equality between two writes of the same value,\n * and writeIntegrations is deterministic (always emits the same\n * shellQuote form for the same input). Skipping dequoting also keeps\n * this immune to quote-handling bugs.\n */\nexport function parseEnvIntegrationsEntries(content: string): Map<string, string> {\n const entries = new Map<string, string>();\n for (const raw of content.split(/\\r?\\n/)) {\n const line = raw.trim();\n if (!line || line.startsWith('#')) continue;\n const eq = line.indexOf('=');\n if (eq <= 0) continue;\n const name = line.slice(0, eq).trim();\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;\n const value = line.slice(eq + 1);\n entries.set(name, value);\n }\n return entries;\n}\n\n/**\n * Just the SET of var names declared in a `.env.integrations` body.\n * Convenience wrapper over parseEnvIntegrationsEntries — preserves\n * the original ENG-4832 API for callers that only need names.\n */\nexport function parseEnvIntegrationsVars(content: string): string[] {\n return [...parseEnvIntegrationsEntries(content).keys()];\n}\n\n/**\n * Diff two `.env.integrations` parses. Returns the names whose value\n * changed (rotated), was added, or was removed between the two\n * snapshots.\n *\n * Used by the manager-worker integration-rotation hook to pass the\n * **precisely-rotated** subset to findMcpServersUsingVars rather than\n * \"every var currently present in the file\", which would over-reap\n * unrelated MCP children on every refresh tick (CodeRabbit on PR #797).\n */\nexport function diffEnvIntegrations(\n oldContent: string | undefined,\n newContent: string,\n): string[] {\n const oldEntries = oldContent === undefined ? new Map<string, string>() : parseEnvIntegrationsEntries(oldContent);\n const newEntries = parseEnvIntegrationsEntries(newContent);\n const changed = new Set<string>();\n for (const [name, value] of newEntries) {\n if (oldEntries.get(name) !== value) changed.add(name);\n }\n // Removed vars also count — a server whose env was depending on a\n // now-absent var has effectively been rotated to \"no value\", which\n // is just as breaking as a value change.\n for (const name of oldEntries.keys()) {\n if (!newEntries.has(name)) changed.add(name);\n }\n return [...changed];\n}\n\n/**\n * A `.env.integrations` diff split by KIND of change. `diffEnvIntegrations`\n * flattens all three into one \"what changed\" list (all the reap path needs);\n * this classifier keeps them apart for the one caller that must tell a benign\n * value-only re-mint from a real membership change.\n */\nexport interface EnvIntegrationsDiff {\n /** Present in BOTH snapshots, value changed — a server-side rotation / re-mint. */\n rotated: string[];\n /** Present in new but not old — a credential / integration added. */\n added: string[];\n /** Present in old but not new — a credential / integration removed. */\n removed: string[];\n}\n\n/**\n * ENG-7541: classify an `.env.integrations` diff into value-only ROTATIONS\n * (present before and after, only the value changed — the GitHub App token\n * re-mint case) versus membership ADDs / REMOVEs. The manager uses this to tell\n * a benign per-poll credential re-mint (which must never pause the agent — it is\n * tagged with the non-tripping `credential-rotation` breaker reason) apart from\n * a real integration add/remove (which wants an MCP rebind and counts toward the\n * provisioning breaker as `hot-reload-mcp`).\n *\n * Semantics are the source of truth for {@link diffEnvIntegrations}: the flat\n * \"changed\" set that function returns is exactly `rotated ∪ added ∪ removed`.\n *\n * Pure and side-effect-free so the classification is unit-testable in isolation.\n */\nexport function classifyEnvIntegrationsDiff(\n oldContent: string | undefined,\n newContent: string,\n): EnvIntegrationsDiff {\n const oldEntries = oldContent === undefined ? new Map<string, string>() : parseEnvIntegrationsEntries(oldContent);\n const newEntries = parseEnvIntegrationsEntries(newContent);\n const rotated: string[] = [];\n const added: string[] = [];\n const removed: string[] = [];\n for (const [name, value] of newEntries) {\n if (!oldEntries.has(name)) added.push(name);\n else if (oldEntries.get(name) !== value) rotated.push(name);\n }\n for (const name of oldEntries.keys()) {\n if (!newEntries.has(name)) removed.push(name);\n }\n return { rotated, added, removed };\n}\n\n/**\n * Pure: given a parsed `.mcp.json` and a set of env-var names that\n * have just rotated, return the MCP server keys whose `env` block\n * references any of those vars via `${VAR}` substitution.\n *\n * Only the `${VAR}` form is checked because that's the only form\n * Claude Code substitutes at MCP launch time from the parent claude's\n * env. A literal value-equals-name (e.g. `\"TOKEN\": \"XERO_ACCESS_TOKEN\"`\n * with no `${}`) is just a constant string the MCP child sees as-is —\n * the rotated value never reaches it, so reaping the child wouldn't\n * fix anything (the child would just respawn with the same broken\n * literal). CodeRabbit on PR #797 caught a doc-vs-impl drift that\n * previously claimed both forms were checked; this comment is now\n * aligned with the implementation, and the test\n * `does NOT match servers whose env contains the var name as a\n * literal value` pins the correct behaviour.\n */\nexport interface McpServerEntry {\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\nexport interface McpConfig {\n mcpServers?: Record<string, McpServerEntry>;\n}\n\nexport function findMcpServersUsingVars(\n mcp: McpConfig | null | undefined,\n changedVars: Iterable<string>,\n): string[] {\n const changedSet = new Set(changedVars);\n if (!mcp?.mcpServers || changedSet.size === 0) return [];\n\n const result: string[] = [];\n for (const [serverKey, entry] of Object.entries(mcp.mcpServers)) {\n if (!entry || typeof entry !== 'object') continue;\n const env = entry.env;\n if (!env || typeof env !== 'object') continue;\n let matches = false;\n for (const value of Object.values(env)) {\n if (typeof value !== 'string') continue;\n // Match ${VAR} substitution form first — the common case.\n const placeholderMatches = value.match(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g);\n if (placeholderMatches) {\n for (const ph of placeholderMatches) {\n const name = ph.slice(2, -1);\n if (changedSet.has(name)) {\n matches = true;\n break;\n }\n }\n }\n if (matches) break;\n }\n if (matches) result.push(serverKey);\n }\n return result;\n}\n\n/**\n * ENG-7510: given the vars that changed in `.env.integrations` and the parsed\n * `.mcp.json`, return the subset that require a full agent SESSION RESPAWN to\n * take effect — the env-only / CLI-tool integration vars.\n *\n * GitHub is the canonical case: `GITHUB_ACCESS_TOKEN` / `GITHUB_TOKEN` live in\n * the agent's own process env (backing the `gh` CLI + curl), not an MCP server's\n * env block, so the child-reap hot-reload path (`findMcpServersUsingVars` +\n * `reapStaleMcpChildren`) can never pick them up. The wrapper sources\n * `.env.integrations` exactly once at spawn (no live re-read), so the running\n * agent stays blind to an added/removed var of this kind until it is bounced.\n *\n * A var needs a respawn when it is BOTH:\n * - not a channel secret (`channelSecretKeys`) — those have their own\n * launch-time respawn watcher (checkChannelSecretDriftAndScheduleRestart,\n * ENG-6062); including them here would double-restart; AND\n * - referenced by NO MCP server (`${VAR}` substitution) — an MCP-referenced\n * var is handled by reaping that child (hot-reload), no full respawn needed.\n *\n * Pure and side-effect-free so the respawn decision is unit-testable in\n * isolation from the manager poll loop.\n */\nexport function envOnlyRespawnVars(\n changedVars: Iterable<string>,\n mcp: McpConfig | null | undefined,\n channelSecretKeys: readonly string[],\n): string[] {\n const channelSet = new Set(channelSecretKeys);\n return [...changedVars].filter(\n (v) => !channelSet.has(v) && findMcpServersUsingVars(mcp, [v]).length === 0,\n );\n}\n\n/**\n * ENG-7748: env-var names that a stdio remote-MCP proxy reads LIVE from the token\n * file per request, via its `AGT_REMOTE_MCP_EXTRA_HEADERS` (the `header:VAR`\n * list). A change to one of these needs NEITHER a child reap NOR a session\n * respawn: the proxy serves the new value on the very next request. Anchor's\n * frequently re-minted `ANCHOR_BROWSER_SESSION_ID` is the case this exists for -\n * without the exemption `envOnlyRespawnVars` would schedule a respawn on every\n * re-mint (the var isn't `${VAR}`-referenced, so it looks env-only), defeating\n * the whole point of routing anchor through the proxy.\n *\n * Covers only the `AGT_REMOTE_MCP_EXTRA_HEADERS` axis. For the primary\n * credential var (`AGT_REMOTE_MCP_TOKEN_VAR`) see {@link liveProxyTokenVars}\n * (ENG-7965).\n *\n * Pure and side-effect-free.\n */\nexport function liveProxyExtraHeaderVars(mcp: McpConfig | null | undefined): Set<string> {\n const out = new Set<string>();\n if (!mcp?.mcpServers) return out;\n for (const entry of Object.values(mcp.mcpServers)) {\n const spec = entry?.env?.['AGT_REMOTE_MCP_EXTRA_HEADERS'];\n if (typeof spec !== 'string') continue;\n for (const pair of spec.split(',')) {\n const colon = pair.indexOf(':');\n if (colon <= 0) continue;\n const varName = pair.slice(colon + 1).trim();\n if (varName) out.add(varName);\n }\n }\n return out;\n}\n\n/**\n * ENG-7965: env-var names that a stdio remote-MCP proxy uses as its primary\n * bearer credential, declared via `AGT_REMOTE_MCP_TOKEN_VAR`. The proxy\n * (remote-oauth-proxy.ts) reads this var from the per-agent secrets file with\n * `readFileSync` on EVERY request, so a mid-session token rotation is picked\n * up automatically - no MCP child reap and no agent session restart are needed.\n *\n * This exemption fixes the confirmed case where OAuth rotations for integrations\n * such as brand-ninja and Xero caused spurious agent session restarts, dropping\n * in-flight tasks (Stirling × 2, Jack × 1 on 2026-07-22 04:14–04:44 UTC).\n *\n * The var looks \"env-only\" to `envOnlyRespawnVars` (it is not referenced via a\n * `${VAR}` placeholder in `.mcp.json` - the literal var NAME is the value of\n * `AGT_REMOTE_MCP_TOKEN_VAR`), so without this exemption the manager schedules a\n * session restart on every credential rotation.\n *\n * Pure and side-effect-free.\n */\nexport function liveProxyTokenVars(mcp: McpConfig | null | undefined): Set<string> {\n const out = new Set<string>();\n if (!mcp?.mcpServers) return out;\n for (const entry of Object.values(mcp.mcpServers)) {\n const tokenVar = entry?.env?.['AGT_REMOTE_MCP_TOKEN_VAR'];\n if (typeof tokenVar === 'string' && tokenVar.trim()) {\n out.add(tokenVar.trim());\n }\n }\n return out;\n}\n\n/**\n * Compile MCP-server-key signatures into argv match patterns. Each MCP\n * server is spawned as some variant of `npx -y <package>` or\n * `node <path>` — the running child's argv contains the package\n * binary name (e.g. `xero-mcp-server`) which is stable enough to\n * match against. Falling back to the raw server-key as a literal\n * (e.g. `xero`) catches the cases where the package name and the\n * mcpServers key match.\n */\n/**\n * Build precise argv-matchers from each server's actual `.mcp.json`\n * `command + args`, NOT from the bare server key.\n *\n * Pre-fix this function used `\\b<serverKey>\\b` against argv, which on\n * a server key like `xero` would also match `xero-other-mcp-server` —\n * any sibling package that happens to share a prefix would get bounced\n * along with the intended target (CodeRabbit on PR #797). The fix:\n * extract a precise per-server signature from the entry's command +\n * args. For an `npx -y @xeroapi/xero-mcp-server@latest` entry we end\n * up matching against:\n * 1. the full package spec (`@xeroapi/xero-mcp-server`) — matches\n * the npm-exec wrapper process\n * 2. the bin basename (`xero-mcp-server`) — matches the actual node\n * child whose argv is `node /path/to/.bin/xero-mcp-server`\n *\n * Both forms are needed because the parent npm-exec process and the\n * child node process have different argv shapes but share the same\n * package identity.\n *\n * Falls back to a `(?<![A-Za-z0-9_])${key}(?![A-Za-z0-9_])` matcher\n * (alnum/underscore boundary, but `-` and `/` allowed) when the entry\n * has no resolvable package signature — the bare-key form is still\n * tighter than the original `\\b...\\b` because `_` is now part of the\n * \"name continuation\" set, so a server key `xero` doesn't match\n * `xero_thing` either.\n */\nfunction buildArgvMatchersForEntry(key: string, entry: McpServerEntry | undefined): RegExp[] {\n const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const patterns: RegExp[] = [];\n\n // Try to extract a precise package signature from the args. The\n // common shapes we provision through buildMcpJson:\n // npx -y @scope/package@version\n // npx -y package@version\n // node /absolute/path/to/.bin/package-name\n // Walk args from last to first, looking for the first token that\n // looks like a package spec or an absolute path with a bin basename.\n const args = entry?.args ?? [];\n for (let i = args.length - 1; i >= 0; i--) {\n const arg = args[i];\n if (typeof arg !== 'string' || !arg) continue;\n // Skip flags\n if (arg.startsWith('-')) continue;\n\n // Strip @version suffix from npm package specs.\n const stripped = arg.replace(/@[^/@]*$/, (m) =>\n // @latest, @1.2.3, etc. — drop. But @scope at the START of a\n // package spec must NOT be stripped, so we only strip a tail\n // `@...` if it doesn't itself start with a slash inside.\n // The regex above only matches a single trailing `@...`\n // segment after the last `/`, so this is safe for `@scope/pkg`.\n m.includes('/') ? m : '',\n );\n\n // Tail-boundary that disallows the package-name continuation\n // characters: alnum, `_`, `-`. Without this, matching\n // `@integrity-labs/cloud-broker` would also match\n // `@integrity-labs/cloud-broker-experimental` — exactly the\n // overmatch class CodeRabbit's review on PR #797 flagged.\n const tail = '(?![A-Za-z0-9_-])';\n\n // Looks like an npm package spec? (`@scope/pkg` or `pkg-name`)\n if (/^@?[a-z0-9]([a-z0-9._-]*\\/)?[a-z0-9._-]+$/i.test(stripped)) {\n patterns.push(new RegExp(`${escapeRe(stripped)}${tail}`));\n // Also push the basename if the spec is scoped — matches the\n // node child whose argv only has the bin name.\n const basename = stripped.split('/').pop();\n if (basename && basename !== stripped) {\n patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));\n }\n break;\n }\n\n // Looks like a path? Match the basename (the bin name) — the\n // running child's argv typically has the full path.\n if (stripped.includes('/')) {\n const basename = stripped.split('/').pop();\n if (basename) {\n patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));\n break;\n }\n }\n }\n\n if (patterns.length === 0) {\n // Fallback: bare server key with tighter boundaries than `\\b`.\n // Disallow alphanumerics + `_` either side; `-` and `/` are\n // permitted as separators (so e.g. `cloud-broker` matches\n // `@integrity-labs/cloud-broker` correctly).\n const safe = escapeRe(key);\n patterns.push(new RegExp(`(?<![A-Za-z0-9_])${safe}(?![A-Za-z0-9_])`));\n if (safe.includes('_')) {\n const dashed = safe.replace(/_/g, '-');\n patterns.push(new RegExp(`(?<![A-Za-z0-9_])${dashed}(?![A-Za-z0-9_])`));\n }\n }\n return patterns;\n}\n\n/** Match the parent claude bound to <codeName> via `--name agt-<codeName>`. */\nexport function buildClaudeAgentMatcher(codeName: string): RegExp {\n // Restrict to alnum/dash so a malicious codeName can't widen the\n // match (codeName is validated upstream but defence in depth).\n const safe = codeName.replace(/[^A-Za-z0-9_-]/g, '');\n // Use a tail boundary that disallows hyphen-as-word-character —\n // `\\b` between `g` and `-` matches in regex (since `-` is not a\n // word char), so `\\\\b` would let `agt-stirling-other` match the\n // matcher for `stirling`. Require an actual argument boundary\n // (whitespace) or end-of-string instead. CodeRabbit-detected\n // regression class.\n return new RegExp(`\\\\bclaude\\\\b.*--name\\\\s+agt-${safe}(?=\\\\s|$)`);\n}\n\n/**\n * Pure: given parsed ps rows, find MCP child PIDs that belong to\n * <codeName>'s session AND match one of the rotated-env server keys.\n *\n * \"Belong to <codeName>'s session\" = ppid chain (up to maxDepth)\n * reaches a `claude --name agt-<codeName>` process. Same walk shape\n * as the orphan reaper, but the success direction is inverted: that\n * one collects MCPs whose ancestry does NOT reach a live claude\n * (orphans); this one collects MCPs whose ancestry DOES reach a\n * specific live claude (this agent's claude).\n *\n * Empty `serverKeys` returns []. Empty `psRows` returns [].\n * maxDepth defaults to 8 — same as the orphan reaper.\n */\nexport function findMcpChildrenForAgent(args: {\n rows: PsRow[];\n codeName: string;\n serverKeys: string[];\n /**\n * Parsed `.mcp.json` so the matcher can derive precise per-server\n * argv signatures from each entry's `command + args`. When omitted,\n * matching falls back to the bare-key form which is tighter than\n * the original `\\b...\\b` but can still overmatch for short keys.\n * Production callers pass this; some tests pass only `serverKeys`\n * to exercise the fallback path.\n */\n mcpJson?: McpConfig | null;\n maxDepth?: number;\n /**\n * ENG-6660: rows come from `docker exec ps` INSIDE the agent's own container\n * (Docker-isolated agent). The container is a single-agent boundary AND its\n * claude carries no `--name agt-<codeName>` flag, so the strict host matcher\n * can't anchor. We still validate ownership - the ppid walk must reach SOME\n * claude (a relaxed matcher) so a stray argv match without a live claude\n * ancestor isn't counted as a healthy child (CodeRabbit, PR #2345).\n */\n inContainer?: boolean;\n}): number[] {\n const { rows, codeName, serverKeys, mcpJson } = args;\n const maxDepth = args.maxDepth ?? 8;\n const inContainer = args.inContainer ?? false;\n if (serverKeys.length === 0 || rows.length === 0) return [];\n\n // In-container: any `claude` ancestor counts (single-agent boundary, no --name\n // flag). Host: the exact `claude --name agt-<codeName>` parent.\n const claudeMatcher = inContainer ? /\\bclaude\\b/ : buildClaudeAgentMatcher(codeName);\n // Build precise per-server matchers from each entry's command + args\n // when mcpJson is provided. Falls back to the bare-key form when\n // a key has no resolvable entry (e.g. it was just removed from\n // mcp.json mid-tick or the test passed only serverKeys).\n const argvMatchers: RegExp[] = [];\n for (const key of serverKeys) {\n const entry = mcpJson?.mcpServers?.[key];\n argvMatchers.push(...buildArgvMatchersForEntry(key, entry));\n }\n const byPid = new Map<number, PsRow>(rows.map((r) => [r.pid, r]));\n\n const matched: number[] = [];\n for (const row of rows) {\n // Must match at least one of the server-key argv patterns.\n if (!argvMatchers.some((re) => re.test(row.args))) continue;\n // Must NOT itself be a claude (we want children, not the agent's\n // claude process).\n if (/\\bclaude\\b/.test(row.args) && row.args.includes(`--name agt-${codeName}`)) continue;\n\n // Walk parents looking for the agent's claude (relaxed in-container).\n let cur: PsRow | undefined = byPid.get(row.ppid);\n let belongs = false;\n for (let depth = 0; depth < maxDepth && cur; depth++) {\n if (claudeMatcher.test(cur.args)) {\n belongs = true;\n break;\n }\n if (cur.pid === 1) break;\n cur = byPid.get(cur.ppid);\n }\n if (belongs) matched.push(row.pid);\n }\n return matched;\n}\n\n/**\n * Run `ps`, find the stale MCP children for <codeName> belonging to\n * the listed serverKeys, and SIGTERM them. After `graceMs`, SIGKILL\n * any that didn't exit. Returns the list of pids that were sent\n * SIGTERM (informational — tests use this without mocking\n * process.kill).\n *\n * Idempotent. Cheap enough to call on every integration-rotation\n * tick — when nothing matches, the work is one ps invocation + a\n * regex pass.\n */\nexport function reapStaleMcpChildren(args: {\n log: (msg: string) => void;\n codeName: string;\n serverKeys: string[];\n /**\n * Parsed `.mcp.json` so the matcher can derive precise per-server\n * argv signatures from each entry's command + args. Production\n * callers always pass this; tests can omit to exercise the\n * fallback (bare-key with tightened boundaries).\n */\n mcpJson?: McpConfig | null;\n graceMs?: number;\n /** Test seam: substitute the `ps` invocation. */\n runPs?: () => string;\n /** Test seam: substitute the kill function. */\n killProcess?: (pid: number, signal: 'SIGTERM' | 'SIGKILL') => void;\n /** Test seam: substitute the still-alive check. */\n isAlive?: (pid: number) => boolean;\n /**\n * ENG-6670: when the agent runs under Docker isolation, its MCP children live\n * in the container's PID namespace. The default ps/kill/isAlive then operate\n * via `docker exec agt-<code>`, so we enumerate AND signal the CORRECT\n * (container-namespace) pids — a host `process.kill(<container-pid>)` would\n * hit an unrelated host process. Mirrors mcp-presence-reaper's `isolated`\n * (ENG-6660). Default false = unchanged host behaviour.\n */\n isolated?: boolean;\n}): number[] {\n const { log, codeName, serverKeys, mcpJson, graceMs = 5_000, isolated = false } = args;\n if (serverKeys.length === 0) return [];\n\n // ENG-6670: under Docker isolation, enumerate AND signal inside the container's\n // PID namespace (docker exec). The pids from `docker exec ps` are\n // container-namespace pids — a host process.kill on them would signal an\n // unrelated host process. Mirrors mcp-presence-reaper's isolated runPs.\n const runPs =\n args.runPs ??\n (isolated\n ? () =>\n execFileSync('docker', ['exec', `agt-${codeName}`, 'ps', '-eo', 'pid,ppid,args'], {\n encoding: 'utf-8',\n timeout: 8_000,\n })\n : () => execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', timeout: 5_000 }));\n const killProcess =\n args.killProcess ??\n (isolated\n ? (pid: number, signal: 'SIGTERM' | 'SIGKILL') => {\n try {\n execFileSync(\n 'docker',\n ['exec', `agt-${codeName}`, 'kill', signal === 'SIGKILL' ? '-KILL' : '-TERM', String(pid)],\n { timeout: 8_000, stdio: 'ignore' },\n );\n } catch {\n // Already exited / container gone — fine.\n }\n }\n : (pid: number, signal: 'SIGTERM' | 'SIGKILL') => {\n try {\n process.kill(pid, signal);\n } catch {\n // Process already exited — fine.\n }\n });\n const isAlive =\n args.isAlive ??\n (isolated\n ? (pid: number) => {\n try {\n execFileSync('docker', ['exec', `agt-${codeName}`, 'kill', '-0', String(pid)], {\n timeout: 8_000,\n stdio: 'ignore',\n });\n return true;\n } catch {\n return false;\n }\n }\n : (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n });\n\n let psOutput: string;\n try {\n psOutput = runPs();\n } catch (err) {\n log(`[stale-mcp-reaper] ps invocation failed for '${codeName}': ${(err as Error).message} — skipping reap`);\n return [];\n }\n\n const rows = parsePsRows(psOutput);\n const targets = findMcpChildrenForAgent({ rows, codeName, serverKeys, mcpJson, inContainer: isolated });\n if (targets.length === 0) return [];\n\n const byPid = new Map<number, PsRow>(rows.map((r) => [r.pid, r]));\n const describe = (pid: number): string => {\n const argv = byPid.get(pid)?.args ?? '';\n // Pull the first identifiable npm package or script name from argv.\n const pkgMatch = argv.match(/(@[a-z0-9_-]+\\/[a-z0-9_-]+|[a-z0-9_-]+-mcp-server|[a-z0-9_-]+-mcp\\b)/i);\n return pkgMatch ? `${pkgMatch[1]} (pid ${pid})` : `pid ${pid}`;\n };\n\n log(\n `[stale-mcp-reaper] '${codeName}': rotating ${targets.length} stale MCP child(ren) for [${serverKeys.join(', ')}]: ${targets.map(describe).join(', ')}`,\n );\n for (const pid of targets) {\n killProcess(pid, 'SIGTERM');\n }\n // ENG-5344: record the rotation so the presence-reaper running on the\n // next manager tick (≥ ~2s later) doesn't see the now-dead children as\n // \"declared MCP missing\" and tear down the whole tmux session. The\n // child won't respawn until Claude Code makes its next tool call, so a\n // grace window is the only correct read here. Only record for keys we\n // actually killed — recording the full `serverKeys` list would grace\n // keys whose env rotated but had no live children to begin with.\n const killedKeys = new Set<string>();\n for (const pid of targets) {\n const argv = byPid.get(pid)?.args ?? '';\n for (const key of serverKeys) {\n const entry = mcpJson?.mcpServers?.[key];\n const matchers = buildArgvMatchersForEntry(key, entry);\n if (matchers.some((re) => re.test(argv))) killedKeys.add(key);\n }\n }\n if (killedKeys.size > 0) recordStaleRotation(codeName, killedKeys);\n\n setTimeout(() => {\n try {\n // CodeRabbit on PR #797: re-resolve ownership before SIGKILL.\n // 5s grace is long enough for the kernel to recycle a freed\n // PID into an entirely unrelated process — `targets.filter(isAlive)`\n // alone would happily kill that bystander. Re-running the\n // original ps + match pipeline against fresh process state\n // confirms the PID is still one of *our* MCP children for this\n // agent. Anything that doesn't survive that re-check is either\n // already dead (good) or has been recycled (do NOT kill).\n let freshPsOutput: string;\n try {\n freshPsOutput = runPs();\n } catch (err) {\n log(`[stale-mcp-reaper] '${codeName}': fresh ps for SIGKILL re-verify failed: ${(err as Error).message} — skipping SIGKILL pass`);\n return;\n }\n const stillOwned = new Set(\n findMcpChildrenForAgent({\n rows: parsePsRows(freshPsOutput),\n codeName,\n serverKeys,\n mcpJson,\n inContainer: isolated,\n }),\n );\n const stragglers = targets.filter((pid) => isAlive(pid) && stillOwned.has(pid));\n if (stragglers.length === 0) return;\n log(\n `[stale-mcp-reaper] '${codeName}': ${stragglers.length} child(ren) survived SIGTERM; sending SIGKILL: ${stragglers.map(describe).join(', ')}`,\n );\n for (const pid of stragglers) {\n killProcess(pid, 'SIGKILL');\n }\n } catch (err) {\n log(`[stale-mcp-reaper] '${codeName}': error in SIGKILL pass: ${(err as Error).message}`);\n }\n }, graceMs).unref();\n\n return targets;\n}\n","// ENG-5053: detect MCP children that should be running but aren't, and\n// trigger a session restart so they come back online.\n//\n// Why this exists: an MCP child can die under a live parent claude\n// (transient error during init, OOM, downstream service blip during a\n// tool call). Claude Code's MCP transport advertises that it'll\n// respawn a dead child on the next request — but in practice we've\n// seen agents go silent for hours with a missing MCP and no recovery\n// (Stirling, 2026-05-13, Xero MCP). The agent keeps reporting \"X is\n// still warming up\" indefinitely. Killing the tmux session forces the\n// manager's ensure-session pass to respawn it, which gets the missing\n// child back.\n//\n// This module is the *presence* counterpart to stale-mcp-reaper.ts\n// (which handles env rotation — child alive but holding a stale\n// credential) and orphan-channel-mcp-reaper.ts (which handles dead\n// parents — child alive but its parent claude is gone). The gap they\n// don't cover is the one this fills: parent claude alive, declared\n// MCP child absent, no rotation pending.\n//\n// Pure helpers exposed for tests:\n// • findMissingMcpServers() — given parsed ps rows + the agent's\n// codeName + parsed .mcp.json, return the declared mcpServers\n// keys that have ZERO matching live children belonging to this\n// agent's claude.\n//\n// Side-effecting:\n// • reapMissingMcpSessions() — runs `ps`, checks presence, applies\n// the cold-start grace window, and calls back to stopSession()\n// so the manager's ensure-session pass respawns the agent.\n\nimport { execFileSync } from 'node:child_process';\nimport { parsePsRows, type PsRow } from './orphan-channel-mcp-reaper.js';\nimport {\n DEFAULT_ROTATION_GRACE_MS,\n classifyRotation,\n clearStaleRotation,\n findMcpChildrenForAgent,\n getRotationRecordedAt,\n type McpConfig,\n} from './stale-mcp-reaper.js';\n\n/**\n * Cold-start grace window — how long after a session starts we wait\n * before flagging a missing MCP. The Composio MCP spawns via npx which\n * can take 20-30s to install on a fresh box; without this grace a\n * brand-new session would be repeatedly killed mid-init. (Xero used to\n * share this failure mode but is now CLI-bundled and starts instantly,\n * ENG-7579 — the grace still covers Composio and any other npx server.)\n */\nexport const DEFAULT_COLD_START_GRACE_MS = 90_000;\n\n/**\n * ENG-5279: maximum consecutive presence-reaper restarts before we give\n * up on a permanently-broken declared MCP. Set to 3 to match the\n * agent-thrash-monitor threshold — if we keep restarting past that\n * point, the thrash detector pages Slack with a false alarm for a\n * failure the reaper can't fix by restarting (orphan in .mcp.json from\n * a pre-ENG-5277 install, an unpublished MCP package, a transiently\n * failing npx install that's now permanently broken, etc.).\n */\nexport const MAX_PRESENCE_RESTART_ATTEMPTS = 3;\n\n/**\n * ENG-6480: circuit-breaker / backoff for a persistently-FLAPPING MCP.\n *\n * The ENG-5279 `attempts` cap above only catches a server that NEVER comes\n * live (an orphaned config) — because the per-tick reset loop zeroes\n * `attempts` the moment a key is seen live on ANY poll. A *crash-looping*\n * MCP (starts → dies → starts → dies, e.g. sterling's xero on 2026-06-14)\n * resets `attempts` every cycle and so never reaches give-up → it bounces the\n * whole tmux session on an unbounded loop (≥5 restarts/window, tripping the\n * ENG-5286 flapping alarm, and starving in-flight admin restart acks →\n * `agent_restart_stuck`).\n *\n * The fix is a per-(codeName, serverKey) restart HISTORY that — unlike\n * `attempts` — survives a brief green blip (pruned only by age, and cleared\n * only after a *sustained* live stretch). From it we derive:\n * 1. an escalating backoff interval that SKIPS a restart that would land\n * too soon after the previous one (so the restart RATE drops below the\n * flapping threshold instead of firing every poll); and\n * 2. the SAME give-up terminal (onGiveUp → mark-unhealthy → dropped from\n * .mcp.json) once the key flaps past a ceiling — so a crash-looper ends\n * in the identical \"broken\" state as an orphan, not a third notion.\n *\n * `BACKOFF_AFTER_RESTARTS` is tied to the ENG-5279 cap on purpose: the first\n * `MAX_PRESENCE_RESTART_ATTEMPTS` restarts stay immediate (`backoffMs = 0`),\n * so the orphan path (which gives up at the cap before backoff is ever\n * consulted) and existing transient-recovery behaviour are byte-for-byte\n * unchanged; backoff only engages in the crash-loop case where `attempts`\n * keeps resetting.\n */\nexport const BACKOFF_AFTER_RESTARTS = MAX_PRESENCE_RESTART_ATTEMPTS;\n/** Base backoff once past BACKOFF_AFTER_RESTARTS; doubles per extra restart. */\nexport const BACKOFF_BASE_MS = 60_000;\n/** Ceiling on the escalating backoff interval. */\nexport const BACKOFF_MAX_MS = 30 * 60_000;\n/** Rolling window over which restarts are counted for backoff + rate give-up. */\nexport const BACKOFF_WINDOW_MS = 60 * 60_000;\n/**\n * Restarts within the window that trip the rate-based give-up (mark-unhealthy).\n * Set to 2× the cap: a crash-looper escalates BASE→2×→4× between restarts and\n * reaches this in ~15 min, then gets disabled — coherent with the ENG-5286\n * flapping alarm rather than a separate notion of \"broken\".\n */\nexport const BACKOFF_GIVE_UP_RESTARTS = 2 * MAX_PRESENCE_RESTART_ATTEMPTS;\n/**\n * Continuous-liveness dwell that clears the restart history (genuine\n * recovery). A brief green blip in a crash-loop never reaches this — its\n * liveness stretch is broken by the next miss — so the history (and thus the\n * backoff/breaker) survives blips, while a truly-recovered MCP resets ~here.\n */\nexport const BACKOFF_STABLE_RESET_MS = 5 * 60_000;\n\n/**\n * ENG-8062: absolute wall-clock floor beyond which a CONTINUOUSLY-missing key\n * is treated as genuinely dead, overriding the ENG-8013 storm-awareness\n * suppression. Storm-awareness (`currentGenerationExternallyStarted`) stops a\n * healthy channel being quarantined as collateral of a respawn storm by\n * re-basing the dwell + skipping the attempts increment on externally-started\n * generations — but a channel that is truly dead (its child never once comes\n * live, e.g. stirling's spawn-lock deadlock) never accrues dwell and so churns\n * the session forever. A healthy channel comes live at least once inside any\n * plausible storm generation, which resets `continuouslyMissingSince`; only a\n * channel that is missing on EVERY poll for this long is genuinely dead, so the\n * floor is set generously (15 min) to keep the storm-awareness guarantee intact\n * while still letting a dead channel reach give-up / quarantine / backoff.\n */\nexport const DEAD_CHANNEL_MS = 15 * 60_000;\n\n/**\n * Per-(codeName, serverKey) state for the cap logic.\n *\n * `attempts` counts distinct restart cycles where the server never\n * came back live — NOT distinct polls. CodeRabbit on PR #1167: pre-fix\n * the counter incremented on every tick the server was missing, so a\n * slow respawn (3+ polls during a single in-flight restart) would\n * exhaust the cap before three actual session generations had been\n * observed. The `lastAttemptedSessionStartedAt` field is the per-\n * generation gate — we only increment when this differs from the\n * current `sessionStartedAt` (i.e. the manager spawned a fresh\n * session since our last attempt), and after incrementing we record\n * the current generation so subsequent polls within the same\n * generation are no-ops for the counter.\n *\n * `gaveUpLogged` records whether we've already emitted the give-up\n * audit line so subsequent ticks stay silent.\n *\n * Cleared per-agent by `clearPresenceReaperState()` when the manager\n * sees `.mcp.json` content change — that's the signal that something\n * external fixed the config and the give-up state is stale.\n */\ninterface PresenceReaperKeyState {\n attempts: number;\n lastSeenLiveAt: number | null;\n lastAttemptedSessionStartedAt: number | null;\n gaveUpLogged: boolean;\n /**\n * ENG-5932: epoch ms when this key was first observed continuously missing\n * in the current stretch (null when live). Drives the quarantine dwell gate\n * — an OPTIONAL channel isn't a quarantine candidate until it's been missing\n * for `quarantineDwellMs` AFTER the cold-start grace. Reset to null the\n * moment the key is seen live again (so a channel that recovers doesn't\n * carry a stale dwell clock).\n */\n firstMissingAt: number | null;\n /**\n * ENG-5932: gates the once-per-key quarantine log line + onQuarantine\n * callback, mirroring `gaveUpLogged`. Reset on liveness.\n */\n quarantineLogged: boolean;\n /**\n * ENG-6480: epoch-ms timestamps of session restarts the reaper triggered on\n * THIS key's behalf, within the rolling `BACKOFF_WINDOW_MS`. Unlike\n * `attempts` it is NOT reset by a brief green blip (only pruned by age, and\n * cleared after a sustained live stretch — see `stableLiveSince`), so it\n * catches a crash-looping MCP that comes live each cycle. Drives the\n * escalating backoff interval and the rate-based give-up.\n */\n restartHistory: number[];\n /**\n * ENG-6480: epoch ms when the key's CURRENT continuous-live stretch began\n * (null while missing). Once it has been continuously live for\n * `BACKOFF_STABLE_RESET_MS` the restart history is cleared (genuine\n * recovery). A single green blip in a crash-loop never reaches the dwell —\n * the next miss nulls this — so the breaker survives blips.\n */\n stableLiveSince: number | null;\n /**\n * ENG-8062: epoch ms when the key entered its CURRENT continuously-missing\n * stretch (null while live). Unlike {@link firstMissingAt} this is NEVER\n * re-based by the ENG-8013 storm-awareness path — it resets only when the key\n * is actually seen live again. Once `now - continuouslyMissingSince` exceeds\n * {@link DEAD_CHANNEL_MS} the key is genuinely dead and escapes the\n * externally-started-generation suppression (counts attempts + can quarantine)\n * so a dead channel stops churning the session indefinitely.\n */\n continuouslyMissingSince: number | null;\n /**\n * ENG-8062: gates the once-per-stretch \"genuinely dead, overriding storm-\n * awareness\" log line so it doesn't repeat every poll. Reset on liveness.\n */\n deadChannelLogged: boolean;\n /**\n * ENG-6480: true while the key is currently given up on (either cause:\n * the ENG-5279 attempts cap OR the rate-based flap ceiling). Reset on\n * liveness. Read by {@link givenUpMcpServerKeys} so the resume reconciler\n * sees a rate-given-up key as broken too, not just attempts-given-up ones.\n */\n gaveUp: boolean;\n}\n\nconst presenceReaperState = new Map<string, PresenceReaperKeyState>();\n\n/**\n * ENG-8225: the outcome of a stale-mcp-reaper rotation, from the point of view\n * of the child it SIGTERM'd.\n *\n * 'respawned-on-tool-call' — the child came back on its own, because the\n * agent happened to call a tool on that\n * server. The ENG-5344 happy path.\n * 'respawned-after-prompt-probe' — the child came back only after we armed\n * ENG-7429's prompt bind-probe to MANUFACTURE\n * that tool call.\n * 'never-respawned' — the prompt-probe budget was exhausted with\n * the child still absent. Escalated.\n *\n * Deliberately NOT named \"respawned-on-timeout\": nothing here respawns a child\n * directly. A stdio MCP child is a subprocess of Claude Code on the session's\n * own stdio pipes, and `findMcpChildrenForAgent` only counts a child whose ppid\n * chain reaches a `claude` ancestor — so a manager-spawned child would be both\n * invisible to this reaper and unreachable by any tool call. The only lever the\n * manager actually has is to cause the next tool call, which is what these\n * values describe. See ENG-8225's re-framing.\n */\nexport type RotationRespawnOutcome =\n | 'respawned-on-tool-call'\n | 'respawned-after-prompt-probe'\n | 'never-respawned';\n\n/**\n * ENG-8225: how many prompt bind-probes we arm for a single stalled rotation\n * before escalating. The probe is best-effort (it asks the agent to exercise its\n * tool surface; a busy or wedged session may not get to it promptly), so a small\n * budget rather than one shot — the same reasoning as ENG-7429's own\n * PROMPT_BIND_PROBE_ATTEMPTS budget.\n */\nexport const MAX_ROTATION_PROMPT_PROBES = 3;\n\n/**\n * ENG-8225: minimum spacing between prompt-probe arms for the same stalled\n * rotation. ENG-7429's probe budget spans ~60s of force-due re-checks at the\n * manager's 10s poll cadence, so arming on every poll would burn the whole\n * ENG-8225 budget while the FIRST probe was still in flight and escalate a\n * rotation that was about to recover. 30s gives each arm a fair chance.\n */\nexport const ROTATION_PROMPT_PROBE_INTERVAL_MS = 30_000;\n\n/**\n * ENG-8225: per-(agent, serverKey) state for a rotation whose grace window\n * expired with the child still absent. Separate from\n * {@link PresenceReaperKeyState} because it tracks the ROTATION's fate, not the\n * key's restart budget: it is created by a rotation, cleared by the child coming\n * back, and irrelevant to a key that was never rotated.\n *\n * Because the fate it tracks belongs to ONE rotation while the map key belongs\n * to the server, every entry carries {@link RotationStallState.rotationRecordedAt}\n * — read that field's doc before touching this map. An entry is valid only for\n * the rotation whose stamp it holds; anything else is a leftover.\n */\ninterface RotationStallState {\n /**\n * ENG-8225: the SIGTERM timestamp of the rotation this state describes, as\n * reported by `getRotationRecordedAt`. This is the state's LIFETIME ANCHOR:\n * the map is keyed by (agent, serverKey), which outlives any one rotation, so\n * the stamp is the only thing that distinguishes \"this budget belongs to the\n * rotation I am looking at\" from \"this is a leftover from a rotation that is\n * over\". A stamp mismatch means the entry is garbage and must be replaced,\n * NOT consulted — see the read site in `reapMissingMcpSessions`.\n */\n rotationRecordedAt: number;\n /** Prompt bind-probes armed for this stalled rotation so far. */\n probeAttempts: number;\n /** Epoch-ms of the most recent arm, for the {@link ROTATION_PROMPT_PROBE_INTERVAL_MS} spacing. */\n lastProbeAt: number | null;\n /** True once the budget was exhausted and the escalation fired (once-guard). */\n escalated: boolean;\n}\n\nconst rotationStallState = new Map<string, RotationStallState>();\n\nfunction stateKey(codeName: string, serverKey: string): string {\n return `${codeName}\\x00${serverKey}`;\n}\n\n/**\n * Clear every per-server give-up state for `codeName`. Wipes ALL\n * entries for the agent regardless of which keys changed.\n *\n * Prefer {@link clearPresenceReaperStateForKeys} from production code\n * (ENG-5285) — value-only drifts on .mcp.json (e.g. composio rotating\n * a signed proxy URL every ~20 min) shouldn't reset the cap on keys\n * whose presence-classification didn't actually change. This whole-\n * agent wipe was the root cause of Stirling flapping 450× over six\n * days post-ENG-5279: every URL rotation reset the cap before the\n * reaper could give up on the broken xero MCP.\n *\n * Kept exported for tests + any future caller that genuinely needs a\n * full agent-scoped wipe (none in production today).\n */\nexport function clearPresenceReaperState(codeName: string): void {\n const prefix = `${codeName}\\x00`;\n for (const key of presenceReaperState.keys()) {\n if (key.startsWith(prefix)) presenceReaperState.delete(key);\n }\n}\n\n/**\n * ENG-5285: clear give-up state only for the named MCP server keys.\n *\n * Call from the manager-worker's `.mcp.json` drift handler with the\n * set of keys that were ADDED or REMOVED relative to the previous\n * .mcp.json. Keys present in both old and new (even if their values\n * changed — e.g. composio rotated a signed URL) must NOT be cleared,\n * because their presence-classification is unchanged and the cap on\n * a permanently-broken MCP must keep accumulating across config\n * rewrites.\n *\n * No-op when `keys` is empty.\n */\nexport function clearPresenceReaperStateForKeys(\n codeName: string,\n keys: Iterable<string>,\n): void {\n for (const key of keys) {\n presenceReaperState.delete(stateKey(codeName, key));\n // ENG-8225: a key that was ADDED or REMOVED from .mcp.json has no in-flight\n // rotation to chase — drop its stall state alongside its restart budget so a\n // re-added server starts with a fresh prompt-probe budget.\n rotationStallState.delete(stateKey(codeName, key));\n }\n}\n\n/**\n * Test-only seam: reset all state. Production code should use\n * `clearPresenceReaperState(codeName)` instead — wiping the whole map\n * would lose give-up state for unrelated agents.\n */\nexport function __resetPresenceReaperStateForTests(): void {\n presenceReaperState.clear();\n rotationStallState.clear();\n}\n\n/**\n * ENG-6383 (ENG-6375 Slice C): the set of this agent's MCP server keys the\n * presence-reaper has GIVEN UP on — declared servers still missing after the\n * full restart budget (`attempts >= MAX_PRESENCE_RESTART_ATTEMPTS`). A given-up\n * key is a declared MCP that is genuinely absent, so the safe-resume reconciler\n * must NOT clear a circuit-breaker trip while one is outstanding (it feeds\n * `ResumeHealthSnapshot.mcpPresent` via `deriveMcpPresent`). Read-only.\n */\nexport function givenUpMcpServerKeys(codeName: string): Set<string> {\n const prefix = `${codeName}\\x00`;\n const out = new Set<string>();\n for (const [key, st] of presenceReaperState) {\n // ENG-6480: `gaveUp` covers BOTH give-up causes (attempts cap + rate\n // ceiling); keep the legacy attempts predicate as a belt-and-braces\n // superset for any state populated before `gaveUp` was set this tick.\n if (key.startsWith(prefix) && (st.gaveUp || st.attempts >= MAX_PRESENCE_RESTART_ATTEMPTS)) {\n out.add(key.slice(prefix.length));\n }\n }\n return out;\n}\n\n/**\n * Pure: given parsed ps rows + the agent's codeName + parsed\n * `.mcp.json`, return the declared `mcpServers` keys that have ZERO\n * matching live children belonging to this agent's claude.\n *\n * Reuses `findMcpChildrenForAgent` from stale-mcp-reaper so the argv-\n * matching rules and ppid-walk are shared with the existing reapers\n * (one source of truth for \"which child belongs to which agent + which\n * server key\").\n */\nexport function findMissingMcpServers(args: {\n rows: PsRow[];\n codeName: string;\n mcpJson: McpConfig | null | undefined;\n /** ENG-6660: rows are from `docker exec ps` in the agent's container - use the\n * relaxed in-container claude-ancestor walk (see findMcpChildrenForAgent). */\n inContainer?: boolean;\n}): string[] {\n const { rows, codeName, mcpJson, inContainer } = args;\n const servers = mcpJson?.mcpServers ?? {};\n const declared = Object.keys(servers);\n if (declared.length === 0) return [];\n\n const missing: string[] = [];\n for (const key of declared) {\n // ENG-5075: skip URL-based remote MCP entries (Streamable HTTP /\n // SSE — anything with a `url` field and no `command`). Claude\n // Code dials those endpoints directly; there's no stdio child\n // process to discover via `ps`. Pre-fix, the reaper iterated\n // every declared server and flagged URL-based ones (e.g. Granola\n // post-ENG-5074) as \"missing\" every poll cycle → killed the\n // session every ~3 minutes → Scout flapped indefinitely with the\n // signature \"declared MCP(s) [granola] have no live children\".\n //\n // The presence check is only meaningful for stdio MCPs — those\n // are the ones with a child process that could die under a live\n // parent. URL-based entries can't have that failure mode at the\n // child-process layer (their failure mode is \"HTTP request fails\n // at call time\", which is the MCP transport's job to handle, not\n // the reaper's).\n const entry = (servers as Record<string, unknown>)[key] as\n | { command?: unknown; url?: unknown }\n | undefined;\n const isStdio = typeof entry?.command === 'string';\n if (!isStdio) continue;\n\n const live = findMcpChildrenForAgent({\n rows,\n codeName,\n serverKeys: [key],\n mcpJson,\n inContainer,\n });\n if (live.length === 0) missing.push(key);\n }\n return missing;\n}\n\nexport interface ReapMissingMcpSessionsArgs {\n log: (msg: string) => void;\n codeName: string;\n mcpJson: McpConfig | null | undefined;\n /**\n * When the parent claude session was last started (Date.now() ms).\n * Used to apply the cold-start grace window — we don't flag a\n * missing MCP within `graceMs` of startup, because the npx-delivered\n * Composio MCP can take ~30s to npm-install on first spawn (xero is\n * now CLI-bundled and exempt from this, ENG-7579). Pass null when\n * the session start time is unknown — the reaper then *skips* the\n * agent rather than risk a false-positive restart.\n */\n sessionStartedAt: number | null;\n /**\n * ENG-6486: true when the manager already has a session restart SCHEDULED or\n * DEFERRED for this agent (i.e. `pendingSessionRestarts.has(codeName)`). When\n * a restart is pending, a declared-but-not-yet-live MCP is EXPECTED — the\n * pending restart will spawn it once it fires — so the reaper must not fire\n * again. Pre-fix, a freshly-ADDED channel (e.g. slack added to valai's set)\n * whose convergence restart was deferred by the maintenance window made the\n * reaper re-detect \"declared MCP has no live children\" every poll; each firing\n * its `onRestart` callback (→ McpReaperRestartsHourly) even though the restart\n * was itself deferred, tripping a FALSE `agent_reaper_flapping` alarm\n * (gccm-host/valai, 2026-06-15, 04:08-04:11). Firing while a restart is\n * already pending achieves nothing (the reaper's own restart defers too), so\n * skipping is strictly correct. Default false → legacy behaviour unchanged.\n */\n restartPending?: boolean;\n /**\n * ENG-8013 (storm-awareness): true when the agent's CURRENT session generation\n * was torn down + respawned by an EXTERNAL cause (anything but this reaper's\n * own mcp-presence recovery restart) - e.g. an integration-change /\n * channel-set-change / day-rollover / wedge / operator restart, or a respawn\n * STORM. When true, a declared-but-missing channel MCP is treated as COLLATERAL\n * of the churn: its \"no live child\" observation does NOT count toward the\n * give-up / quarantine budget (`attempts` stays put, the quarantine dwell clock\n * re-bases), because the MCP hasn't had a stable session generation to come up\n * in. Only a generation the reaper ITSELF restarted counts as a genuine failed\n * recovery. Default false → legacy behaviour (every generation counts) is\n * byte-for-byte unchanged.\n *\n * This is the durable fix for ENG-8003: an integration-hash respawn storm\n * quarantined Sherlock's HEALTHY Slack MCP by inflating `attempts` one storm\n * generation at a time until the cap tripped, even though Slack was never\n * broken and the reaper's own recovery restarts were deferred by the\n * maintenance window and never ran.\n */\n currentGenerationExternallyStarted?: boolean;\n /** Callback that tears the agent's session down so the manager's\n * ensure-session pass respawns it on the next tick.\n *\n * ENG-5547: `ctx.activeKeys` is the set of under-cap missing MCP servers\n * that prompted *this* restart (the same keys logged on the restart line).\n * The manager uses its cardinality to decide whether the restart counts\n * toward the agent-wide circuit breaker: a single persistently-failing\n * server is isolated by its own ENG-5279 give-up cap (→ marked unhealthy →\n * dropped from .mcp.json), so it must NOT pause the whole agent; a restart\n * driven by several servers at once is a genuine storm that should. */\n stopSession: (codeName: string, ctx: { activeKeys: string[] }) => void;\n /** Override the cold-start grace (default: 90s). */\n graceMs?: number;\n /**\n * ENG-5344: post-rotation grace. When stale-mcp-reaper SIGTERMs an MCP\n * child during a credential rotation, the child won't respawn until\n * Claude Code makes the next tool call. Without this grace, the next\n * manager tick (~2s later) sees zero live children for the rotated\n * key and kills the whole tmux session — observed on Stirling at the\n * 20-min xero token rotation cadence (54 false-positive restarts/day).\n *\n * Default: {@link DEFAULT_ROTATION_GRACE_MS} (15s — comfortably above\n * the SIGTERM→SIGKILL grace plus typical respawn latency).\n */\n rotationGraceMs?: number;\n /**\n * ENG-8225: the rotation grace above expired and the rotated child is STILL\n * absent — nothing made the tool call the grace window was banking on. Arm\n * ENG-7429's prompt bind-probe for this agent so the next tool call is\n * MANUFACTURED, and skip the whole-session restart this tick.\n *\n * MUST return whether the probe was actually armed. When it returns false (the\n * `session-tool-probe` flag is off, so arming is inert), or is not wired at\n * all, or throws, the key falls straight through to the pre-ENG-8225 path — a\n * suppressed restart with no probe behind it would be strictly worse than the\n * restart, so this fails OPEN to the legacy behaviour.\n *\n * Called at most once per {@link ROTATION_PROMPT_PROBE_INTERVAL_MS} per key,\n * up to {@link MAX_ROTATION_PROMPT_PROBES} times, then escalates via\n * {@link onRotationRespawnGaveUp}.\n */\n onRotationRespawnStalled?: (\n codeName: string,\n serverKey: string,\n ctx: { attempt: number; maxAttempts: number },\n ) => boolean;\n /**\n * ENG-8225 (AC3): the prompt-probe budget is exhausted and the rotated child\n * never came back — outcome `never-respawned`. Fired EXACTLY ONCE per stalled\n * rotation. Production use: manager-worker marks the integration unhealthy via\n * `/host/integration-health` and opens a `reaper_action` alert via\n * `/host/reaper-event` — the same two escalation paths `onGiveUp`/`onRestart`\n * already use, so this adds no new alerting channel.\n *\n * After escalating, the key is released to the pre-ENG-8225 path (it joins\n * `missing` on this and every later tick) rather than being suppressed\n * forever: the restart budget + ENG-6480 backoff already bound that path, and\n * permanent suppression of a genuinely dead MCP would be worse than a bounded\n * restart. Errors are swallowed — the reaper's correctness must not depend on\n * the callback.\n */\n onRotationRespawnGaveUp?: (\n codeName: string,\n serverKey: string,\n ctx: { attempts: number },\n ) => void;\n /**\n * ENG-8225 (AC2): fired for every observed rotation outcome — including the\n * healthy `respawned-on-tool-call` one — so the three outcomes are countable,\n * not just the failure. Errors are swallowed.\n */\n onRotationRespawnOutcome?: (\n codeName: string,\n serverKey: string,\n outcome: RotationRespawnOutcome,\n ) => void;\n /** ENG-8225: override the prompt-probe budget (default {@link MAX_ROTATION_PROMPT_PROBES}). */\n maxRotationPromptProbes?: number;\n /** ENG-8225: override the arm spacing (default {@link ROTATION_PROMPT_PROBE_INTERVAL_MS}). */\n rotationPromptProbeIntervalMs?: number;\n /** Test seam: substitute the `ps` invocation. */\n runPs?: () => string;\n /**\n * ENG-6660: the agent is Docker-isolated (AGT_ISOLATION=docker). When true,\n * the default `runPs` enumerates the agent's OWN container via\n * `docker exec agt-<codeName> ps` (its MCP children live in the container's PID\n * namespace, invisible to the host), and matching skips the claude-parent-walk\n * (the container is a single-agent boundary). Without this, every declared MCP\n * looks \"missing\" on the host -> restart loop -> reaper flapping.\n */\n isolated?: boolean;\n /** Test seam: substitute the wall clock for grace-window checks. */\n now?: () => number;\n /**\n * ENG-5292: invoked EXACTLY ONCE per (codeName, serverKey) when the\n * restart cap (ENG-5279) is first exceeded — i.e. the first poll\n * where we'd give up on the key. Gated by the same `gaveUpLogged`\n * flag that prevents duplicate log lines, so subsequent ticks while\n * the key remains given-up don't re-fire.\n *\n * Production use: manager-worker registers a callback that POSTs to\n * `/host/integration-health` with action=mark_unhealthy. The endpoint\n * flips agent_integrations.status, which makes /host/managed-toolkits\n * drop the broken MCP from its response, which causes the next\n * .mcp.json regeneration to omit it cleanly.\n *\n * Errors thrown by the callback are swallowed (logged) so an API\n * outage doesn't break the reaper itself. The give-up is recorded\n * locally regardless — at worst the integration stays in active\n * state on the DB side and the next give-up tick after the API\n * recovers will retry the call (the cap state still trips immediately\n * since `gaveUpLogged` is local to this process).\n */\n onGiveUp?: (codeName: string, serverKey: string) => void;\n /**\n * ENG-5286: invoked every time the reaper actually triggers a session\n * restart (i.e. just before `stopSession()`). Production use: manager-\n * worker registers a callback that POSTs to /host/mcp-reaper-restart\n * so the API can roll up restart counts per agent into a CloudWatch\n * metric + flapping alarm.\n *\n * Errors thrown by the callback are swallowed (logged) — the reaper's\n * own correctness must not depend on the API call succeeding.\n */\n onRestart?: (\n codeName: string,\n args: { activeKeys: string[]; givenUpKeys: string[] },\n ) => void | Promise<void>;\n /**\n * ENG-5932: classify a declared stdio server as 'essential' or 'optional'.\n * Only OPTIONAL servers can be quarantined; ESSENTIAL ones keep the legacy\n * declared-but-dead give-up behaviour. Default: always 'essential' — i.e.\n * quarantine is a strict no-op unless the caller wires a real classifier, so\n * every existing caller and test is byte-for-byte unchanged.\n */\n classifyKey?: (serverKey: string) => 'essential' | 'optional';\n /**\n * ENG-5932: minimum continuous-missing duration (epoch-ms delta, measured\n * from `firstMissingAt`) before an OPTIONAL key becomes a quarantine\n * candidate — a belt-and-braces time floor ON TOP of the restart budget so a\n * channel that's merely slow to respawn isn't quarantined. Default 0 (the\n * dwell gate is satisfied immediately; the restart budget alone governs).\n */\n quarantineDwellMs?: number;\n /**\n * ENG-8062: absolute continuous-missing floor (epoch-ms delta from\n * `continuouslyMissingSince`) past which a key is treated as genuinely dead\n * and overrides the ENG-8013 storm-awareness suppression — so a dead channel\n * reaches give-up / quarantine / backoff instead of churning forever under\n * external respawns. Default {@link DEAD_CHANNEL_MS} (15 min).\n */\n deadChannelMs?: number;\n /**\n * ENG-5932: 'off' | 'shadow' | 'enforce'. Default 'off'.\n * off — no quarantine logic at all (legacy behaviour).\n * shadow — compute the decision and LOG \"would quarantine X\", but take no\n * action (the session still gives up / stays declared-but-dead\n * exactly as before). For one bake cycle before enforcing.\n * enforce — additionally fire {@link onQuarantine} so the manager persists\n * the marker and drops the channel from the provisioned set.\n */\n quarantineMode?: 'off' | 'shadow' | 'enforce';\n /**\n * ENG-5932: fired EXACTLY ONCE per (codeName, serverKey) when an OPTIONAL key\n * is quarantined in enforce mode (gated by `quarantineLogged`). Production\n * use: manager-worker persists the quarantine marker to disk; the next\n * provisioning poll then omits the channel from `.mcp.json`. Errors are\n * swallowed (logged) — the reaper's correctness must not depend on it.\n */\n onQuarantine?: (codeName: string, serverKey: string) => void;\n}\n\nexport interface ReapMissingMcpSessionsResult {\n /** The declared server keys we found missing. May be empty when\n * everything is healthy, when the session is still in cold-start,\n * or when sessionStartedAt is unknown. */\n missing: string[];\n /** True iff we actually called stopSession() this invocation. */\n restarted: boolean;\n /** Stable reason code for the no-op path, useful in tests. */\n reason?:\n | 'healthy'\n | 'cold-start'\n | 'session-start-unknown'\n | 'no-declared-servers'\n | 'no-mcp-json'\n | 'restart-pending'\n | 'all-keys-over-cap'\n | 'backoff';\n /** ENG-5279: server keys that were missing but skipped because they\n * hit the consecutive-restart cap. The session would otherwise have\n * been restarted; we held off. Empty in the common case. */\n givenUp?: string[];\n /** ENG-6480: server keys that were missing and under the give-up ceilings\n * but whose escalating backoff interval had not yet elapsed — the restart\n * was held off this tick (it'll fire once the interval passes). Present\n * only when at least one missing key is backing off. */\n backingOff?: string[];\n /** ENG-5344: server keys that were missing but skipped because they\n * were rotated by stale-mcp-reaper within the rotation grace window.\n * Treated as in-flight (will respawn on next tool call), not missing. */\n rotationGraced?: string[];\n /** ENG-8225: keys whose rotation grace EXPIRED with the child still absent and\n * for which a prompt bind-probe was armed this tick. Excluded from `missing`\n * — the point of the probe is to avoid the whole-session restart. */\n rotationProbeArmed?: string[];\n /** ENG-8225: keys with a probe already armed and its {@link\n * ROTATION_PROMPT_PROBE_INTERVAL_MS} spacing not yet elapsed. Also excluded\n * from `missing` (we're waiting on the probe, not on a restart). */\n rotationProbeWaiting?: string[];\n /** ENG-8225: keys that exhausted the prompt-probe budget this tick — outcome\n * `never-respawned`, escalated, and released to the legacy restart path. */\n rotationProbeExhausted?: string[];\n /** ENG-8225 (AC2): rotation outcomes observed this invocation, keyed by server\n * key. Present only when at least one outcome was reached. */\n rotationRespawnOutcomes?: Record<string, RotationRespawnOutcome>;\n /** ENG-5932 (enforce mode): OPTIONAL keys quarantined this invocation —\n * onQuarantine fired, marker to be persisted by the caller. */\n quarantined?: string[];\n /** ENG-5932 (shadow mode): OPTIONAL keys that WOULD have been quarantined\n * had enforce mode been on. Logged, no action taken. */\n wouldQuarantine?: string[];\n /** ENG-8062: keys that have been continuously missing past `deadChannelMs`\n * and so overrode the ENG-8013 storm-awareness suppression this tick\n * (counted attempts / became quarantine-eligible despite an externally-\n * started generation). Present only when at least one key tripped it. */\n genuinelyDead?: string[];\n}\n\n/**\n * Run `ps`, find declared MCP servers that have no live children for\n * <codeName>, and trigger a session restart if any are missing AND the\n * cold-start grace has elapsed. Idempotent — repeated calls when a\n * restart is already in flight are harmless (stopSession is a no-op\n * once the tmux session is already gone).\n */\nexport function reapMissingMcpSessions(\n args: ReapMissingMcpSessionsArgs,\n): ReapMissingMcpSessionsResult {\n const {\n log,\n codeName,\n mcpJson,\n sessionStartedAt,\n restartPending = false,\n stopSession,\n graceMs = DEFAULT_COLD_START_GRACE_MS,\n rotationGraceMs = DEFAULT_ROTATION_GRACE_MS,\n onGiveUp,\n onRestart,\n // ENG-8225: unwired callbacks make the prompt-probe path a strict no-op —\n // `onRotationRespawnStalled` absent means \"not armed\", which falls through to\n // the pre-ENG-8225 restart. Legacy callers and tests are unchanged.\n onRotationRespawnStalled,\n onRotationRespawnGaveUp,\n onRotationRespawnOutcome,\n maxRotationPromptProbes = MAX_ROTATION_PROMPT_PROBES,\n rotationPromptProbeIntervalMs = ROTATION_PROMPT_PROBE_INTERVAL_MS,\n // ENG-5932: defaults make quarantine a strict no-op (classify everything\n // essential, mode off) so legacy callers/tests are unaffected.\n classifyKey = () => 'essential',\n quarantineDwellMs = 0,\n deadChannelMs = DEAD_CHANNEL_MS,\n quarantineMode = 'off',\n onQuarantine,\n isolated = false,\n // ENG-8013: default false → a \"no live child\" observation is only excluded\n // from the give-up/quarantine budget when the caller positively identifies\n // the current generation as externally-churned. Legacy callers/tests unchanged.\n currentGenerationExternallyStarted = false,\n } = args;\n const now = args.now ?? Date.now;\n // ENG-6660: under Docker the agent's claude + MCP children live in the\n // container's PID namespace; enumerate THAT (docker exec), not the host.\n const runPs =\n args.runPs ??\n (isolated\n ? () =>\n execFileSync('docker', ['exec', `agt-${codeName}`, 'ps', '-eo', 'pid,ppid,args'], {\n encoding: 'utf-8',\n timeout: 8_000,\n })\n : () => execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', timeout: 5_000 }));\n\n if (!mcpJson?.mcpServers) {\n return { missing: [], restarted: false, reason: 'no-mcp-json' };\n }\n const declaredCount = Object.keys(mcpJson.mcpServers).length;\n if (declaredCount === 0) {\n return { missing: [], restarted: false, reason: 'no-declared-servers' };\n }\n\n // ENG-6486: a session restart is already scheduled/deferred for this agent.\n // Any declared-but-not-yet-live MCP will be spawned by that pending restart,\n // so flagging it now is a false positive — and worse, our own restart would\n // also be deferred while still firing onRestart (inflating the flapping\n // metric → false agent_reaper_flapping alarm). Skip entirely; the pending\n // restart, followed by the cold-start grace, covers the add→spawn window.\n if (restartPending) {\n return { missing: [], restarted: false, reason: 'restart-pending' };\n }\n\n // Skip rather than risk a false-positive when we don't know when the\n // session started. The manager-worker passes null for agents whose\n // PersistentSession state hasn't been populated yet.\n if (sessionStartedAt === null) {\n return { missing: [], restarted: false, reason: 'session-start-unknown' };\n }\n\n // Cold-start grace: skip if the session is still in its grace window.\n if (now() - sessionStartedAt < graceMs) {\n return { missing: [], restarted: false, reason: 'cold-start' };\n }\n\n let psOutput: string;\n try {\n psOutput = runPs();\n } catch (err) {\n log(`[mcp-presence-reaper] ps invocation failed for '${codeName}': ${(err as Error).message} — skipping`);\n return { missing: [], restarted: false, reason: 'healthy' };\n }\n\n const rows = parsePsRows(psOutput);\n const missingRaw = findMissingMcpServers({ rows, codeName, mcpJson, inContainer: isolated });\n\n // ENG-5344: stale-mcp-reaper rotation grace. When stale-reaper SIGTERMs\n // an MCP child during credential rotation, the child won't come back\n // until Claude Code makes its next tool call. Within the grace window,\n // treat \"no live children\" for that key as in-flight, not missing —\n // pre-fix this was the entire root cause of Stirling's 54 daily false-\n // positive restarts (and matching kills on don/phil/maven).\n const rotationGraced: string[] = [];\n // ENG-8225: the three buckets for a rotation whose grace expired with the child\n // still absent. Keys in `rotationProbeArmed` / `rotationProbeWaiting` are held\n // OUT of `missing` — a prompt bind-probe is the cheap targeted repair, and\n // falling through to `missing` would tear down the whole session, which is the\n // outcome this ticket exists to prevent.\n const rotationProbeArmed: string[] = [];\n const rotationProbeWaiting: string[] = [];\n const rotationProbeExhausted: string[] = [];\n const rotationRespawnOutcomes: Record<string, RotationRespawnOutcome> = {};\n const recordRotationOutcome = (key: string, outcome: RotationRespawnOutcome): void => {\n rotationRespawnOutcomes[key] = outcome;\n if (!onRotationRespawnOutcome) return;\n try {\n onRotationRespawnOutcome(codeName, key, outcome);\n } catch (err) {\n log(\n `[mcp-presence-reaper] onRotationRespawnOutcome callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${(err as Error).message}`,\n );\n }\n };\n const missing: string[] = [];\n for (const key of missingRaw) {\n const rotation = classifyRotation(codeName, key, rotationGraceMs, now);\n if (rotation === 'within-grace') {\n rotationGraced.push(key);\n continue;\n }\n if (rotation === 'none') {\n // ENG-8225: no rotation on record, so any stall state under this key\n // describes a rotation the tracker has already forgotten (the child never\n // came back and the record aged past ROTATION_TRACKING_RETENTION_MS). Drop\n // it here rather than leaving a dead entry in the map — the stamp check\n // below is what makes correctness independent of this cleanup firing, but\n // an entry nothing will ever consult should not linger.\n rotationStallState.delete(stateKey(codeName, key));\n missing.push(key);\n continue;\n }\n // ENG-8225: rotated, grace expired, still no live children. ENG-5344's grace\n // window banked on \"the child respawns on the next tool call\" — and nothing\n // makes that tool call happen. dwight's `anchor-browser` child died at\n // 16:00:05 and never came back because dwight had no reason to call an\n // anchor-browser tool all evening; the rarer the server, the longer the hole\n // stays open, which inverts the risk.\n //\n // We cannot respawn the child ourselves: a stdio MCP child belongs to Claude\n // Code, on the session's own stdio pipes, and findMcpChildrenForAgent only\n // counts children whose ppid chain reaches a `claude` ancestor — a\n // manager-spawned child would be invisible HERE and unreachable by any tool\n // call. So we cause the next tool call instead, by arming ENG-7429's prompt\n // bind-probe.\n const sk = stateKey(codeName, key);\n // ENG-8225 follow-up: `rotationStallState` is keyed by (agent, serverKey),\n // which outlives any single rotation, so an entry is only meaningful for the\n // rotation that created it. The rotation's SIGTERM timestamp is its identity\n // (grace and retention are both measured from it, and a new SIGTERM\n // overwrites it), so we anchor the entry to that stamp and treat a mismatch\n // as \"no state\": a NEW rotation always starts with a full probe budget and an\n // unset escalation flag.\n //\n // Why the stamp rather than clearing the entry at each place a rotation could\n // end: the cleanup sites are all conditional on the reaper OBSERVING an\n // intermediate state (a 'within-grace' tick, a 'none' tick, the child seen\n // live). None of those is guaranteed — at a 10s poll cadence against a 15s\n // grace, one slow tick is enough for an 'expired' read to be the FIRST\n // observation of a brand-new rotation, and a stalled child that never returns\n // is never seen live at all. That gap is precisely the leak this fixes:\n // pre-fix, one escalated rotation left `escalated: true` under the key\n // forever, so every later rotation of that server skipped the probe path and\n // went straight to a whole-session restart — the flapping ENG-8225 exists to\n // remove. Comparing identity at the single READ site cannot miss a cycle,\n // whereas adding another flag to remember \"this escalation is stale\" would\n // just be more unanchored state of the same shape.\n //\n // `?? now()` is unreachable (classifyRotation returned 'expired', which\n // requires a live record) and only avoids a non-null assertion; if it ever\n // did fire, a stamp of `now()` still forces a fresh budget, which is the safe\n // direction.\n const rotationRecordedAt = getRotationRecordedAt(codeName, key) ?? now();\n const priorStall = rotationStallState.get(sk);\n const stall: RotationStallState =\n priorStall && priorStall.rotationRecordedAt === rotationRecordedAt\n ? priorStall\n : {\n rotationRecordedAt,\n probeAttempts: 0,\n lastProbeAt: null,\n escalated: false,\n };\n if (priorStall && priorStall !== stall) {\n log(\n `[mcp-presence-reaper] '${codeName}:${key}' discarding stall state from a previous rotation (rotation=${priorStall.rotationRecordedAt}, probes=${priorStall.probeAttempts}, escalated=${priorStall.escalated}) — this is a NEW rotation (rotation=${rotationRecordedAt}) and gets a full prompt-probe budget (ENG-8225)`,\n );\n }\n rotationStallState.set(sk, stall);\n if (stall.escalated) {\n // Budget already spent and the escalation already fired — hand the key back\n // to the pre-ENG-8225 path rather than suppressing it forever.\n missing.push(key);\n continue;\n }\n const spacingElapsed =\n stall.lastProbeAt === null || now() - stall.lastProbeAt >= rotationPromptProbeIntervalMs;\n if (!spacingElapsed) {\n // A probe is armed and hasn't had its fair chance yet. Waiting is the whole\n // point; don't restart, don't burn another attempt.\n rotationProbeWaiting.push(key);\n continue;\n }\n if (stall.probeAttempts >= maxRotationPromptProbes) {\n // AC3: escalate rather than degrade into an endless session-restart loop.\n stall.escalated = true;\n rotationProbeExhausted.push(key);\n recordRotationOutcome(key, 'never-respawned');\n log(\n `[mcp-presence-reaper] '${codeName}:${key}' rotation outcome=never-respawned — child SIGTERM'd by stale-mcp-reaper never returned after ${stall.probeAttempts} prompt bind-probe(s); escalating (integration marked unhealthy + reaper_action alert) and releasing to the normal restart path (ENG-8225)`,\n );\n if (onRotationRespawnGaveUp) {\n try {\n onRotationRespawnGaveUp(codeName, key, { attempts: stall.probeAttempts });\n } catch (err) {\n log(\n `[mcp-presence-reaper] onRotationRespawnGaveUp callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${(err as Error).message}`,\n );\n }\n }\n missing.push(key);\n continue;\n }\n // Arm the next prompt bind-probe. Fails OPEN: an unwired / disabled / throwing\n // arm means no probe is coming, so suppressing the restart would leave the\n // agent with a dead MCP and nothing working to fix it — worse than today.\n const attempt = stall.probeAttempts + 1;\n let armed = false;\n if (onRotationRespawnStalled) {\n try {\n armed = onRotationRespawnStalled(codeName, key, {\n attempt,\n maxAttempts: maxRotationPromptProbes,\n });\n } catch (err) {\n log(\n `[mcp-presence-reaper] onRotationRespawnStalled callback threw for '${codeName}:${key}' (suppressed; falling back to the pre-ENG-8225 restart path): ${(err as Error).message}`,\n );\n armed = false;\n }\n }\n if (!armed) {\n missing.push(key);\n continue;\n }\n stall.probeAttempts = attempt;\n stall.lastProbeAt = now();\n rotationProbeArmed.push(key);\n log(\n `[mcp-presence-reaper] '${codeName}:${key}' rotation grace expired (${rotationGraceMs}ms) with no live child and no tool call to respawn it — armed prompt bind-probe ${attempt}/${maxRotationPromptProbes} (ENG-7429) instead of restarting the session (ENG-8225)`,\n );\n }\n if (rotationGraced.length > 0) {\n log(\n `[mcp-presence-reaper] '${codeName}': skipping [${rotationGraced.join(', ')}] within rotation grace window (${rotationGraceMs}ms) — child(ren) just SIGTERM'd by stale-mcp-reaper, awaiting respawn on next tool call (ENG-5344)`,\n );\n }\n if (rotationProbeWaiting.length > 0) {\n log(\n `[mcp-presence-reaper] '${codeName}': skipping [${rotationProbeWaiting.join(', ')}] — prompt bind-probe already armed for the stalled rotation, waiting out the ${rotationPromptProbeIntervalMs}ms re-arm interval (ENG-8225)`,\n );\n }\n\n // ENG-5279: update per-(agent, server) recovery state on EVERY tick so\n // a key that came back to life — even briefly — resets its counter.\n // Walk the full declared stdio set (not just `missing`), so transitions\n // from missing→live clear the attempt count immediately. Without this\n // a flapping MCP would accumulate restarts forever.\n //\n // ENG-5344: use the UNFILTERED `missingRaw` to classify liveness — a\n // rotation-graced key has zero live children right now, so it must\n // not reset the cap counter (which would let a chronically broken MCP\n // never reach give-up if it gets rotated frequently).\n const declared = mcpJson.mcpServers ?? {};\n const liveKeys = new Set<string>();\n for (const key of Object.keys(declared)) {\n const entry = (declared as Record<string, unknown>)[key] as { command?: unknown } | undefined;\n if (typeof entry?.command !== 'string') continue; // stdio-only\n if (!missingRaw.includes(key)) liveKeys.add(key);\n }\n // ENG-8225 (AC2): a live key with a rotation still on record means the rotation\n // COMPLETED — the child is back. Attribute it: if we had to manufacture a tool\n // call with ENG-7429's prompt probe it's `respawned-after-prompt-probe`,\n // otherwise the agent called a tool on its own and ENG-5344's assumption held\n // (`respawned-on-tool-call`). Then forget the rotation, so a later disappearance\n // is treated as a fresh fault rather than this rotation's tail.\n //\n // Runs OUTSIDE the `if (state)` guard below: a key that has been healthy all\n // along has no presenceReaperState entry, and its respawn is exactly the\n // outcome we most want counted.\n for (const key of liveKeys) {\n const sk = stateKey(codeName, key);\n if (classifyRotation(codeName, key, rotationGraceMs, now) === 'none') {\n // ENG-8225 follow-up: no rotation on record, so there is no outcome to\n // attribute — but a stall entry under this key describes a rotation that is\n // definitively over (this is the path a key takes when it goes live only\n // AFTER its record aged past ROTATION_TRACKING_RETENTION_MS, which the\n // pre-fix `continue` skipped, stranding the entry). Drop it.\n rotationStallState.delete(sk);\n continue;\n }\n const stall = rotationStallState.get(sk);\n const viaProbe = (stall?.probeAttempts ?? 0) > 0;\n recordRotationOutcome(\n key,\n viaProbe ? 'respawned-after-prompt-probe' : 'respawned-on-tool-call',\n );\n // One line per completed rotation, carrying the outcome value verbatim so the\n // three-way split is greppable/countable from manager.log alone (AC2). Cheap:\n // the rotation record is cleared immediately below, so this fires once per\n // rotation, not once per poll.\n log(\n viaProbe\n ? `[mcp-presence-reaper] '${codeName}:${key}' rotation outcome=respawned-after-prompt-probe — child is live again after ${stall?.probeAttempts} prompt bind-probe(s)${\n stall?.escalated\n ? ' (recovered AFTER escalation — the earlier never-respawned verdict stands for that window)'\n : ''\n }, no session restart needed (ENG-8225)`\n : `[mcp-presence-reaper] '${codeName}:${key}' rotation outcome=respawned-on-tool-call — child returned on the agent's own next tool call, ENG-5344's assumption held (ENG-8225)`,\n );\n clearStaleRotation(codeName, [key]);\n rotationStallState.delete(sk);\n }\n for (const key of liveKeys) {\n const state = presenceReaperState.get(stateKey(codeName, key));\n if (state) {\n state.attempts = 0;\n state.lastSeenLiveAt = now();\n state.lastAttemptedSessionStartedAt = null;\n state.gaveUpLogged = false;\n // ENG-5932: the key recovered — clear the dwell clock + quarantine log\n // gate so a future failure stretch starts fresh (a channel that comes\n // back to life must not carry a stale firstMissingAt toward quarantine).\n state.firstMissingAt = null;\n state.quarantineLogged = false;\n // ENG-8062: real liveness is the ONLY thing that resets the genuinely-dead\n // clock (storm-awareness never touches it), so a channel that actually\n // recovers sheds its dead-channel status cleanly.\n state.continuouslyMissingSince = null;\n state.deadChannelLogged = false;\n // ENG-6480: the key is live again — it's no longer given up.\n state.gaveUp = false;\n // ENG-6480: track the START of this continuous-live stretch (only when\n // null; do NOT advance it on every live poll, or the dwell never\n // accrues). Once continuously live past BACKOFF_STABLE_RESET_MS, clear\n // the restart history — that's genuine recovery. A crash-loop's green\n // blip never reaches the dwell because the next miss nulls this below.\n if (state.stableLiveSince === null) state.stableLiveSince = now();\n if (now() - state.stableLiveSince >= BACKOFF_STABLE_RESET_MS) {\n state.restartHistory = [];\n }\n }\n }\n\n if (missing.length === 0) {\n return {\n missing,\n restarted: false,\n reason: 'healthy',\n rotationGraced: rotationGraced.length > 0 ? rotationGraced : undefined,\n rotationProbeArmed: rotationProbeArmed.length > 0 ? rotationProbeArmed : undefined,\n rotationProbeWaiting: rotationProbeWaiting.length > 0 ? rotationProbeWaiting : undefined,\n rotationProbeExhausted:\n rotationProbeExhausted.length > 0 ? rotationProbeExhausted : undefined,\n rotationRespawnOutcomes:\n Object.keys(rotationRespawnOutcomes).length > 0 ? rotationRespawnOutcomes : undefined,\n };\n }\n\n // Tally attempts and partition missing into active (under cap) vs\n // given-up (at or over cap). Per-generation guard via\n // `lastAttemptedSessionStartedAt`: only count an attempt when the\n // manager has spawned a fresh session since our last increment for\n // this key. A slow respawn (multiple reaper polls between\n // `stopSession()` and the new session actually coming up) must NOT\n // burn the cap — the manager's stop wrapper is idempotent and\n // those extra calls aren't real restart cycles.\n const nowMs = now();\n const givenUp: string[] = [];\n const active: string[] = [];\n // ENG-6480: missing keys under the give-up ceilings whose escalating backoff\n // interval hasn't elapsed — held off this tick, NOT a restart driver.\n const backingOff: string[] = [];\n // ENG-6480: keys for which THIS tick is a fresh session generation. Only a\n // new generation may append to restartHistory — repeated polls during a\n // single in-flight restart (same sessionStartedAt) must not inflate the\n // window count, exactly like the `attempts` per-generation guard.\n const newGenerationKeys = new Set<string>();\n // ENG-5932: OPTIONAL keys quarantined (enforce) / would-be-quarantined\n // (shadow) this invocation.\n const quarantined: string[] = [];\n const wouldQuarantine: string[] = [];\n // ENG-8062: keys that tripped the genuinely-dead floor and so overrode the\n // ENG-8013 storm-awareness suppression this tick.\n const genuinelyDead: string[] = [];\n for (const key of missing) {\n const sk = stateKey(codeName, key);\n const state = presenceReaperState.get(sk) ?? {\n attempts: 0,\n lastSeenLiveAt: null,\n lastAttemptedSessionStartedAt: null,\n gaveUpLogged: false,\n firstMissingAt: null,\n quarantineLogged: false,\n restartHistory: [],\n stableLiveSince: null,\n continuouslyMissingSince: null,\n deadChannelLogged: false,\n gaveUp: false,\n };\n // ENG-5932: stamp the start of this missing stretch (cleared on liveness in\n // the reset loop above). Drives the quarantine dwell floor. Set here — i.e.\n // only AFTER the cold-start grace returned early — so the dwell is measured\n // from \"missing past cold start\", which is exactly the floor we want.\n //\n // ENG-8062: the never-re-based continuous-missing clock. Set once when the\n // stretch begins and cleared ONLY by real liveness (the reset loop above),\n // so storm-awareness can't keep it perpetually young. Once it's older than\n // `deadChannelMs` the key is genuinely dead — a storm doesn't keep a channel\n // missing on every poll for 15 min without it ever coming live — and the\n // externally-started-generation excuse no longer applies.\n if (state.continuouslyMissingSince === null) {\n state.continuouslyMissingSince = nowMs;\n }\n const isGenuinelyDead = nowMs - state.continuouslyMissingSince >= deadChannelMs;\n // Suppress budget/dwell as churn collateral ONLY while the storm excuse\n // still holds (ENG-8013) — never once the key is genuinely dead (ENG-8062).\n const suppressAsChurn = currentGenerationExternallyStarted && !isGenuinelyDead;\n if (isGenuinelyDead) {\n genuinelyDead.push(key);\n if (currentGenerationExternallyStarted && !state.deadChannelLogged) {\n state.deadChannelLogged = true;\n log(\n `[mcp-presence-reaper] '${codeName}:${key}' continuously missing ${Math.round((nowMs - state.continuouslyMissingSince) / 60_000)}m (> ${Math.round(deadChannelMs / 60_000)}m) — genuinely dead, overriding storm-awareness so it can reach give-up/quarantine instead of churning (ENG-8062)`,\n );\n }\n }\n // ENG-8013 (storm-awareness): the dwell measures \"continuously missing across\n // a STABLE session\". When the current generation was born from an EXTERNAL\n // respawn (or a storm), re-base the dwell clock to now so it can only elapse\n // once the session has been churn-free for the full window, so a channel that's\n // missing purely as collateral of respawn churn never accrues dwell — but stop\n // re-basing once the key is genuinely dead so the dwell can finally elapse.\n if (state.firstMissingAt === null || suppressAsChurn) {\n state.firstMissingAt = nowMs;\n }\n // ENG-6480: the key is missing → its continuous-live stretch is broken, so\n // a prior brief green blip cannot accrue toward the stable-reset dwell.\n state.stableLiveSince = null;\n if (state.lastAttemptedSessionStartedAt !== sessionStartedAt) {\n // Advance the per-generation guard either way so we don't re-evaluate this\n // generation on later polls within it. But only a generation the reaper\n // itself restarted is a genuine failed recovery; an externally-started\n // generation is churn collateral (ENG-8013), so it must NOT increment the\n // give-up/quarantine budget or record a restart-history entry — UNLESS the\n // key is genuinely dead (ENG-8062), in which case we DO count it so it\n // reaches the give-up/quarantine terminal.\n state.lastAttemptedSessionStartedAt = sessionStartedAt;\n if (!suppressAsChurn) {\n state.attempts += 1;\n newGenerationKeys.add(key);\n }\n }\n // ENG-6480: prune the restart history to the rolling window, then derive\n // the rate-based give-up + escalating backoff from what remains.\n state.restartHistory = state.restartHistory.filter((t) => nowMs - t < BACKOFF_WINDOW_MS);\n const restartsInWindow = state.restartHistory.length;\n presenceReaperState.set(sk, state);\n const overAttemptsCap = state.attempts > MAX_PRESENCE_RESTART_ATTEMPTS;\n const overRateCap = restartsInWindow >= BACKOFF_GIVE_UP_RESTARTS;\n if (overAttemptsCap || overRateCap) {\n givenUp.push(key);\n state.gaveUp = true;\n if (!state.gaveUpLogged) {\n const cause = overAttemptsCap\n ? `${MAX_PRESENCE_RESTART_ATTEMPTS} consecutive failed restarts — declared but never recovers (likely orphaned config; see ENG-5279)`\n : `${restartsInWindow} restarts within ${Math.round(BACKOFF_WINDOW_MS / 60_000)}m — persistently flapping (crash-loop; ENG-6480)`;\n log(\n `[mcp-presence-reaper] giving up on '${codeName}:${key}' after ${cause}`,\n );\n state.gaveUpLogged = true;\n // ENG-5292: fire the give-up callback exactly once per\n // (codeName, serverKey). Manager-worker uses this to POST to\n // /host/integration-health so the broken MCP gets marked\n // unhealthy and dropped from the next .mcp.json regeneration.\n // Swallow errors — the reaper's own correctness must not\n // depend on the callback succeeding.\n if (onGiveUp) {\n try {\n onGiveUp(codeName, key);\n } catch (err) {\n log(\n `[mcp-presence-reaper] onGiveUp callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${(err as Error).message}`,\n );\n }\n }\n }\n // ENG-5932: quarantine decision, folded into the give-up terminal state\n // rather than bolted on as a separate branch. A key is a quarantine\n // candidate only once it has (a) exhausted the restart budget — the same\n // ENG-5279 cap that put it in `givenUp` — AND (b) sat missing past the\n // dwell floor. Gated to OPTIONAL keys; ESSENTIAL (incl. everything\n // unclassified — fail closed) keeps the legacy declared-but-dead path,\n // so the agent's control-plane channels can never be quarantined.\n // Fires once per key (quarantineLogged), reset on liveness.\n if (\n quarantineMode !== 'off' &&\n !state.quarantineLogged &&\n classifyKey(key) === 'optional'\n ) {\n const dwellElapsed =\n quarantineDwellMs <= 0 ||\n (state.firstMissingAt !== null && nowMs - state.firstMissingAt >= quarantineDwellMs);\n if (dwellElapsed) {\n state.quarantineLogged = true;\n if (quarantineMode === 'enforce') {\n quarantined.push(key);\n log(\n `[mcp-presence-reaper] QUARANTINE '${codeName}:${key}' — optional channel dead past restart budget + ${quarantineDwellMs}ms dwell; dropping from provisioned set so the dead channel stops restarting the whole session (ENG-5932)`,\n );\n if (onQuarantine) {\n try {\n onQuarantine(codeName, key);\n } catch (err) {\n log(\n `[mcp-presence-reaper] onQuarantine callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${(err as Error).message}`,\n );\n }\n }\n } else {\n // shadow — observe only; legacy give-up behaviour is unchanged.\n wouldQuarantine.push(key);\n log(\n `[mcp-presence-reaper] SHADOW would-quarantine '${codeName}:${key}' — optional channel dead past restart budget + ${quarantineDwellMs}ms dwell; no action taken (ENG-5932 shadow mode)`,\n );\n }\n }\n }\n } else {\n // ENG-6480: under both give-up ceilings — decide whether to restart now\n // or back off. The first BACKOFF_AFTER_RESTARTS restarts are immediate\n // (backoffMs = 0), preserving the orphan/transient path exactly; beyond\n // that the interval doubles (capped at BACKOFF_MAX_MS) so a crash-looper\n // is spaced out instead of bouncing every poll.\n const backoffMs =\n restartsInWindow < BACKOFF_AFTER_RESTARTS\n ? 0\n : Math.min(\n BACKOFF_BASE_MS * 2 ** (restartsInWindow - BACKOFF_AFTER_RESTARTS),\n BACKOFF_MAX_MS,\n );\n const lastRestartAt = state.restartHistory[state.restartHistory.length - 1] ?? null;\n if (lastRestartAt !== null && nowMs - lastRestartAt < backoffMs) {\n backingOff.push(key);\n } else {\n active.push(key);\n }\n }\n }\n\n if (active.length === 0) {\n // No missing key is eligible to drive a restart this tick. Distinguish\n // the two no-op causes: at least one key is merely waiting out its\n // backoff interval ('backoff', ENG-6480) vs every key has hit a give-up\n // ceiling ('all-keys-over-cap'). Stays quiet on repeat give-up ticks\n // (gaveUpLogged guard); logs once per backoff hold so the spacing is\n // visible without spamming.\n const reason: 'backoff' | 'all-keys-over-cap' =\n backingOff.length > 0 ? 'backoff' : 'all-keys-over-cap';\n if (backingOff.length > 0) {\n log(\n `[mcp-presence-reaper] '${codeName}': holding off restart for [${backingOff.join(', ')}] — escalating backoff interval not yet elapsed (ENG-6480)${\n givenUp.length > 0 ? ` (given up: [${givenUp.join(', ')}])` : ''\n }`,\n );\n }\n return {\n missing,\n restarted: false,\n reason,\n givenUp,\n backingOff: backingOff.length > 0 ? backingOff : undefined,\n rotationGraced: rotationGraced.length > 0 ? rotationGraced : undefined,\n rotationProbeArmed: rotationProbeArmed.length > 0 ? rotationProbeArmed : undefined,\n rotationProbeWaiting: rotationProbeWaiting.length > 0 ? rotationProbeWaiting : undefined,\n rotationProbeExhausted:\n rotationProbeExhausted.length > 0 ? rotationProbeExhausted : undefined,\n rotationRespawnOutcomes:\n Object.keys(rotationRespawnOutcomes).length > 0 ? rotationRespawnOutcomes : undefined,\n quarantined: quarantined.length > 0 ? quarantined : undefined,\n wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : undefined,\n genuinelyDead: genuinelyDead.length > 0 ? genuinelyDead : undefined,\n };\n }\n\n // Audit line: emit the missing key(s) so we can spot patterns (\"xero\n // crashes every 4h\") without grepping pane logs. Single line per\n // restart event by design. Mention given-up keys too so the operator\n // sees the full picture.\n const givenUpSuffix = givenUp.length > 0\n ? ` (skipping over-cap: [${givenUp.join(', ')}])`\n : '';\n log(\n `[mcp-presence-reaper] '${codeName}': declared MCP(s) [${active.join(', ')}] have no live children — restarting session${givenUpSuffix}`,\n );\n // ENG-5286: fire the restart-observer callback before stopSession so\n // the API records the event with the exact key set that prompted the\n // restart. Errors are swallowed — reaper correctness wins.\n if (onRestart) {\n try {\n // CodeRabbit on PR #1186: callbacks may be sync OR async.\n // The synchronous try/catch catches throws from sync callbacks\n // and the synchronous part of async ones; the .catch() below\n // catches rejections from async callbacks so they don't escape\n // as unhandled promise rejections.\n const maybePromise = onRestart(codeName, { activeKeys: active, givenUpKeys: givenUp });\n if (\n maybePromise &&\n typeof (maybePromise as PromiseLike<void>).then === 'function'\n ) {\n void (maybePromise as PromiseLike<void>).then(undefined, (err: unknown) => {\n log(\n `[mcp-presence-reaper] onRestart callback rejected for '${codeName}' (suppressed; restart proceeds): ${(err as Error).message}`,\n );\n });\n }\n } catch (err) {\n log(\n `[mcp-presence-reaper] onRestart callback threw for '${codeName}' (suppressed; restart proceeds): ${(err as Error).message}`,\n );\n }\n }\n stopSession(codeName, { activeKeys: active });\n // ENG-6480: record this restart against each key that drove it, so the\n // window count grows and the escalating backoff / rate give-up engage on\n // the next ticks. Only count it once per fresh session generation — a slow\n // respawn produces several same-generation polls that all (idempotently)\n // call stopSession, but they are ONE restart cycle, not many. (A key still\n // in `backingOff` did not drive this restart, so it isn't recorded — it gets\n // respawned by the global restart anyway.)\n for (const key of active) {\n if (!newGenerationKeys.has(key)) continue;\n const st = presenceReaperState.get(stateKey(codeName, key));\n if (st) st.restartHistory.push(nowMs);\n }\n return {\n missing,\n restarted: true,\n givenUp: givenUp.length > 0 ? givenUp : undefined,\n backingOff: backingOff.length > 0 ? backingOff : undefined,\n rotationGraced: rotationGraced.length > 0 ? rotationGraced : undefined,\n rotationProbeArmed: rotationProbeArmed.length > 0 ? rotationProbeArmed : undefined,\n rotationProbeWaiting: rotationProbeWaiting.length > 0 ? rotationProbeWaiting : undefined,\n rotationProbeExhausted:\n rotationProbeExhausted.length > 0 ? rotationProbeExhausted : undefined,\n rotationRespawnOutcomes:\n Object.keys(rotationRespawnOutcomes).length > 0 ? rotationRespawnOutcomes : undefined,\n quarantined: quarantined.length > 0 ? quarantined : undefined,\n wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : undefined,\n genuinelyDead: genuinelyDead.length > 0 ? genuinelyDead : undefined,\n };\n}\n","/**\n * ENG-5441 — circuit breaker for manager-initiated session restarts.\n *\n * Today's Scout incident on prod-integritylabs-agt-aws-1 respawned an\n * agent every ~55s for hours (~100 spawns/hr) because day-rollover\n * ([ENG-5431]) repeated indefinitely. Each respawn burned subscription\n * budget, flooded pane.log, drowned manager.log signal, and re-floods\n * `/host/direct-chat/poll`. The per-MCP reaper has a give-up cap\n * ([ENG-5279]) and the [ENG-5286] alarm fires when the cap-attempts\n * cross a threshold — but neither *stopped* the whole-agent loop.\n *\n * This breaker sits at the manager's spawn gate. Every manager-initiated\n * stop (day-rollover, auth-tuple change, reaper trigger, hot-reload\n * .mcp.json restart, channel-set restart) records a restart event. When\n * `> AGT_RESTART_BREAKER_MAX` events land inside the sliding\n * `AGT_RESTART_BREAKER_WINDOW_MS` window, the agent's breaker trips:\n * the manager stops respawning until an operator clears the trip (by\n * resuming the agent in the webapp / via the CLI).\n *\n * ENG-7560 — reason-aware tallies. Adding one integration fans out into a\n * burst of restarts (doorbell respawn + managed-toolkit convergence +\n * `.mcp.json` drift + bind-remediation), all tagged provisioning-reload\n * reasons; counting that burst on the tight crash bar auto-paused Pepper on\n * an Outlook add. So provisioning-reload reasons (see\n * {@link isProvisioningReloadReason}) are counted on a SEPARATE, looser tally\n * (`AGT_RESTART_BREAKER_PROVISIONING_MAX` in\n * `AGT_RESTART_BREAKER_PROVISIONING_WINDOW_MS`); a routine add can't trip it,\n * but sustained provisioning thrash still accumulates and trips+pages. Crash /\n * all other reasons keep the tight bar, counted exactly as before. Every event\n * is still recorded — only the trip threshold is class-aware — so relaxing the\n * bar can never silently defeat the breaker.\n *\n * ENG-7812: per-integration provisioning buckets. The single looser provisioning\n * tally still auto-paused a HEALTHY agent mid-onboarding: bringing up 5+\n * integrations legitimately fires 6+ provisioning-reload restarts in 30 min\n * (each add fans out into `integration-change` + `hot-reload-mcp`), clearing the\n * flat >5 bar even though nothing is wrong. The bar couldn't tell \"many distinct\n * integrations each binding once\" (onboarding) from \"one integration looping\"\n * (the pathology). So the provisioning trip is now bucketed by the integration a\n * restart is attributable to ({@link RestartEvent.integrationKey}, supplied by the\n * manager from a same-window membership delta) and fires on the LARGEST single\n * bucket, not the total. Onboarding N distinct adds ⇒ N buckets of ~1 ⇒ no trip;\n * one integration re-binding >5 times ⇒ one overflowing bucket ⇒ trip+page.\n * Unattributed provisioning reloads (a reprovision of an unchanged set) share one\n * `''` bucket that still trips on its own, so the breaker keeps its teeth.\n *\n * Pure state, no I/O — the caller (manager-worker.ts) owns serialisation\n * to `manager-state.json` so a manager restart preserves trip state and\n * doesn't accidentally clear a tripped breaker. Tests drive `now()` via\n * the constructor to avoid sleeping in CI.\n */\n\nimport {\n RESTART_BREAKER_PROVISIONING_MAX,\n RESTART_BREAKER_PROVISIONING_WINDOW_MS,\n RESTART_BREAKER_OPERATOR_MAX,\n BIND_FAILURE_QUARANTINE_THRESHOLD,\n MIN_PROVISIONING_MAX_FOR_QUARANTINE_ORDERING,\n} from '@augmented/core';\n\nexport interface RestartBreakerOptions {\n /** Max restart events allowed inside the window; the (max+1)th trips. Default 2 (env: AGT_RESTART_BREAKER_MAX). */\n max?: number;\n /** Sliding window in ms. Default 600_000 = 10 min (env: AGT_RESTART_BREAKER_WINDOW_MS). */\n windowMs?: number;\n /**\n * ENG-7560: max provisioning-reload restart events allowed inside the\n * provisioning window; the (max+1)th trips. Default 5\n * (env: AGT_RESTART_BREAKER_PROVISIONING_MAX). Looser than `max` so a single\n * integration-add's 3-4 restart burst can't pause the agent.\n */\n provisioningMax?: number;\n /**\n * ENG-7560: sliding window in ms for the provisioning-reload tally. Default\n * 1_800_000 = 30 min (env: AGT_RESTART_BREAKER_PROVISIONING_WINDOW_MS). Wider\n * than `windowMs` so sustained provisioning thrash still accumulates and trips.\n */\n provisioningWindowMs?: number;\n /**\n * ENG-8388: budget for provisioning reloads a console user demonstrably asked\n * for, counted per integration over `provisioningWindowMs`. Defaults to\n * RESTART_BREAKER_OPERATOR_MAX (looser than `provisioningMax`); env override\n * `AGT_RESTART_BREAKER_OPERATOR_MAX`.\n */\n operatorMax?: number;\n /** Injectable clock for tests. */\n now?: () => number;\n /**\n * ENG-7891 slice 5b (ADR-0049 durable-state rule): resolve a code_name to the\n * rename-stable DURABLE key used for the events/trips maps and their\n * manager-state.json persistence. On an id-keyed host this returns the agent's\n * stable agent_id, so a trip that paused a crash-looping agent survives a\n * codename rename instead of being stranded under the old codename (which would\n * silently un-pause the agent the operator meant to keep down). Defaults to\n * identity, so the legacy fleet (no symlink) keys by code_name exactly as\n * before — a pure no-op until the id-keyed layout is armed. Injected rather than\n * importing `agentRuntimeKey` here so the class stays pure \"no I/O\" state for\n * its unit tests; the manager wires in the real (fs-backed) resolver.\n */\n runtimeKey?: (codeName: string) => string;\n}\n\n/** All known restart trigger sources. Adding a new source? Add it here so the trip detail is self-describing. */\nexport type RestartReason =\n | 'day-rollover'\n | 'auth-tuple-change'\n | 'model-change'\n | 'mcp-presence-reaper'\n | 'hot-reload-mcp'\n // ENG-7239: a .mcp.json drift restart whose changed server keys are ALL\n // managed-MCP/brokered toolkits (Composio and friends). Split from the\n // generic 'hot-reload-mcp' so the breaker carve-out for a flapping managed-MCP\n // set applies ONLY to managed-MCP churn, never to a co-occurring channel or\n // credential restart that happens to share the cool-off (CodeRabbit PR #2870).\n // Gated and respawn-verified identically to 'hot-reload-mcp'.\n | 'managed-mcp-churn'\n // ENG-7576: a dashboard-initiated integration add/remove reload\n // (host_agents.restart_reason='integration-change', mirroring the API's\n // RESTART_REASON_INTEGRATION_CHANGE) serviced by the poll's restart-consumption\n // loop. Before ENG-7576 that stop passed NO breaker reason, so the most common\n // provisioning restart was invisible to the breaker and to restart telemetry.\n // Counted on the LOOSER provisioning tally like its hot-reload siblings; a\n // plain operator 'manual' dashboard restart stays uncounted.\n // Deliberately NOT in update-window-gate.ts GATEABLE_RESTART_REASONS (nor the\n // window-exempt set): integration loads bypass the maintenance window entirely\n // (ENG-6568 - gating this class once left valai ~30h on stale tools). Do not\n // \"tidy\" it into that allowlist.\n | 'integration-change'\n // ENG-8388: the SAME reload as 'integration-change', but one the API could\n // prove a human asked for — the host_agents stamp carried\n // `restart_actor: 'user'`, set only on a route with an authenticated console\n // user behind it. Split out as its own reason (not a flag on the event) so it\n // gets its own class and its own looser budget: the breaker exists to catch a\n // LOOP, and six reloads from six deliberate clicks is not one. Two live\n // failures forced this: a new customer's first agent auto-paused inside ten\n // minutes for connecting four integrations (ENG-8204), and `don` auto-paused\n // for six credential re-entries while someone was debugging that very\n // integration (ENG-8388) — the worst possible moment, because a paused agent\n // binds nothing, so the pause destroys your ability to tell whether the fix\n // worked. An autonomous storm carries no user actor and stays\n // 'integration-change', on the unchanged tally.\n | 'operator-integration-change'\n | 'channel-set-change'\n // ENG-5858: effective sender_policy flipped on either the org or\n // per-agent column. The slack-channel / teams-channel MCP children read\n // SLACK_SENDER_POLICY (and friends) from process.env once at boot, so\n // the rewrite of .mcp.json on its own doesn't propagate — the running\n // child keeps its stale env and stale gate. This restart class fires\n // independently of the .mcp.json value-only drift suppression\n // (ENG-5537) so composio URL rotation can't accidentally trigger it.\n | 'sender-policy-change'\n // ENG-6024: a Teams behaviour field (thread_auto_follow,\n // channel_response_mode, adaptive_cards_*) changed via the webapp's\n // Advanced panel. Same last-hop gap as sender-policy-change: the\n // teams-channel MCP child reads MSTEAMS_THREAD_AUTO_FOLLOW (and friends)\n // from process.env once at boot, so the .mcp.json rewrite alone doesn't\n // propagate. Keyed on a behaviour-subset hash, so token/credential churn\n // in the wider channel config can't fire it.\n | 'channel-behaviour-change'\n | 'manual'\n // ENG-5963: a channel MCP wrote a restart flag after an unrecoverable\n // runtime error. This is RECOVERY (a broken channel needs a restart to heal),\n // not non-urgent maintenance — so it is deliberately NOT in the\n // maintenance-window gate's allowlist and restarts immediately, while still\n // counting toward the restart breaker.\n | 'channel-restart-flag'\n | 'stale-mcp'\n // ENG-6203: a SESSION-level self-heal — the manager's restart-verifier reached\n // `unverified final` (a hot-reload restart that never respawned a healthy\n // session and never re-bound its MCP tools) and is forcing one more gated\n // re-respawn. Counts toward the breaker so a session that repeatedly fails to\n // respawn healthy is bounded (original restart + one remediation, then trip →\n // visible/circuit-tripped) rather than looping forever. Session-scoped, unlike\n // the per-server `mcp-presence-reaper` reason (which ENG-5547 keeps off the\n // breaker so one bad tool can't pause the whole agent).\n | 'bind-remediation'\n // ENG-6229: the in-session agent called `request_restart` after confirming\n // with the human (its existing ask_user HITL) that it needs a reload to pick\n // up a change it can SEE is missing but the manager's desired-state drift\n // detection structurally cannot (e.g. a realized-state MCP-binding fault, or\n // a hot-reload gap). RECOVERY class like `channel-restart-flag` — restarts\n // immediately, NOT held behind the maintenance window — because the agent\n // only asks when it's actively stuck serving someone. Counts toward the\n // breaker so a structurally-confused agent that keeps asking (a restart it\n // perceives as needed but that a restart cannot fix) is bounded and trips\n // into operator-cleared pause rather than looping.\n | 'agent-requested'\n // ENG-7541: a value-only re-mint of an already-present env-only integration\n // credential. GitHub is the canonical case: the App installation token is\n // re-minted server-side on every provision poll (POST\n // /app/installations/{id}/access_tokens is NOT idempotent, so each cache-miss\n // hands back a brand-new token string for the SAME logical credential). The\n // manager sees a changed GITHUB_ACCESS_TOKEN value, respawns the env-only\n // session to pick it up (ENG-7510), and the next poll re-mints again → a\n // respawn every 2-3 min. This is NOT a crash and NOT a membership change (the\n // integration set is unchanged), so it must NEVER be able to pause the agent.\n // Split from 'hot-reload-mcp' so it lands on its OWN non-tripping breaker class\n // (restartReasonClass → 'credential-rotation'): still RECORDED for forensics +\n // the churn signal, but excluded from every trip tally. A benign per-poll\n // re-mint and a pathological storm share the SAME rate, so no finite threshold\n // can separate them — the real cure is a durable shared token cache (ENG-7541\n // fix 1) / a no-respawn credential reload (ENG-7510 follow-up), not a breaker\n // threshold. Emitted ONLY when every changed env-only var is a value-only\n // rotation of an already-present var; any add/remove keeps the membership\n // 'hot-reload-mcp' class. Generic, not GitHub-specific — the live-registry\n // reconcile sibling (ENG-7559) shares it.\n | 'credential-rotation'\n // ENG-7949: a mcp-presence-reaper restart where at least one of the declared-\n // but-dead MCPs (activeKeys) is a known CHANNEL server (slack/telegram/\n // msteams/direct-chat/...), as opposed to a tool MCP (composio_*/xero) or an\n // unrecognised key. A dead channel MCP makes the agent unreachable on that\n // channel — indistinguishable from a crashed agent from the user's side — so\n // this reason is window-exempt (update-window-gate.ts) even though the\n // GENERIC 'mcp-presence-reaper' reason is not: a routine tool-MCP reaper\n // restart still politely waits for the off-peak window, but a dead channel\n // must not sit silent for up to ~14h until the window opens. Still subject to\n // defer-until-idle (never interrupts a live turn) and does NOT change breaker\n // counting — the reaper's own single-vs-multi-server breakerReason logic\n // (reaperRestartBreakerReason) is unchanged; this is a GATE reason only, kept\n // distinct from the breaker-count reason exactly like the existing\n // 'mcp-presence-reaper' gate/breaker split (see stopSession in\n // manager-worker.ts and its ENG-6264 comment).\n | 'channel-mcp-down'\n | 'unknown';\n\n/**\n * ENG-5547: decide whether an mcp-presence-reaper restart should count\n * toward the agent-wide circuit breaker.\n *\n * A restart attributable to a SINGLE persistently-failing MCP server is\n * already governed by that server's own ENG-5279 give-up cap (3 cycles →\n * the integration is marked unhealthy via /host/integration-health and\n * dropped from the next .mcp.json), which isolates the one broken tool and\n * lets the agent keep running degraded. Counting it would let the breaker\n * (default max 2) trip on the 3rd restart and pause the ENTIRE agent for one\n * bad integration — racing, and usually beating, the give-up that would have\n * isolated it. That sledgehammer is exactly what auto-paused phil.\n *\n * A restart where MULTIPLE distinct servers are simultaneously missing is a\n * genuine multi-cause storm the per-server cap can't see (the rotating-MCP\n * case ENG-5441 exists for), so it still counts.\n *\n * Returns the {@link RestartReason} to record (the restart counts toward the\n * breaker), or `undefined` to restart WITHOUT counting.\n */\nexport function reaperRestartBreakerReason(\n activeKeys: readonly string[],\n): RestartReason | undefined {\n return activeKeys.length >= 2 ? 'mcp-presence-reaper' : undefined;\n}\n\n/**\n * ENG-7560: which breaker tally a restart reason counts against.\n *\n * `provisioning` — a reload driven by a routine operator/provisioning action\n * (adding an integration binds new MCP tools). One integration-add fans out\n * into several restarts across adjacent poll cycles (doorbell respawn +\n * managed-toolkit convergence + `.mcp.json` drift + bind-remediation), all\n * tagged with these reasons. Counting that burst on the tight crash tally\n * auto-paused Pepper on an Outlook add. These get their own LOOSER tally so a\n * single add can't trip, while sustained provisioning thrash still trips+pages.\n *\n * `crash` — everything else. A crash loop / structurally-stuck agent must still\n * trip the tight ENG-5441 bar, so all non-provisioning reasons (including\n * `agent-requested`, `channel-restart-flag`, `day-rollover`, the multi-server\n * `mcp-presence-reaper`) stay on it, counted exactly as before.\n *\n * The split is reason-scoped, NOT time-scoped: a crash restart landing inside a\n * provisioning burst still counts on the crash tally, so a genuine crash loop\n * masquerading under provisioning churn is not coalesced away.\n *\n * `credential-rotation` — ENG-7541: a value-only server-side re-mint of an\n * already-present env-only credential (see the 'credential-rotation'\n * RestartReason). Its own class precisely BECAUSE the provisioning tally can't\n * help it: the provisioning bar still trips on sustained churn, and a per-poll\n * re-mint is sustained churn by rate — indistinguishable from a storm — so\n * folding it into `provisioning` would just move the auto-pause, not remove it.\n * This class is RECORDED (forensics + classCounts visibility) but NEVER trips on\n * its own; a co-occurring crash / provisioning restart still trips its own tally.\n *\n * Shared predicates so the sibling churn-family tickets inherit one\n * classification: the live-registry reconcile sibling (ENG-7559) uses the same\n * credential-rotation class.\n */\nexport type RestartReasonClass =\n | 'crash'\n | 'provisioning'\n | 'credential-rotation'\n | 'self-healing'\n // ENG-8388: a provisioning reload the API proved a console user asked for.\n // Counted on its own, looser tally (RESTART_BREAKER_OPERATOR_MAX) instead of\n // the shared provisioning one, because the thing the breaker is built to catch\n // is a loop and a burst of deliberate clicks is not one.\n | 'operator-config';\n\n/**\n * ENG-7577: membership test for a rehydrated `trippedClass`.\n *\n * Derived from a `Record<RestartReasonClass, true>` rather than a `Set` literal\n * so it really is compiler-locked: a new union member that is not listed here is\n * a type error, where a `Set<RestartReasonClass>([...])` would have compiled\n * happily with the member missing (CodeRabbit on PR #3861).\n */\nconst KNOWN_REASON_CLASS_MAP: Record<RestartReasonClass, true> = {\n crash: true,\n provisioning: true,\n 'credential-rotation': true,\n // ENG-8388: provisioning reloads a human demonstrably asked for. Same\n // Record-not-Set reasoning as above — a new class cannot be silently omitted.\n 'operator-config': true,\n // ENG-8215: the self-healing class (bind-remediation - the manager's OWN\n // repair attempts, which must never count toward a trip). Adding it here is\n // forced by the Record type, which is exactly why that shape was chosen over\n // a Set literal (ENG-7577, CodeRabbit on #3861) - a new union member cannot\n // be silently omitted from the rehydration allowlist.\n 'self-healing': true,\n};\nconst KNOWN_REASON_CLASSES: ReadonlySet<RestartReasonClass> = new Set(\n Object.keys(KNOWN_REASON_CLASS_MAP) as RestartReasonClass[],\n);\n\nconst PROVISIONING_RELOAD_REASONS: ReadonlySet<RestartReason> = new Set<RestartReason>([\n 'hot-reload-mcp',\n 'managed-mcp-churn',\n // ENG-8215: 'bind-remediation' MOVED OUT of this set into SELF_HEALING_REASONS.\n // It is the manager repairing itself, not a config reload, and counting it\n // meant the harder the platform tried to fix an agent the more certain it\n // became that it should give up. Deliberately NOT re-added here.\n // ENG-7576: the dashboard integration-add stop itself - the first restart of\n // the very burst this class exists for, previously uncounted entirely.\n 'integration-change',\n // ENG-7771: a sender_policy delivery restart is a deliberate config-delivery\n // reload (operator changed the policy, or the fail-closed first-poll verify\n // after a manager restart with no persisted baseline), not a crash. On the\n // tight crash tally, a host whose sender-policy-baseline.json write keeps\n // failing (disk full, permissions) would convert the fail-closed restart\n // into repeated crash tallies across a multi-deploy day and trip the\n // breaker - taking the agent DOWN, which is strictly worse than the stale\n // policy the restart exists to fix. The looser provisioning tally still\n // trips + pages on sustained thrash.\n 'sender-policy-change',\n]);\n\nexport function isProvisioningReloadReason(reason: RestartReason): boolean {\n return PROVISIONING_RELOAD_REASONS.has(reason);\n}\n\n// ENG-7541: reasons that must NEVER trip the breaker on their own. A value-only\n// server-side credential re-mint (see the 'credential-rotation' RestartReason)\n// respawns the env-only session but is neither a crash nor a genuine membership\n// change; its benign per-poll cadence is indistinguishable BY RATE from a\n// pathological storm, so it gets its own class that is RECORDED (forensics +\n// classCounts visibility) yet excluded from every trip tally. Only the pure\n// credential-rotation reason is non-tripping — a co-occurring crash or\n// provisioning restart still trips its own tally independently.\nconst CREDENTIAL_ROTATION_REASONS: ReadonlySet<RestartReason> = new Set<RestartReason>([\n 'credential-rotation',\n]);\n\nexport function isCredentialRotationReason(reason: RestartReason): boolean {\n return CREDENTIAL_ROTATION_REASONS.has(reason);\n}\n\n// ENG-8215: a SELF-HEALING action must never be able to trip the breaker.\n// 'bind-remediation' is by definition the manager attempting a repair; counting\n// it meant that the harder the platform tried to fix an agent, the more certain\n// it became that it should give up. Live proof (sherlock, agt-demo-1,\n// 2026-07-28): six provisioning restarts on one integration, FIVE of them\n// bind-remediation, agent auto-paused with nobody having changed any config.\n//\n// Modelled on the ENG-7541 credential-rotation class: RECORDED and surfaced in\n// classCounts (so the repair loop stays visible to operators and forensics), but\n// excluded from every trip tally. A co-occurring crash or provisioning restart\n// still trips its own tally independently.\n//\n// This is only safe BECAUSE quarantine now fires first (the derived\n// BIND_FAILURE_QUARANTINE_THRESHOLD): without it, a permanently-unbindable MCP\n// would retry forever with nothing to stop it. The two changes are a pair.\n// 'hot-reload-mcp' deliberately does NOT join this set - it is a real config\n// reload, and exempting it would blind the breaker to genuine .mcp.json flap.\nconst SELF_HEALING_REASONS: ReadonlySet<RestartReason> = new Set<RestartReason>([\n 'bind-remediation',\n]);\n\nexport function isSelfHealingReason(reason: RestartReason): boolean {\n return SELF_HEALING_REASONS.has(reason);\n}\n\n/**\n * ENG-8388: provisioning reloads carrying proof that a console user asked for\n * them. Kept OUT of PROVISIONING_RELOAD_REASONS deliberately — membership there\n * is what feeds the shared per-integration tally, and the whole point is that\n * these are counted separately. They remain fully recorded and surfaced in\n * `classCounts`.\n */\nconst OPERATOR_CONFIG_REASONS: ReadonlySet<RestartReason> = new Set<RestartReason>([\n 'operator-integration-change',\n]);\n\nexport function isOperatorConfigReason(reason: RestartReason): boolean {\n return OPERATOR_CONFIG_REASONS.has(reason);\n}\n\n/**\n * ENG-8388 (CodeRabbit on PR #4033): does this reason's tally bucket PER\n * INTEGRATION?\n *\n * Both `record()` (which pins the key onto the event) and the manager's caller\n * (which RESOLVES the key to pass in) must agree, and they are in different\n * files. They did not: `record()` bucketed provisioning + operator-config,\n * while the caller resolved a key only for provisioning. Since\n * 'operator-integration-change' is not a provisioning reason, every operator\n * reload reached the breaker with no key and shared the '' bucket — making the\n * operator-config bucketing unreachable, and letting OPERATOR_MAX+1 DISTINCT\n * integration adds auto-pause an agent during ordinary first-time setup.\n *\n * One exported predicate, used by both sides, so the two can no longer drift.\n * This is deliberately NOT a parity test over two hand-maintained lists (the\n * ENG-8214 trap): there is now only one list to maintain.\n */\nexport function isIntegrationBucketedReason(reason: RestartReason): boolean {\n return isProvisioningReloadReason(reason) || isOperatorConfigReason(reason);\n}\n\nexport function restartReasonClass(reason: RestartReason): RestartReasonClass {\n if (isCredentialRotationReason(reason)) return 'credential-rotation';\n if (isSelfHealingReason(reason)) return 'self-healing';\n if (isOperatorConfigReason(reason)) return 'operator-config';\n return isProvisioningReloadReason(reason) ? 'provisioning' : 'crash';\n}\n\nfunction countByClass(events: readonly RestartEvent[]): Record<RestartReasonClass, number> {\n let crash = 0;\n let provisioning = 0;\n let credentialRotation = 0;\n let selfHealing = 0;\n let operatorConfig = 0;\n for (const e of events) {\n const klass = restartReasonClass(e.reason);\n if (klass === 'provisioning') provisioning += 1;\n else if (klass === 'credential-rotation') credentialRotation += 1;\n else if (klass === 'self-healing') selfHealing += 1;\n else if (klass === 'operator-config') operatorConfig += 1;\n else crash += 1;\n }\n return {\n crash,\n provisioning,\n 'credential-rotation': credentialRotation,\n 'self-healing': selfHealing,\n 'operator-config': operatorConfig,\n };\n}\n\n/**\n * ENG-7812: the largest single-integration provisioning bucket among `events`.\n *\n * Provisioning-reload events are bucketed by their {@link RestartEvent.integrationKey}\n * (absent ⇒ the shared `''` unattributed bucket). Onboarding many DISTINCT\n * integrations spreads restarts across many buckets of ~1 (never trips); ONE\n * integration looping (or a run of unattributed reprovisions) piles into a\n * single bucket that crosses the bar. Returns the winning bucket's count + key\n * (count 0 / key `''` when there are no provisioning events).\n */\nfunction maxProvisioningBucket(events: readonly RestartEvent[]): { count: number; key: string } {\n return maxBucketWhere(events, isProvisioningReloadReason);\n}\n\n/**\n * ENG-8388: the same per-integration bucketing, over an arbitrary reason class.\n * Extracted so the operator-config tally buckets identically to the provisioning\n * one — a human wiring four DIFFERENT integrations still spreads across four\n * buckets, and only a run against ONE integration piles up.\n */\nfunction maxBucketWhere(\n events: readonly RestartEvent[],\n matches: (reason: RestartReason) => boolean,\n): { count: number; key: string } {\n const buckets = new Map<string, number>();\n for (const e of events) {\n if (!matches(e.reason)) continue;\n const key = e.integrationKey ?? '';\n buckets.set(key, (buckets.get(key) ?? 0) + 1);\n }\n let best = { count: 0, key: '' };\n for (const [key, count] of buckets) {\n if (count > best.count) best = { count, key };\n }\n return best;\n}\n\nexport interface RestartEvent {\n reason: RestartReason;\n at: number;\n /**\n * ENG-7812: for a provisioning-reload event, the integration this restart is\n * attributable to (its `definition_id`), when the manager could pin one down\n * (a membership delta in the same poll window). Provisioning events are\n * TRIPPED per-integration (bucketed by this key) so onboarding many DISTINCT\n * integrations (each binding once) never trips, while ONE integration looping\n * still does. Absent when no single integration could be attributed (e.g. a\n * reprovision of an unchanged set); those share one `''` bucket that still\n * trips on its own, preserving the breaker against unattributed churn. Unused\n * for crash / credential-rotation events.\n */\n integrationKey?: string;\n}\n\nexport interface TripState {\n trippedAt: number;\n eventsAtTrip: RestartEvent[];\n /** Pre-formatted `status_message` for the agents row + decision-log. */\n statusMessage: string;\n /**\n * ENG-7577: which tally tripped, persisted so the RECOVERY path can tell a\n * provisioning-reload pause (routine onboarding churn — safe to auto-recover\n * once quiet) from a crash-class pause (a structurally-stuck agent — must stay\n * paused for a human). Optional because trips persisted by a pre-ENG-7577\n * manager into `manager-state.json` have no such field; {@link tripClass}\n * reconstructs it from `eventsAtTrip` for those, fail-safe toward `crash`.\n */\n trippedClass?: RestartReasonClass;\n}\n\n/**\n * ENG-7577: the class of a trip, for hydrated trips too.\n *\n * Prefers the persisted {@link TripState.trippedClass}. For a legacy trip that\n * predates the field, reconstruct it from the event snapshot — `record()` scopes\n * `eventsAtTrip` to the tripping class alone (crash trips snapshot only crash\n * events; a provisioning trip snapshots only the overflowing integration's\n * provisioning events), so the snapshot is an exact reconstruction rather than a\n * guess. Fail-safe: an empty snapshot, or ANY crash-class event in it, resolves\n * to `crash` — the class that demands manual operator clearance. Mis-reading a\n * crash trip as provisioning would hand a genuinely broken agent an unattended\n * resume; the reverse merely costs an operator click.\n */\nexport function tripClass(trip: TripState): RestartReasonClass {\n if (trip.trippedClass) return trip.trippedClass;\n if (!Array.isArray(trip.eventsAtTrip) || trip.eventsAtTrip.length === 0) return 'crash';\n return trip.eventsAtTrip.every((e) => isProvisioningReloadReason(e.reason)) ? 'provisioning' : 'crash';\n}\n\nexport interface RecordResult {\n /** True on the call that flips the breaker from closed → tripped. */\n tripped: boolean;\n /** Always populated when `tripped` is true. */\n trip?: TripState;\n /**\n * Current count inside the tight (`windowMs`) sliding window, ALL classes\n * (post-record). Preserved from the pre-ENG-7560 shape for callers/tests that\n * read it; the per-class breakdown lives in `classCounts`.\n */\n windowCount: number;\n /**\n * ENG-7560: per-class in-window counts at record time — `crash` inside the\n * tight window, `provisioning` inside the provisioning window. Lets the caller\n * emit an \"elevated provisioning churn\" signal before the looser bar trips.\n */\n classCounts: Record<RestartReasonClass, number>;\n /** ENG-7560: which class's threshold tripped (only set when `tripped`). */\n trippedClass?: RestartReasonClass;\n /**\n * ENG-7812: the largest single-integration provisioning bucket in the\n * provisioning window (post-record), and the integration key it belongs to.\n * The provisioning trip fires on THIS, not on the raw `classCounts.provisioning`\n * total, so the caller's early-warning must gate on `maxProvisioningBucket`\n * (a churning integration) rather than the total (a legitimate bulk onboard).\n * `''` key ⇒ the unattributed-provisioning bucket.\n */\n maxProvisioningBucket: number;\n maxProvisioningBucketKey?: string;\n}\n\nconst DEFAULT_MAX = 2;\nconst DEFAULT_WINDOW_MS = 600_000;\n// ENG-7560: provisioning-reload churn (a burst of hot-reload / bind-remediation\n// restarts from one integration-add) gets its own looser tally so a routine add\n// can't trip the tight crash breaker, while sustained provisioning thrash still\n// trips and pages. 5-in-30min clears a single add's 3-4 burst but a flap\n// re-firing more often than ~every 6 min still accumulates to a trip.\n// ENG-8215: sourced from @augmented/core so the MCP quarantine threshold can be\n// DERIVED from the same number. Tuning the breaker here now moves quarantine\n// with it, instead of silently re-opening the race where the breaker always\n// tripped before quarantine could isolate the failing integration.\nconst DEFAULT_PROVISIONING_MAX = RESTART_BREAKER_PROVISIONING_MAX;\nconst DEFAULT_PROVISIONING_WINDOW_MS = RESTART_BREAKER_PROVISIONING_WINDOW_MS;\nconst DEFAULT_OPERATOR_MAX = RESTART_BREAKER_OPERATOR_MAX;\n\nexport function readEnvNumber(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\n/**\n * ENG-7891 slice 5b: move a map entry from the legacy code_name key onto the\n * rename-stable runtime key on first touch after the id-keyed layout is armed.\n * No-op when the target already holds an entry or the source is absent, so it is\n * safe to call on every access. Never invoked on the legacy fleet (keyFor only\n * calls it when from !== to, and identity makes from === to).\n */\nfunction migrateMapKey<V>(map: Map<string, V>, from: string, to: string): void {\n if (map.has(to) || !map.has(from)) return;\n map.set(to, map.get(from)!);\n map.delete(from);\n}\n\n/**\n * Per-agent breaker. One instance per manager, keyed internally by code_name.\n * Held in module-level state in manager-worker.ts so it survives across\n * polls within a worker generation; trip state is mirrored to\n * `manager-state.json` so it survives across worker generations too.\n */\nexport class RestartBreaker {\n private readonly max: number;\n private readonly windowMs: number;\n /**\n * ENG-8215: NOT readonly - the constructor may clamp it upward to preserve the\n * quarantine-fires-first ordering against a runtime override. Never mutated\n * after construction.\n */\n private provisioningMax: number;\n private readonly provisioningWindowMs: number;\n /**\n * ENG-8388: the looser operator-config budget. Shares provisioningWindowMs.\n * NOT readonly - the constructor floors it at `provisioningMax` so setting one\n * env var without the other cannot invert the two tallies.\n */\n private operatorMax: number;\n /** Longest window either tally needs — how long the events log must retain. */\n private readonly retentionMs: number;\n private readonly now: () => number;\n /** ENG-7891 slice 5b: code_name → durable runtime key (agent_id when id-keyed). Identity on the legacy fleet. */\n private readonly runtimeKey: (codeName: string) => string;\n private readonly events = new Map<string, RestartEvent[]>();\n private readonly trips = new Map<string, TripState>();\n /**\n * ENG-8215: set when a supplied `provisioningMax` was raised to keep MCP\n * quarantine ahead of the breaker. Read once by the manager at construction so\n * an overridden-and-clamped value is visible in manager.log rather than\n * silently discarded.\n */\n private quarantineOrderingClamped?: {\n requested: number;\n applied: number;\n quarantineThreshold: number;\n };\n\n constructor(opts: RestartBreakerOptions = {}) {\n this.max = opts.max ?? readEnvNumber('AGT_RESTART_BREAKER_MAX', DEFAULT_MAX);\n this.windowMs = opts.windowMs ?? readEnvNumber('AGT_RESTART_BREAKER_WINDOW_MS', DEFAULT_WINDOW_MS);\n this.provisioningMax =\n opts.provisioningMax ?? readEnvNumber('AGT_RESTART_BREAKER_PROVISIONING_MAX', DEFAULT_PROVISIONING_MAX);\n this.provisioningWindowMs =\n opts.provisioningWindowMs ??\n readEnvNumber('AGT_RESTART_BREAKER_PROVISIONING_WINDOW_MS', DEFAULT_PROVISIONING_WINDOW_MS);\n this.operatorMax =\n opts.operatorMax ?? readEnvNumber('AGT_RESTART_BREAKER_OPERATOR_MAX', DEFAULT_OPERATOR_MAX);\n this.now = opts.now ?? Date.now;\n this.runtimeKey = opts.runtimeKey ?? ((codeName) => codeName);\n // Reject NaN / Infinity explicitly — `??` treats them as present\n // values, so without these guards an operator who sets\n // `AGT_RESTART_BREAKER_MAX=NaN` (or a programmatic caller passing\n // `Number.NaN`) would silently disable the breaker rather than\n // fall through to the default. CodeRabbit on PR #1302.\n if (!Number.isFinite(this.max) || this.max < 1) {\n throw new Error('restart-breaker max must be a finite number >= 1');\n }\n if (!Number.isFinite(this.windowMs) || this.windowMs < 1000) {\n throw new Error('restart-breaker windowMs must be a finite number >= 1000');\n }\n // ENG-7560: guard the provisioning tally with the same shape so a\n // fat-fingered AGT_RESTART_BREAKER_PROVISIONING_* can't silently disable it.\n if (!Number.isFinite(this.provisioningMax) || this.provisioningMax < 1) {\n throw new Error('restart-breaker provisioningMax must be a finite number >= 1');\n }\n // ENG-8388: same shape for the operator tally. Floored at the provisioning\n // max rather than at 1 — an operator budget BELOW the autonomous one would\n // invert the whole point of the class (proving a human asked would make the\n // agent MORE likely to be paused, not less), and that inversion is easy to\n // reach by setting only one of the two env vars.\n if (!Number.isFinite(this.operatorMax) || this.operatorMax < 1) {\n throw new Error('restart-breaker operatorMax must be a finite number >= 1');\n }\n if (this.operatorMax < this.provisioningMax) {\n this.operatorMax = this.provisioningMax;\n }\n // ENG-8215 (CodeRabbit on this PR): coordinating the DEFAULTS is not enough.\n // `AGT_RESTART_BREAKER_PROVISIONING_MAX` and `opts.provisioningMax` can each\n // lower the breaker's bar on their own, and the quarantine threshold is\n // enforced server-side where a host's env is invisible - so an operator\n // setting `2` would trip the breaker on the 3rd provisioning restart while\n // quarantine still waited for its 3rd bind failure, silently re-opening the\n // exact race this ticket closes. The ordering therefore has to be enforced\n // HERE, on the only side that can see both numbers.\n //\n // Clamp rather than throw: a manager that refuses to boot on a fat-fingered\n // env var is a worse outcome than one running with a slightly looser\n // breaker. The breaker is the safety net; quarantine-fires-first is the\n // invariant. `quarantineOrderingClamped` records that we overrode the\n // operator, so the caller can log it rather than discard it silently.\n if (this.provisioningMax < MIN_PROVISIONING_MAX_FOR_QUARANTINE_ORDERING) {\n this.quarantineOrderingClamped = {\n requested: this.provisioningMax,\n applied: MIN_PROVISIONING_MAX_FOR_QUARANTINE_ORDERING,\n quarantineThreshold: BIND_FAILURE_QUARANTINE_THRESHOLD,\n };\n this.provisioningMax = MIN_PROVISIONING_MAX_FOR_QUARANTINE_ORDERING;\n }\n if (!Number.isFinite(this.provisioningWindowMs) || this.provisioningWindowMs < 1000) {\n throw new Error('restart-breaker provisioningWindowMs must be a finite number >= 1000');\n }\n this.retentionMs = Math.max(this.windowMs, this.provisioningWindowMs);\n }\n\n /**\n * ENG-8215: the clamp record, if the configured provisioning max had to be\n * raised to keep MCP quarantine ahead of the breaker. `undefined` when the\n * configured value already satisfied the ordering.\n */\n getQuarantineOrderingClamp(): { requested: number; applied: number; quarantineThreshold: number } | undefined {\n return this.quarantineOrderingClamped;\n }\n\n /**\n * ENG-7891 slice 5b: the DURABLE key for this agent's events/trips entries.\n * Resolves the code_name to the runtime key (agent_id on id-keyed hosts) and,\n * when they differ, lazily migrates a legacy code_name-keyed entry onto it — so\n * a trip persisted under the old key by a pre-arming manager (or from before a\n * codename rename) is found under the new key instead of being lost. Identity on\n * the legacy fleet ⇒ from === to ⇒ no migration and no behaviour change. Both\n * maps migrate together so events and a trip for the same agent never split\n * across keys mid-generation (a migration can flip the resolver mid-process).\n *\n * BOUNDARY — a pure-LEGACY rename (a host that was never armed) still strands a\n * held trip, exactly as it did before slice 5b: with no symlink there is no\n * agent_id to bridge the old codename to the new, so the runtime key stays the\n * (new) codename and the trip persisted under the old codename is not found.\n * That is unchanged, pre-existing behaviour and it is benign — the fresh\n * generation re-evaluates the agent under its new name and a still-pathological\n * agent simply re-trips within the window. The rename-survival guarantee this\n * slice adds is an id-keyed-only, post-arming property (the symlink is the\n * bridge); it is deliberately NOT retrofitted onto the legacy path.\n */\n private keyFor(codeName: string): string {\n const key = this.runtimeKey(codeName);\n if (key !== codeName) {\n migrateMapKey(this.events, codeName, key);\n migrateMapKey(this.trips, codeName, key);\n }\n return key;\n }\n\n /** True if this agent's breaker is currently tripped (manager must skip spawn). */\n isTripped(codeName: string): boolean {\n return this.trips.has(this.keyFor(codeName));\n }\n\n getTrip(codeName: string): TripState | undefined {\n return this.trips.get(this.keyFor(codeName));\n }\n\n /**\n * Record a restart event. If recording this event puts the count\n * inside the window above `max`, the breaker trips and the call site\n * should NOT respawn.\n *\n * Idempotent on already-tripped breakers: returns the existing trip\n * without double-counting events. Callers may still record the reason\n * via the decision-log for forensics.\n */\n record(codeName: string, reason: RestartReason, integrationKey?: string): RecordResult {\n const key = this.keyFor(codeName);\n const existing = this.trips.get(key);\n if (existing) {\n const bucket = maxProvisioningBucket(existing.eventsAtTrip);\n return {\n tripped: false,\n trip: existing,\n windowCount: existing.eventsAtTrip.length,\n classCounts: countByClass(existing.eventsAtTrip),\n maxProvisioningBucket: bucket.count,\n maxProvisioningBucketKey: bucket.key,\n };\n }\n\n const at = this.now();\n // Retain for the longer of the two windows so the provisioning tally can see\n // events older than the tight crash window. Per-class counting below applies\n // each class's own (shorter) window on top of this retained log.\n const retentionCutoff = at - this.retentionMs;\n const prior = (this.events.get(key) ?? []).filter((e) => e.at >= retentionCutoff);\n // ENG-7812: pin the integration key only onto provisioning-reload events (it\n // buckets the provisioning trip). Crash / credential-rotation events keep it\n // undefined so a stray caller key can't perturb their tallies.\n // ENG-8388: operator-config events are bucketed the same way, so they need\n // the key pinned too - otherwise every operator reload would share the ''\n // bucket and four distinct integration adds would count as one run.\n // The predicate is shared with the manager's caller (which resolves the key\n // this pins) so the two halves of the contract cannot drift apart again -\n // they did, and it made this bucketing unreachable. See\n // isIntegrationBucketedReason.\n const bucketed = isIntegrationBucketedReason(reason);\n prior.push(bucketed && integrationKey ? { reason, at, integrationKey } : { reason, at });\n this.events.set(key, prior);\n\n // ENG-7560: reason-aware, two independent tallies. Crash reasons keep the\n // tight ENG-5441 bar; provisioning-reload churn gets its own looser bar.\n // Every event is still recorded (forensics + the elevated-churn signal the\n // caller emits) — we relax the TRIP threshold, we do not skip/mute records,\n // so a slow flap can never space recorded events past its window and quietly\n // defeat the breaker.\n const crashCount = prior.filter(\n (e) => e.at >= at - this.windowMs && restartReasonClass(e.reason) === 'crash',\n ).length;\n const provEvents = prior.filter(\n (e) => e.at >= at - this.provisioningWindowMs && isProvisioningReloadReason(e.reason),\n );\n // ENG-7541: credential-rotation re-mints are RECORDED and surfaced (so the\n // un-paused respawn loop is visible in classCounts / logs) but never\n // contribute to a trip. Counted over the provisioning window so\n // \"re-mints in the last 30 min\" is a meaningful operator number. There is\n // deliberately NO trip check for this count below.\n const credRotCount = prior.filter(\n (e) => e.at >= at - this.provisioningWindowMs && isCredentialRotationReason(e.reason),\n ).length;\n // ENG-8215: self-healing restarts (bind-remediation) are RECORDED and\n // surfaced for exactly the same reason as credential-rotation above - the\n // repair loop must stay visible - but there is deliberately NO trip check\n // for this count either. A repair attempt is not evidence of ill health.\n const selfHealCount = prior.filter(\n (e) => e.at >= at - this.provisioningWindowMs && isSelfHealingReason(e.reason),\n ).length;\n // ENG-8388: provisioning reloads a console user demonstrably asked for.\n // RECORDED and surfaced like every other class, but tallied against its own\n // looser bar below rather than against `provisioningMax`.\n const operatorEvents = prior.filter(\n (e) => e.at >= at - this.provisioningWindowMs && isOperatorConfigReason(e.reason),\n );\n // The back-compat `windowCount`: all events in the tight window, unchanged.\n const windowCount = prior.filter((e) => e.at >= at - this.windowMs).length;\n const classCounts: Record<RestartReasonClass, number> = {\n crash: crashCount,\n provisioning: provEvents.length,\n 'credential-rotation': credRotCount,\n 'self-healing': selfHealCount,\n 'operator-config': operatorEvents.length,\n };\n // ENG-7812: the provisioning trip fires PER-INTEGRATION, on the largest\n // single-integration bucket, not on `provEvents.length`. A bulk onboard of N\n // distinct integrations spreads N restarts across N buckets of ~1 (max bucket\n // stays small, no trip); ONE integration looping (or a run of unattributed\n // reprovisions sharing the `''` bucket) piles into a single bucket that\n // crosses `provisioningMax`. This is what stops a healthy new agent from\n // auto-pausing mid-onboarding while still catching a genuinely churning add.\n const provBucket = maxProvisioningBucket(provEvents);\n // ENG-8388: the operator tally buckets identically, so wiring four DIFFERENT\n // integrations spreads across four buckets and never approaches the bar,\n // while a genuine run against ONE integration still has a backstop.\n const operatorBucket = maxBucketWhere(operatorEvents, isOperatorConfigReason);\n\n // Crash bar is the more urgent classification — evaluate it first so a mixed\n // burst that crosses BOTH reports as a crash trip.\n let trippedClass: RestartReasonClass | undefined;\n let tripEvents: RestartEvent[] | undefined;\n let tripWindowMs = this.windowMs;\n // ENG-8388: which bucket key the status message should name. The two bucketed\n // classes have DIFFERENT keys, and passing provBucket.key unconditionally\n // (as this did before the operator class existed) makes an operator trip name\n // the wrong integration - or none at all, when only operator events exist.\n let tripBucketKey = '';\n if (crashCount > this.max) {\n trippedClass = 'crash';\n tripEvents = prior.filter(\n (e) => e.at >= at - this.windowMs && restartReasonClass(e.reason) === 'crash',\n );\n tripWindowMs = this.windowMs;\n } else if (operatorBucket.count > this.operatorMax) {\n // ENG-8388: a human-attributed run against ONE integration that is still\n // going after `operatorMax` reloads. Evaluated BEFORE the provisioning bar\n // only in the sense that it has its own bucket set - the two tallies are\n // disjoint by construction (a reason is in exactly one class), so the\n // order between these two branches never decides which fires.\n trippedClass = 'operator-config';\n tripEvents = operatorEvents.filter((e) => (e.integrationKey ?? '') === operatorBucket.key);\n tripWindowMs = this.provisioningWindowMs;\n tripBucketKey = operatorBucket.key;\n } else if (provBucket.count > this.provisioningMax) {\n trippedClass = 'provisioning';\n // Snapshot ONLY the overflowing integration's events so the status message\n // names the culprit (\"... integration-change=6 [gmail]\"), not the whole\n // multi-integration onboard burst that happened to share the window.\n tripEvents = provEvents.filter((e) => (e.integrationKey ?? '') === provBucket.key);\n tripWindowMs = this.provisioningWindowMs;\n tripBucketKey = provBucket.key;\n }\n\n if (trippedClass && tripEvents) {\n const trip: TripState = {\n trippedAt: at,\n eventsAtTrip: [...tripEvents],\n statusMessage: formatStatusMessage(tripEvents, tripWindowMs, trippedClass, tripBucketKey),\n // ENG-7577: persist the tripping class so the recovery path can gate on\n // it after a manager restart, without re-deriving it from prose.\n trippedClass,\n };\n this.trips.set(key, trip);\n // Free the events log — the trip carries the snapshot from now on.\n this.events.delete(key);\n return {\n tripped: true,\n trip,\n windowCount,\n classCounts,\n trippedClass,\n maxProvisioningBucket: provBucket.count,\n maxProvisioningBucketKey: provBucket.key,\n };\n }\n\n return {\n tripped: false,\n windowCount,\n classCounts,\n maxProvisioningBucket: provBucket.count,\n maxProvisioningBucketKey: provBucket.key,\n };\n }\n\n /** Operator-initiated reset: drops the trip + the events log for this agent. */\n clear(codeName: string): void {\n const key = this.keyFor(codeName);\n this.trips.delete(key);\n this.events.delete(key);\n // keyFor's migration leaves a legacy code_name entry behind only when the\n // runtime key was already populated (both keys present); drop it too so an\n // operator clear leaves nothing tripped under either key.\n if (key !== codeName) {\n this.trips.delete(codeName);\n this.events.delete(codeName);\n }\n }\n\n /** Snapshot tripped agents for `manager-state.json`. */\n serialize(): Record<string, TripState> {\n return Object.fromEntries(this.trips.entries());\n }\n\n /**\n * Rehydrate trip state from `manager-state.json`. Called once at\n * worker startup. Window history is intentionally NOT persisted — only\n * tripped state. A tripped breaker survives manager restart; an\n * un-tripped one starts a fresh window in the new worker.\n */\n hydrate(saved: Record<string, TripState> | null | undefined): void {\n if (!saved) return;\n for (const [codeName, trip] of Object.entries(saved)) {\n if (trip && typeof trip.trippedAt === 'number' && Array.isArray(trip.eventsAtTrip)) {\n // ENG-7577: a persisted `trippedClass` governs whether this trip may\n // auto-recover, so an unrecognised value must not survive rehydration —\n // drop it and let `tripClass` reconstruct from the (fail-safe) event\n // snapshot rather than trusting a garbled state file.\n const klass = trip.trippedClass;\n this.trips.set(\n codeName,\n klass === undefined || KNOWN_REASON_CLASSES.has(klass) ? trip : { ...trip, trippedClass: undefined },\n );\n }\n }\n }\n\n /** Test helper — current in-window event count for `codeName`. */\n windowCount(codeName: string): number {\n const cutoff = this.now() - this.windowMs;\n return (this.events.get(this.keyFor(codeName)) ?? []).filter((e) => e.at >= cutoff).length;\n }\n}\n\nfunction formatStatusMessage(\n events: RestartEvent[],\n windowMs: number,\n klass: RestartReasonClass = 'crash',\n integrationKey = '',\n): string {\n const last = events[events.length - 1]!;\n // Render the window in seconds for sub-minute thresholds (operator-\n // tuned via AGT_RESTART_BREAKER_WINDOW_MS) and in decimal minutes\n // otherwise. Math.round(/60_000) was returning \"0min\" for a 30s\n // tuning, which is unhelpful in the dashboard. CodeRabbit on PR #1302.\n const windowLabel = windowMs < 60_000\n ? `${Math.round(windowMs / 1000)}s`\n : `${(windowMs / 60_000).toFixed(1).replace(/\\.0$/, '')}min`;\n const reasonCounts = new Map<RestartReason, number>();\n for (const e of events) reasonCounts.set(e.reason, (reasonCounts.get(e.reason) ?? 0) + 1);\n const breakdown = Array.from(reasonCounts.entries())\n .map(([r, n]) => `${r}=${n}`)\n .join(', ');\n // ENG-7560: name the tally that tripped so the operator sees \"provisioning-reload\n // thrash\" (a churning integration) vs a genuine crash loop at a glance.\n // ENG-8388: an operator-config trip is a DIFFERENT operator story - it means a\n // human kept re-applying config against one integration well past the point\n // the platform expected, so the message must not read as autonomous thrash.\n const classLabel =\n klass === 'provisioning'\n ? 'provisioning-reload '\n : klass === 'operator-config'\n ? 'operator-requested config-reload '\n : '';\n // ENG-7812: on a provisioning trip, name the single integration whose bucket\n // overflowed (the events are already scoped to it) so the operator sees WHICH\n // integration is churning, not just that provisioning churned.\n const integrationLabel =\n (klass === 'provisioning' || klass === 'operator-config') && integrationKey\n ? ` for integration '${integrationKey}'`\n : '';\n return (\n `Circuit breaker tripped: ${events.length} ${classLabel}restarts in ${windowLabel}` +\n `${integrationLabel} (${breakdown}); most recent=${last.reason} at ${new Date(last.at).toISOString()}`\n );\n}\n","/**\n * ENG-8363 — WHY an MCP server config could not be read, and what verdict that\n * earns.\n *\n * Before this module the two config readers in connectivity-probe-context.ts\n * collapsed three genuinely different conditions into a bare `null`, and the two\n * probe legs then disagreed about what that null meant: the HTTP leg called it\n * `transient_error`, the stdio leg called it `down` — for the identical input.\n * The stdio leg carried the reasoning in its own comment (\"a missing server\n * entry is a real `down` — the tools aren't wired\") and the HTTP leg contradicted\n * it. Neither was right on its own, because `null` was three different facts:\n *\n * 1. `file-unreadable` — `.mcp.json` missing or unparseable. Could be a\n * read-during-write race, or an agent whose config has never been written.\n * Never a statement about the integration. Always retryable.\n * 2. `key-absent` — the file parsed, but carries no entry under this key.\n * AMBIGUOUS BY TIME: permanent if the writer will never emit that key\n * (a derivation mismatch, an integration the adapter skips), but\n * GUARANTEED-TRANSIENT right after an integration is added.\n * 3. `wrong-shape` — the entry exists but is not url-bearing (http) /\n * command-bearing (stdio). The writer produced something this leg cannot\n * probe: a real wiring fault, not a timing artifact.\n *\n * WHY `key-absent` CANNOT SIMPLY BE `down` (the trap ENG-8355 declined to walk\n * into, and the reason this issue's own suggested direction needed amending):\n * the manager probes at step 6b-i.5 of the poll but writes `.mcp.json` further\n * down the SAME poll, in `writeIntegrations`. A never-probed row is always due\n * (connectivity-probe-runner `isDue`: no `last_connectivity_check_at` ⇒ due). So\n * the FIRST probe of EVERY newly-added integration necessarily runs against a\n * `.mcp.json` that does not contain its key yet. A flat \"absent ⇒ down\" would\n * therefore fire on every single integration add — and worse, it would break\n * ENG-7575's first-connect backoff, which keys on `transient_error`\n * (`computeFirstConnectUnsettled`): the deferral would stop engaging and\n * bind-remediation would go back to force-respawning to bind tools that cannot\n * bind yet — the exact ENG-7560 churn loop that backoff exists to prevent.\n *\n * Hence the verdict is age-aware. Inside the first-connect window a missing key\n * is the designed state and reads `transient_error`; outside it, the writer has\n * had its chance and the absence is a real `down`. The window is deliberately\n * THE SAME constant ENG-7575's backoff uses ({@link FIRST_CONNECT_WINDOW_MS}) —\n * if the probe escalated to `down` before the backoff stopped deferring, a row\n * would sit in a state where the probe says \"broken\" and the remediator says\n * \"still settling\". One constant, imported by both, is what keeps them honest.\n *\n * Fail-open in two places, both load-bearing:\n * - an unknown/unparseable `created_at` is treated as INSIDE the window. We\n * escalate to `down` only on positive evidence the row is old; an older API\n * that stops forwarding `created_at` degrades to the pre-ENG-8363 HTTP\n * behaviour (permanent transient) rather than to a fleet of false `down`s.\n * - an `expectedAbsent` row (ENG-7916 quarantine) never escalates at all. A\n * quarantined integration is REMOVED from `.mcp.json` deliberately, and is\n * re-probed only so ENG-8036 auto-resume can observe it recover. Its absence\n * is the precondition of the probe, not a fault to report.\n */\nimport { readEnvNumber } from './restart-breaker.js';\n\n/** Why a `.mcp.json` server config could not be produced for a probe leg. */\nexport type McpConfigUnavailableCause =\n /** `.mcp.json` missing or unparseable — says nothing about the integration. */\n | 'file-unreadable'\n /** File parsed; no entry under this server key. */\n | 'key-absent'\n /** Entry present but not url-bearing (http) / command-bearing (stdio). */\n | 'wrong-shape';\n\n/**\n * A config read that either produced a usable config or names WHY it did not.\n * Replaces the bare `T | null` the readers used to return — the null discarded\n * exactly the fact the caller needed to pick an honest verdict.\n */\nexport type McpConfigLookup<T> =\n | { ok: true; value: T }\n | { ok: false; cause: McpConfigUnavailableCause };\n\n/**\n * ENG-8363 — stamp the quarantine mark onto the rows the manager concatenates\n * into the probe set (ENG-8036 auto-resume). Extracted as a named seam rather\n * than inlined at the call site so a test can drive THIS, not a re-derivation of\n * it: the mark is the only thing standing between a quarantined row and a hard\n * `down` for being in exactly the state quarantine put it in, and an inline\n * `.map()` inside manager-worker is unreachable from a unit test.\n */\nexport function markQuarantinedForProbe<T extends object>(\n rows: readonly T[],\n): Array<T & { quarantined: true }> {\n return rows.map((row) => ({ ...row, quarantined: true as const }));\n}\n\n/**\n * Wrap an ENG-8049 INLINE-entry resolve into the same shape the file readers\n * return. On the inline path there is no file and no key lookup, so the only way\n * to fail is the entry not being probeable by this transport — `wrong-shape` is\n * the sole reachable cause, and saying so keeps the legs on one code path.\n */\nexport function inlineEntryLookup<T>(value: T | null): McpConfigLookup<T> {\n return value ? { ok: true, value } : { ok: false, cause: 'wrong-shape' };\n}\n\n/**\n * ENG-7575's first-connect window, owned here so the connectivity probe and\n * bind-remediation cannot drift apart (see the header). manager-worker imports\n * this rather than defining its own copy; the env var name is unchanged, so an\n * operator override still moves both together.\n */\nexport const FIRST_CONNECT_WINDOW_MS = readEnvNumber(\n 'AGT_BIND_REMEDIATION_FIRST_CONNECT_WINDOW_MS',\n 900_000, // 15 min\n);\n\n/**\n * Is this integration row young enough that a missing `.mcp.json` key is still\n * the expected state? Unknown / unparseable ages answer TRUE (fail-open — see\n * the header: we escalate only on positive evidence of age).\n */\nexport function isWithinFirstConnectWindow(\n createdAt: string | null | undefined,\n now: number,\n windowMs: number = FIRST_CONNECT_WINDOW_MS,\n): boolean {\n if (!createdAt) return true;\n const addedAt = Date.parse(createdAt);\n if (!Number.isFinite(addedAt)) return true;\n return now - addedAt <= windowMs;\n}\n\nexport interface McpConfigVerdictContext {\n /** The `.mcp.json` key the leg was looking for (for the message). */\n serverKey: string;\n /** Which reader ran — only affects wording, never the verdict. */\n transport: 'http' | 'stdio';\n /** Whether the config came from the file or an ENG-8049 inline entry. */\n source: 'inline config' | '.mcp.json';\n /** The integration row's `created_at`, for the age test. */\n createdAt?: string | null;\n /**\n * ENG-7916/ENG-8036: this row is QUARANTINED, so its absence from `.mcp.json`\n * is by design. Never escalates — see the header.\n */\n expectedAbsent?: boolean;\n now: number;\n windowMs?: number;\n}\n\n/**\n * The single mapping from \"why the config was unavailable\" to a probe verdict.\n *\n * Both MCP legs (and the Composio account leg) route through this one function\n * on purpose. The bug this issue fixes was two adjacent branches independently\n * deciding what the same condition meant; a shared helper is the only shape that\n * makes \"both paths agree\" a property of the code rather than of whoever edits\n * it next. Adding a cause here changes every leg at once.\n */\nexport function verdictForUnavailableMcpConfig(\n cause: McpConfigUnavailableCause,\n ctx: McpConfigVerdictContext,\n): { status: 'transient_error' | 'down'; message: string } {\n const where = `MCP ${ctx.transport} server '${ctx.serverKey}'`;\n\n if (cause === 'file-unreadable') {\n // Never the integration's fault: no file, no verdict about the integration.\n return { status: 'transient_error', message: `${where}: ${ctx.source} unreadable or unparseable` };\n }\n\n if (cause === 'key-absent') {\n if (ctx.expectedAbsent) {\n return {\n status: 'transient_error',\n message: `${where}: not in ${ctx.source} (quarantined — absence expected)`,\n };\n }\n if (isWithinFirstConnectWindow(ctx.createdAt, ctx.now, ctx.windowMs)) {\n return {\n status: 'transient_error',\n message: `${where}: not yet written to ${ctx.source} (within first-connect window)`,\n };\n }\n return { status: 'down', message: `${where}: not wired in ${ctx.source}` };\n }\n\n // wrong-shape: the entry is there but this leg cannot probe it. Not a timing\n // artifact — the writer emitted something unusable — so it escalates. A\n // quarantined row is still exempt: its inline entry is a reconstruction, and a\n // reconstruction we got wrong must not be reported as the integration failing.\n if (ctx.expectedAbsent) {\n return {\n status: 'transient_error',\n message: `${where}: reconstructed ${ctx.source} entry is not ${ctx.transport}-probeable (quarantined)`,\n };\n }\n return {\n status: 'down',\n message: `${where}: ${ctx.source} entry is not ${ctx.transport}-probeable`,\n };\n}\n","/**\n * ENG-6441: the host-side connectivity-probe execution context, extracted from\n * manager-worker.ts so BOTH the manager's periodic probe loop AND the on-demand\n * `agt integration probe` command build the SAME probeEnv + probeDeps — there is\n * exactly one definition of \"how a probe runs on this host\", so the two can't\n * drift (a managed-Composio false-RED fixed in one would silently persist in the\n * other).\n *\n * `executeConnectivityProbe` needs host-only inputs the central API does not have:\n * the agent's project `.mcp.json` (the wired server's url + headers — the exact\n * path the agent's tool calls take) and its `.env.integrations` (the per-agent\n * tokens that resolve templated auth like `Bearer ${GRANOLA_ACCESS_TOKEN}`). This\n * module assembles them; the manager and the CLI command both consume it. This is\n * also why `POST /integrations/:id/test` can only echo the last host-recorded\n * status — none of this context exists centrally.\n */\nimport { join } from 'node:path';\nimport { existsSync, readFileSync } from 'node:fs';\nimport {\n probeComposioAccount,\n probeComposioMcpToolCall,\n resolveConnectivityProbe,\n REMOTE_MCP_PROXY_ENV,\n buildForwardHeaders,\n readRemoteMcpAuthConfig,\n isDefaultRemoteMcpConnection,\n remoteMcpServerKey,\n} from '@augmented/core/integrations';\nimport type { ConnectivityTestOverride, ToolkitSourceType } from '@augmented/core/integrations';\nimport type { IntegrationAuthType } from '@augmented/core';\nimport type { ConnectivityProbeDeps } from './connectivity-probe-executor.js';\nimport { probeMcpHttp } from './mcp-probe-client.js';\nimport { probeMcpStdio } from './mcp-stdio-probe.js';\nimport { parseEnvIntegrations, expandTemplateVars, LATE_BOUND_VARS } from './mcp-env-probe.js';\nimport {\n inlineEntryLookup,\n verdictForUnavailableMcpConfig,\n type McpConfigLookup,\n} from './mcp-config-lookup.js';\nimport { runCliProbe } from './cli-probe.js';\nimport { resolveIntegrationServerKeys, sanitizeServerKey } from './session-tool-bind-runner.js';\n\n/**\n * ENG-8049: a raw `.mcp.json` server entry (the object stored under\n * `mcpServers[key]`). Shared by the file-read path and the inline path so a\n * quarantined integration - absent from `.mcp.json` - can be probed from a\n * server entry the manager reconstructs via the framework adapter.\n */\nexport interface McpJsonServerEntry {\n type?: string;\n url?: string;\n headers?: Record<string, string>;\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\n// ENG-6232: substitute `${VAR}` placeholders in the url + header values against\n// the probe env (the agent's `.env.integrations` overlaid onto the manager env).\n// The .mcp.json carries templated auth like `Authorization: Bearer ${GRANOLA_ACCESS_TOKEN}`;\n// Claude Code expands it from the agent's spawn env, but the manager process\n// running this probe does not carry per-agent tokens. Without substitution the\n// probe sends the literal `${VAR}` → guaranteed 401 → false `down`. `unresolved`\n// names any var we could not substitute so the caller can skip rather than fire\n// a doomed request. ENG-8049: extracted from readMcpHttpServerConfig so the\n// inline-entry path resolves identically.\nexport function resolveHttpServerEntry(\n entry: McpJsonServerEntry,\n env?: NodeJS.ProcessEnv,\n): { url: string; headers?: Record<string, string>; unresolved: string[] } | null {\n const unresolved = new Set<string>();\n const sub = (value: string): string => {\n if (!env) return value;\n const r = expandTemplateVars(value, env);\n for (const name of r.unresolved) unresolved.add(name);\n return r.value;\n };\n\n if (typeof entry.url === 'string' && (entry.type === 'http' || entry.type === undefined)) {\n const url = sub(entry.url);\n let headers: Record<string, string> | undefined;\n if (entry.headers) {\n headers = {};\n for (const [k, v] of Object.entries(entry.headers)) headers[k] = sub(v);\n }\n return { url, ...(headers ? { headers } : {}), unresolved: [...unresolved] };\n }\n\n // ENG-6859: the remote OAuth integrations (brand-ninja, granola) are now\n // wired as the stdio remote-oauth-proxy rather than a direct http entry, so\n // reconstruct the equivalent {url, headers} the probe needs from the proxy's\n // env. The proxy forwards to AGT_REMOTE_MCP_URL with the credential read live\n // from AGT_REMOTE_MCP_TOKEN_VAR (resolved here from the overlaid\n // .env.integrations, exactly like the old templated header). An unresolved\n // token var lands in `unresolved` so the caller skips rather than false-downs.\n //\n // ENG-8357: this used to hardcode `Authorization: Bearer <token>` while the\n // proxy had grown AGT_REMOTE_MCP_AUTH_HEADER + AGT_REMOTE_MCP_EXTRA_HEADERS\n // (ENG-7748). For a non-Bearer remote the agent authenticated correctly and\n // the probe got a 401 — and a 401 at `initialize` is a REAL failure\n // observation, so it records `down`, which escalates to a hard-down /\n // needs-reauth on a WORKING integration within ~3 cycles. Both sides now build\n // headers from the same core helpers, so neither can grow an auth input the\n // other ignores.\n const proxyUrl = entry.env?.[REMOTE_MCP_PROXY_ENV.url];\n const tokenVar = entry.env?.[REMOTE_MCP_PROXY_ENV.tokenVar];\n if (entry.command && typeof proxyUrl === 'string' && typeof tokenVar === 'string') {\n const url = sub(proxyUrl);\n const token = sub(`\\${${tokenVar}}`);\n const { authHeader, extras } = readRemoteMcpAuthConfig(entry.env);\n // Extra-header vars are read DIRECTLY from the probe env, not through\n // `sub()`, and an unreadable one ships empty rather than joining\n // `unresolved`. That mirrors the proxy exactly (empty ⇒ \"no session bound\"),\n // and it is load-bearing: Anchor's ANCHOR_BROWSER_SESSION_ID is in\n // LATE_BOUND_VARS, which expandTemplateVars marks unresolved\n // unconditionally — routing extras through `sub()` would make every such\n // entry skip forever, trading the false `down` for the perpetual false\n // `transient_error` of ENG-6428 / ENG-8205.\n const headers = buildForwardHeaders(token, authHeader, extras, (varName) => {\n const value = env?.[varName];\n return typeof value === 'string' ? value : null;\n });\n return { url, headers, unresolved: [...unresolved] };\n }\n\n return null;\n}\n\n// ENG-5641: read a streamable-HTTP MCP server's url + headers from the agent's\n// project .mcp.json, for the connectivity probe.\n//\n// ENG-8363: returns a discriminated {@link McpConfigLookup} instead of a bare\n// null. That null collapsed three different facts — no file, no key, wrong shape\n// — and each probe leg then guessed what it meant (the HTTP leg guessed\n// `transient_error`, the stdio leg guessed `down`, for identical input). The\n// cause now reaches the caller so `verdictForUnavailableMcpConfig` decides once,\n// for both.\nexport function readMcpHttpServerConfig(\n projectDir: string,\n serverKey: string,\n env?: NodeJS.ProcessEnv,\n): McpConfigLookup<{ url: string; headers?: Record<string, string>; unresolved: string[] }> {\n let servers: Record<string, McpJsonServerEntry>;\n try {\n const raw = readFileSync(join(projectDir, '.mcp.json'), 'utf-8');\n servers = (JSON.parse(raw) as {\n mcpServers?: Record<string, McpJsonServerEntry>;\n }).mcpServers ?? {};\n } catch {\n return { ok: false, cause: 'file-unreadable' };\n }\n const entry = servers[serverKey];\n if (!entry) return { ok: false, cause: 'key-absent' };\n const value = resolveHttpServerEntry(entry, env);\n return value ? { ok: true, value } : { ok: false, cause: 'wrong-shape' };\n}\n\n/**\n * ENG-8345: pick the `.mcp.json` server key that ACTUALLY backs an integration,\n * by intersecting deterministic candidates against the keys the file really\n * declares — rather than trusting a DERIVED key to match what the writer wrote.\n *\n * `deriveMcpServerKey` sanitises the definition_id (`brand-ninja` →\n * `brand_ninja`), but the remote-MCP writer keys its entries by the RAW\n * definition_id (`mcpServers['brand-ninja']`, claudecode/index.ts). For any\n * hyphenated remote definition the derived key therefore names a server that\n * does not exist, the lookup returns null, and the probe reports a\n * non-escalating `transient_error` — every cycle, forever. Because\n * `transient_error` never escalates to `down` (hysteresis needs a `down`\n * OBSERVATION) and the streak only resets on an `ok`, that grew an unbounded\n * FALSE streak: brand-ninja reached 433 and tripped the CRITICAL transient\n * spike alert, the same shape as the origami 339 (ENG-7405) and broker 280/282\n * (ENG-6428/ENG-8205) incidents before it.\n *\n * The durable fix is the invariant \"a probe never INVENTS a server key\" —\n * resolve it from what is declared. Reuses `resolveIntegrationServerKeys`, the\n * same candidate intersection the session-tool-bind probe already runs against\n * this very file, so a future writer branch cannot silently desync one probe\n * while the other keeps working.\n *\n * The derived key is passed as the HINT and therefore wins whenever it is\n * genuinely declared — Composio (`composio_outlook`) and every already-agreeing\n * case keep their exact current behaviour. Returns null when the file is\n * unreadable or nothing matches; callers fall back to the derived key so the\n * not-resolvable diagnostics are unchanged.\n */\nexport function resolveDeclaredServerKey(\n projectDir: string,\n definitionId: string,\n derivedKey: string,\n): string | null {\n try {\n const raw = readFileSync(join(projectDir, '.mcp.json'), 'utf-8');\n const servers = (JSON.parse(raw) as {\n mcpServers?: Record<string, McpJsonServerEntry>;\n }).mcpServers ?? {};\n const declared = Object.keys(servers);\n\n // A NAMED connection (ENG-7543) derives `<base>-<connection_key>`, which is\n // neither bare form. For one of those an EXACT match is the only safe\n // answer: the candidate fallback would otherwise match the bare key — i.e.\n // probe the DEFAULT connection's server and report that verdict as the\n // named connection's health. That is a false GREEN, strictly worse than the\n // false alarm this function exists to remove, because an `ok` zeroes the\n // streak and closes any open alert.\n //\n // ENG-8359 made the exact match satisfiable: the writer now emits a\n // per-connection key (`<sanitized-base>-<connection_key>`) built from the\n // same shared rule this probe derives, so a live named connection resolves\n // here. Requiring the exact match is therefore the PERMANENT contract\n // between writer and probe, not a placeholder — a null now means the named\n // connection genuinely is not on disk, which is the honest answer.\n // (Before that change the writer keyed every entry `mcpServers[definition_id]`\n // bare, so two named connections clobbered each other and no suffixed key\n // was ever written.)\n if (derivedKey !== definitionId && derivedKey !== sanitizeServerKey(definitionId)) {\n return declared.includes(derivedKey) ? derivedKey : null;\n }\n\n return resolveIntegrationServerKeys(definitionId, declared, derivedKey)[0] ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * ENG-7405: read a LOCAL STDIO MCP server's spawn config (command/args/env)\n * from the agent's `.mcp.json`, `${VAR}`-substituted from the probe env — the\n * stdio sibling of {@link readMcpHttpServerConfig}. Returns null when the key\n * isn't a stdio (command-bearing) entry or the file/key isn't resolvable.\n * `unresolved` names any `${VAR}` we couldn't substitute so the caller can skip\n * rather than spawn a server with a literal `${VAR}` in its env.\n */\n// ENG-8049: extracted from readMcpStdioServerConfig so the inline-entry path\n// resolves identically. Given a raw stdio server entry, `${VAR}`-substitute its\n// args + env from the probe env, returning null when it isn't a command-bearing\n// (stdio) entry.\nexport function resolveStdioServerEntry(\n entry: McpJsonServerEntry,\n env?: NodeJS.ProcessEnv,\n): { command: string; args: string[]; env: Record<string, string>; unresolved: string[] } | null {\n if (typeof entry.command !== 'string') return null;\n\n // Only NON-late-bound unresolved vars block the probe. A late-bound var\n // (AGT_TOKEN et al.) unresolved in the manager's probe context is the\n // DESIGNED state, not a fault - see the late-bound handling for the env below.\n const unresolved = new Set<string>();\n const sub = (value: string): string => {\n if (!env) return value;\n const r = expandTemplateVars(value, env);\n for (const name of r.unresolved) if (!LATE_BOUND_VARS.has(name)) unresolved.add(name);\n return r.value;\n };\n\n // ENG-7405 follow-up: a spawn-env value whose ONLY unresolved refs are\n // late-bound (e.g. `AGT_TOKEN=${AGT_TOKEN}`) must NOT skip the probe. Those\n // vars are bound by a lower layer at real spawn time, or the server resolves\n // them itself - origami's broker identity exchanges AGT_API_KEY → /host/exchange\n // when AGT_TOKEN is absent (packages/origami-mcp-server identity.ts). Passing\n // the literal `${AGT_TOKEN}` would poison the child; treating it as a hard\n // unresolved var skips the probe every cycle, and since a non-'ok' verdict\n // increments consecutive_connectivity_failures, that yielded a false\n // `transient_error` streak that never cleared (the 339-count on every origami\n // agent). So OMIT the key - the child sees it unset and takes its designed\n // fallback, exactly as the live session does. A NON-late-bound unresolved var\n // is still a real gap and blocks (surfaced in `unresolved`).\n const resolvedEnv: Record<string, string> = {};\n for (const [k, v] of Object.entries(entry.env ?? {})) {\n if (!env) {\n resolvedEnv[k] = v;\n continue;\n }\n const r = expandTemplateVars(v, env);\n const hardUnresolved = r.unresolved.filter((name) => !LATE_BOUND_VARS.has(name));\n for (const name of hardUnresolved) unresolved.add(name);\n // Late-bound-only unresolved → drop the key (leave it unset for the child).\n if (hardUnresolved.length === 0 && r.unresolved.length > 0) continue;\n resolvedEnv[k] = r.value;\n }\n return {\n command: entry.command,\n args: (entry.args ?? []).map(sub),\n env: resolvedEnv,\n unresolved: [...unresolved],\n };\n}\n\n// ENG-8363: same discriminated result as the HTTP reader — see there for why the\n// bare null had to go. Keeping the two readers structurally identical is the\n// point: the verdict asymmetry this fixes grew out of two lookups that LOOKED\n// alike but reported differently.\nexport function readMcpStdioServerConfig(\n projectDir: string,\n serverKey: string,\n env?: NodeJS.ProcessEnv,\n): McpConfigLookup<{ command: string; args: string[]; env: Record<string, string>; unresolved: string[] }> {\n let servers: Record<string, McpJsonServerEntry>;\n try {\n const raw = readFileSync(join(projectDir, '.mcp.json'), 'utf-8');\n servers = (JSON.parse(raw) as {\n mcpServers?: Record<string, McpJsonServerEntry>;\n }).mcpServers ?? {};\n } catch {\n return { ok: false, cause: 'file-unreadable' };\n }\n const entry = servers[serverKey];\n if (!entry) return { ok: false, cause: 'key-absent' };\n const value = resolveStdioServerEntry(entry, env);\n return value ? { ok: true, value } : { ok: false, cause: 'wrong-shape' };\n}\n\n/**\n * ENG-6396: derive the `.mcp.json` server key for an integration's connectivity\n * probe — or `undefined` when the integration is not MCP-`tools/list`-probeable.\n *\n * The key is needed for any integration whose RESOLVED probe kind is an MCP\n * `tools/list` handshake against the agent's wired server: `managed_composite`\n * (Composio) AND `mcp_tools_list` (remote streamable-HTTP OAuth providers like\n * granola). The historical bug (the granola/Dwight incident) was keying off the\n * raw `source_type` string (`managed`/`mcp_server`) — granola's toolkit carries\n * `source_type: 'native'` despite being a remote MCP, so it got no key, its host\n * probe never ran, and it sat \"Connected\" with a dead token. Keying off the\n * resolver's kind (which already owns the `mcpUrl` detection) fixes every\n * remote-MCP provider, not just granola, and keeps the predicate in one place.\n *\n * Key derivation matches the managed-toolkit .mcp.json writer: definition_id\n * sanitised (non-alphanumeric → '_', lowercased) — e.g. 'composio_outlook',\n * 'granola'. An unresolvable key degrades to transient_error, never a false 'down'.\n */\nexport function deriveMcpServerKey(input: {\n definitionId: string;\n sourceType: ToolkitSourceType | null;\n authType: IntegrationAuthType | null;\n connectivityTest?: ConnectivityTestOverride | null;\n /**\n * ENG-7543: the connection this row is (default | slug). A named connection's\n * `.mcp.json` server is written under `<base>-<connection_key>` (Lane B's\n * mcp_server_key), so the probe must target the SAME suffixed key or it\n * handshakes the wrong (default) server and false-fails the named connection.\n */\n connectionKey?: string | null;\n}): string | undefined {\n const kind = resolveConnectivityProbe({\n definitionId: input.definitionId,\n sourceType: input.sourceType,\n authType: input.authType,\n connectivityTest: input.connectivityTest ?? null,\n }).kind;\n if (kind !== 'mcp_tools_list' && kind !== 'managed_composite') return undefined;\n const base = input.definitionId.replace(/[^a-z0-9]/gi, '_').toLowerCase();\n // Default/absent → the sanitized base. Deliberately NOT the raw definition_id\n // even though the remote-MCP writer keys the default connection that way:\n // ENG-8345's resolveDeclaredServerKey intersects {raw, sanitized, hint}\n // against the keys actually declared, which bridges the two forms for the\n // default connection AND keeps this byte-identical for the managed lane.\n if (isDefaultRemoteMcpConnection(input.connectionKey)) return base;\n // ENG-8359: a NAMED connection's key comes from the WRITER's own rule, not a\n // local copy of it. resolveDeclaredServerKey requires an EXACT declared match\n // for a suffixed key (falling back to the bare key would probe the default\n // connection and report it as the named one's health — a false green), so if\n // this derivation ever drifted from what provisioning wrote, the named\n // connection would be permanently unprobeable with no error anywhere.\n return remoteMcpServerKey(input.definitionId, input.connectionKey);\n}\n\n/**\n * ENG-6206: overlay the agent's `.env.integrations` onto the manager's env so\n * CLI probes that assert auth (e.g. `gh auth status`) see the integration token\n * (GH_TOKEN/GITHUB_TOKEN) — it lives in the agent's env file, never the manager's\n * process env. Without this the gh probe would always report not-authenticated\n * (a false 'down'). Best-effort: a missing/unreadable file just falls back to the\n * manager env (the prior --version behaviour).\n */\nexport function buildProbeEnv(projectDir: string): NodeJS.ProcessEnv {\n const probeEnv: NodeJS.ProcessEnv = { ...process.env };\n try {\n const envIntPath = join(projectDir, '.env.integrations');\n if (existsSync(envIntPath)) {\n Object.assign(probeEnv, parseEnvIntegrations(readFileSync(envIntPath, 'utf-8')));\n }\n } catch {\n /* a probe must never break on a malformed env file */\n }\n return probeEnv;\n}\n\n/**\n * Assemble the four probe legs against an agent's wired config: a CLI `--version`\n * probe, an MCP streamable-HTTP `tools/list` handshake, a Composio connected-\n * account binding check, and a live read-only tool-call. All read the agent's OWN\n * `.mcp.json` (url + headers) so a probe exercises the exact path the agent's\n * tool calls take. `probeEnv` (from {@link buildProbeEnv}) resolves the templated\n * auth in those headers.\n */\nexport function buildConnectivityProbeDeps(\n projectDir: string,\n probeEnv: NodeJS.ProcessEnv,\n): ConnectivityProbeDeps {\n return {\n fetchImpl: fetch,\n runCli: (binary, args) => runCliProbe(binary, args, { env: probeEnv }),\n mcpProbe: async (target) => {\n // ENG-8049: a quarantined integration is gone from .mcp.json, so probe its\n // inline reconstructed entry when the executor threaded one; else read the file.\n // ENG-8345: resolve the key against what `.mcp.json` actually declares —\n // a derived key that names no declared server was the 433-count false\n // streak. Falls back to the derived key so the diagnostic below is\n // unchanged when nothing matches.\n const serverKey = target.inlineServerEntry\n ? target.serverKey\n : resolveDeclaredServerKey(projectDir, target.definitionId, target.serverKey) ?? target.serverKey;\n const lookup = target.inlineServerEntry\n ? inlineEntryLookup(resolveHttpServerEntry(target.inlineServerEntry as McpJsonServerEntry, probeEnv))\n : readMcpHttpServerConfig(projectDir, serverKey, probeEnv);\n // ENG-8363: the cause decides the verdict, via the SAME helper the stdio\n // leg uses. This branch used to hard-code `transient_error` while its stdio\n // twin hard-coded `down` for identical input.\n if (!lookup.ok) {\n return verdictForUnavailableMcpConfig(lookup.cause, {\n serverKey,\n transport: 'http',\n source: target.inlineServerEntry ? 'inline config' : '.mcp.json',\n createdAt: target.createdAt,\n expectedAbsent: target.expectedAbsent || Boolean(target.inlineServerEntry),\n now: Date.now(),\n });\n }\n const cfg = lookup.value;\n if (cfg.unresolved.length > 0) {\n // ENG-6232: the auth header references env var(s) we can't substitute\n // from the agent's .env.integrations. Sending the literal `${VAR}` would\n // earn a guaranteed 401 → false 'down'; skip as non-escalating instead.\n return {\n status: 'transient_error',\n message: `MCP '${serverKey}' auth unresolved: ${cfg.unresolved.join(', ')}`,\n };\n }\n return probeMcpHttp(cfg);\n },\n // ENG-7405: local-STDIO MCP probe. Spawns the toolkit's bundled server with\n // the agent's env (the exact command/args/env from `.mcp.json`), handshakes,\n // and calls the read-only `connectivity_test` tool when the descriptor set\n // one (threaded via target.toolName). An unresolvable `${VAR}` in the spawn\n // env → skip as non-escalating, never spawn a doomed server.\n //\n // ENG-8363: a missing server entry is no longer a flat `down`. It still is\n // one once the writer has had its chance, but the manager probes BEFORE it\n // writes `.mcp.json` in the same poll, so the first probe of every\n // newly-added integration necessarily finds the key absent — that window\n // reads `transient_error` (and is what keeps ENG-7575's first-connect\n // backoff engaged). `verdictForUnavailableMcpConfig` owns the distinction.\n mcpStdioProbe: async (target) => {\n // ENG-8049: prefer the inline reconstructed entry for a quarantined row.\n // ENG-8345: same resolve-don't-derive rule as the HTTP leg. Since\n // ENG-8363 both legs escalate an unmatched key to `down` once the row is\n // past the first-connect window, so the rule matters equally on each —\n // this leg is no longer the only one that can hard-fail on a bad key.\n const serverKey = target.inlineServerEntry\n ? target.serverKey\n : resolveDeclaredServerKey(projectDir, target.definitionId, target.serverKey) ?? target.serverKey;\n const lookup = target.inlineServerEntry\n ? inlineEntryLookup(resolveStdioServerEntry(target.inlineServerEntry as McpJsonServerEntry, probeEnv))\n : readMcpStdioServerConfig(projectDir, serverKey, probeEnv);\n if (!lookup.ok) {\n return verdictForUnavailableMcpConfig(lookup.cause, {\n serverKey,\n transport: 'stdio',\n source: target.inlineServerEntry ? 'inline config' : '.mcp.json',\n createdAt: target.createdAt,\n expectedAbsent: target.expectedAbsent || Boolean(target.inlineServerEntry),\n now: Date.now(),\n });\n }\n const cfg = lookup.value;\n if (cfg.unresolved.length > 0) {\n return {\n status: 'transient_error',\n message: `MCP stdio '${serverKey}' env unresolved: ${cfg.unresolved.join(', ')}`,\n };\n }\n return probeMcpStdio({\n command: cfg.command,\n args: cfg.args,\n env: cfg.env,\n cwd: projectDir,\n connectivityTest: target.toolName ? { tool: target.toolName, args: target.toolArgs ?? null } : null,\n });\n },\n // ENG-6139: connected-account binding check for managed (Composio) toolkits.\n // The MCP handshake reads green on a dead/mis-bound account, so the managed\n // probe also verifies the account is ACTIVE + bound to the entity the agent\n // queries with. Inputs come from the agent's OWN wired MCP server: the\n // `x-api-key` header and the `user_id` query param (the agent already\n // authenticates with these), plus the recorded connected_account_id.\n composioProbe: async (serverKey, credentials, inlineServerEntry, rowCtx) => {\n // ENG-7543: the executor passes the already-resolved (connection-suffixed)\n // server key so the binding check reads the correct connection's server —\n // no toolkit-level re-derivation here.\n // ENG-8049: for a quarantined row, resolve the inline reconstructed entry.\n const lookup = inlineServerEntry\n ? inlineEntryLookup(resolveHttpServerEntry(inlineServerEntry as McpJsonServerEntry, probeEnv))\n : readMcpHttpServerConfig(projectDir, serverKey, probeEnv);\n // ENG-8363: this leg is a THIRD site that had its own idea of what an\n // unreadable config means (it hard-coded `transient_error`). Routed through\n // the shared helper so all three agree by construction.\n if (!lookup.ok) {\n return verdictForUnavailableMcpConfig(lookup.cause, {\n serverKey,\n transport: 'http',\n source: inlineServerEntry ? 'inline config' : '.mcp.json',\n createdAt: rowCtx?.createdAt,\n expectedAbsent: rowCtx?.expectedAbsent || Boolean(inlineServerEntry),\n now: Date.now(),\n });\n }\n const cfg = lookup.value;\n if (cfg.unresolved.length > 0) {\n // ENG-6232: unresolved templated auth (e.g. x-api-key) → skip, never a false 'down'.\n return {\n status: 'transient_error',\n message: `MCP '${serverKey}' auth unresolved: ${cfg.unresolved.join(', ')}`,\n };\n }\n // HTTP header names are case-insensitive — match `x-api-key` regardless of\n // the casing the .mcp.json writer used (e.g. `X-Api-Key`), not just two\n // hardcoded variants, so a probe can't false-fail on a casing mismatch.\n const apiKey =\n Object.entries(cfg.headers ?? {}).find(([k]) => k.toLowerCase() === 'x-api-key')?.[1] ?? '';\n let expectedUserId = '';\n // ENG-6157: the wired serverId is embedded in the MCP URL path\n // (`/v3/mcp/<serverId>/mcp`). Passing it lets the probe assert the\n // account's auth_config is one this exact server resolves with — the\n // server the agent ACTUALLY queries, the ground-truth source.\n let serverId: string | undefined;\n try {\n const u = new URL(cfg.url);\n expectedUserId = u.searchParams.get('user_id') ?? '';\n const m = u.pathname.match(/\\/v3\\/mcp\\/([^/]+)\\/mcp/);\n serverId = m?.[1] ? decodeURIComponent(m[1]) : undefined;\n } catch {\n expectedUserId = '';\n }\n const connectedAccountId =\n typeof credentials?.['connected_account_id'] === 'string'\n ? (credentials['connected_account_id'] as string)\n : '';\n return probeComposioAccount({ connectedAccountId, apiKey, expectedUserId, serverId });\n },\n // ENG-6157 (Phase 2): the live tool-call leg. Uses the agent's OWN wired MCP\n // URL + headers (the exact path tool calls take), so a broken auth_config\n // linkage surfaces as a real `No connected account found` instead of a\n // green handshake. Skips (`null`) when no safe read-only tool is callable.\n composioToolCallProbe: async (target) => {\n // ENG-8049: prefer the inline reconstructed entry for a quarantined row.\n const lookup = target.inlineServerEntry\n ? inlineEntryLookup(resolveHttpServerEntry(target.inlineServerEntry as McpJsonServerEntry, probeEnv))\n : readMcpHttpServerConfig(projectDir, target.serverKey, probeEnv);\n // ENG-8363: deliberately still a SKIP, not a verdict — unlike the three\n // legs above. This leg only ever ADDS signal (`managed_composite` folds\n // worst-wins, and the mcpProbe leg has already produced the config verdict\n // for the same server key). Returning a verdict here would double-count the\n // identical fact; returning null leaves the fold untouched.\n if (!lookup.ok) return null;\n const cfg = lookup.value;\n // ENG-6232: can't substitute the auth header → skip (null = not reported),\n // never fire a doomed tool call that would read as a false signal.\n if (cfg.unresolved.length > 0) return null;\n // ENG-6242: call the toolkit's prescribed `connectivity_test.tool` (when set)\n // — the SAME read-only tool the central Test path uses — instead of\n // auto-picking. probeComposioMcpToolCall re-validates it read-only against the\n // live tools/list and falls back to the heuristic on drift, so a stale/missing\n // override can never run an unsafe tool. Fixes the false-RED that auto-pick\n // produced for managed Linear (LINEAR_GET_CURRENT_USER).\n return probeComposioMcpToolCall({\n url: cfg.url,\n headers: cfg.headers,\n toolName: target.toolName,\n toolArgs: target.toolArgs,\n });\n },\n };\n}\n","/**\n * ENG-7405 - real MCP connectivity probe client (local STDIO).\n *\n * The stdio sibling of `probeMcpHttp`. Spawns the toolkit's bundled MCP server\n * exactly as the agent's `.mcp.json` entry would (same command / args / env),\n * runs `initialize -> notifications/initialized -> tools/list`, optionally\n * `tools/call` the toolkit's read-only `connectivity_test` tool, then tears the\n * child down. Maps the result to a {@link ConnectivityProbeOutcome}.\n *\n * Why spawn a throwaway instead of talking to the live session server: Claude\n * Code owns the live child's stdio pipes; the manager can't reach them. A fresh\n * spawn with the agent's real env is the honest host-side reachability check -\n * for broker-identity servers (origami) it even exercises the credential-fetch\n * path end to end, so a dead key / unreachable vendor surfaces as `down` rather\n * than the `builtin` rubber-stamp this replaces.\n *\n * MCP stdio framing is newline-delimited JSON-RPC (one message per line). We\n * hand-roll it (no MCP SDK dep in apps/cli), mirroring probeMcpHttp's\n * hand-rolled HTTP JSON-RPC. Read-only: initialize + tools/list + an optional\n * read-only tools/call.\n */\nimport { spawn } from 'node:child_process';\nimport type { ConnectivityProbeOutcome, ConnectivityTestOverride } from '@augmented/core/integrations';\nimport { handshakeToolsListOutcome } from '@augmented/core/integrations';\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\nexport interface McpStdioProbeConfig {\n command: string;\n args?: string[];\n /** Resolved env for the child (the `.mcp.json` entry env, `${VAR}`-substituted). */\n env?: Record<string, string>;\n cwd?: string;\n /** Optional read-only `connectivity_test` tool to call after tools/list. */\n connectivityTest?: ConnectivityTestOverride | null;\n timeoutMs?: number;\n}\n\ninterface RpcResult {\n result?: unknown;\n error?: { message?: string };\n}\n\n/**\n * Spawn the server, exchange the JSON-RPC messages, resolve the outcome. All\n * process/stream side effects are contained here so the mapping is exercised\n * against a real child in the unit test (a tiny fixture MCP server).\n */\nexport async function probeMcpStdio(config: McpStdioProbeConfig): Promise<ConnectivityProbeOutcome> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const child = spawn(config.command, config.args ?? [], {\n // Faithful reproduction of the real spawn: the child inherits the manager's\n // process env (PATH/HOME so `node`/`npx` resolve) with the entry's env\n // overlaid, exactly as Claude Code spawns it.\n env: { ...process.env, ...(config.env ?? {}) },\n cwd: config.cwd,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderrTail = '';\n const pending = new Map<number, (rpc: RpcResult) => void>();\n\n return await new Promise<ConnectivityProbeOutcome>((resolve) => {\n let done = false;\n const finish = (outcome: ConnectivityProbeOutcome) => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n child.stdout.removeAllListeners();\n try {\n child.kill('SIGKILL');\n } catch {\n /* already gone */\n }\n resolve(outcome);\n };\n\n const timer = setTimeout(() => {\n finish({ status: 'transient_error', message: `MCP stdio handshake timed out after ${timeoutMs / 1000}s` });\n }, timeoutMs);\n // The probe owns the whole lifecycle; don't let the timer keep the loop alive.\n if (typeof timer.unref === 'function') timer.unref();\n\n child.on('error', (err) => {\n // spawn failure (ENOENT, EACCES): the server can't even start - a real down.\n finish({ status: 'down', message: `MCP server failed to spawn: ${err.message}` });\n });\n child.on('exit', (code) => {\n // Exited before answering: dead server. Surface stderr tail for diagnosis.\n if (!done) {\n finish({\n status: 'down',\n message: `MCP server exited (code ${code ?? 'null'})${stderrTail ? `: ${stderrTail.trim().slice(-200)}` : ''}`,\n });\n }\n });\n // A write to a closed pipe (child exited mid-handshake) emits an async\n // `EPIPE` on stdin; without a listener that's an unhandled 'error' that\n // crashes the probe instead of returning an outcome. Swallow it - the real\n // failure already surfaces via the 'exit'/'error'/timeout paths as `down`.\n child.stdin.on('error', () => {});\n child.stderr.on('data', (d: Buffer) => {\n stderrTail = (stderrTail + d.toString()).slice(-500);\n });\n child.stdout.on('data', (d: Buffer) => {\n stdout += d.toString();\n let nl: number;\n while ((nl = stdout.indexOf('\\n')) >= 0) {\n const line = stdout.slice(0, nl).trim();\n stdout = stdout.slice(nl + 1);\n if (!line) continue;\n let msg: (RpcResult & { id?: unknown }) | null = null;\n try {\n msg = JSON.parse(line) as RpcResult & { id?: unknown };\n } catch {\n continue; // non-JSON server log line on stdout - ignore\n }\n if (msg && typeof msg.id === 'number' && pending.has(msg.id)) {\n const cb = pending.get(msg.id)!;\n pending.delete(msg.id);\n cb(msg);\n }\n }\n });\n\n const send = (msg: Record<string, unknown>) => child.stdin.write(`${JSON.stringify(msg)}\\n`);\n const request = (id: number, method: string, params?: Record<string, unknown>): Promise<RpcResult> =>\n new Promise((res) => {\n pending.set(id, res);\n send({ jsonrpc: '2.0', id, method, ...(params ? { params } : {}) });\n });\n\n void (async () => {\n const init = await request(1, 'initialize', {\n protocolVersion: '2025-03-26',\n capabilities: {},\n clientInfo: { name: 'augmented-connectivity-probe', version: '1.0.0' },\n });\n if (done) return;\n if (init.error) return finish({ status: 'down', message: `MCP initialize error: ${init.error.message ?? 'unknown'}` });\n\n send({ jsonrpc: '2.0', method: 'notifications/initialized' });\n\n const list = await request(2, 'tools/list');\n if (done) return;\n if (list.error) return finish({ status: 'down', message: `MCP tools/list error: ${list.error.message ?? 'unknown'}` });\n const toolCount = Array.isArray((list.result as { tools?: unknown[] })?.tools)\n ? (list.result as { tools: unknown[] }).tools.length\n : undefined;\n\n const testTool = config.connectivityTest?.tool;\n if (testTool) {\n const rawArgs = config.connectivityTest?.args;\n // An MCP tools/call takes an object. A string[] (the cli_tool `args`\n // form) is misconfigured metadata - fail loud rather than silently\n // calling with `{}`, so a bad seed can't read as a real green.\n if (Array.isArray(rawArgs)) {\n return finish({\n status: 'down',\n message: `connectivity_test.args for ${testTool} must be an object (got a string[])`,\n });\n }\n const toolArgs = rawArgs ?? {};\n const call = await request(3, 'tools/call', { name: testTool, arguments: toolArgs });\n if (done) return;\n if (call.error) return finish({ status: 'down', message: `MCP tools/call ${testTool} error: ${call.error.message ?? 'unknown'}` });\n // Tool-level failure: the call resolved but the tool reported isError -\n // a real \"can't execute\" (dead key / vendor down), not a green.\n if ((call.result as { isError?: boolean })?.isError === true) {\n return finish({ status: 'down', message: `MCP tool ${testTool} returned an error result` });\n }\n return finish({\n status: 'ok',\n message: `${testTool} succeeded`,\n details: { ...(toolCount !== undefined ? { toolCount } : {}), testTool },\n });\n }\n\n // ENG-8358: a zero-tool manifest is NOT a green (see handshakeToolsListOutcome).\n finish(handshakeToolsListOutcome(toolCount));\n })();\n });\n}\n","/**\n * ENG-5641 — read-only CLI connectivity probe (manager CLI).\n *\n * Runs a single read-only command (e.g. `<bin> --version`, `gcloud version`)\n * and maps the result to a ConnectivityProbeOutcome. The manager already\n * installs/manages these CLI tools, so this just confirms the binary is on\n * PATH and answers — the cheapest \"is this tool usable\" signal.\n *\n * - exit 0 → 'ok'\n * - ENOENT (missing) → 'down' (not installed / not on PATH)\n * - non-zero exit → 'down' (present but erroring — needs attention)\n * - timeout → 'transient_error'\n *\n * Read-only by contract: callers pass only inspection args (the resolver's\n * cliArgs, defaulting to `--version`). Never runs a mutating subcommand.\n */\n\nimport { execFile } from 'node:child_process';\nimport type { ConnectivityProbeOutcome } from '@augmented/core/integrations';\n\nconst DEFAULT_TIMEOUT_MS = 8_000;\n\nexport interface CliProbeOptions {\n timeoutMs?: number;\n /** Extra env for the child (e.g. the integration's API key var). */\n env?: NodeJS.ProcessEnv;\n}\n\nexport function runCliProbe(\n binary: string,\n args: string[],\n opts: CliProbeOptions = {},\n): Promise<ConnectivityProbeOutcome> {\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n return new Promise((resolve) => {\n execFile(\n binary,\n args,\n { timeout: timeoutMs, env: opts.env ?? process.env, windowsHide: true },\n (err, stdout) => {\n if (!err) {\n const firstLine = String(stdout).split(/\\r?\\n/, 1)[0]?.trim();\n resolve({ status: 'ok', message: firstLine ? `${binary}: ${firstLine}` : `${binary}: ok` });\n return;\n }\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === 'ENOENT') {\n resolve({ status: 'down', message: `${binary} not found on PATH (not installed)` });\n return;\n }\n if (e.killed || e.signal === 'SIGTERM') {\n resolve({ status: 'transient_error', message: `${binary} probe timed out after ${timeoutMs / 1000}s` });\n return;\n }\n resolve({ status: 'down', message: `${binary} exited non-zero: ${e.message}` });\n },\n );\n });\n}\n","// ENG-7053 Slice C — live-session tool-presence verdict (pure core).\n//\n// Decides whether an integration's MCP tools are actually BOUND in the running\n// agent session, by composing the signals the manager already has:\n//\n// • stdio MCP servers (incl. the ENG-6859 remote-OAuth stdio proxy): a server\n// is bound iff it has a live child process. `findMissingMcpServers`\n// (mcp-presence-reaper) already computes the declared-stdio keys with ZERO\n// live children — exactly the \"tools fell off the session\" symptom. A key in\n// that missing-set ⇒ not bound.\n// • URL/HTTP remote MCP servers: no child process to find, so presence is an\n// HTTP `initialize → tools/list` reachability check (`probeMcpHttp`). Tools\n// bound ⇒ the endpoint answers; unreachable/unauthorized ⇒ not bound.\n//\n// This is the OBSERVATIONAL signal for Slice C (flag AGT_SESSION_TOOL_PROBE_ENABLED).\n// Slice A consumes the same verdict to trigger an in-session rebind. Kept pure\n// (no `ps`/network here) so the decision logic is unit-testable; the manager\n// supplies the assembled inputs.\n\nimport type { McpConfig } from './stale-mcp-reaper.js';\n\n/** Verdict persisted to `<scope>_integrations.last_session_tool_bind_status`. */\nexport type SessionToolBindStatus = 'bound' | 'missing' | 'unreachable' | 'unknown';\n\n/** How Claude Code talks to a declared MCP server. */\nexport type McpServerKind = 'stdio' | 'http' | 'oauth-proxy' | 'unknown';\n\n/**\n * Classify a declared `.mcp.json` server entry.\n *\n * • `oauth-proxy` - the ENG-6859 remote-OAuth stdio proxy (granola,\n * brand-ninja). It has a `command` (a spawned child) but is really a shim in\n * front of a REMOTE endpoint (`AGT_REMOTE_MCP_URL`) whose Bearer is read live\n * from a token file (`AGT_REMOTE_MCP_TOKEN_VAR`). Its tools are bound only if\n * BOTH the child is alive AND the upstream still answers `tools/list`, so it\n * needs the stdio child-liveness leg AND the http reachability leg. Detected\n * by the proxy's baked env so a plain native stdio server is not misclassified\n * (ENG-7318: keying purely on `command` classified this `stdio`, so an\n * alive-child/stale-upstream proxy falsely read `bound` and never rebound).\n * • `stdio` - a plain spawned child (native MCP): bound iff the child is alive.\n * • `http` - a `url` (no `command`) remote Streamable-HTTP/SSE endpoint Claude\n * Code dials directly (no child).\n *\n * Mirrors the stdio test in `findMissingMcpServers`.\n */\nexport function classifyMcpServer(entry: unknown): McpServerKind {\n if (!entry || typeof entry !== 'object') return 'unknown';\n const e = entry as { command?: unknown; url?: unknown; env?: unknown };\n if (typeof e.command === 'string') {\n const env = e.env;\n if (\n env &&\n typeof env === 'object' &&\n typeof (env as Record<string, unknown>)['AGT_REMOTE_MCP_URL'] === 'string' &&\n typeof (env as Record<string, unknown>)['AGT_REMOTE_MCP_TOKEN_VAR'] === 'string'\n ) {\n return 'oauth-proxy';\n }\n return 'stdio';\n }\n if (typeof e.url === 'string') return 'http';\n return 'unknown';\n}\n\n/**\n * Split an integration's declared MCP server keys into stdio / http / oauth-proxy\n * using the agent's parsed `.mcp.json`. Keys with no entry (or an unclassifiable\n * one) are dropped - they can't be probed for presence either way.\n *\n * `proxy` keys (the ENG-6859 remote-OAuth stdio proxy) are a hybrid: they need\n * the stdio child-liveness leg (a dead child = `missing`) AND the http `tools/list`\n * reachability leg (an alive child whose upstream stopped answering = `unreachable`),\n * so they're kept separate rather than folded into `stdio` (ENG-7318).\n */\nexport function splitServerKeysByKind(\n mcpJson: McpConfig | null | undefined,\n serverKeys: readonly string[],\n): { stdio: string[]; http: string[]; proxy: string[] } {\n const servers = mcpJson?.mcpServers ?? {};\n const stdio: string[] = [];\n const http: string[] = [];\n const proxy: string[] = [];\n for (const key of serverKeys) {\n const kind = classifyMcpServer((servers as Record<string, unknown>)[key]);\n if (kind === 'stdio') stdio.push(key);\n else if (kind === 'http') http.push(key);\n else if (kind === 'oauth-proxy') proxy.push(key);\n }\n return { stdio, http, proxy };\n}\n\nexport interface SessionToolBindInput {\n /** Declared plain-stdio MCP server keys (native MCP) backing this integration. */\n stdioServerKeys: readonly string[];\n /** Declared URL/HTTP MCP server keys backing this integration. */\n httpServerKeys: readonly string[];\n /**\n * Declared OAuth-proxy stdio server keys (ENG-6859 remote-OAuth proxy, e.g.\n * granola) backing this integration. A hybrid: bound iff BOTH the proxy child\n * is alive (via `missingStdioKeys`) AND the upstream answers `tools/list` (via\n * `httpReachable`, reconstructed from the proxy env). An alive child whose\n * upstream stopped answering ⇒ `unreachable`, not a false `bound` (ENG-7318).\n */\n proxyServerKeys: readonly string[];\n /**\n * Stdio/proxy server keys found to have NO live child (from\n * `findMissingMcpServers`, which enumerates every `command` entry incl. the\n * OAuth proxy). Any of this integration's stdio OR proxy keys appearing here ⇒\n * its backing child died and its tools fell out of the session.\n */\n missingStdioKeys: ReadonlySet<string>;\n /**\n * Per-key reachability of the `tools/list` probe for http AND oauth-proxy keys:\n * true = answered (upstream healthy), false = unreachable/unauthorized, absent =\n * not probed this cycle.\n */\n httpReachable: ReadonlyMap<string, boolean>;\n /**\n * The `.mcp.json` `mcpServers` keys the running session actually loaded at\n * spawn (ENG-7263, from the session's persisted spawn-time snapshot). HTTP/\n * remote MCP servers have no child process, so a reachable endpoint proves the\n * endpoint+creds are good but NOT that THIS session loaded the (possibly newly\n * added) server key. This set is the HTTP analogue of stdio child-liveness:\n * only an HTTP key present here can promote to `bound`. A reachable key NOT in\n * the set (added after spawn, awaiting restart) or an empty/unknown set keeps\n * the integration out of `bound` - so we never claim \"live\" before the agent's\n * restart actually loaded it. Replaces the old `.mcp.json` file-mtime gate,\n * which the manager's post-spawn re-renders made perpetually false. Irrelevant\n * to stdio keys, whose live child is itself the load proof.\n */\n sessionLoadedServerKeys: ReadonlySet<string>;\n /**\n * ENG-8448: true when the running session is still inside its cold-start grace\n * window (`< DEFAULT_COLD_START_GRACE_MS` since spawn). A stdio/proxy child can\n * take seconds (CLI-bundled) to tens of seconds (npx-delivered, e.g.\n * cloud-broker) to appear in `ps` after a (re)spawn, so a bind probe that races\n * that window sees \"no live child\" for a broker that is simply still launching.\n * Within the window a would-be `missing` verdict is downgraded to `unknown`\n * (dropped, never counted toward the ENG-7916 bind-failure quarantine) -\n * mirroring the presence-reaper's cold-start grace, which suppresses the\n * symmetric reap. Defaults to false, so the on-demand / unknown-start paths\n * behave exactly as before.\n */\n coldStartActive?: boolean;\n}\n\n/**\n * The verdict. `unknown` is deliberate, not a failure: it means we had no\n * probeable signal (no declared MCP servers, an HTTP key we didn't get to this\n * cycle, or a reachable HTTP key the running session hasn't loaded yet) - so an\n * observer (and Slice A) must NOT treat it as \"broken\".\n *\n * missing - a backing stdio OR oauth-proxy child is gone (the classic \"fell off\")\n * unreachable - a backing HTTP MCP, or an alive oauth-proxy's upstream, didn't\n * answer tools/list\n * bound - every backing server is present in the running session\n * unknown - nothing conclusive to decide on yet\n *\n * Negative signals win (missing > unreachable > bound) so a half-down\n * integration never reports healthy.\n *\n * For a pure-HTTP integration, `bound` additionally requires each backing HTTP\n * key to be in `sessionLoadedServerKeys` - reachability alone can't prove the\n * running session loaded the server, so a reachable key the session didn't load\n * at spawn reports `unknown` (pending) rather than a false `bound`. A live stdio\n * child is itself proof the session loaded that server, so a mixed/stdio\n * integration doesn't need the set.\n */\nexport function computeSessionToolBindStatus(input: SessionToolBindInput): SessionToolBindStatus {\n const {\n stdioServerKeys,\n httpServerKeys,\n proxyServerKeys,\n missingStdioKeys,\n httpReachable,\n sessionLoadedServerKeys,\n coldStartActive = false,\n } = input;\n\n if (stdioServerKeys.length === 0 && httpServerKeys.length === 0 && proxyServerKeys.length === 0)\n return 'unknown';\n\n // A backing stdio OR oauth-proxy child gone = tools not bound in the session.\n // (`missingStdioKeys` enumerates every `command` entry, proxy included.) This\n // is the Phil/ENG-7301 case for the proxy: the child never launched / died.\n if (\n stdioServerKeys.some((k) => missingStdioKeys.has(k)) ||\n proxyServerKeys.some((k) => missingStdioKeys.has(k))\n ) {\n // ENG-8448: within the cold-start grace window a still-spawning child reads as\n // \"no live child\" but isn't a real failure - report `unknown` (dropped by the\n // runner, never counted toward the ENG-7916 quarantine) instead of a false\n // `missing`, so a forced post-restart probe (ENG-7429) can't spuriously\n // quarantine a broker that simply hasn't finished launching. Outside the\n // window (or when the spawn time is unknown) this stays `missing`.\n return coldStartActive ? 'unknown' : 'missing';\n }\n\n // OAuth-proxy keys: the child is alive (checked above), so now exercise the\n // upstream through the reconstructed endpoint. An alive proxy child whose\n // upstream no longer answers `tools/list` is the ENG-7318 \"healthy connection,\n // but the running session's tools went stale under a token refresh\" case -\n // report `unreachable` instead of the false `bound` the old command-only\n // classification produced. A live child + reachable upstream is positive\n // evidence; an unprobed/transient miss (`undefined`) does NOT penalise (the\n // live child alone is at least as healthy as the prior child-only verdict).\n for (const k of proxyServerKeys) {\n if (httpReachable.get(k) === false) return 'unreachable';\n }\n\n // Walk the HTTP keys. Reachability only carries an in-session verdict for a key\n // the running session actually LOADED (its key was in the spawn-time\n // `.mcp.json` snapshot). For a key NOT in the snapshot - added after spawn,\n // awaiting restart, or snapshot unavailable - the session can't use it yet\n // regardless of endpoint state, so it stays \"unconfirmed\" (unknown/pending);\n // we do NOT report `unreachable` for a server the session never loaded\n // (CodeRabbit, PR #2893). For a loaded key: a down endpoint is a hard negative\n // (unreachable), reachable is positive evidence, unprobed stays unconfirmed.\n let anyHttpUnconfirmed = false;\n for (const k of httpServerKeys) {\n if (!sessionLoadedServerKeys.has(k)) {\n anyHttpUnconfirmed = true; // session didn't load it; reachability is moot\n continue;\n }\n const reachable = httpReachable.get(k);\n if (reachable === false) return 'unreachable'; // loaded, but endpoint is down\n if (reachable === undefined) anyHttpUnconfirmed = true; // loaded, not probed this cycle\n }\n\n // All stdio/proxy children present and no upstream hard-down. A live stdio or\n // oauth-proxy child proves session-load, so if any such key backs this\n // integration we're bound. For a pure-HTTP integration we only claim bound when\n // every HTTP key is confirmed reachable-and-loaded; an unconfirmed one (unprobed,\n // or reachable-but-pre-restart) stays 'unknown' rather than a premature 'bound'.\n if (anyHttpUnconfirmed && stdioServerKeys.length === 0 && proxyServerKeys.length === 0)\n return 'unknown';\n return 'bound';\n}\n","/**\n * ENG-7053 / ENG-7220 - rolling host-side session-tool-bind probe orchestration.\n *\n * Mirrors `connectivity-probe-runner`: given an agent's installed integrations,\n * selects the ones DUE for a probe (`last_session_tool_bind_at` older than the\n * interval, NULLS first), bounds the batch so we don't probe everything at once,\n * computes a per-integration `bound|missing|unreachable|unknown` verdict from\n * signals the manager already has, and returns the report batch to POST to\n * `/host/session-tool-bind`.\n *\n * Unlike the connectivity probe (which asks \"are the credentials good / does the\n * managed account answer\"), this asks the narrower runtime question: \"are this\n * integration's MCP tools actually BOUND in the running agent session right now\"\n * - a backing stdio child is alive, or a remote MCP answers `tools/list`. That\n * is the signal the Add Integration modal polls to confirm a newly-added\n * integration came online after the agent's next restart, instead of resolving\n * at \"credentials verified\".\n *\n * Pure + injectable (clock, interval, batch cap, the missing-stdio set, and the\n * HTTP reachability probe) so due-selection, key-resolution and verdict mapping\n * are unit-testable. The real ps-row gathering, `.mcp.json` read and the POST\n * live in the poll-loop wiring (`runAgentSessionToolBindProbes`).\n */\n\nimport {\n computeSessionToolBindStatus,\n splitServerKeysByKind,\n type SessionToolBindStatus,\n} from './session-tool-probe.js';\nimport { DEFAULT_COLD_START_GRACE_MS } from './mcp-presence-reaper.js';\nimport type { McpConfig } from './stale-mcp-reaper.js';\n\n/** One installed integration as returned (enriched) by /host/agent-integrations. */\nexport interface SessionToolBindRunnerIntegration {\n id: string;\n definition_id: string;\n scope: 'agent' | 'team' | 'organization';\n last_session_tool_bind_at?: string | null;\n /**\n * Optional explicit `.mcp.json` server-key hint (e.g. from `deriveMcpServerKey`\n * for managed/remote-MCP kinds). Tried first when resolving the backing key.\n */\n mcp_server_key?: string | null;\n}\n\nexport interface SessionToolBindReport {\n integration_id: string;\n scope: 'agent' | 'team' | 'organization';\n status: SessionToolBindStatus;\n}\n\n/**\n * ENG-7318: an integration whose verdict is a hard-negative in-session state that\n * a targeted MCP-child reap can fix - `missing` (a dead stdio/proxy child) or\n * `unreachable` (an alive oauth-proxy child whose upstream stopped answering\n * `tools/list`). Carries the resolved child-backed `.mcp.json` keys so the caller\n * can SIGTERM exactly those children (Claude Code respawns them, no full restart).\n * The long-running manager acts on these only behind the `session-tool-rebind`\n * flag; the on-demand CLI \"Probe now\" ignores them (a probe never mutates state).\n * Pure-HTTP negatives produce no candidate (no child to respawn).\n */\nexport interface SessionToolRebindCandidate {\n integration_id: string;\n scope: 'agent' | 'team' | 'organization';\n status: 'missing' | 'unreachable';\n /** Child-backed server keys (stdio + oauth-proxy) to reap for a respawn. */\n serverKeys: string[];\n}\n\nexport interface SessionToolBindRunnerOptions {\n now?: () => Date;\n /** A row is due when its last probe is older than this (default 1h). */\n intervalMs?: number;\n /** Max probes per run - bounds the rolling batch (default 25). */\n maxPerRun?: number;\n /** Parsed project `.mcp.json` (the declared MCP servers). */\n mcpJson: McpConfig | null | undefined;\n /**\n * Declared stdio server keys found to have NO live child this tick (from\n * `findMissingMcpServers`). Computed once over the whole `.mcp.json`.\n */\n missingStdioKeys: ReadonlySet<string>;\n /**\n * Probe a declared HTTP/remote MCP server key for `tools/list` reachability.\n * `true` = answered (tools bound), `false` = unreachable/unauthorized,\n * `undefined` = couldn't probe this cycle (transient - don't penalise).\n */\n probeHttp: (serverKey: string) => Promise<boolean | undefined>;\n /**\n * The MCP server keys the running session loaded at spawn (ENG-7263, from the\n * persisted spawn-time snapshot). Gates HTTP `bound` per-server on membership,\n * so a reachable remote endpoint the session didn't load at spawn isn't\n * reported as live before the agent's restart. Defaults to empty (conservative:\n * a pure-HTTP integration stays `unknown` until we can prove the session loaded\n * it). Replaces the old session-start-vs-file-mtime gate.\n */\n sessionLoadedServerKeys?: ReadonlySet<string>;\n /**\n * ENG-8448: epoch ms the running session (re)spawned, or null when unknown. The\n * runner re-evaluates the cold-start grace (`nowMs - sessionStartedMs <\n * DEFAULT_COLD_START_GRACE_MS`) immediately before EACH integration's verdict,\n * off the injected clock - not once up-front - so a serial `probeHttp` that\n * crosses the grace boundary mid-run can't leave a later still-`missing`\n * stdio/proxy child wrongly suppressed as `unknown` (CodeRabbit on #4068).\n * Within the window a would-be `missing` becomes `unknown` (dropped, never\n * counted toward the ENG-7916 quarantine); null ⇒ no suppression.\n */\n sessionStartedMs?: number | null;\n}\n\nexport interface SessionToolBindRunnerResult {\n /** Reports to POST up (only integrations with an ACTIONABLE verdict). */\n reports: SessionToolBindReport[];\n /**\n * ENG-7318: subset of reports whose negative verdict a targeted child reap can\n * fix, enriched with the child-backed server keys to reap. The manager acts on\n * these behind the `session-tool-rebind` flag; the CLI probe ignores them.\n */\n rebindCandidates: SessionToolRebindCandidate[];\n /** How many integrations were due this run. */\n due: number;\n /** How many were actually probed (bounded by maxPerRun). */\n probed: number;\n /**\n * Probed but produced no actionable verdict - no backing MCP server (direct\n * HTTP / composio-account / builtin), or an `unknown` we deliberately do not\n * write (so it can't regress a prior good state to NULL-equivalent).\n */\n skipped: number;\n}\n\nconst DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\nconst DEFAULT_MAX_PER_RUN = 25;\n\n/**\n * Sanitised `definition_id`, matching the managed-toolkit `.mcp.json` writer and\n * `deriveMcpServerKey` (non-alphanumeric → '_', lowercased): `composio/stripe`\n * → `composio_stripe`.\n */\nexport function sanitizeServerKey(definitionId: string): string {\n return definitionId.replace(/[^a-z0-9]/gi, '_').toLowerCase();\n}\n\n/**\n * Resolve which declared `.mcp.json` server key(s) back an integration, by\n * intersecting deterministic candidates with the keys actually present. The\n * candidate set covers every wiring path without re-implementing the generator:\n * • managed (Composio) / remote-MCP writer sanitises the definition_id\n * (`composio/stripe` → `composio_stripe`, `granola` → `granola`);\n * • the remote-OAuth / custom-header writer uses the raw definition_id\n * (`anchor-browser`);\n * • native MCP and the hardcoded specials (xero/postiz/cloud-broker) key on\n * the definition_id (or a registry key passed as the `hint`).\n * An integration that matches nothing has no MCP server in the session\n * (direct-HTTP API, composio-account binding-only, builtin) → no verdict.\n */\nexport function resolveIntegrationServerKeys(\n definitionId: string,\n declaredKeys: readonly string[],\n hint?: string | null,\n): string[] {\n const declared = new Set(declaredKeys);\n const candidates = [hint, definitionId, sanitizeServerKey(definitionId)].filter(\n (k): k is string => typeof k === 'string' && k.length > 0,\n );\n const out = new Set<string>();\n for (const c of candidates) if (declared.has(c)) out.add(c);\n return [...out];\n}\n\nfunction isDue(last: string | null | undefined, now: number, intervalMs: number): boolean {\n if (!last) return true; // never probed\n const t = Date.parse(last);\n if (Number.isNaN(t)) return true; // unparseable → treat as due\n return now - t >= intervalMs;\n}\n\nexport async function runSessionToolBindProbes(\n integrations: SessionToolBindRunnerIntegration[],\n options: SessionToolBindRunnerOptions,\n): Promise<SessionToolBindRunnerResult> {\n const now = (options.now?.() ?? new Date()).getTime();\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const maxPerRun = options.maxPerRun ?? DEFAULT_MAX_PER_RUN;\n\n // Due rows, oldest-first (NULLS first) so the rolling cursor clears the\n // backlog fairly and load spreads across ticks.\n const due = integrations\n .filter((i) => isDue(i.last_session_tool_bind_at, now, intervalMs))\n .sort((a, b) => {\n const ta = a.last_session_tool_bind_at ? Date.parse(a.last_session_tool_bind_at) : 0;\n const tb = b.last_session_tool_bind_at ? Date.parse(b.last_session_tool_bind_at) : 0;\n return ta - tb;\n });\n\n const batch = due.slice(0, maxPerRun);\n const declaredKeys = Object.keys(options.mcpJson?.mcpServers ?? {});\n const reports: SessionToolBindReport[] = [];\n const rebindCandidates: SessionToolRebindCandidate[] = [];\n let skipped = 0;\n // Rows that reached an actual bind probe (resolved to >=1 backing MCP server).\n // Distinct from batch.length, which also counts rows skipped below for having\n // no declared server - counting those as \"probed\" would inflate rollout\n // metrics (CodeRabbit, PR #2860).\n let probed = 0;\n\n for (const integ of batch) {\n const keys = resolveIntegrationServerKeys(\n integ.definition_id,\n declaredKeys,\n integ.mcp_server_key,\n );\n if (keys.length === 0) {\n // No backing MCP server - \"bound\" isn't a meaningful question for this\n // integration. Leave the column NULL rather than write 'unknown'.\n skipped += 1;\n continue;\n }\n probed += 1;\n\n const { stdio, http, proxy } = splitServerKeysByKind(options.mcpJson, keys);\n\n // Probe tools/list reachability for both direct-HTTP keys and oauth-proxy\n // keys (ENG-7318). For a proxy key, `probeHttp` reconstructs the upstream\n // {url, Authorization} from the proxy's baked env (readMcpHttpServerConfig),\n // so an alive-child/stale-upstream proxy reports the upstream as down.\n const httpReachable = new Map<string, boolean>();\n for (const key of [...http, ...proxy]) {\n let reachable: boolean | undefined;\n try {\n reachable = await options.probeHttp(key);\n } catch {\n reachable = undefined; // transient - leave unprobed\n }\n if (reachable !== undefined) httpReachable.set(key, reachable);\n }\n\n // ENG-8448: re-evaluate the cold-start grace HERE, per integration, off the\n // injected clock - NOT once up-front - so time spent in the serial `probeHttp`\n // awaits above (which can cross DEFAULT_COLD_START_GRACE_MS mid-run) can't\n // leave a later genuinely-missing child suppressed as `unknown` once real\n // elapsed time has left the window (CodeRabbit on #4068).\n const nowMs = (options.now?.() ?? new Date()).getTime();\n const coldStartActive =\n options.sessionStartedMs != null &&\n nowMs - options.sessionStartedMs < DEFAULT_COLD_START_GRACE_MS;\n\n const status = computeSessionToolBindStatus({\n stdioServerKeys: stdio,\n httpServerKeys: http,\n proxyServerKeys: proxy,\n missingStdioKeys: options.missingStdioKeys,\n httpReachable,\n sessionLoadedServerKeys: options.sessionLoadedServerKeys ?? new Set<string>(),\n coldStartActive,\n });\n\n // Don't persist 'unknown': it carries no actionable signal and writing it\n // would regress a previously-good row to a NULL-equivalent. The modal falls\n // back to its honest \"applies on next restart\" timeout state instead.\n if (status === 'unknown') {\n skipped += 1;\n continue;\n }\n\n reports.push({ integration_id: integ.id, scope: integ.scope, status });\n\n // ENG-7318: a hard-negative verdict backed by a real child (stdio/proxy) is\n // rebindable - SIGTERM the child so Claude Code respawns it with a fresh\n // connection. Narrow to the keys that ACTUALLY failed, not every child-backed\n // key on the integration: the manager forwards these straight to\n // reapStaleMcpChildren, so including a healthy sibling child would needlessly\n // SIGTERM it (CodeRabbit, PR #2985). A dead stdio/proxy child (in\n // missingStdioKeys) drove the `missing`; a proxy whose upstream is down\n // (httpReachable=false) drove the `unreachable`. A pure-HTTP negative has no\n // child, so it contributes nothing and produces no candidate.\n if (status === 'missing' || status === 'unreachable') {\n const failingKeys = new Set<string>();\n for (const k of [...stdio, ...proxy]) {\n if (options.missingStdioKeys.has(k)) failingKeys.add(k); // dead child\n }\n for (const k of proxy) {\n if (httpReachable.get(k) === false) failingKeys.add(k); // upstream down through the proxy\n }\n if (failingKeys.size > 0) {\n rebindCandidates.push({\n integration_id: integ.id,\n scope: integ.scope,\n status,\n serverKeys: [...failingKeys],\n });\n }\n }\n }\n\n return { reports, rebindCandidates, due: due.length, probed, skipped };\n}\n","// ENG-7255 / ENG-7220 - host-side session-tool-bind probe gathering, extracted\n// from the manager poll loop so the SAME logic can run as a one-shot from the\n// `agt` CLI (operator \"Probe now\" in the live terminal modal).\n//\n// Gathers the live host signals - the agent's declared `.mcp.json`, stdio MCP\n// child liveness (via `ps`), HTTP/remote MCP `tools/list` reachability, and\n// whether the running session post-dates the current config - then computes the\n// per-integration bound|missing|unreachable|unknown verdicts via\n// `runSessionToolBindProbes`.\n//\n// Deliberately free of the manager's IN-MEMORY session map: the caller passes\n// `sessionStartedMs` (ENG-8448 - the persisted `direct-chat-session.json`\n// `startedAtMs`, the same file both callers already read for\n// `sessionLoadedServerKeys`), used to compute the cold-start grace so a\n// still-launching stdio broker isn't counted `missing` - so the same gather runs\n// correctly in either process.\n\nimport { execFileSync as syncExecFile } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ConnectivityTestOverride, ToolkitSourceType } from '@augmented/core/integrations';\nimport type { IntegrationAuthType } from '@augmented/core';\nimport type { McpConfig } from './stale-mcp-reaper.js';\nimport { findMissingMcpServers } from './mcp-presence-reaper.js';\nimport { parsePsRows } from './orphan-channel-mcp-reaper.js';\nimport {\n buildProbeEnv,\n buildConnectivityProbeDeps,\n deriveMcpServerKey,\n} from './connectivity-probe-context.js';\nimport { isolationMode } from './persistent-session.js';\nimport {\n runSessionToolBindProbes,\n type SessionToolBindRunnerResult,\n} from './session-tool-bind-runner.js';\n\n/** One installed integration as returned (enriched) by /host/agent-integrations. */\nexport interface SessionToolBindProbeIntegration {\n id: string;\n definition_id: string;\n scope: string;\n auth_type: string;\n source_type?: string | null;\n last_session_tool_bind_at?: string | null;\n connectivity_test?: ConnectivityTestOverride | null;\n}\n\nexport interface GatherSessionToolBindOptions {\n /**\n * The MCP server keys the running session loaded at spawn (ENG-7263, from the\n * persisted spawn-time snapshot in direct-chat-session.json). Gates HTTP\n * `bound` per-server on membership. Empty/unknown => conservative: a pure-HTTP\n * integration stays `unknown` rather than a premature `bound`.\n */\n sessionLoadedServerKeys: ReadonlySet<string>;\n /**\n * Epoch ms the running session (re)spawned (`startedAtMs` from the persisted\n * `direct-chat-session.json`), or null when unknown. ENG-8448: within\n * `DEFAULT_COLD_START_GRACE_MS` of spawn a still-launching stdio/proxy child is\n * reported `unknown` rather than `missing`, so a forced post-restart probe\n * (ENG-7429) can't spuriously bind-quarantine it. Null ⇒ no grace (behave as\n * before - conservative on the on-demand path, where a genuinely dead broker\n * must still surface `missing` even when the spawn time is unavailable).\n */\n sessionStartedMs: number | null;\n /**\n * Due-window ms. Pass `0` to force EVERY integration due (the operator's\n * on-demand \"Probe now\"); the manager passes its rolling cadence (~1h).\n */\n intervalMs: number;\n /**\n * Cap on probes this run. The manager leaves this unset (runner default 25,\n * rolling); on-demand passes the agent's integration count so the whole set is\n * probed at once.\n */\n maxPerRun?: number;\n}\n\n/**\n * Run the session-tool-bind probe for ONE agent's integrations against the live\n * host. Returns the runner result (the reports to persist + counts), or `null`\n * when there's nothing to probe (no/empty/unparseable `.mcp.json`). Never throws\n * on host-gather failures - a failed `ps` just leaves stdio liveness unknown,\n * matching the manager's best-effort posture.\n */\nexport async function gatherSessionToolBindProbe(\n agent: { agent_id: string; code_name: string },\n integrations: SessionToolBindProbeIntegration[],\n projectDir: string,\n opts: GatherSessionToolBindOptions,\n): Promise<SessionToolBindRunnerResult | null> {\n if (integrations.length === 0) return null;\n\n // The agent's declared `.mcp.json` - ground truth for which MCP servers back\n // which integration.\n let mcpJson: McpConfig | null = null;\n try {\n mcpJson = JSON.parse(readFileSync(join(projectDir, '.mcp.json'), 'utf-8'));\n } catch {\n return null; // no/unparseable .mcp.json => nothing to probe\n }\n if (!mcpJson?.mcpServers || Object.keys(mcpJson.mcpServers).length === 0) return null;\n\n // Declared stdio MCP children with NO live process this tick - computed once\n // over the whole `.mcp.json`, reusing the presence-reaper's argv/ppid matching.\n const isolated = isolationMode(agent.code_name) === 'docker';\n const hasDeclaredStdio = Object.values(mcpJson.mcpServers).some(\n (entry) => typeof (entry as { command?: unknown } | null)?.command === 'string',\n );\n let missingStdioKeys: Set<string> = new Set();\n try {\n const psOutput = isolated\n ? syncExecFile('docker', ['exec', `agt-${agent.code_name}`, 'ps', '-eo', 'pid,ppid,args'], {\n encoding: 'utf-8',\n timeout: 8_000,\n })\n : syncExecFile('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', timeout: 5_000 });\n missingStdioKeys = new Set(\n findMissingMcpServers({\n rows: parsePsRows(psOutput),\n codeName: agent.code_name,\n mcpJson,\n inContainer: isolated,\n }),\n );\n } catch {\n // ps gathering failed: we couldn't observe stdio child liveness this cycle.\n // An empty missing-set would make stdio-backed integrations read 'bound' on\n // no evidence, so if any declared server is stdio we bail (probe nothing this\n // run) rather than risk a false 'bound' (CodeRabbit, PR #2884). A pure-HTTP\n // set is unaffected - its liveness doesn't depend on ps.\n if (hasDeclaredStdio) return null;\n }\n\n // Reuse the connectivity-probe context for remote/HTTP MCP reachability: it\n // resolves the `.mcp.json` server URL + templated headers and runs a real\n // initialize -> tools/list handshake.\n const probeEnv = buildProbeEnv(projectDir);\n const probeDeps = buildConnectivityProbeDeps(projectDir, probeEnv);\n const probeHttp = async (serverKey: string): Promise<boolean | undefined> => {\n if (!probeDeps.mcpProbe) return undefined;\n const outcome = await probeDeps.mcpProbe({ serverKey, definitionId: serverKey });\n if (outcome.status === 'ok') return true;\n if (outcome.status === 'down') return false;\n return undefined; // degraded / transient_error - don't penalise\n };\n\n // HTTP/remote MCP servers have no child process, so reachability alone can't\n // prove THIS session loaded a (possibly newly-added) server. The runner gates\n // HTTP `bound` on the spawn-time server-key snapshot the caller supplies\n // (ENG-7263): a reachable key the session didn't load at spawn stays `unknown`.\n return runSessionToolBindProbes(\n integrations.map((i) => ({\n id: i.id,\n definition_id: i.definition_id,\n scope: i.scope as 'agent' | 'team' | 'organization',\n last_session_tool_bind_at: i.last_session_tool_bind_at ?? null,\n // Server-key hint for managed/remote-MCP kinds; the runner also tries the\n // raw + sanitised definition_id against the real `.mcp.json` keys.\n mcp_server_key: deriveMcpServerKey({\n definitionId: i.definition_id,\n sourceType: (i.source_type ?? null) as ToolkitSourceType | null,\n authType: i.auth_type as IntegrationAuthType,\n connectivityTest: (i.connectivity_test ?? null) as ConnectivityTestOverride | null,\n // ENG-7543: bind-probe this connection's own server (default fleet: 'default').\n connectionKey: (i as { connection_key?: string | null }).connection_key ?? null,\n }),\n })),\n {\n mcpJson,\n missingStdioKeys,\n probeHttp,\n sessionLoadedServerKeys: opts.sessionLoadedServerKeys,\n sessionStartedMs: opts.sessionStartedMs,\n intervalMs: opts.intervalMs,\n ...(opts.maxPerRun !== undefined ? { maxPerRun: opts.maxPerRun } : {}),\n },\n );\n}\n","import { createHash } from 'node:crypto';\nimport type { ProvisionInput, ProvisionOutput } from './types.js';\nimport { getFramework } from './framework-registry.js';\n\nfunction sha256(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\n/**\n * Orchestrates agent provisioning: delegates to the framework adapter to build\n * artifacts, computes content hashes. Returns a ProvisionOutput describing the\n * artifacts to be written (does NOT write to disk — the CLI handles that).\n */\nexport function provision(input: ProvisionInput, frameworkId: string = 'claude-code'): ProvisionOutput {\n const adapter = getFramework(frameworkId);\n const artifacts = adapter.buildArtifacts(input);\n\n const charterHash = sha256(input.charterContent);\n const toolsHash = sha256(input.toolsContent);\n\n const teamDir = `.augmented/${input.agent.code_name}`;\n\n return {\n teamDir,\n artifacts,\n charterHash,\n toolsHash,\n };\n}\n","/**\n * ENG-7833: host-side CLI version pinning.\n *\n * `AGT_CLI_PIN_VERSION=<version>` freezes a host on an exact `agt-cli`\n * version. Pin wins: the manager installs that exact version (including a\n * downgrade) and, once matched, stops moving the host forward. Neither the\n * `latest`/channel poll nor the `urgent` dist-tag escape hatch overrides a\n * valid pin. The one opt-out is `AGT_CLI_PIN_ALLOW_URGENT=1`, which lets an\n * emergency `urgent` hotfix land over the pin while still holding the host on\n * the pin for ordinary `latest` movement.\n *\n * The pin value is materialized into the host environment (bootstrap persists\n * it to `/etc/environment`, the systemd unit carries it, and the manager reads\n * it here), mirroring how `AGT_CLI_RELEASE_CHANNEL` already works. It is a\n * value, not an `_ENABLED`/`_DISABLED`/`_MODE` gate, so it is deliberately a\n * plain env var rather than a registry flag (a per-host fleet flag is tracked\n * as a follow-up).\n *\n * Everything here is a pure function of its inputs so the decision logic can be\n * unit-tested without a live registry or a real install.\n */\n\n/**\n * A pin must look like a concrete semver (optionally prerelease/build tagged).\n * We validate strictly rather than pass an arbitrary string to `npm install\n * -g @integrity-labs/agt-cli@<pin>`: a garbage pin would otherwise make every\n * poll attempt an install of a nonexistent version and fail in a tight loop.\n * An invalid pin is treated as \"not pinned\" by the caller (with a warning) so a\n * typo never silently freezes a host on nothing.\n */\nexport function isValidPinVersion(v: string): boolean {\n return PIN_VERSION_RE.test(v);\n}\n\n/**\n * Strict SemVer 2.0.0 (semver.org's official regex, anchored). Deliberately\n * NOT the loose `\\d+\\.\\d+\\.\\d+` form: that accepts leading-zero components\n * (`01.2.3`) and empty prerelease segments (`1.2.3-..`, `1.2.3-alpha..1`),\n * which npm rejects but which would still reach `npm install -g …@<pin>` and\n * loop on failure. `packages/api/src/lib/host-bootstrap.ts` MUST keep the same\n * pattern (it can't import this CLI module across packages); change both.\n */\nexport const PIN_VERSION_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/;\n\n/** True when the operator set a non-blank `AGT_CLI_PIN_VERSION`. */\nexport function isPinSet(raw: string | undefined): boolean {\n return typeof raw === 'string' && raw.trim() !== '';\n}\n\n/**\n * ENG-7853: resolve the effective pin source from the two possible inputs. The\n * explicit host env var (`AGT_CLI_PIN_VERSION`, set at bootstrap or over SSH) is\n * the highest-precedence per-host override; when it is unset (or blank), fall\n * back to the DB-driven pin delivered on the heartbeat (set from the console).\n * The returned value still flows through {@link decidePin}, so a garbage value\n * from either source is ignored with a warning rather than looping on a\n * nonexistent install. Pure so the precedence is unit-testable.\n */\nexport function resolveEffectivePinRaw(\n envPin: string | undefined,\n dbPin: string | null | undefined,\n): string | undefined {\n if (isPinSet(envPin)) return envPin;\n return dbPin ?? undefined;\n}\n\n/**\n * Normalize a raw `AGT_CLI_PIN_VERSION` to a concrete version string, or null\n * when it is unset, blank, or not a valid version. Strips an optional leading\n * `v` so both `0.28.330` and `v0.28.330` work.\n */\nexport function normalizePinVersion(raw: string | undefined): string | null {\n if (!isPinSet(raw)) return null;\n const v = raw!.trim().replace(/^v/, '');\n return isValidPinVersion(v) ? v : null;\n}\n\n/**\n * True when `AGT_CLI_PIN_ALLOW_URGENT` opts a pinned host back into taking an\n * `urgent` dist-tag hotfix over the pin. Accepts the usual truthy spellings.\n */\nexport function pinAllowsUrgent(raw: string | undefined): boolean {\n const v = (raw ?? '').trim().toLowerCase();\n return v === '1' || v === 'true' || v === 'yes' || v === 'on';\n}\n\nexport type PinDecision =\n /** No valid pin in effect; the caller runs its normal channel logic. */\n | { kind: 'unpinned' }\n /** Pin set but not a valid version; warn once, then behave as unpinned. */\n | { kind: 'invalid'; raw: string }\n /** Installed version already equals the pin; nothing to do. */\n | { kind: 'satisfied'; version: string }\n /** Install this exact version (may be a downgrade). */\n | { kind: 'install'; version: string };\n\n/**\n * The pin decision, given the installed version and the two env vars. The\n * urgent opt-out is handled separately by the caller (it needs a network fetch\n * of the `urgent` tag), so this function only decides pin vs. install vs.\n * satisfied; a `satisfied`/`install` result still lets the caller consult\n * `urgent` first when {@link pinAllowsUrgent} is true.\n */\nexport function decidePin(opts: {\n installed: string;\n pinRaw: string | undefined;\n}): PinDecision {\n const { installed, pinRaw } = opts;\n if (!isPinSet(pinRaw)) return { kind: 'unpinned' };\n const pinned = normalizePinVersion(pinRaw);\n if (!pinned) return { kind: 'invalid', raw: pinRaw!.trim() };\n if (installed === pinned) return { kind: 'satisfied', version: pinned };\n return { kind: 'install', version: pinned };\n}\n","import chalk from 'chalk';\nimport { existsSync, realpathSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir, userInfo } from 'node:os';\nimport { spawn } from 'node:child_process';\nimport { getApiKey, getHost } from '../lib/config.js';\nimport { startWatchdog, stopWatchdog, getManagerStatus } from '../lib/watchdog.js';\nimport { success, error, info, table } from '../lib/output.js';\nimport { isJsonMode, jsonOutput } from '../lib/globals.js';\nimport { normalizePinVersion } from '../lib/self-update-pin.js';\n\n/**\n * ENG-7833: only forward a VALID pin into the supervisor/unit env. An invalid\n * AGT_CLI_PIN_VERSION must not reach the unit, or the ExecStartPre repair would\n * bake it into `npm install -g …@<bad>` and loop-fail, blocking the manager from\n * starting. Drop it so repair falls back to the release channel; a valid pin is\n * normalized (leading `v` stripped) to match what the self-updater installs.\n */\nfunction normalizePinInEnv(env: Record<string, string>): void {\n const normalized = normalizePinVersion(env.AGT_CLI_PIN_VERSION);\n if (normalized) env.AGT_CLI_PIN_VERSION = normalized;\n else delete env.AGT_CLI_PIN_VERSION;\n}\n\n// ---------------------------------------------------------------------------\n// agt manager start\n// ---------------------------------------------------------------------------\n\ninterface ManagerStartOptions {\n interval?: string;\n configDir?: string;\n supervise?: boolean;\n}\n\nexport function managerStartCommand(opts: ManagerStartOptions): void {\n const json = isJsonMode();\n\n // ENG-4632: when the manager is launched without a user shell session\n // (e.g. via `aws ssm send-command`, systemd without User=, or a\n // bare-metal init script), HOME and USER may be missing from the\n // process env. Every agent the manager spawns under tmux inherits\n // that env — and Claude Code without HOME can't resolve\n // ~/.claude/.credentials.json, so the agent silently falls back to\n // the interactive login picker and never spawns its MCP servers.\n // This was the root cause of the prod scout outage on 2026-05-01.\n // Backfill from os primitives before the watchdog/spawn layer reads\n // the env. We log a warning so operators can still see the underlying\n // misconfiguration — silent recovery would just hide the next\n // regression.\n // Treat empty-string as missing — HOME=\"\" makes ~ resolve to cwd,\n // which fails the same way as no HOME but is harder to diagnose.\n if (!process.env.HOME || !process.env.HOME.trim()) {\n const fallback = homedir();\n process.env.HOME = fallback;\n if (!json) {\n info(`HOME was not set in the manager env — defaulting to ${fallback}.`);\n info(' This usually means the manager was launched without a user shell session.');\n info(' For reboot survival, install the supervisor: agt manager install');\n }\n }\n if (!process.env.USER || !process.env.USER.trim()) {\n const fallback = userInfo().username;\n process.env.USER = fallback;\n if (!json) info(`USER was not set in the manager env — defaulting to ${fallback}.`);\n }\n\n const apiKey = getApiKey();\n if (!apiKey) {\n const msg = 'AGT_API_KEY is not set. Export it with your host API key (tlk_...)';\n if (json) { jsonOutput({ ok: false, error: msg }); } else { error(msg); }\n process.exitCode = 1;\n return;\n }\n\n const intervalSec = parseInt(opts.interval ?? '10', 10);\n if (isNaN(intervalSec) || intervalSec < 5) {\n if (json) {\n jsonOutput({ ok: false, error: 'Interval must be at least 10 seconds' });\n } else {\n error('Interval must be at least 10 seconds.');\n }\n process.exitCode = 1;\n return;\n }\n\n const configDir = opts.configDir ?? join(homedir(), '.augmented');\n\n // ENG-4488: --supervise runs the manager in a respawn loop. The manager\n // signals \"please restart me\" by exiting with SUPERVISOR_RESTART_EXIT_CODE\n // (75); only that exact code triggers a respawn. Exit 0 is a normal\n // graceful stop (e.g. from `agt manager stop` / SIGTERM) and causes the\n // supervisor to exit too. Any other non-zero code is propagated out so\n // tmux/launchctl/etc. see the failure. This is the loop that makes\n // auto-upgrade transparent: after `brew upgrade` the manager exits 75,\n // the supervisor re-runs `agt manager start` → new Cellar version loads.\n if (opts.supervise) {\n // Supervisor emits human-readable [supervisor] lines to stdout and the\n // child inherits stdio — not compatible with --json's one-shot JSON\n // contract. Reject the combination loudly rather than silently\n // corrupting a machine-parseable stream.\n if (json) {\n jsonOutput({ ok: false, error: '--supervise is not supported with --json' });\n process.exitCode = 1;\n return;\n }\n runSupervisorLoop(intervalSec, configDir);\n return;\n }\n\n try {\n const { pid } = startWatchdog({\n intervalMs: intervalSec * 1000,\n configDir,\n });\n\n if (json) {\n jsonOutput({ ok: true, pid, interval: intervalSec, configDir });\n } else {\n success(`Manager started (PID ${pid}, interval ${intervalSec}s)`);\n info(`Config dir: ${configDir}`);\n info('Stop with: agt manager stop');\n }\n } catch (err) {\n if (json) {\n jsonOutput({ ok: false, error: (err as Error).message });\n } else {\n error((err as Error).message);\n }\n process.exitCode = 1;\n }\n}\n\n/**\n * Respawn-on-restart-code loop for `agt manager start --supervise`. Each\n * iteration runs the manager as a child process until it exits; only\n * SUPERVISOR_RESTART_EXIT_CODE triggers a re-spawn. Exit 0 is treated as\n * a true stop so `agt manager stop` actually stops a supervised manager.\n * Any other non-zero exit propagates out so an external watchdog can see\n * the failure rather than the supervisor masking it as a crash loop.\n * Lives at the command layer (not watchdog) so it stays outside the\n * manager's own ESM graph — the whole point is that `brew upgrade` will\n * replace that graph on disk between iterations.\n */\n// Dedicated exit code the manager uses to signal \"restart me\" to the\n// supervisor (e.g. after a successful `brew upgrade`). Must not collide\n// with Node's default codes (0 = clean stop, 1 = generic error, 128+sig).\n// 75 = EX_TEMPFAIL in sysexits.h, which is semantically close to\n// \"temporary failure — please retry\" and doesn't shadow anything else\n// the CLI uses. Exported so manager-worker and this supervisor share\n// exactly one constant — overloading exit code 0 conflated `agt manager\n// stop` with auto-upgrade restart, so the supervisor would keep respawning.\nexport const SUPERVISOR_RESTART_EXIT_CODE = 75;\n\nfunction runSupervisorLoop(intervalSec: number, configDir: string): void {\n const SUPERVISOR_RESPAWN_DELAY_MS = 2_000;\n const stdoutWrite = (line: string) => process.stdout.write(`${line}\\n`);\n\n // Supervisor-level state — one set of signal handlers for the whole\n // process. Per-child handlers accumulate on every respawn and leave gaps\n // during the backoff window where a kill would not propagate to anything.\n let currentChild: ReturnType<typeof spawn> | null = null;\n let respawnTimer: ReturnType<typeof setTimeout> | null = null;\n // Flips when the supervisor receives a shutdown signal. Without this,\n // forwarding SIGTERM to the child makes the child exit 0 (graceful stop),\n // which looks identical to the auto-upgrade clean-restart case and the\n // exit handler would respawn — so `agt manager start --supervise` could\n // never actually be stopped by SIGTERM/SIGINT.\n let shutdownRequested = false;\n\n const forwardOrExit = (sig: NodeJS.Signals) => (): void => {\n shutdownRequested = true;\n // Cancel any pending respawn first so we don't race a new child into\n // existence after the operator asked us to stop.\n if (respawnTimer) {\n clearTimeout(respawnTimer);\n respawnTimer = null;\n }\n if (currentChild && currentChild.exitCode === null) {\n // .killed just indicates a signal was sent, not that the child has\n // exited. A second Ctrl-C during graceful shutdown must forward\n // again so the operator can escalate to hard-kill the child; it\n // must NOT fall through to process.exit(0) with the child still\n // running, which would orphan it outside the supervisor.\n currentChild.kill(sig);\n return;\n }\n // Child already exited (we're in the 2s backoff) — nothing to kill,\n // just stop the supervisor.\n process.exit(0);\n };\n\n process.on('SIGTERM', forwardOrExit('SIGTERM'));\n process.on('SIGINT', forwardOrExit('SIGINT'));\n\n const runOne = (): void => {\n respawnTimer = null;\n currentChild = spawn(\n process.execPath,\n [process.argv[1]!, 'manager', 'start', '--interval', String(intervalSec), '--config-dir', configDir],\n { stdio: 'inherit', env: process.env },\n );\n // Without this, a spawn failure (missing Node binary, permission\n // denied, etc.) crashes the supervisor without running any of the\n // exit bookkeeping below — operator sees no log, tmux session just\n // disappears. Log and exit 1 deterministically.\n currentChild.once('error', (err) => {\n currentChild = null;\n stdoutWrite(`[supervisor] failed to spawn manager: ${err.message}`);\n process.exit(1);\n });\n currentChild.on('exit', (code, signal) => {\n currentChild = null;\n if (shutdownRequested) {\n // Operator asked us to stop via SIGTERM/SIGINT; the child's exit\n // (code 0 on graceful stop, or non-zero on crash mid-shutdown)\n // does not re-enter the respawn path.\n stdoutWrite('[supervisor] shutdown requested — exiting');\n process.exit(0);\n return;\n }\n if (signal) {\n stdoutWrite(`[supervisor] manager terminated by signal ${signal} — exiting`);\n process.exit(1);\n return;\n }\n if (code === SUPERVISOR_RESTART_EXIT_CODE) {\n stdoutWrite(`[supervisor] manager requested restart (exit ${code}) — respawning in ${SUPERVISOR_RESPAWN_DELAY_MS / 1000}s`);\n respawnTimer = setTimeout(runOne, SUPERVISOR_RESPAWN_DELAY_MS);\n return;\n }\n if (code === 0) {\n // Normal graceful stop (e.g. `agt manager stop` sent SIGTERM, the\n // child drained, then exited 0). Do NOT respawn — that would make\n // `agt manager stop` effectively useless against a supervised\n // manager. Only the dedicated restart code triggers respawn.\n stdoutWrite('[supervisor] manager exited cleanly (no restart requested) — exiting');\n process.exit(0);\n return;\n }\n stdoutWrite(`[supervisor] manager exited with code ${code} — not respawning`);\n process.exit(code ?? 1);\n });\n };\n\n stdoutWrite(`[supervisor] starting manager with respawn-on-restart-code=${SUPERVISOR_RESTART_EXIT_CODE} (interval=${intervalSec}s, configDir=${configDir})`);\n runOne();\n}\n\n// ---------------------------------------------------------------------------\n// agt manager stop\n// ---------------------------------------------------------------------------\n\ninterface ManagerCommonOptions {\n configDir?: string;\n}\n\nexport async function managerStopCommand(opts: ManagerCommonOptions = {}): Promise<void> {\n const json = isJsonMode();\n const configDir = opts.configDir ?? join(homedir(), '.augmented');\n\n try {\n const result = await stopWatchdog(configDir);\n\n if (!result.stopped && !result.pid) {\n if (json) {\n jsonOutput({ ok: false, error: 'Manager is not running' });\n } else {\n error('Manager is not running.');\n }\n process.exitCode = 1;\n return;\n }\n\n if (json) {\n jsonOutput({ ok: true, stopped: true, pid: result.pid });\n } else {\n success(`Manager stopped (PID ${result.pid})`);\n }\n } catch (err) {\n if (json) {\n jsonOutput({ ok: false, error: (err as Error).message });\n } else {\n error((err as Error).message);\n }\n process.exitCode = 1;\n }\n}\n\n// ---------------------------------------------------------------------------\n// agt manager status\n// ---------------------------------------------------------------------------\n\nexport function managerStatusCommand(opts: ManagerCommonOptions = {}): void {\n const json = isJsonMode();\n const configDir = opts.configDir ?? join(homedir(), '.augmented');\n\n const status = getManagerStatus(configDir);\n\n if (!status) {\n if (json) {\n jsonOutput({ ok: true, running: false });\n } else {\n info('Manager is not running.');\n }\n return;\n }\n\n if (json) {\n jsonOutput({ ok: true, running: true, ...status });\n return;\n }\n\n console.log(chalk.bold('\\nManager Status\\n'));\n\n info(`PID: ${status.pid}`);\n info(`Started: ${status.startedAt}`);\n info(`Last poll: ${status.lastPollAt ?? chalk.dim('none')}`);\n info(`Polls: ${status.pollCount}`);\n info(`Errors: ${status.errorCount}`);\n console.log();\n\n if (status.agents.length === 0) {\n info('No agents discovered yet.');\n return;\n }\n\n const rows = status.agents.map((a) => {\n let gwStatus = chalk.dim('—');\n if (a.gatewayRunning) {\n gwStatus = chalk.green(`:${a.gatewayPort} (PID ${a.gatewayPid})`);\n } else if (a.gatewayPort) {\n gwStatus = chalk.red(`:${a.gatewayPort} (down)`);\n }\n\n return [\n a.codeName,\n a.status === 'active' ? chalk.green(a.status) : a.status === 'paused' ? chalk.yellow(a.status) : chalk.dim(a.status ?? '—'),\n a.charterVersion || chalk.dim('—'),\n gwStatus,\n a.lastProvisionAt ? new Date(a.lastProvisionAt).toLocaleTimeString() : chalk.dim('—'),\n a.lastDriftCheckAt ? new Date(a.lastDriftCheckAt).toLocaleTimeString() : chalk.dim('—'),\n ];\n });\n\n table(\n ['Agent', 'Status', 'Charter', 'Gateway', 'Last Provision', 'Last Drift'],\n rows,\n );\n\n // Show ACP sessions if any agent has active ones\n const acpAgents = status.agents.filter((a) => a.acpSessions && a.acpSessions.length > 0);\n if (acpAgents.length > 0) {\n console.log(chalk.bold('\\nACP Sessions\\n'));\n const acpRows = acpAgents.flatMap((a) =>\n a.acpSessions.map((s) => [\n a.codeName,\n s.agentCommand,\n s.sessionName ?? chalk.dim('default'),\n s.queueState === 'running' ? chalk.green(s.queueState) : s.queueState === 'queued' ? chalk.yellow(s.queueState) : chalk.dim(s.queueState),\n String(s.turnCount),\n new Date(s.startedAt).toLocaleTimeString(),\n ]),\n );\n table(\n ['Agent', 'Coding Agent', 'Session', 'Queue', 'Turns', 'Started'],\n acpRows,\n );\n }\n}\n\n/**\n * Replace a versioned Homebrew Cellar path with the stable\n * `<prefix>/bin/agt` symlink so the launchd plist survives upgrades.\n *\n * /opt/homebrew/Cellar/agt/0.15.36/bin/agt.js\n * → /opt/homebrew/bin/agt (if that exists)\n *\n * Returns the input untouched when:\n * - The path is not inside a Cellar (npm-global, dev, etc.).\n * - The expected `<prefix>/bin/agt` symlink doesn't resolve.\n *\n * Exported for testing.\n */\nexport function resolveStableAgtBin(rawPath: string): string {\n // Match `<prefix>/Cellar/<formula>/<version>/...` — works for both\n // `/opt/homebrew/Cellar` and `/home/linuxbrew/.linuxbrew/Cellar`.\n const match = rawPath.match(/^(.*?)\\/Cellar\\/([^/]+)\\/[^/]+\\//);\n if (!match) return rawPath;\n const prefix = match[1];\n const formula = match[2];\n if (!prefix || !formula) return rawPath;\n\n // brew links the formula's bin entries under `<prefix>/bin/<name>`.\n // The convention for our tap is the formula name itself (`agt`), but\n // be defensive: try the formula name first, fall back to literal `agt`.\n // Resolve via realpathSync so we still detect the symlink as broken\n // when its target is missing (the exact failure mode this fix exists\n // to prevent — we'd otherwise happily write a path that ENOENTs).\n const candidates = [`${prefix}/bin/${formula}`, `${prefix}/bin/agt`];\n for (const candidate of candidates) {\n if (!existsSync(candidate)) continue;\n try {\n // realpath confirms the symlink resolves to a real file. If brew\n // is mid-upgrade and the symlink is dangling, fall through.\n realpathSync(candidate);\n return candidate;\n } catch { /* dangling symlink — try the next candidate */ }\n }\n return rawPath;\n}\n\n// ---------------------------------------------------------------------------\n// agt manager install / uninstall — OS-level supervisor (ENG-4593)\n// ---------------------------------------------------------------------------\n\ninterface ManagerInstallOptions {\n interval?: string;\n configDir?: string;\n}\n\nexport async function managerInstallCommand(opts: ManagerInstallOptions = {}): Promise<void> {\n const json = isJsonMode();\n const { installSupervisor, supervisorStatus } = await import('../lib/manager-supervisor.js');\n\n const intervalSec = parseInt(opts.interval ?? '10', 10);\n if (isNaN(intervalSec) || intervalSec < 5) {\n const msg = 'Interval must be at least 5 seconds.';\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n\n const configDir = opts.configDir ?? join(homedir(), '.augmented');\n\n // Resolve the agt binary the supervisor will launch. process.argv[1]\n // is the entry point of the currently-running agt, but on Homebrew\n // installs that's the *versioned* Cellar path\n // (e.g. /opt/homebrew/Cellar/agt/0.15.36/bin/agt.js). brew upgrade\n // deletes that exact directory once the new version is staged, so the\n // launchd plist would point at a path that vanishes on the first\n // self-update — launchd then throttles into a permanent ENOENT loop\n // and the manager never comes back. Promote to the stable\n // `<prefix>/bin/agt` symlink that brew keeps pointing at the current\n // version. npm-global installs already live at a stable path\n // (`<prefix>/lib/node_modules/.../bin/agt.js`) that npm overwrites\n // in place, so the resolver leaves those untouched.\n const rawAgtBin = process.argv[1];\n if (!rawAgtBin) {\n const msg = 'Could not resolve the agt binary path from argv. Re-run via the installed `agt` command.';\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n const agtBin = resolveStableAgtBin(rawAgtBin);\n\n // macOS TCC sandboxes launchd-spawned processes — they EPERM on\n // reads under user folders like Documents/Downloads/Desktop/etc.\n // The install would succeed and the manager would crash on first\n // launch with no operator-actionable signal. Refuse here instead.\n if (process.platform === 'darwin') {\n const home = homedir();\n const protectedRoots = ['Documents', 'Downloads', 'Desktop', 'Movies', 'Music', 'Pictures'];\n const offending = protectedRoots\n .map((r) => join(home, r))\n .find((p) => agtBin === p || agtBin.startsWith(`${p}/`));\n if (offending) {\n const msg = `agt binary at ${agtBin} sits inside a macOS TCC-protected folder (${offending}). launchd-spawned processes cannot read files there and the manager would EPERM on startup. Either install agt globally (\\`npm install -g @integrity-labs/agt-cli\\`) or copy the dist outside protected folders before running this command.`;\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n }\n\n // AGT_HOST must go through getHost() so the CLI's production-default\n // fallback applies — without it, an install on a host that hasn't\n // exported AGT_HOST in the shell would pass an empty value through\n // to launchd and the manager would fail to find the API.\n //\n // ENG-4632: HOME and USER are required so Claude Code in spawned\n // agent sessions can resolve ~/.claude/.credentials.json. Bake the\n // operator's HOME/USER into the unit/plist explicitly — relying on\n // launchd / systemd defaults left at least one prod host with a\n // PATH-only env and silently broken agents.\n const env: Record<string, string> = {\n AGT_HOST: getHost(),\n // ?? alone wouldn't catch `HOME=\"\"` from a stripped systemd env.\n // Treat empty / whitespace-only as missing.\n HOME: (process.env.HOME?.trim()) || homedir(),\n USER: (process.env.USER?.trim()) || userInfo().username,\n };\n const apiKey = getApiKey();\n if (apiKey) env.AGT_API_KEY = apiKey;\n // AGT_CLI_RELEASE_CHANNEL gates the self-update channel ('test' on the\n // test environment's hosts). Carry it through so the supervised manager's\n // self-update tracks the right npm dist-tag; unset ⇒ the manager defaults\n // to 'latest', unchanged for prod hosts.\n // ENG-7833: AGT_CLI_PIN_VERSION / AGT_CLI_PIN_ALLOW_URGENT pin the host to an\n // exact version, so carry them through too so both the manager's self-update and\n // the unit's ExecStartPre repair honor the pin.\n for (const k of ['AGT_TEAM', 'AGT_CLI_RELEASE_CHANNEL', 'AGT_CLI_PIN_VERSION', 'AGT_CLI_PIN_ALLOW_URGENT', 'PATH', 'CLAUDE_PATH'] as const) {\n const v = process.env[k];\n if (v != null) env[k] = v;\n }\n normalizePinInEnv(env);\n\n const result = await installSupervisor({ agtBin, intervalSec, configDir, env });\n if (!result.ok) {\n if (json) jsonOutput({ ok: false, error: result.error });\n else error(result.error);\n process.exitCode = 1;\n return;\n }\n\n const status = supervisorStatus();\n if (json) {\n jsonOutput({ ok: true, status, details: result.details });\n return;\n }\n success('Supervisor installed.');\n info(result.details);\n if (status.kind === 'installed' && status.pid != null) {\n info(`Manager already running under the supervisor — PID ${status.pid}.`);\n }\n}\n\nexport async function managerUninstallCommand(): Promise<void> {\n const json = isJsonMode();\n const { uninstallSupervisor } = await import('../lib/manager-supervisor.js');\n\n const result = await uninstallSupervisor();\n if (!result.ok) {\n if (json) jsonOutput({ ok: false, error: result.error });\n else error(result.error);\n process.exitCode = 1;\n return;\n }\n if (json) jsonOutput({ ok: true, details: result.details });\n else {\n success('Supervisor uninstalled.');\n info(result.details);\n }\n}\n\n// ---------------------------------------------------------------------------\n// agt manager install-system-unit / uninstall-system-unit (ENG-4706)\n// ---------------------------------------------------------------------------\n//\n// Sibling of `agt manager install` that targets a system-level systemd\n// unit at /etc/systemd/system/agt-manager.service. Used by the EC2\n// host-bootstrap (host-bootstrap.ts) and by the SSM backfill runbook\n// for existing hosts. Requires root — the --user variant is for local\n// dev and doesn't survive headless reboot.\n\ninterface ManagerInstallSystemUnitOptions {\n interval?: string;\n configDir?: string;\n user?: string;\n}\n\nexport async function managerInstallSystemUnitCommand(\n opts: ManagerInstallSystemUnitOptions = {},\n): Promise<void> {\n const json = isJsonMode();\n const { installSystemUnit, systemUnitStatus } = await import('../lib/manager-supervisor.js');\n\n const intervalSec = parseInt(opts.interval ?? '10', 10);\n if (isNaN(intervalSec) || intervalSec < 5) {\n const msg = 'Interval must be at least 5 seconds.';\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n\n const user = opts.user ?? 'root';\n const configDir = opts.configDir ?? (user === 'root' ? '/root/.augmented' : join('/home', user, '.augmented'));\n\n const rawAgtBin = process.argv[1];\n if (!rawAgtBin) {\n const msg = 'Could not resolve the agt binary path from argv. Re-run via the installed `agt` command.';\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n const agtBin = resolveStableAgtBin(rawAgtBin);\n\n // System unit only makes sense on Linux. The wrapper enforces this\n // too, but failing fast in the CLI surface gives a cleaner error.\n if (process.platform !== 'linux') {\n const msg = `install-system-unit is Linux-only (current platform: ${process.platform}). For local dev on macOS use \\`agt manager install\\`.`;\n if (json) jsonOutput({ ok: false, error: msg });\n else error(msg);\n process.exitCode = 1;\n return;\n }\n\n const env: Record<string, string> = {\n AGT_HOST: getHost(),\n HOME: user === 'root' ? '/root' : `/home/${user}`,\n USER: user,\n };\n const apiKey = getApiKey();\n if (apiKey) env.AGT_API_KEY = apiKey;\n // AGT_CLI_RELEASE_CHANNEL gates the self-update channel ('test' on the\n // test environment's hosts). Carry it through so the supervised manager's\n // self-update tracks the right npm dist-tag; unset ⇒ the manager defaults\n // to 'latest', unchanged for prod hosts.\n // ENG-7833: AGT_CLI_PIN_VERSION / AGT_CLI_PIN_ALLOW_URGENT pin the host to an\n // exact version, so carry them through too so both the manager's self-update and\n // the unit's ExecStartPre repair honor the pin.\n for (const k of ['AGT_TEAM', 'AGT_CLI_RELEASE_CHANNEL', 'AGT_CLI_PIN_VERSION', 'AGT_CLI_PIN_ALLOW_URGENT', 'PATH', 'CLAUDE_PATH'] as const) {\n const v = process.env[k];\n if (v != null) env[k] = v;\n }\n normalizePinInEnv(env);\n\n const result = await installSystemUnit({ agtBin, intervalSec, configDir, env, user });\n if (!result.ok) {\n if (json) jsonOutput({ ok: false, error: result.error });\n else error(result.error);\n process.exitCode = 1;\n return;\n }\n\n const status = systemUnitStatus();\n if (json) {\n jsonOutput({ ok: true, status, details: result.details });\n return;\n }\n success('System unit installed.');\n info(result.details);\n if (status.kind === 'installed' && status.pid != null) {\n info(`Manager running under systemd — PID ${status.pid}.`);\n }\n}\n\nexport async function managerUninstallSystemUnitCommand(): Promise<void> {\n const json = isJsonMode();\n const { uninstallSystemUnit } = await import('../lib/manager-supervisor.js');\n\n const result = await uninstallSystemUnit();\n if (!result.ok) {\n if (json) jsonOutput({ ok: false, error: result.error });\n else error(result.error);\n process.exitCode = 1;\n return;\n }\n if (json) jsonOutput({ ok: true, details: result.details });\n else {\n success('System unit uninstalled.');\n info(result.details);\n }\n}\n","/**\n * Manager process — single-process manager with PID management and state files.\n * No fork/IPC — the poll loop runs directly in this process.\n */\n\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, openSync, closeSync, chmodSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { spawn, execFileSync } from 'node:child_process';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface WatchdogOptions {\n intervalMs: number;\n configDir: string;\n /**\n * When true, fork a detached child process to run the manager instead of\n * running it in-process. Required for callers like `agt setup` that need\n * to exit cleanly after emitting output — without detach, the manager's\n * timers and subscriptions keep the parent Node process alive forever,\n * which hangs any caller using `$(agt setup ...)` command substitution.\n */\n detached?: boolean;\n}\n\nexport interface ManagerStatus {\n pid: number;\n startedAt: string;\n lastPollAt: string | null;\n pollCount: number;\n errorCount: number;\n agents: Array<{\n agentId: string;\n codeName: string;\n status: string;\n charterVersion: string;\n toolsVersion: string;\n lastRefreshAt: string | null;\n lastProvisionAt: string | null;\n lastDriftCheckAt: string | null;\n gatewayPort: number | null;\n gatewayPid: number | null;\n gatewayRunning: boolean;\n acpSessions: Array<{\n sessionId: string;\n agentCommand: string;\n sessionName?: string;\n queueState: string;\n turnCount: number;\n startedAt: string;\n }>;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// Paths — all resolved relative to the caller's configDir so a non-default\n// dir doesn't split manager PID/state/log across locations.\n// ---------------------------------------------------------------------------\n\n/** Default config dir. Exported so command definitions share one source. */\nexport const DEFAULT_CONFIG_DIR = join(process.env['HOME'] ?? '/tmp', '.augmented');\n\nexport function getManagerPaths(configDir: string): { pidFile: string; stateFile: string; logFile: string } {\n return {\n pidFile: join(configDir, 'manager.pid'),\n stateFile: join(configDir, 'manager-state.json'),\n logFile: join(configDir, 'manager.log'),\n };\n}\n\nfunction ensureDir(configDir: string): void {\n if (!existsSync(configDir)) {\n mkdirSync(configDir, { recursive: true });\n }\n}\n\n// ---------------------------------------------------------------------------\n// PID file management\n// ---------------------------------------------------------------------------\n\nfunction writePidFile(configDir: string, pid: number): void {\n ensureDir(configDir);\n writeFileSync(getManagerPaths(configDir).pidFile, String(pid), { mode: 0o600 });\n}\n\nfunction readPidFile(configDir: string): number | null {\n try {\n const raw = readFileSync(getManagerPaths(configDir).pidFile, 'utf-8').trim();\n const pid = parseInt(raw, 10);\n return isNaN(pid) ? null : pid;\n } catch {\n return null;\n }\n}\n\nfunction removePidFile(configDir: string): void {\n try {\n unlinkSync(getManagerPaths(configDir).pidFile);\n } catch {\n // may not exist\n }\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * ENG-4714: scan for other `agt manager start` processes regardless of\n * what manager.pid says. Catches the multi-manager bug we hit on Brad's\n * Mac during the Bob-Telegram debug session: launchd respawned the\n * manager mid-`stop` (pidfile already deleted), the operator's\n * subsequent `nohup agt manager start` saw no pidfile and proceeded,\n * resulting in two managers polling the same agents and writing the\n * same `manager.log` (every line duplicated, channel-credentials cache\n * thrashing, etc.).\n *\n * Returns the PIDs of any matching processes other than the current\n * one. Tolerates absent pgrep / unsupported flags by returning an\n * empty list — additive defence over the pidfile check, never a hard\n * dependency.\n */\nexport function findOtherManagerPids(\n // Injection seam for tests — defaults to the real pgrep call.\n pgrepImpl: () => string = defaultPgrep,\n selfPid: number = process.pid,\n): number[] {\n let out: string;\n try {\n out = pgrepImpl();\n } catch {\n // pgrep absent / errored / no matches (exit 1 is the no-match\n // case). Treat all of these as \"no duplicates detected\" — the\n // pidfile check is the primary line of defence.\n return [];\n }\n return out\n .split('\\n')\n .map((line) => parseInt(line.trim(), 10))\n .filter((pid) => !isNaN(pid) && pid !== selfPid);\n}\n\nfunction defaultPgrep(): string {\n // -f matches against the full command line. macOS (BSD) and Linux\n // (procps-ng) pgrep both support this flag.\n return execFileSync('pgrep', ['-f', 'agt manager start'], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n}\n\n// ---------------------------------------------------------------------------\n// State file management\n// ---------------------------------------------------------------------------\n\nfunction readStateFile(configDir: string): ManagerStatus | null {\n try {\n const raw = readFileSync(getManagerPaths(configDir).stateFile, 'utf-8');\n return JSON.parse(raw) as ManagerStatus;\n } catch {\n return null;\n }\n}\n\nfunction removeStateFile(configDir: string): void {\n try {\n unlinkSync(getManagerPaths(configDir).stateFile);\n } catch {\n // may not exist\n }\n}\n\n// ---------------------------------------------------------------------------\n// Manager (single-process, no fork)\n// ---------------------------------------------------------------------------\n\n/**\n * Start the manager. Runs the poll loop directly in this process.\n * This function does not return until the process is stopped.\n */\nexport function startWatchdog(opts: WatchdogOptions): { pid: number } {\n const { configDir } = opts;\n // Check for existing process — pidfile path\n const existingPid = readPidFile(configDir);\n if (existingPid !== null) {\n if (isProcessAlive(existingPid)) {\n throw new Error(`Manager already running (PID ${existingPid}). Use \\`agt manager stop\\` first.`);\n }\n // Stale PID file — clean up\n removePidFile(configDir);\n removeStateFile(configDir);\n }\n\n // ENG-4714: process-scan fallback. Pidfile-only detection misses the\n // race where `agt manager stop` deletes the pidfile while a launchd /\n // systemd respawn is still in flight, then a fresh `agt manager\n // start` from the operator's shell sees no pidfile and proceeds — two\n // managers end up coexisting. The scan fires regardless of pidfile\n // state and refuses to start when another `agt manager start` is\n // already running.\n const others = findOtherManagerPids();\n if (others.length > 0) {\n const pidList = others.join(', ');\n throw new Error(\n `Manager already running (PID ${pidList}). Use \\`agt manager stop\\` first.`,\n );\n }\n\n if (opts.detached) {\n // Fork a detached child running `agt manager start`. The child writes its\n // own PID file via the in-process path below; we return its PID so the\n // caller can exit cleanly.\n ensureDir(configDir);\n // Log captures full stdout/stderr including tokens — lock down perms.\n const { logFile } = getManagerPaths(configDir);\n const logFd = openSync(logFile, 'a', 0o600);\n // Normalize perms on an existing file in case it was created with a\n // different umask previously.\n try {\n chmodSync(logFile, 0o600);\n } catch {\n // non-fatal — file might have just been created with 0o600 above\n }\n const intervalSec = String(Math.max(Math.floor(opts.intervalMs / 1000), 5));\n // ENG-4585: launch under the supervisor so an exit-75 from self-update\n // respawns the manager onto the new binary. Without --supervise the\n // detached child exits cleanly, the gateway pool stays stopped, and\n // agents go dark until an operator runs `agt manager start` again.\n const child = spawn(\n process.execPath,\n [process.argv[1]!, 'manager', 'start', '--interval', intervalSec, '--config-dir', configDir, '--supervise'],\n {\n detached: true,\n stdio: ['ignore', logFd, logFd],\n env: process.env,\n },\n );\n // unref so the parent can exit without waiting for the child\n child.unref();\n closeSync(logFd);\n if (!child.pid) {\n throw new Error('Failed to spawn detached manager process');\n }\n\n // Bounded readiness check: wait for the child to write its PID file, or\n // fail fast if it exits early. Without this, `agt setup --json` would\n // report success even when the manager crashed on startup (e.g. missing\n // AGT_API_KEY), silently producing dead bootstraps.\n const { pidFile } = getManagerPaths(configDir);\n const deadline = Date.now() + 5_000;\n const sleepBuf = new Int32Array(new SharedArrayBuffer(4));\n while (Date.now() < deadline) {\n if (existsSync(pidFile)) {\n return { pid: child.pid };\n }\n if (child.exitCode !== null) {\n throw new Error(\n `Manager exited during startup (code ${child.exitCode}). See ${logFile} for details.`,\n );\n }\n Atomics.wait(sleepBuf, 0, 0, 100);\n }\n throw new Error(\n `Manager did not become ready within 5s. See ${logFile} for details.`,\n );\n }\n\n // In-process path — the manager timers and subscriptions keep this Node\n // process alive. Used by `agt manager start` where blocking is intentional.\n writePidFile(configDir, process.pid);\n\n void import('./manager-worker.js').then(({ startManager }) => {\n startManager({\n intervalMs: opts.intervalMs,\n configDir,\n });\n });\n\n // Clean up PID file on exit\n process.on('exit', () => {\n removePidFile(configDir);\n });\n\n return { pid: process.pid };\n}\n\n/**\n * Stop a running manager by reading the PID file and sending SIGTERM.\n */\nexport async function stopWatchdog(configDir: string = DEFAULT_CONFIG_DIR): Promise<{ stopped: boolean; pid?: number }> {\n const pid = readPidFile(configDir);\n if (pid === null) {\n return { stopped: false };\n }\n\n if (!isProcessAlive(pid)) {\n // Stale PID — clean up\n removePidFile(configDir);\n removeStateFile(configDir);\n return { stopped: true, pid };\n }\n\n // Send SIGTERM\n process.kill(pid, 'SIGTERM');\n\n // Poll for up to 5 seconds until the process exits\n const deadline = Date.now() + 5_000;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 200));\n if (!isProcessAlive(pid)) {\n removePidFile(configDir);\n return { stopped: true, pid };\n }\n }\n\n // Still alive after 5s — force kill\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // may have died between checks\n }\n removePidFile(configDir);\n removeStateFile(configDir);\n return { stopped: true, pid };\n}\n\n/**\n * Get the current manager status by reading PID + state files.\n */\nexport function getManagerStatus(configDir: string = DEFAULT_CONFIG_DIR): ManagerStatus | null {\n const pid = readPidFile(configDir);\n if (pid === null) return null;\n\n if (!isProcessAlive(pid)) {\n removePidFile(configDir);\n removeStateFile(configDir);\n return null;\n }\n\n return readStateFile(configDir);\n}\n","import chalk from 'chalk';\nimport Table from 'cli-table3';\n\nexport function success(msg: string): void {\n console.log(chalk.green(`\\u2714 ${msg}`));\n}\n\nexport function error(msg: string): void {\n console.error(chalk.red(`\\u2718 ${msg}`));\n}\n\nexport function warn(msg: string): void {\n console.warn(chalk.yellow(`\\u26A0 ${msg}`));\n}\n\nexport function info(msg: string): void {\n console.log(chalk.cyan(`\\u2139 ${msg}`));\n}\n\n/**\n * Print a formatted table to stdout.\n *\n * @param headers - Column header labels\n * @param rows - Array of row arrays (one value per column)\n */\nexport function table(headers: string[], rows: string[][]): void {\n const t = new Table({\n head: headers.map((h) => chalk.bold.cyan(h)),\n style: { head: [], border: [] },\n });\n\n for (const row of rows) {\n t.push(row);\n }\n\n console.log(t.toString());\n}\n","/**\n * ENG-5641 — host-side connectivity probe executor (manager CLI).\n *\n * Given an installed integration, resolves its probe strategy (shared core\n * resolver) and executes the corresponding read-only probe, returning a\n * ConnectivityProbeOutcome to report up via POST /host/integration-connectivity.\n *\n * Runs in the manager because that is where the agent's real credentials,\n * network egress and live MCP servers are — the only vantage point that can\n * honestly reach all four source types.\n *\n * PURE + INJECTABLE: every side-effecting capability (HTTP, CLI exec, the MCP\n * handshake, the Composio account check) is injected, so dispatch + outcome\n * mapping is unit-testable without a live host. The real implementations of\n * `mcpProbe` (a true MCP `initialize → tools/list` client), `runCli` and\n * `composioProbe` are wired into the poll loop separately and exercised against\n * live hosts — this module owns the routing and the read-only invariant.\n *\n * Returns `null` when there is no probe to run (kind 'unsupported', or an\n * injectable capability the caller didn't supply yet) — callers skip reporting\n * rather than inventing a status.\n */\n\nimport {\n resolveConnectivityProbe,\n probeHttpProvider,\n worseConnectivityOutcome,\n bestConnectivityEvidence,\n type ConnectivityEvidence,\n type ConnectivityProbeOutcome,\n type ConnectivityTestOverride,\n type ToolkitSourceType,\n} from '@augmented/core/integrations';\nimport type { IntegrationAuthType } from '@augmented/core';\n\nexport interface McpProbeTarget {\n /** Server key / name as wired in the agent's .mcp.json. */\n serverKey: string;\n definitionId: string;\n /**\n * ENG-8049: an inline `.mcp.json`-shaped server entry to probe INSTEAD of\n * looking `serverKey` up in the agent's `.mcp.json`. Set for a quarantined\n * integration, which the quarantine removed from `.mcp.json` - the manager\n * reconstructs its server entry via the framework adapter and passes it here so\n * recovery can be observed. Absent ⇒ the probe reads `.mcp.json` as before.\n * Typed as `unknown` to avoid a cross-module type dependency; the probe deps\n * that consume it (connectivity-probe-context) narrow it to McpJsonServerEntry.\n */\n inlineServerEntry?: unknown;\n /**\n * ENG-6242 — for the managed tool-call leg: the operator-stored\n * `connectivity_test.tool` to call (e.g. `LINEAR_GET_CURRENT_USER`) instead of\n * letting the probe auto-pick. Carried VERBATIM from the toolkit override; the\n * tool-call probe re-validates it against the live tools/list and falls back to\n * the heuristic on drift, so this only ever CHOOSES which safe tool runs.\n */\n toolName?: string | null;\n /** ENG-6242 — args for {@link toolName} (default `{}`). Ignored unless the override is used. */\n toolArgs?: Record<string, unknown> | null;\n /**\n * ENG-8363 — the integration row's `created_at`, so a leg can tell \"the\n * `.mcp.json` key is missing because the writer has not run yet\" from \"the key\n * is missing and never will be\". The manager probes BEFORE it writes\n * `.mcp.json` in the same poll, so the first probe of every newly-added\n * integration finds its key absent; without an age the leg cannot avoid\n * calling that a hard `down`. Absent/unparseable ⇒ treated as still inside the\n * first-connect window (fail-open — never escalate without evidence of age).\n */\n createdAt?: string | null;\n /**\n * ENG-8363 — this row is ENG-7916-quarantined, so its absence from `.mcp.json`\n * is the PRECONDITION of the probe (ENG-8036 re-probes quarantined rows purely\n * to observe recovery), not a fault. Never escalates to `down`.\n */\n expectedAbsent?: boolean;\n}\n\nexport interface ConnectivityProbeDeps {\n /** Injected fetch for the HTTP-provider probes (defaults to global fetch). */\n fetchImpl?: typeof fetch;\n /** Run a read-only CLI command (e.g. `<bin> --version`); resolves an outcome. */\n runCli?: (binary: string, args: string[]) => Promise<ConnectivityProbeOutcome>;\n /** Real MCP client: initialize → tools/list against the agent's wired server. */\n mcpProbe?: (target: McpProbeTarget) => Promise<ConnectivityProbeOutcome>;\n /**\n * ENG-7405: local-STDIO MCP probe — spawns the toolkit's bundled server with\n * the agent's env, handshakes, and (when `target.toolName` is set) calls that\n * read-only tool. The stdio sibling of `mcpProbe`; the manager can't reach the\n * live session server's pipes, so it spawns a throwaway with the same env.\n */\n mcpStdioProbe?: (target: McpProbeTarget) => Promise<ConnectivityProbeOutcome>;\n /**\n * Composio connected-account check (account ACTIVE + bound to the runtime\n * user_id). Takes the resolved `.mcp.json` server key (ENG-7543:\n * connection-suffixed for a named connection) so the binding check reads the\n * exact server the agent queries, not a toolkit-level re-derivation.\n */\n composioProbe?: (\n serverKey: string,\n credentials: Record<string, unknown>,\n // ENG-8049: inline `.mcp.json`-shaped entry for a quarantined integration\n // (absent from `.mcp.json`), so the account-binding leg can read the same\n // url + x-api-key it would from the file. Absent ⇒ read `.mcp.json`.\n inlineServerEntry?: unknown,\n // ENG-8363: row facts this leg needs to classify an unreadable config the\n // same way the MCP legs do. Passed as a bag rather than more positionals\n // because this signature is already three deep. See McpProbeTarget for what\n // each means.\n rowCtx?: { createdAt?: string | null; expectedAbsent?: boolean },\n ) => Promise<ConnectivityProbeOutcome>;\n /**\n * ENG-6157 (Phase 2): live read-only tool call through the agent's wired MCP\n * server — the only leg that proves tool calls actually resolve the connected\n * account end-to-end. Returns `null` when there's no safe tool to call (skip).\n */\n composioToolCallProbe?: (target: McpProbeTarget) => Promise<ConnectivityProbeOutcome | null>;\n}\n\nexport interface ConnectivityProbeTarget {\n definitionId: string;\n sourceType?: ToolkitSourceType | null;\n authType?: IntegrationAuthType | null;\n /** Decrypted credentials (the manager already holds these from /host/agent-integrations). */\n credentials: Record<string, unknown>;\n /** CLI binary name on PATH, when this is a cli_tool integration. */\n cliBinary?: string;\n /** MCP server key in the agent's .mcp.json, when this is an mcp_server integration. */\n mcpServerKey?: string;\n /**\n * ENG-6242 — the toolkit's `connectivity_test` override, when set. For managed\n * (Composio) toolkits this pins the tool-call leg to the SAME read-only tool the\n * central `POST /integrations/:id/test` path uses (e.g. `LINEAR_GET_CURRENT_USER`),\n * so the hourly probe and the Test button can't disagree. Null/absent ⇒ the\n * tool-call leg auto-picks a safe read-only tool (the pre-ENG-6242 behaviour).\n */\n connectivityTest?: ConnectivityTestOverride | null;\n /**\n * ENG-8049: for a quarantined integration (removed from `.mcp.json`), the\n * inline `.mcp.json`-shaped server entry the manager reconstructed via the\n * framework adapter. Threaded into every MCP leg so recovery can be observed\n * despite the missing file entry. Absent ⇒ the probe reads `.mcp.json`.\n * Opaque (`unknown`) here; the probe deps narrow it to McpJsonServerEntry.\n */\n inlineServerEntry?: unknown;\n /**\n * ENG-8363 — the row's `created_at`, forwarded by `/host/agent-integrations`\n * (added for ENG-7575). Threaded into every MCP leg so a missing `.mcp.json`\n * key can be read as \"not written yet\" rather than \"never will be\". See\n * {@link McpProbeTarget.createdAt}.\n */\n createdAt?: string | null;\n /** ENG-8363 — ENG-7916-quarantined row; see {@link McpProbeTarget.expectedAbsent}. */\n expectedAbsent?: boolean;\n}\n\n/**\n * Execute the connectivity probe for one integration. Returns the outcome, or\n * `null` when no probe is available / wired (caller should not report a status).\n */\nexport async function executeConnectivityProbe(\n target: ConnectivityProbeTarget,\n deps: ConnectivityProbeDeps = {},\n): Promise<ConnectivityProbeOutcome | null> {\n const descriptor = resolveConnectivityProbe({\n definitionId: target.definitionId,\n sourceType: target.sourceType,\n authType: target.authType,\n // ENG-6242: carry the toolkit's connectivity_test override so a managed\n // descriptor surfaces `probeTool`/`probeArgs` (the prescribed read-only tool)\n // — without this the hourly probe auto-picked a different tool than the Test\n // path, the false-RED that flagged every managed Linear install \"unreachable\".\n connectivityTest: target.connectivityTest ?? null,\n });\n\n // The resolver only ever returns read-only strategies; assert it so a future\n // mistake fails loud rather than letting a mutating probe through.\n if (!descriptor.readOnly) {\n throw new Error(`Refusing non-read-only probe for ${target.definitionId}`);\n }\n\n // ENG-8363: the row facts every MCP leg needs to classify an unreadable\n // config. Built ONCE and spread into each leg rather than repeated per call\n // site — there are five, and a leg that silently missed one of these would\n // fall back to the fail-open default and never escalate, which is exactly the\n // kind of one-sided drift that makes a probe read a meaningless green.\n const rowFacts = {\n createdAt: target.createdAt ?? null,\n expectedAbsent: target.expectedAbsent ?? false,\n } as const;\n\n switch (descriptor.kind) {\n case 'http_provider': {\n // null → unknown provider; surface as \"no probe\" rather than a fake status.\n const outcome = await probeHttpProvider(target.definitionId, target.credentials, deps.fetchImpl ?? fetch);\n // ENG-8226: an HTTP provider probe is an authenticated request against the\n // provider with the agent's own credentials (Linear's viewer query, Xero's\n // connections read). That IS a live call.\n return outcome ? withEvidence(outcome, 'live_call') : outcome;\n }\n\n case 'composio_account': {\n if (!deps.composioProbe) return null;\n const outcome = await deps.composioProbe(\n target.mcpServerKey ?? target.definitionId,\n target.credentials,\n target.inlineServerEntry,\n rowFacts,\n );\n // ENG-8226: this leg reads Composio's stored connected-account record. It\n // proves the record says ACTIVE, not that the grant behind it still works\n // — exactly the \"record-only check, no live call\" ENG-6328 identified.\n return outcome ? withEvidence(outcome, 'record_only') : outcome;\n }\n\n case 'managed_composite': {\n // ENG-6139: managed toolkits — run the MCP handshake AND the connected-\n // account binding check, return the worse outcome so a green handshake\n // never masks a dead/mis-bound account. Each capability is optional: an\n // older manager (or a target missing inputs) falls back to whichever runs,\n // and `null` only when neither is available.\n const outcomes: ConnectivityProbeOutcome[] = [];\n if (deps.mcpProbe) {\n outcomes.push(\n withEvidence(\n await deps.mcpProbe({\n serverKey: target.mcpServerKey ?? target.definitionId,\n definitionId: target.definitionId,\n inlineServerEntry: target.inlineServerEntry,\n ...rowFacts,\n }),\n // ENG-8226: initialize + tools/list. The transport answered; no\n // authenticated operation ran. The dwight `needs_reauth` rows all\n // had healthy handshakes — this is why it is not enough for green.\n 'handshake',\n ),\n );\n }\n if (deps.composioProbe) {\n outcomes.push(\n withEvidence(\n await deps.composioProbe(target.mcpServerKey ?? target.definitionId, target.credentials, target.inlineServerEntry, rowFacts),\n 'record_only',\n ),\n );\n }\n // ENG-6157 (Phase 2): the deepest leg — a real read-only tool call. Skips\n // (`null`) when no safe tool is callable, so it only ever ADDS signal,\n // never a false negative.\n if (deps.composioToolCallProbe) {\n const toolCall = await deps.composioToolCallProbe({\n serverKey: target.mcpServerKey ?? target.definitionId,\n definitionId: target.definitionId,\n inlineServerEntry: target.inlineServerEntry,\n ...rowFacts,\n // ENG-6242: thread the prescribed tool through to the live tool-call\n // leg. resolveConnectivityProbe only sets these for managed toolkits\n // with a stored override; the probe re-validates read-only and falls\n // back to auto-pick on drift, so a missing/invalid override is safe.\n toolName: descriptor.probeTool ?? null,\n toolArgs: descriptor.probeArgs ?? null,\n });\n // ENG-8226: the deepest leg — a genuine read-only tool call against the\n // provider. This is the ONLY leg that earns `live_call`.\n if (toolCall) outcomes.push(withEvidence(toolCall, 'live_call'));\n }\n if (outcomes.length === 0) return null;\n // ENG-8226: statuses fold WORST-wins (unchanged, ENG-6139); evidence folds\n // BEST-wins. Both are right: if the live tool call failed, the verdict is\n // `down` AND the evidence is `live_call` — we did make the call.\n const worst = outcomes.reduce((acc, o) => worseConnectivityOutcome(acc, o));\n const evidence = outcomes.reduce<ConnectivityEvidence>(\n (acc, o) => bestConnectivityEvidence(acc, o.evidence ?? 'none'),\n 'none',\n );\n // Assign unconditionally, NOT via withEvidence: every leg is already\n // stamped, so `worst.evidence` is always truthy and withEvidence would\n // no-op, silently discarding the fold above. That inverted the whole point\n // — when all three legs pass, worseConnectivityOutcome's tie-break returns\n // the first-pushed leg (the handshake), so a fully-verified integration\n // would report `handshake` and render `unverified`. (CodeRabbit on #3865.)\n return { ...worst, evidence };\n }\n\n case 'mcp_tools_list': {\n if (!deps.mcpProbe) return null;\n const outcome = await deps.mcpProbe({\n serverKey: target.mcpServerKey ?? target.definitionId,\n definitionId: target.definitionId,\n inlineServerEntry: target.inlineServerEntry,\n ...rowFacts,\n });\n return withEvidence(outcome, 'handshake');\n }\n\n case 'mcp_stdio': {\n // ENG-7405: local-STDIO MCP server (origami, …). Spawn the wired server\n // and handshake; call the read-only `connectivity_test` tool when the\n // resolver surfaced one (descriptor.probeTool). No `mcpStdioProbe` wired\n // (older manager / test without it) → skip, don't invent a status.\n if (!deps.mcpStdioProbe) return null;\n const outcome = await deps.mcpStdioProbe({\n serverKey: target.mcpServerKey ?? target.definitionId,\n definitionId: target.definitionId,\n inlineServerEntry: target.inlineServerEntry,\n ...rowFacts,\n toolName: descriptor.probeTool ?? null,\n toolArgs: descriptor.probeArgs ?? null,\n });\n // ENG-8226: a prescribed `connectivity_test` tool means this leg actually\n // called something; without one it is a handshake only.\n return withEvidence(outcome, descriptor.probeTool ? 'live_call' : 'handshake');\n }\n\n case 'cli_command': {\n if (!deps.runCli) return null;\n const outcome = await deps.runCli(target.cliBinary ?? target.definitionId, descriptor.cliArgs ?? ['--version']);\n // ENG-8226: the default `--version` proves the binary exists, not that its\n // credentials work — `handshake`. A toolkit that stored a real auth check\n // as its connectivity_test override (e.g. `gh auth status`) earns\n // `live_call`; `cliArgs` is only non-default when such an override exists.\n return withEvidence(outcome, descriptor.cliArgs ? 'live_call' : 'handshake');\n }\n\n case 'builtin':\n // ENG-8226 criterion 4 — THE fake green. This returned a hardcoded\n // `status: 'ok'` having contacted nothing at all, and that value was\n // persisted into `last_connectivity_status` and rendered as a green\n // \"reachable\" chip, indistinguishable from a verified live tool call. Any\n // toolkit with source_type='native' that isn't an HTTP provider and has no\n // mcpUrl lands here, so a whole class of integrations reported healthy\n // without a single byte leaving the host.\n //\n // The honest answer is `unverified`: we have no evidence either way. It is\n // deliberately not `down` (nothing observed a failure) and deliberately\n // not `ok`. A real per-module check is still the follow-up; until then the\n // data no longer claims something it never checked.\n return {\n status: 'unverified',\n message: `${target.definitionId}: built-in module — no live check available, not verified`,\n evidence: 'none',\n };\n\n case 'host_unprobeable':\n // ENG-8316 — no network call, but a REAL outcome rather than null.\n //\n // Returning null here (what `unsupported` does) drops the row from the\n // report batch, and a row that is never reported never reaches\n // `applyConnectivityReports` — where the server-side handling for these\n // rows lives, including ENG-8205's self-healing clear of stale state.\n // That clear runs \"on the next report\", and for a dropped row there is no\n // next report. cloud-broker / aws-cli / gcloud froze at `transient_error`\n // from 2026-06-14 and xero-broker reached 495 consecutive failures for\n // precisely that reason: the remedy written for these rows could not see\n // them.\n //\n // `unverified` is already neutral to both hysteresis counters\n // (integration-connectivity-report.ts, ENG-8226) and already in\n // CONNECTIVITY_STATUSES, so nothing downstream needs to change to accept\n // it.\n //\n // Use the resolver's own label rather than a hardcoded string. There are\n // TWO broker families here and they fail differently: a cloud broker\n // mints credentials per task, an APPROVAL broker (xero-broker) holds no\n // credential at all and authenticates server-side. Hardcoding the first\n // wording would have persisted a message that is simply untrue for the\n // second - which is the same \"assert something you did not check\" habit\n // this ticket exists to fix (CodeRabbit).\n return {\n status: 'unverified',\n message: descriptor.label,\n evidence: 'none',\n };\n\n case 'unsupported':\n default:\n return null;\n }\n}\n\n/**\n * ENG-8226: stamp a probe leg's outcome with what that leg actually did.\n *\n * Applied at the executor rather than inside each probe implementation so the\n * evidence classification lives in ONE readable place next to the dispatch that\n * chose the strategy — the alternative (each probe self-reporting) is how the\n * `status_message` grammars fragmented across eight producers in the first place.\n * An explicit `evidence` already on the outcome wins, so a probe that knows\n * better can still say so.\n */\nfunction withEvidence(\n outcome: ConnectivityProbeOutcome,\n evidence: ConnectivityEvidence,\n): ConnectivityProbeOutcome {\n return outcome.evidence ? outcome : { ...outcome, evidence };\n}\n"],"mappings":"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A,KAAK,IAAI,IAAI;AAEf,EAAAE,eAAc,gBAAgB,YAAY,EAAE,MAAM,IAAK,CAAE;AAazD,QAAM,gBAAgBF,MAAK,WAAW,yBAAyB;AAoB/D,QAAM,qBACJ;AAEF,QAAM,kBAAkB;IACtB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,0CAA0C,kBAAkB;IAC5D;IACA;IACA;IACA,gDAAgD,kBAAkB;IAClE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,gGAAgG,kBAAkB;IAClH;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,KAAK,IAAI,IAAI;AAEf,EAAAE,eAAc,eAAe,iBAAiB,EAAE,MAAM,IAAK,CAAE;AAO7D,QAAM,kBAAkB,yBAAyB,QAAQ;AAGzD,QAAM,eAAeF,MAAK,WAAW,qBAAqB;AAC1D,MAAI,WAAoC,CAAA;AACxC,MAAI;AACF,eAAW,KAAK,MAAMC,cAAa,cAAc,OAAO,CAAC;EAC3D,QAAQ;EAA0B;AAElC,QAAM,QAAS,SAAS,OAAO,KAAK,CAAA;AAMpC,QAAM,MAAM,IAAI;IACd;MACE,OAAO;QACL,EAAE,MAAM,WAAW,SAAS,eAAc;QAC1C,EAAE,MAAM,WAAW,SAAS,cAAa;QACzC,EAAE,MAAM,WAAW,SAAS,gBAAe;;;;AAIjD,WAAS,OAAO,IAAI;AAEpB,EAAAC,eAAc,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAC/D;AAYM,SAAU,uBAAuB,UAAkB,SAAgB;AACvE,QAAM,aAAa,cAAc,QAAQ;AACzC,QAAM,YAAYF,MAAK,YAAY,SAAS;AAC5C,EAAAI,WAAU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","names":["provision","readFileSync","writeFileSync","mkdirSync","existsSync","chmodSync","renameSync","join","dirname","homedir","chmodSync","existsSync","readFileSync","renameSync","unlinkSync","writeFileSync","join","readFileSync","writeFileSync","chmodSync","mkdirSync","homedir","existsSync","renameSync","dirname","unlinkSync","execFile","mcpConfig","agentDir","error","readFileSync","writeFileSync","mkdirSync","existsSync","join","homedir","renameSync","mkdirSync","dirname","existsSync","readFileSync","join","join","existsSync","readFileSync","execFileSync","execFileSync","join","existsSync","readFileSync","execFile","DEFAULT_TIMEOUT_MS","readFileSync","join","existsSync","readFileSync","join","readFileSync","join","chalk","existsSync","realpathSync","join","homedir","spawn","readFileSync","writeFileSync","unlinkSync","existsSync","mkdirSync","openSync","closeSync","chmodSync","join","spawn","execFileSync","chalk","homedir","join","spawn","chalk","existsSync","realpathSync"]}
|