agent-trellis 0.5.2 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/adapters/codex.js +4 -1
  2. package/dist/adapters/jsonMcp.js +4 -1
  3. package/dist/adapters/kiro.js +9 -1
  4. package/dist/adapters/mcpPlan.d.ts +31 -0
  5. package/dist/adapters/mcpPlan.js +67 -3
  6. package/dist/adapters/symlinkPlan.js +3 -0
  7. package/dist/cli.js +65 -3
  8. package/dist/commands/doctor.d.ts +13 -3
  9. package/dist/commands/doctor.js +20 -10
  10. package/dist/commands/mcpAuth.d.ts +28 -0
  11. package/dist/commands/mcpAuth.js +99 -0
  12. package/dist/commands/mcpGateway.d.ts +53 -0
  13. package/dist/commands/mcpGateway.js +148 -0
  14. package/dist/commands/memory.d.ts +15 -0
  15. package/dist/commands/memory.js +19 -1
  16. package/dist/commands/migrate.d.ts +4 -0
  17. package/dist/commands/migrate.js +46 -4
  18. package/dist/commands/onboard.d.ts +67 -0
  19. package/dist/commands/onboard.js +283 -14
  20. package/dist/commands/onboardVerdict.d.ts +55 -0
  21. package/dist/commands/onboardVerdict.js +175 -0
  22. package/dist/core/adapter.d.ts +7 -0
  23. package/dist/core/canonical.d.ts +27 -1
  24. package/dist/core/canonical.js +62 -0
  25. package/dist/core/types.d.ts +30 -4
  26. package/dist/lib/backup.js +32 -3
  27. package/dist/lib/envVarNames.d.ts +23 -6
  28. package/dist/lib/envVarNames.js +26 -7
  29. package/dist/lib/gatewayBackend.d.ts +75 -0
  30. package/dist/lib/gatewayBackend.js +131 -0
  31. package/dist/lib/mcpConnect.d.ts +53 -0
  32. package/dist/lib/mcpConnect.js +123 -0
  33. package/dist/lib/mcpToolRegistry.d.ts +89 -0
  34. package/dist/lib/mcpToolRegistry.js +94 -0
  35. package/dist/lib/oauth/discovery.d.ts +53 -0
  36. package/dist/lib/oauth/discovery.js +95 -0
  37. package/dist/lib/oauth/flow.d.ts +47 -0
  38. package/dist/lib/oauth/flow.js +193 -0
  39. package/dist/lib/oauth/lock.d.ts +25 -0
  40. package/dist/lib/oauth/lock.js +98 -0
  41. package/dist/lib/oauth/pkce.d.ts +27 -0
  42. package/dist/lib/oauth/pkce.js +40 -0
  43. package/dist/lib/oauth/refresh.d.ts +33 -0
  44. package/dist/lib/oauth/refresh.js +93 -0
  45. package/dist/lib/oauth/store.d.ts +55 -0
  46. package/dist/lib/oauth/store.js +85 -0
  47. package/dist/pi-bridge/bundle.js +14376 -14336
  48. package/dist/pi-bridge/index.d.ts +5 -9
  49. package/dist/pi-bridge/index.js +6 -106
  50. package/dist/pi-bridge/schemaTranslate.d.ts +8 -2
  51. package/dist/pi-bridge/schemaTranslate.js +8 -4
  52. package/docs/architecture.md +108 -25
  53. package/docs/getting-started.md +156 -7
  54. package/docs/research.md +166 -0
  55. package/docs/roadmap.md +18 -0
  56. package/package.json +4 -3
  57. package/schema/servers.example.yaml +33 -11
