@miadi/connector 0.1.9 → 0.1.11

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Provider capability discovery and selection policy (jgwill/Miadi#893, Phase 1C).
3
+ *
4
+ * The connector never assumes a provider works because a task text names one:
5
+ * it reports what is installed on this host, what sign-in evidence exists (the
6
+ * presence of the provider's own config directory — never its contents), and
7
+ * applies an explicit preference order. Claude Code is never selected silently:
8
+ * it requires an explicit request. When nothing safe is configured the answer
9
+ * is `provider_unavailable` and the mission is left unstarted.
10
+ *
11
+ * No bearer token is read, logged or returned here. Version probes run the
12
+ * provider's own `--version` with a short timeout.
13
+ */
14
+ import { execFileSync } from "node:child_process";
15
+ import { existsSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ /** Preference order when a run request names no provider (jgwill/Miadi#893). */
19
+ export const PROVIDERS = [
20
+ { id: "codex", cli: "codex", name: "Codex CLI", selectable_by_default: true, auth_evidence: [".codex"] },
21
+ { id: "pi", cli: "pi", name: "Pi coding agent", selectable_by_default: true, auth_evidence: [".pi", ".config/pi"] },
22
+ { id: "copilot", cli: "copilot", name: "GitHub Copilot CLI", selectable_by_default: true, auth_evidence: [".copilot"] },
23
+ { id: "claude", cli: "claude", name: "Claude Code", selectable_by_default: false, auth_evidence: [".claude"] },
24
+ ];
25
+ const defaultWhich = (cli) => {
26
+ try {
27
+ return execFileSync("sh", ["-c", `command -v "$1"`, "sh", cli], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ };
33
+ const defaultVersionOf = (cli) => {
34
+ try {
35
+ return execFileSync(cli, ["--version"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0]?.trim() || undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ };
41
+ export function discoverProviders(probe = {}, specs = PROVIDERS) {
42
+ const which = probe.which ?? defaultWhich;
43
+ const versionOf = probe.versionOf ?? defaultVersionOf;
44
+ const exists = probe.exists ?? existsSync;
45
+ const home = probe.home ?? homedir();
46
+ return specs.map((s) => {
47
+ const installed = which(s.cli) !== null;
48
+ return {
49
+ id: s.id,
50
+ name: s.name,
51
+ cli: s.cli,
52
+ installed,
53
+ ...(installed ? { version: versionOf(s.cli) } : {}),
54
+ auth_evidence: s.auth_evidence.some((p) => exists(join(home, p))) ? "found" : "none",
55
+ selectable_by_default: s.selectable_by_default,
56
+ };
57
+ });
58
+ }
59
+ /**
60
+ * Resolve the provider a run will ask the intake for. A requested provider must
61
+ * be known and installed; with no request the first installed provider in the
62
+ * preference order that may be selected by default wins — Claude Code only ever
63
+ * by an explicit request.
64
+ */
65
+ export function resolveProvider(requested, found) {
66
+ const installed = found.filter((p) => p.installed);
67
+ if (requested) {
68
+ const spec = PROVIDERS.find((p) => p.id === requested || p.cli === requested);
69
+ if (!spec) {
70
+ return {
71
+ state: "provider_unknown",
72
+ message: `Unknown provider "${requested}". Known: ${PROVIDERS.map((p) => p.id).join(", ")}.`,
73
+ available: installed,
74
+ };
75
+ }
76
+ const avail = found.find((p) => p.id === spec.id);
77
+ if (!avail.installed) {
78
+ return {
79
+ state: "provider_unavailable",
80
+ message: `${avail.name} is not installed on the connector host. The mission is left unstarted; name an available provider or install this one.`,
81
+ available: installed,
82
+ };
83
+ }
84
+ return { state: "ok", provider: avail, explicit: true };
85
+ }
86
+ const first = PROVIDERS.map((s) => found.find((p) => p.id === s.id)).find((p) => p.installed && p.selectable_by_default);
87
+ if (first)
88
+ return { state: "ok", provider: first, explicit: false };
89
+ if (installed.length) {
90
+ return {
91
+ state: "provider_needs_explicit_selection",
92
+ message: `Only providers that need an explicit request are installed (${installed.map((p) => p.id).join(", ")}). Name one in the request; none is chosen silently.`,
93
+ available: installed,
94
+ };
95
+ }
96
+ return { state: "provider_unavailable", message: "No agent provider is installed on the connector host.", available: [] };
97
+ }
98
+ /**
99
+ * The read-only Jev routing preflight (jgwill/Miadi#893, section E): is a Jev
100
+ * decision-aide endpoint configured and reachable from this host? Nothing is
101
+ * sent but a health probe; no key is read here and no source text ever leaves.
102
+ */
103
+ export async function routerPreflight(env = process.env, fetchImpl = fetch) {
104
+ const url = (env.MIADI_JEV_URL ?? "").replace(/\/$/, "");
105
+ if (!url) {
106
+ return {
107
+ state: "unconfigured",
108
+ note: "No MIADI_JEV_URL on the connector host: model/tool routing falls back to the explicit provider policy and the person's choice, never to a silent default.",
109
+ };
110
+ }
111
+ try {
112
+ const res = await fetchImpl(`${url}/health`, { signal: AbortSignal.timeout(5000) });
113
+ if (res.ok)
114
+ return { state: "reachable", url };
115
+ return { state: "router_unavailable", url, message: `Jev answered ${res.status}; route by the explicit provider policy instead.` };
116
+ }
117
+ catch (error) {
118
+ return { state: "router_unavailable", url, message: `Jev is not reachable (${error instanceof Error ? error.message : String(error)}); route by the explicit provider policy instead.` };
119
+ }
120
+ }
package/dist/records.d.ts CHANGED
@@ -35,7 +35,7 @@ export declare const DOMAINS: readonly ["creative", "ceremonial", "engineering"]
35
35
  /** #815 "claim category": what kind of statement a record makes. */
36
36
  export declare const CLAIM_CATEGORIES: readonly ["observed", "hypothesis", "desired_future", "fiction", "verified_implementation"];
37
37
  export type ClaimCategory = (typeof CLAIM_CATEGORIES)[number];
38
- export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request"];
38
+ export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request", "session_steer_request"];
39
39
  export type RecordKind = (typeof RECORD_KINDS)[number];
40
40
  /** #816 handoff states. */
41
41
  export declare const HANDOFF_STATES: readonly ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
package/dist/records.js CHANGED
@@ -60,6 +60,7 @@ export const RECORD_KINDS = [
60
60
  "session_completion_proposal",
61
61
  "research_artifact",
62
62
  "inquiry_episode_request",
63
+ "session_steer_request",
63
64
  ];
64
65
  /** #816 handoff states. */
65
66
  export const HANDOFF_STATES = ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
@@ -17,7 +17,7 @@
17
17
  * work proposals) lists them as outstanding. The projection never says they ran.
18
18
  */
19
19
  import type { ConnectorRecord } from "./records.js";
20
- export declare const APPROVABLE_KINDS: readonly ["run_request", "review_operation_request", "deep_diver_request", "review_episode_link_request", "work_proposal", "session_completion_proposal", "inquiry_episode_request"];
20
+ export declare const APPROVABLE_KINDS: readonly ["run_request", "review_operation_request", "deep_diver_request", "review_episode_link_request", "work_proposal", "session_completion_proposal", "inquiry_episode_request", "session_steer_request"];
21
21
  export declare const APPROVABLE: ReadonlySet<string>;
22
22
  /** `recording`: a decision was taken a moment ago and its run may still be going; it is not the ledger's state. */
23
23
  export type RequestStage = "awaiting_decision" | "recording" | "declined" | "sent_back" | "dispatching" | "completed" | "failed";
@@ -6,6 +6,7 @@ export const APPROVABLE_KINDS = [
6
6
  "work_proposal",
7
7
  "session_completion_proposal",
8
8
  "inquiry_episode_request",
9
+ "session_steer_request",
9
10
  ];
10
11
  export const APPROVABLE = new Set(APPROVABLE_KINDS);
11
12
  /** The kinds whose approved run carries the approver's instructions into what it does. */
@@ -15,6 +16,7 @@ const CHOICE_WORDS = {
15
16
  approve_with_changes: "Approved with changes",
16
17
  request_revision: "Sent back for revision",
17
18
  decline: "Declined",
19
+ covered_by_grant: "Covered by the approved run's steering grant",
18
20
  };
19
21
  const OUTCOME_WORDS = {
20
22
  queued: "queued for the factory intake",
@@ -25,6 +27,8 @@ const OUTCOME_WORDS = {
25
27
  marked_ready: "marked ready for Deep Diver",
26
28
  decided: "recorded",
27
29
  nothing_to_run: "nothing to run",
30
+ accepted: "accepted by the session",
31
+ submitted: "typed into the session, acceptance unconfirmed",
28
32
  failed: "failed",
29
33
  };
30
34
  const NOT_CARRIED = "The changes you approved are recorded and did not run: this kind of request does not carry instructions into its run. They are still to do.";
@@ -71,7 +75,10 @@ export function projectRequest(record, ctx = {}) {
71
75
  lines.push(`Instructions for whoever asked: ${instructions}`);
72
76
  return finish({ ...base, stage: "sent_back", standing: "waiting", decision, label: "Sent back for revision" }, lines);
73
77
  }
74
- if (record.approval_state !== "approved")
78
+ // A grant-covered steer runs without its own decision: its authorization is
79
+ // the approved run's steering grant, which the receipt names.
80
+ const approved = record.approval_state === "approved" || (record.approval_state === "not_required" && record.kind === "session_steer_request");
81
+ if (!approved)
75
82
  return null;
76
83
  const changes = choice === "approve_with_changes";
77
84
  const outcome = text(receipt.outcome) || undefined;
package/dist/server.d.ts CHANGED
@@ -3,12 +3,20 @@ import type { MiadiPerson } from "./people.js";
3
3
  import { Ledger } from "./store.js";
4
4
  import type { ConnectorRecord } from "./records.js";
5
5
  import type { ConnectorConfig } from "./config.js";
6
+ import type { TmuxLike } from "./steer.js";
7
+ import type { DiscoveryProbe } from "./providers.js";
6
8
  export interface ConnectorDeps {
7
9
  identify: (token: string) => Promise<MiadiPerson | null>;
8
10
  fetchImpl?: typeof fetch;
9
11
  /** The factory directory holding queue/ (default ~/.miadi/factory). */
10
12
  factoryDir?: string;
11
13
  tmuxSessions?: () => string[];
14
+ /** The steering surface (jgwill/Miadi#893); tests inject a fake, production uses tmux. */
15
+ tmux?: TmuxLike;
16
+ /** Provider discovery probes (jgwill/Miadi#893); tests inject fakes. */
17
+ providersProbe?: DiscoveryProbe;
18
+ /** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
19
+ steerSettleMs?: number;
12
20
  /** Person-token attempts allowed per address in 15 minutes on each sign-in form (default 30). */
13
21
  tokenAttemptsPerWindow?: number;
14
22
  }
package/dist/server.js CHANGED
@@ -85,6 +85,8 @@ export function createConnectorApp(config, deps) {
85
85
  fetchImpl: deps.fetchImpl,
86
86
  factoryDir: deps.factoryDir,
87
87
  tmuxSessions: deps.tmuxSessions,
88
+ tmux: deps.tmux,
89
+ steerSettleMs: deps.steerSettleMs,
88
90
  };
89
91
  registerApprovalRoutes(app, approvalDeps);
90
92
  const decisionTokens = new DecisionTokens(config.stateDir);
@@ -94,7 +96,20 @@ export function createConnectorApp(config, deps) {
94
96
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpUrl),
95
97
  });
96
98
  const handle = async (req, res) => {
97
- const ctx = { config, miadi, reviews, ledger, oauthState, factoryDir: deps.factoryDir, approval: approvalDeps, decisionTokens };
99
+ const ctx = {
100
+ config,
101
+ miadi,
102
+ reviews,
103
+ ledger,
104
+ oauthState,
105
+ factoryDir: deps.factoryDir,
106
+ approval: approvalDeps,
107
+ decisionTokens,
108
+ tmux: deps.tmux,
109
+ tmuxSessions: deps.tmuxSessions,
110
+ providersProbe: deps.providersProbe,
111
+ steerSettleMs: deps.steerSettleMs,
112
+ };
98
113
  const server = new McpServer({ name: "miadi", version: VERSION }, { instructions: INSTRUCTIONS });
99
114
  registerTools(server, ctx);
100
115
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
@@ -0,0 +1,71 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { ConnectorRecord } from "./records.js";
3
+ export declare const STEER_STATUSES: readonly ["prepared", "submitted", "accepted", "busy", "blocked", "wrong_pane", "stale_binding", "expired_grant", "duplicate"];
4
+ export type SteerStatus = (typeof STEER_STATUSES)[number];
5
+ /** The steering grant an approved run carries: `body.steering` on the run_request or work proposal. */
6
+ export interface SteeringGrant {
7
+ /** How many steers the approval covers. */
8
+ max_attempts?: number;
9
+ /** ISO time after which no steer runs under this grant. */
10
+ expires_at?: string;
11
+ /** The pane the seat lives in, `window.pane` (default the session's first pane). */
12
+ pane?: string;
13
+ }
14
+ export interface PaneInfo {
15
+ /** tmux pane id, e.g. %67 — evidence only, never the address. */
16
+ id: string;
17
+ window: number;
18
+ pane: number;
19
+ command: string;
20
+ active: boolean;
21
+ }
22
+ /** The tmux surface the steer needs, injectable so tests never touch a live pane. */
23
+ export interface TmuxLike {
24
+ listPanes(session: string): PaneInfo[];
25
+ capture(target: string): string;
26
+ sendLiteral(target: string, text: string): void;
27
+ sendEnter(target: string): void;
28
+ }
29
+ /** Commands that mark a pane as an agent seat a steer may land in. */
30
+ export declare const AGENT_COMMANDS: string[];
31
+ /** Signs a seat is mid-turn; typed text would queue behind its work. */
32
+ export declare const BUSY_PATTERNS: RegExp[];
33
+ /** The production adapter. All tmux addressing is by exact session and window.pane. */
34
+ export declare function tmuxAdapter(exec?: typeof execFileSync): TmuxLike;
35
+ export declare function steeringGrantOf(run: ConnectorRecord): SteeringGrant | null;
36
+ export type GrantCheck = "ok" | "no_grant" | "expired_grant" | "duplicate";
37
+ /** Whether a grant covers one more steer. `priorAttempts` counts the steers already run under it. */
38
+ export declare function checkGrant(grant: SteeringGrant | null, priorAttempts: number, now?: Date): GrantCheck;
39
+ /** Steers already run under a grant: decided (or grant-covered) steer records that cite it. */
40
+ export declare function steerAttempts(records: ConnectorRecord[], grantId: string): number;
41
+ export declare function isBusy(capture: string, patterns?: RegExp[]): boolean;
42
+ export interface SteerDeps {
43
+ tmux: TmuxLike;
44
+ stateDir: string;
45
+ now?: () => Date;
46
+ settleMs?: number;
47
+ sleep?: (ms: number) => Promise<void>;
48
+ agentCommands?: string[];
49
+ }
50
+ export interface SteerResult {
51
+ status: SteerStatus;
52
+ message: string;
53
+ target?: string;
54
+ artifact?: {
55
+ file: string;
56
+ sha256: string;
57
+ };
58
+ }
59
+ /** The artifact as last written, when a caller asks again with the same idempotency key. */
60
+ export declare function readSteerArtifact(stateDir: string, recordId: string): Record<string, unknown> | null;
61
+ /**
62
+ * Run one steer under its grant. The record is the session_steer_request; the
63
+ * grant record is the approved run the steer belongs to. A returned status
64
+ * other than `accepted`/`submitted` means nothing was typed.
65
+ */
66
+ export declare function steerSession(deps: SteerDeps, args: {
67
+ record: ConnectorRecord;
68
+ grantRecord: ConnectorRecord;
69
+ priorAttempts: number;
70
+ authorized?: boolean;
71
+ }): Promise<SteerResult>;
package/dist/steer.js ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Approval-aware session steering (jgwill/Miadi#893, Phase 1B): one authorized
3
+ * follow-up to one existing live agent, never raw tmux handed to a model.
4
+ *
5
+ * The rules this file keeps:
6
+ * - The target is the pane the mission is bound to — the pane the grant names,
7
+ * else the session's first pane (lowest window, lowest pane, jgwill/Miadi#725).
8
+ * The active pane plays no part.
9
+ * - peek → scoped act → re-peek (jgwill/Miadi#345): visible pasted text is not
10
+ * acceptance; acceptance is the pane recaptured and the agent seen
11
+ * processing, with the instruction scrolled out of the input line.
12
+ * - Every attempt leaves a durable artifact: the instruction, its sha256, the
13
+ * grant it ran under, the target, the timestamps and both captures.
14
+ * - Pane content is evidence, never instruction: nothing an agent printed is
15
+ * parsed as a directive.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { execFileSync } from "node:child_process";
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ export const STEER_STATUSES = [
22
+ "prepared",
23
+ "submitted",
24
+ "accepted",
25
+ "busy",
26
+ "blocked",
27
+ "wrong_pane",
28
+ "stale_binding",
29
+ "expired_grant",
30
+ "duplicate",
31
+ ];
32
+ /** Commands that mark a pane as an agent seat a steer may land in. */
33
+ export const AGENT_COMMANDS = ["claude", "codex", "copilot", "pi", "gemini", "node"];
34
+ /** Signs a seat is mid-turn; typed text would queue behind its work. */
35
+ export const BUSY_PATTERNS = [/esc (to )?interrupt/i, /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/, /\bWorking\b/, /\bThinking\b/];
36
+ /** The production adapter. All tmux addressing is by exact session and window.pane. */
37
+ export function tmuxAdapter(exec = execFileSync) {
38
+ const tmux = (args) => exec("tmux", args, { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
39
+ return {
40
+ listPanes(session) {
41
+ const out = tmux(["list-panes", "-s", "-t", session, "-F", "#{pane_id}\t#{window_index}\t#{pane_index}\t#{pane_current_command}\t#{pane_active}"]);
42
+ return out
43
+ .split("\n")
44
+ .filter(Boolean)
45
+ .map((line) => {
46
+ const [id, w, p, command, active] = line.split("\t");
47
+ return { id, window: Number(w), pane: Number(p), command, active: active === "1" };
48
+ })
49
+ .sort((a, b) => a.window - b.window || a.pane - b.pane);
50
+ },
51
+ capture(target) {
52
+ return tmux(["capture-pane", "-p", "-t", target]);
53
+ },
54
+ // -l: the text is literal; no tmux key names in an instruction are interpreted.
55
+ sendLiteral(target, text) {
56
+ tmux(["send-keys", "-t", target, "-l", "--", text]);
57
+ },
58
+ sendEnter(target) {
59
+ tmux(["send-keys", "-t", target, "Enter"]);
60
+ },
61
+ };
62
+ }
63
+ export function steeringGrantOf(run) {
64
+ const g = run.body.steering;
65
+ if (!g || typeof g !== "object")
66
+ return null;
67
+ const grant = g;
68
+ return {
69
+ ...(typeof grant.max_attempts === "number" ? { max_attempts: grant.max_attempts } : {}),
70
+ ...(typeof grant.expires_at === "string" ? { expires_at: grant.expires_at } : {}),
71
+ ...(typeof grant.pane === "string" ? { pane: grant.pane } : {}),
72
+ };
73
+ }
74
+ /** Whether a grant covers one more steer. `priorAttempts` counts the steers already run under it. */
75
+ export function checkGrant(grant, priorAttempts, now = new Date()) {
76
+ if (!grant)
77
+ return "no_grant";
78
+ if (grant.expires_at && !Number.isNaN(Date.parse(grant.expires_at)) && Date.parse(grant.expires_at) <= now.getTime())
79
+ return "expired_grant";
80
+ if ((grant.max_attempts ?? 0) <= priorAttempts)
81
+ return "duplicate";
82
+ return "ok";
83
+ }
84
+ /** Steers already run under a grant: decided (or grant-covered) steer records that cite it. */
85
+ export function steerAttempts(records, grantId) {
86
+ return records.filter((r) => r.kind === "session_steer_request" &&
87
+ r.refs.includes(grantId) &&
88
+ (r.approval_state === "approved" || r.approval_state === "not_required") &&
89
+ r.completion_receipt !== undefined).length;
90
+ }
91
+ const targetName = (session, pane) => `${session}:${pane.window}.${pane.pane}`;
92
+ export function isBusy(capture, patterns = BUSY_PATTERNS) {
93
+ const tail = capture.split("\n").slice(-25).join("\n");
94
+ return patterns.some((p) => p.test(tail));
95
+ }
96
+ function artifactPath(stateDir, recordId) {
97
+ return join(stateDir, "steers", `${recordId.replace(/[^\w-]/g, "-")}.json`);
98
+ }
99
+ function persistArtifact(stateDir, recordId, artifact) {
100
+ const file = artifactPath(stateDir, recordId);
101
+ mkdirSync(join(stateDir, "steers"), { recursive: true, mode: 0o700 });
102
+ const sha256 = createHash("sha256").update(JSON.stringify(artifact.instruction)).digest("hex");
103
+ writeFileSync(file, JSON.stringify({ ...artifact, instruction_sha256: sha256 }, null, 2) + "\n", { mode: 0o600 });
104
+ return { file, sha256 };
105
+ }
106
+ /** The artifact as last written, when a caller asks again with the same idempotency key. */
107
+ export function readSteerArtifact(stateDir, recordId) {
108
+ const file = artifactPath(stateDir, recordId);
109
+ if (!existsSync(file))
110
+ return null;
111
+ try {
112
+ return JSON.parse(readFileSync(file, "utf8"));
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ const CAPTURE_LINES = 40;
119
+ /**
120
+ * Run one steer under its grant. The record is the session_steer_request; the
121
+ * grant record is the approved run the steer belongs to. A returned status
122
+ * other than `accepted`/`submitted` means nothing was typed.
123
+ */
124
+ export async function steerSession(deps, args) {
125
+ const now = deps.now ?? (() => new Date());
126
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
127
+ const agentCommands = deps.agentCommands ?? AGENT_COMMANDS;
128
+ const { record, grantRecord } = args;
129
+ const body = record.body;
130
+ const grant = steeringGrantOf(grantRecord);
131
+ const instruction = String(body.instruction ?? "");
132
+ const grantSession = String(grantRecord.body.session ?? "") || undefined;
133
+ const session = body.session ?? grantSession;
134
+ const finish = (status, message, extra = {}, target) => {
135
+ const artifact = persistArtifact(deps.stateDir, record.id, {
136
+ record: record.id,
137
+ grant: grantRecord.id,
138
+ instruction,
139
+ session,
140
+ status,
141
+ message,
142
+ at: now().toISOString(),
143
+ ...extra,
144
+ });
145
+ return { status, message, ...(target ? { target } : {}), artifact };
146
+ };
147
+ if (!instruction.trim())
148
+ return { status: "blocked", message: "The steer carries no instruction." };
149
+ if (!session)
150
+ return finish("stale_binding", "Neither the steer nor its grant names a session; a steer never targets whichever pane is active.");
151
+ if (body.session && grantSession && body.session !== grantSession) {
152
+ return finish("stale_binding", `The steer names session "${body.session}" but its grant covers "${grantSession}".`);
153
+ }
154
+ // A person's approval of this exact steer covers one attempt even when the
155
+ // run's grant is exhausted; without it, the grant's own limits decide.
156
+ if (!args.authorized) {
157
+ const check = checkGrant(grant, args.priorAttempts, now());
158
+ if (check === "expired_grant")
159
+ return finish("expired_grant", `The steering grant expired at ${grant?.expires_at}.`, {}, session);
160
+ if (check === "duplicate") {
161
+ return finish("duplicate", `The grant's ${grant?.max_attempts ?? 0} steer(s) are used; a new scoped human decision is needed for more.`, {}, session);
162
+ }
163
+ }
164
+ let panes;
165
+ try {
166
+ panes = deps.tmux.listPanes(session);
167
+ }
168
+ catch {
169
+ return finish("stale_binding", `No tmux session named "${session}"; the binding is stale.`, {}, session);
170
+ }
171
+ if (!panes.length)
172
+ return finish("stale_binding", `Session "${session}" has no panes; the binding is stale.`, {}, session);
173
+ const wanted = body.pane ?? grant?.pane;
174
+ let pane;
175
+ if (wanted) {
176
+ const [w, p] = wanted.split(".").map(Number);
177
+ pane = panes.find((x) => x.window === w && x.pane === p);
178
+ if (!pane)
179
+ return finish("stale_binding", `Pane ${wanted} of "${session}" is gone (stale or renumbered); nothing was typed anywhere else.`, {}, session);
180
+ }
181
+ else {
182
+ pane = panes[0]; // listPanes is sorted: lowest window, lowest pane (jgwill/Miadi#725).
183
+ }
184
+ const target = targetName(session, pane);
185
+ if (!agentCommands.includes(pane.command)) {
186
+ return finish("wrong_pane", `Pane ${target} runs "${pane.command}", not an agent; nothing was typed.`, { pane: { ...pane } }, target);
187
+ }
188
+ let before;
189
+ try {
190
+ before = deps.tmux.capture(target);
191
+ }
192
+ catch (error) {
193
+ return finish("blocked", `The pane could not be read (${error instanceof Error ? error.message : String(error)}); nothing was typed.`, {}, target);
194
+ }
195
+ if (isBusy(before)) {
196
+ return finish("busy", `The agent in ${target} is mid-turn; the steer was not typed. Ask again when it is idle.`, { peek: before.split("\n").slice(-CAPTURE_LINES) }, target);
197
+ }
198
+ deps.tmux.sendLiteral(target, instruction);
199
+ deps.tmux.sendEnter(target);
200
+ await sleep(deps.settleMs ?? 4000);
201
+ let after;
202
+ try {
203
+ after = deps.tmux.capture(target);
204
+ }
205
+ catch (error) {
206
+ return finish("submitted", `Typed into ${target}, but the re-peek failed (${error instanceof Error ? error.message : String(error)}); acceptance is unconfirmed.`, { peek: before.split("\n").slice(-CAPTURE_LINES) }, target);
207
+ }
208
+ let current;
209
+ try {
210
+ current = deps.tmux.listPanes(session).find((x) => x.window === pane.window && x.pane === pane.pane);
211
+ }
212
+ catch {
213
+ current = undefined;
214
+ }
215
+ const tail = after.split("\n").slice(-CAPTURE_LINES);
216
+ const evidence = { peek: before.split("\n").slice(-CAPTURE_LINES), re_peek: tail };
217
+ if (!current || current.command !== pane.command) {
218
+ return finish("stale_binding", `Typed into ${target}, but the pane's command changed while sending; the seat may have exited. Acceptance is unconfirmed.`, evidence, target);
219
+ }
220
+ const firstLine = instruction.split("\n")[0].slice(0, 80);
221
+ const echoed = firstLine.length > 0 && after.includes(firstLine);
222
+ if (isBusy(after) && echoed) {
223
+ return finish("accepted", `The agent in ${target} is processing the steer.`, evidence, target);
224
+ }
225
+ const stillAtPrompt = tail.some((l) => firstLine.length > 0 && l.includes(firstLine)) && !isBusy(after);
226
+ return finish("submitted", stillAtPrompt
227
+ ? `The instruction sits at the prompt in ${target}; it was typed but the agent has not accepted it.`
228
+ : `Typed into ${target}; the pane shows no processing sign, so acceptance is unconfirmed.`, evidence, target);
229
+ }
package/dist/tools.d.ts CHANGED
@@ -7,6 +7,8 @@ import type { OAuthState } from "./oauth.js";
7
7
  import { type ConnectorRecord } from "./records.js";
8
8
  import { type ApprovalDeps } from "./approve.js";
9
9
  import { type DecisionTokens } from "./decisions.js";
10
+ import { type DiscoveryProbe } from "./providers.js";
11
+ import { type TmuxLike } from "./steer.js";
10
12
  export interface ToolContext {
11
13
  config: ConnectorConfig;
12
14
  miadi: MiadiClient;
@@ -18,6 +20,13 @@ export interface ToolContext {
18
20
  /** What the in-chat approval card needs to run a decided request. */
19
21
  approval?: ApprovalDeps;
20
22
  decisionTokens?: DecisionTokens;
23
+ /** The steering surface (jgwill/Miadi#893); absent, steering answers unavailable. */
24
+ tmux?: TmuxLike;
25
+ tmuxSessions?: () => string[];
26
+ /** Provider discovery probes; tests inject fakes (jgwill/Miadi#893). */
27
+ providersProbe?: DiscoveryProbe;
28
+ /** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
29
+ steerSettleMs?: number;
21
30
  }
22
31
  export declare function registerTools(server: McpServer, ctx: ToolContext): void;
23
32
  /** What get_capabilities reports for each tool. */