@@ -0,0 +1,148 @@
1
+ /**
2
+ * `trellis mcp-gateway --agent <id>` — the stdio MCP server an agent
3
+ * spawns in gateway mode (trellis-mcp-gateway-hosting).
4
+ *
5
+ * Not a user-facing command in the usual sense: nobody types this. An
6
+ * agent's own MCP client spawns it exactly the way it spawns any other
7
+ * stdio server, talks to it for the session, and closes the pipe on exit
8
+ * (design.md D2). There is no daemon, no lifecycle command, and nothing
9
+ * for the user to notice.
10
+ *
11
+ * This file depends on `GatewayBackend` and nothing below it — no
12
+ * connection, no transport, no server definition (design.md D11). That is
13
+ * what lets v2 point the same stdio edge at one shared service instead of
14
+ * an in-process backend without this file, or any agent's config,
15
+ * changing.
16
+ */
17
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
20
+ import { homedir } from "node:os";
21
+ import { loadCanonicalSource } from "../core/canonical.js";
22
+ import { resolveMcpPlan } from "../adapters/mcpPlan.js";
23
+ import { LocalBackend } from "../lib/gatewayBackend.js";
24
+ import { DEFAULT_CONNECT_TIMEOUT_MS } from "../lib/mcpConnect.js";
25
+ import { ALL_AGENTS } from "../core/types.js";
26
+ const CLIENT_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
27
+ const SERVER_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
28
+ export function parseMcpGatewayArgs(argv) {
29
+ const index = argv.indexOf("--agent");
30
+ if (index === -1 || index === argv.length - 1) {
31
+ return { error: `mcp-gateway requires --agent <${ALL_AGENTS.join("|")}>` };
32
+ }
33
+ const value = argv[index + 1];
34
+ if (!ALL_AGENTS.includes(value)) {
35
+ return { error: `unknown agent "${value}" — expected one of ${ALL_AGENTS.join(", ")}` };
36
+ }
37
+ return { agentId: value };
38
+ }
39
+ /**
40
+ * The in-scope upstream set for this agent, resolved through the very same
41
+ * `resolveMcpPlan` that direct mode uses — per-server `agents:` scope,
42
+ * `enabled: false`, host-injected collisions, literal-secret refusals and
43
+ * unresolved `env` names all apply here identically, because it is
44
+ * literally the same function (design.md D6).
45
+ *
46
+ * The one thing that must be stripped is gateway mode itself: asking
47
+ * `resolveMcpPlan` for this agent's plan while gateway mode is on would
48
+ * answer "one entry, pointing at the gateway" — the gateway being told to
49
+ * connect to itself. Resolving against a gateway-less view of the same
50
+ * canonical gives the set gateway mode replaced.
51
+ */
52
+ export function resolveGatewayUpstreams(agentId, canonical) {
53
+ const direct = { ...canonical.mcp, gateway: undefined };
54
+ // The managed set is passed through unchanged, so an unmanaged agent
55
+ // reaches nothing. Being spawned is NOT treated as consent here, which
56
+ // is a deliberate divergence from `src/pi-bridge/index.ts` (which passes
57
+ // ALL_AGENTS on the grounds that pi loading the extension is itself the
58
+ // signal). The triggers differ: pi's bridge only exists on disk because
59
+ // Trellis symlinked it into pi's own directory while pi was managed, so
60
+ // its presence IS the marker. A gateway entry, by contrast, outlives
61
+ // un-managing — it sits in the agent's config until a later sync's
62
+ // ownership-gated removal clears it. Serving that stale entry would hand
63
+ // every server, and every secret resolved into it, to the one agent the
64
+ // user just said to stop involving.
65
+ const { desired, conflicts } = resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
66
+ return {
67
+ upstreams: desired.map(({ name, def }) => ({ name, def })),
68
+ conflicts: conflicts.map((conflict) => conflict.message),
69
+ };
70
+ }
71
+ export async function runMcpGateway(opts) {
72
+ const homeDir = opts.homeDir ?? homedir();
73
+ const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
74
+ let canonical;
75
+ try {
76
+ canonical = loadCanonicalSource(homeDir);
77
+ }
78
+ catch (err) {
79
+ // stderr, never stdout: stdout IS the MCP protocol stream here, and a
80
+ // stray line on it corrupts the agent's parser rather than informing
81
+ // anyone.
82
+ console.error(`trellis-mcp-gateway: ${err instanceof Error ? err.message : String(err)}`);
83
+ return { exitCode: 1 };
84
+ }
85
+ const { upstreams, conflicts } = resolveGatewayUpstreams(opts.agentId, canonical);
86
+ for (const message of conflicts) {
87
+ console.error(`trellis-mcp-gateway: ${message}`);
88
+ }
89
+ const backend = opts.backendFactory
90
+ ? await opts.backendFactory(upstreams, homeDir)
91
+ : await LocalBackend.connect(upstreams, {
92
+ secretsPolicy: canonical.secretsPolicy,
93
+ clientInfo: CLIENT_INFO,
94
+ connectTimeoutMs,
95
+ // Enables silent OAuth refresh-before-connect. Never an
96
+ // interactive grant: that is `trellis mcp auth`'s job, and this
97
+ // process has no terminal to run one in (design.md D9).
98
+ homeDir,
99
+ });
100
+ const server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
101
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await backend.listTools() }));
102
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
103
+ const result = await backend.callTool(request.params.name, request.params.arguments);
104
+ return result;
105
+ });
106
+ const transport = new StdioServerTransport();
107
+ await server.connect(transport);
108
+ await waitForShutdown(backend, server);
109
+ return { exitCode: 0 };
110
+ }
111
+ /**
112
+ * Teardown on client disconnect — the piece neither layer below provides.
113
+ *
114
+ * `StdioServerTransport.start()` registers listeners for `'data'` and
115
+ * `'error'` only, so stdin reaching EOF never reaches `onclose`; and POSIX
116
+ * re-parents an orphan to launchd rather than killing it, while this
117
+ * process's own event loop is held open by every upstream it connected.
118
+ * Without this, each agent session would permanently leak a gateway plus
119
+ * its entire upstream process set (design.md D13).
120
+ */
121
+ function waitForShutdown(backend, server) {
122
+ return new Promise((resolve) => {
123
+ let settled = false;
124
+ const shutdown = async () => {
125
+ if (settled)
126
+ return;
127
+ settled = true;
128
+ try {
129
+ await backend.close();
130
+ }
131
+ catch (err) {
132
+ console.error(`trellis-mcp-gateway: failed to close backend: ${err instanceof Error ? err.message : String(err)}`);
133
+ }
134
+ try {
135
+ await server.close();
136
+ }
137
+ catch {
138
+ // best-effort; the process is going away regardless
139
+ }
140
+ resolve();
141
+ };
142
+ process.stdin.on("end", shutdown);
143
+ process.stdin.on("close", shutdown);
144
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
145
+ process.once(signal, shutdown);
146
+ }
147
+ });
148
+ }
@@ -12,7 +12,22 @@
12
12
  * server process — both are plain file operations, same posture as
13
13
  * every other Trellis write.
14
14
  */
15
+ import type { McpServerDef } from "../core/types.js";
15
16
  import { type MemoryExtractionPlan, type MemorySyncPlan } from "../lib/memoryGraph.js";
17
+ /** Exported so `onboard` (`trellis onboard --memory on|off`,
18
+ * trellis-onboard-mcp-mode) can look up and write this same entry
19
+ * without hardcoding the name a second time. */
20
+ export declare const MEMORY_SERVER_NAME = "memory";
21
+ /**
22
+ * The literal definition `trellis onboard --memory on` writes — the
23
+ * same shape `schema/servers.example.yaml`'s commented-out `memory:`
24
+ * block documents, now a real, code-owned constant instead of the only
25
+ * place this shape existed being prose a user hand-transcribes
26
+ * (trellis-onboard-mcp-mode design.md D9). `MEMORY_FILE_PATH` must be
27
+ * set explicitly — see `resolveMemoryServerGraphPath`'s own comment for
28
+ * why an unset one isn't safe to leave to the server's own default.
29
+ */
30
+ export declare const DEFAULT_MEMORY_SERVER_DEF: McpServerDef;
16
31
  export interface RunMemorySyncOptions {
17
32
  homeDir?: string;
18
33
  json?: boolean;
@@ -17,7 +17,25 @@ import { homedir } from "node:os";
17
17
  import { dirname, join } from "node:path";
18
18
  import { loadCanonicalSource } from "../core/canonical.js";
19
19
  import { parseMemoryGraph, planMemoryExtraction, planMemorySync, renderMemoryGraph } from "../lib/memoryGraph.js";
20
- const MEMORY_SERVER_NAME = "memory";
20
+ /** Exported so `onboard` (`trellis onboard --memory on|off`,
21
+ * trellis-onboard-mcp-mode) can look up and write this same entry
22
+ * without hardcoding the name a second time. */
23
+ export const MEMORY_SERVER_NAME = "memory";
24
+ /**
25
+ * The literal definition `trellis onboard --memory on` writes — the
26
+ * same shape `schema/servers.example.yaml`'s commented-out `memory:`
27
+ * block documents, now a real, code-owned constant instead of the only
28
+ * place this shape existed being prose a user hand-transcribes
29
+ * (trellis-onboard-mcp-mode design.md D9). `MEMORY_FILE_PATH` must be
30
+ * set explicitly — see `resolveMemoryServerGraphPath`'s own comment for
31
+ * why an unset one isn't safe to leave to the server's own default.
32
+ */
33
+ export const DEFAULT_MEMORY_SERVER_DEF = {
34
+ transport: "stdio",
35
+ command: "npx",
36
+ args: ["-y", "@modelcontextprotocol/server-memory"],
37
+ staticEnv: { MEMORY_FILE_PATH: "~/.trellis/memories/graph.jsonl" },
38
+ };
21
39
  /**
22
40
  * Looks up `mcp.servers["memory"]` specifically (matching `schema/
23
41
  * servers.example.yaml`'s own naming convention for the default shared-
@@ -17,6 +17,10 @@ export interface MigratePlanItem {
17
17
  name: string;
18
18
  action: MigrateAction;
19
19
  detail: string;
20
+ /** Only set (and only meaningful) when `action === "conflict"`: the
21
+ * concrete next action, distinct from `detail`'s restatement of why
22
+ * (trellis-onboard-closed-loop design.md D7). */
23
+ remediation?: string;
20
24
  /** Only set when action === "create"; consumed by applyMigratePlan. */
21
25
  sourceDir?: string;
22
26
  sourceContent?: string;
@@ -15,6 +15,7 @@ import { AGENTS_MD_TEMPLATE } from "./init.js";
15
15
  import { decideDirImport } from "../lib/dirEquals.js";
16
16
  import { deepEqual } from "../lib/deepEqual.js";
17
17
  import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
18
+ import { findLiteralSecret } from "../adapters/mcpPlan.js";
18
19
  import { loadCanonicalSource, upsertServerYaml } from "../core/canonical.js";
19
20
  import { ALL_AGENTS } from "../core/types.js";
20
21
  const PROBES = {
@@ -44,7 +45,13 @@ function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
44
45
  case "already-present":
45
46
  return { kind: "skill", name, action: "already-migrated", detail: "canonical content is byte-identical" };
46
47
  case "conflict":
47
- return { kind: "skill", name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
48
+ return {
49
+ kind: "skill",
50
+ name,
51
+ action: "conflict",
52
+ detail: `canonical skills/${name}/ already exists with different content — resolve by hand`,
53
+ remediation: `compare \`~/.trellis/skills/${name}/\` against the source agent's copy and either update canonical by hand or delete the source's copy if canonical's is the one to keep`,
54
+ };
48
55
  }
49
56
  }
50
57
  function planInstructions(snapshot, canonicalAgentsMd) {
@@ -58,7 +65,13 @@ function planInstructions(snapshot, canonicalAgentsMd) {
58
65
  sourceContent = readFileSync(snapshot.instructionsFile.path, "utf-8");
59
66
  }
60
67
  catch (err) {
61
- return { kind: "instructions", name: "agents.md", action: "conflict", detail: `could not read source instructions: ${err instanceof Error ? err.message : String(err)}` };
68
+ return {
69
+ kind: "instructions",
70
+ name: "agents.md",
71
+ action: "conflict",
72
+ detail: `could not read source instructions: ${err instanceof Error ? err.message : String(err)}`,
73
+ remediation: `check that ${snapshot.instructionsFile.path} exists and is readable, then re-run migrate`,
74
+ };
62
75
  }
63
76
  if (!existsSync(canonicalAgentsMd)) {
64
77
  return { kind: "instructions", name: "agents.md", action: "create", detail: "canonical agents.md does not exist yet", sourceContent };
@@ -70,7 +83,13 @@ function planInstructions(snapshot, canonicalAgentsMd) {
70
83
  if (current === sourceContent) {
71
84
  return { kind: "instructions", name: "agents.md", action: "already-migrated", detail: "canonical content is byte-identical" };
72
85
  }
73
- return { kind: "instructions", name: "agents.md", action: "conflict", detail: "canonical agents.md already has different real content — resolve by hand" };
86
+ return {
87
+ kind: "instructions",
88
+ name: "agents.md",
89
+ action: "conflict",
90
+ detail: "canonical agents.md already has different real content — resolve by hand",
91
+ remediation: `compare \`~/.trellis/agents.md\` against ${snapshot.instructionsFile.path} and either merge by hand or delete whichever copy you don't want to keep`,
92
+ };
74
93
  }
75
94
  /**
76
95
  * True when `existing` and `def` differ ONLY in which of `env`/
@@ -92,6 +111,23 @@ export function isSafeReclassification(existing, def) {
92
111
  return deepEqual(resolvedEnvTextMap(existing), resolvedEnvTextMap(def));
93
112
  }
94
113
  function planMcpServer(name, def, existing) {
114
+ // Checked first, before any comparison against `existing`: a source
115
+ // agent's real config holding a literal credential must never reach
116
+ // canonical at all, regardless of whether canonical already has
117
+ // something for this name. `resolveMcpPlan`'s own guard only protects
118
+ // the sync-OUT boundary (canonical -> agent) — this is the matching
119
+ // guard for the migrate-IN boundary (agent -> canonical), the gap a
120
+ // real `mcp-router` token in a real `kiro` config exposed.
121
+ const secretLabel = findLiteralSecret(def);
122
+ if (secretLabel) {
123
+ return {
124
+ kind: "mcp",
125
+ name,
126
+ action: "conflict",
127
+ detail: `refusing to import MCP server "${name}": a value matches a known-dangerous literal pattern (${secretLabel}) — canonical must hold variable NAMES only, never real values (docs/research.md "Secrets")`,
128
+ remediation: `move the real value in the source agent's own config to an environment variable (or your secrets manager), leaving only a name behind, then re-run migrate — canonical never accepts a literal credential, even briefly`,
129
+ };
130
+ }
95
131
  if (existing === undefined) {
96
132
  return { kind: "mcp", name, action: "create", detail: "will add to servers.yaml", mcpDef: def };
97
133
  }
@@ -101,7 +137,13 @@ function planMcpServer(name, def, existing) {
101
137
  if (isSafeReclassification(existing, def)) {
102
138
  return { kind: "mcp", name, action: "reclassify", detail: "same value, only its internal classification changed — safe to update", mcpDef: def };
103
139
  }
104
- return { kind: "mcp", name, action: "conflict", detail: `canonical mcp/servers.yaml already has a different definition for "${name}" — resolve by hand` };
140
+ return {
141
+ kind: "mcp",
142
+ name,
143
+ action: "conflict",
144
+ detail: `canonical mcp/servers.yaml already has a different definition for "${name}" — resolve by hand`,
145
+ remediation: `compare the source agent's own definition for "${name}" against \`~/.trellis/mcp/servers.yaml\` and update canonical by hand if the source's is the one to keep`,
146
+ };
105
147
  }
106
148
  /**
107
149
  * `only` restricts which kind(s) are even considered — not a post-hoc
@@ -18,7 +18,9 @@ import type { SyncReport } from "./sync.js";
18
18
  import type { McpSyncReport } from "./mcp.js";
19
19
  import type { SecretsAuditReport } from "./secretsAudit.js";
20
20
  import type { MemorySyncResult } from "./memory.js";
21
+ import type { DoctorReport } from "./doctor.js";
21
22
  import type { ConfirmAndInstallOptions } from "../lib/installAgent.js";
23
+ import type { VerdictItem } from "./onboardVerdict.js";
22
24
  export interface OnboardAgentSummary {
23
25
  agent: AgentId;
24
26
  present: boolean;
@@ -44,6 +46,20 @@ export interface RunOnboardOptions {
44
46
  * the literal string "none" for "add nothing new this run" — distinct
45
47
  * from omitting the flag, which requires a prompt or refuses. */
46
48
  manage?: string;
49
+ /** Non-interactive MCP-mode choice (trellis-onboard-mcp-mode). Omitted
50
+ * leaves whatever canonical already has untouched and prompts for
51
+ * nothing — "no change requested," not "please choose" (design.md
52
+ * D1), unlike `agent`/`manage` above. */
53
+ mcpMode?: string;
54
+ /** Required when `mcpMode === "hub"`; refused if given otherwise. */
55
+ hubUrl?: string;
56
+ /** Optional when `mcpMode === "gateway"` (comma-separated agent ids;
57
+ * omitted = every managed agent); refused if given otherwise. */
58
+ gatewayAgents?: string;
59
+ /** Non-interactive shared-memory-server toggle: `"on"` or `"off"`.
60
+ * Omitted leaves whatever's already configured untouched — same
61
+ * no-prompt posture as `mcpMode` (design.md D12). */
62
+ memory?: string;
47
63
  dryRun?: boolean;
48
64
  json?: boolean;
49
65
  /** Defaults to the real `~`; overridable for tests only. */
@@ -64,6 +80,10 @@ export interface RunOnboardOptions {
64
80
  * answer — "skip migrate for this run" (design.md D6) — distinct from
65
81
  * `source` being unresolved at all. */
66
82
  promptForMigrateCategories?: (source: OnboardAgentSummary) => Promise<MigrateKind[]>;
83
+ /** Test-only: replaces the real end-of-`--dry-run` "apply now?" picker
84
+ * (design.md D9). `true` means "apply for real", matching what
85
+ * accepting the real picker's "Yes, apply now" option means. */
86
+ promptToApply?: () => Promise<boolean>;
67
87
  /** Test-only: injected into every `confirmAndInstall` call for a
68
88
  * selected, not-yet-present agent. Never a real terminal prompt or a
69
89
  * real `npm install` in a unit test. */
@@ -90,13 +110,60 @@ export interface OnboardResult {
90
110
  migrateSkipped?: string;
91
111
  syncReport?: SyncReport;
92
112
  mcpSyncReport?: McpSyncReport;
113
+ /**
114
+ * The self-verification re-plan (design.md D2b): a dry-run re-plan of
115
+ * `sync`/`mcp sync`, run immediately after their real apply, against
116
+ * the state that apply just wrote. Absent on `--dry-run` (nothing was
117
+ * written, so nothing to verify) and on every early-refusal path.
118
+ * Present, and empty, on a real run whose write actually held.
119
+ */
120
+ syncVerification?: SyncReport;
121
+ mcpSyncVerification?: McpSyncReport;
93
122
  memorySyncResult?: MemorySyncResult;
94
123
  secretsAuditReport?: SecretsAuditReport;
124
+ /**
125
+ * A broader, whole-machine health scan — the same detectors
126
+ * `trellis doctor` itself runs, against every agent (not scoped to
127
+ * `managedAgents`, design.md D3), never with MCP handshake probing
128
+ * (design.md D4). Complementary to, and distinct from, the
129
+ * write-verification above: this cannot itself prove a write took
130
+ * effect (it never reads canonical), only that something looks
131
+ * inconsistent across agents independent of this run.
132
+ */
133
+ doctorReport?: DoctorReport;
134
+ /**
135
+ * The resolved MCP mode after this run (trellis-onboard-mcp-mode
136
+ * design.md D7) — `current` reflects canonical's state whether or not
137
+ * this run changed it, `changed` is `true` only when `--mcp-mode` was
138
+ * given and differed from what canonical had before this run. Absent
139
+ * on every early-refusal path before mode resolution is reached.
140
+ */
141
+ mcpMode?: {
142
+ current: "direct" | "hub" | "gateway";
143
+ previous: "direct" | "hub" | "gateway";
144
+ changed: boolean;
145
+ };
146
+ /** The resolved shared-memory-server state after this run, mirroring
147
+ * `mcpMode` (design.md D13). */
148
+ memory?: {
149
+ current: "on" | "off";
150
+ previous: "on" | "off";
151
+ changed: boolean;
152
+ };
95
153
  refusal?: string;
96
154
  /** Only set when no agent is present — the same values `--json` and
97
155
  * text output both surface, so a machine caller doesn't have to
98
156
  * hardcode them a second time. */
99
157
  installHints?: Record<AgentId, string>;
158
+ /**
159
+ * Every stage's conflicts and findings, normalized to one shape
160
+ * (trellis-onboard-closed-loop design.md D5) — the single source
161
+ * `runOnboard`'s exit code and the terminal verdict block both derive
162
+ * from, so the two can never disagree. Always present, empty on a
163
+ * fully clean run (including refusal paths, where it reflects
164
+ * whatever partial data was collected before the refusal).
165
+ */
166
+ verdict: VerdictItem[];
100
167
  }
101
168
  export declare function collectOnboardPlan(opts?: RunOnboardOptions): Promise<OnboardResult>;
102
169
  export declare function runOnboard(opts?: RunOnboardOptions): Promise<{