@miadi/connector 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -20,9 +20,9 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
20
20
 
21
21
  | kind | tools |
22
22
  |---|---|
23
- | reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `search` `fetch` |
23
+ | reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `search` `fetch` |
24
24
  | contributions | `append_observation` `open_inquiry` `link_records` `propose_narrative_beat` `record_evaluation` `register_source_intent` `record_handoff` `update_handoff` `prepare_calendar_event` `save_research` |
25
- | requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
25
+ | requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_session_steer` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
26
26
  | decisions in the conversation | `show_decisions` (cards), `decide_request` (called by the card only) |
27
27
 
28
28
  - **Reads** need Miadi's read tier and stay within it. A person Miadi gives no API tier reaches only `get_capabilities`. Sessions need the write tier, because a session's name and pane are operator data. For the same reason, below the write tier a record someone else made shows no session, working folder or seat, and no long bodies.
@@ -38,6 +38,16 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
38
38
 
39
39
  The model that asked cannot approve its own request.
40
40
 
41
+ `get_run_result` returns one `projection` of the decision, dispatch/completion state, receipt link and outstanding instructions. The in-chat card, approval page and Miadi's research page use that same read model. Asking `show_decisions` for an already-decided `record_id` shows its receipt without issuing another decision token. An in-flight card can refresh status without repeating the operation. A completed basic review with unapplied approval instructions remains open as **changes outstanding**, not falsely finished.
42
+
43
+ ### Following and steering a mission
44
+
45
+ `mission_status` joins one mission read-only: its source request and decision, the sessions bound to it (each with how it is bound — a signed `session:` ref, the delivered brief's target, or the requested name), its issues and PRs, its steering grant, and where it stands stage by stage — approved, queued, delivered, running, merged, released, closed — each by its evidence, so an idle pane or a merge is never read as completion. Live panes and Miadi's session view are joined only for the write tier.
46
+
47
+ `request_session_steer` sends one authorized follow-up to one existing live agent, within the steering grant an approved run carries (`steering: { max_attempts, expires_at, pane }` on `request_authorized_run`). It peeks at the bound pane — the grant's pane or the session's first pane, never whichever pane is active — refuses a busy agent or a pane that is not an agent, types the exact instruction literally, and re-peeks to verify the agent accepted it (`accepted`) or only holds it at the prompt (`submitted`). Every attempt leaves a durable artifact with the instruction's sha256, its grant, its target and both captures. A steer the grant does not cover waits for one explicit scoped human decision on its `approval_url`, which runs exactly that instruction once.
48
+
49
+ `request_authorized_run` may name a `provider` (`codex`, `pi`, `copilot`, `claude`). The connector verifies it is installed on the host before recording anything; an unknown or unavailable provider answers `provider_unavailable` and the mission stays unstarted. With no provider named, the first installed provider in the preference order is resolved and named in the brief; Claude Code is never chosen silently — it runs only when named. `provider_capabilities` (write tier) reports what the host has installed, the sign-in evidence, the preference order, and the read-only Jev routing preflight (`unconfigured` without `MIADI_JEV_URL`). A provider name inside a task's text selects nothing.
50
+
41
51
  ### Deciding inside the conversation
42
52
 
43
53
  In a client that renders MCP Apps (ChatGPT among them), `show_decisions` draws one card per request awaiting a decision: what is asked, by whom, and what approving does. The person decides on the card with their own click: Approve, Approve with my changes, Send back for revision, or Decline. The card then shows the receipt.
package/dist/approve.d.ts CHANGED
@@ -15,6 +15,9 @@ import type { Ledger } from "./store.js";
15
15
  import type { MiadiClient } from "./miadi.js";
16
16
  import { type MiadiPerson } from "./people.js";
17
17
  import { type ConnectorRecord } from "./records.js";
18
+ import { type RequestProjection } from "./request-state.js";
19
+ import { type TmuxLike } from "./steer.js";
20
+ export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
18
21
  export interface ApprovalDeps {
19
22
  config: ConnectorConfig;
20
23
  ledger: Ledger<ConnectorRecord>;
@@ -24,6 +27,10 @@ export interface ApprovalDeps {
24
27
  /** The factory directory holding queue/ (default ~/.miadi/factory). */
25
28
  factoryDir?: string;
26
29
  tmuxSessions?: () => string[];
30
+ /** The steering surface (jgwill/Miadi#893); tests inject a fake, production uses tmux. */
31
+ tmux?: TmuxLike;
32
+ /** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
33
+ steerSettleMs?: number;
27
34
  }
28
35
  /**
29
36
  * What a person may decide on a request. Approving with changes runs it with the
@@ -32,20 +39,6 @@ export interface ApprovalDeps {
32
39
  */
33
40
  export declare const DECISIONS: readonly ["approve", "approve_with_changes", "request_revision", "decline"];
34
41
  export type Decision = (typeof DECISIONS)[number];
35
- export declare function factoryDir(env?: NodeJS.ProcessEnv): string;
36
- export declare function runBriefFile(record: {
37
- id: string;
38
- }): string;
39
- /** Where a run request's brief is: waiting, delivered by the intake, cancelled, or not queued. */
40
- export declare function readQueueState(record: ConnectorRecord, dir?: string): {
41
- state: string;
42
- file: string;
43
- note?: undefined;
44
- } | {
45
- state: string;
46
- file: string;
47
- note: string;
48
- };
49
42
  /** The task a work proposal asks for, written from the proposal's own fields. */
50
43
  export declare function proposalTask(record: ConnectorRecord): string;
51
44
  export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson, sessions: string[], instructions?: string): {
@@ -64,6 +57,8 @@ export declare function executeApproved(record: ConnectorRecord, approver: Miadi
64
57
  /** A request is decided once: a second decision while one runs, or after it, is refused. */
65
58
  export declare class AlreadyDecided extends Error {
66
59
  }
60
+ /** Whether a decision on this request is being recorded now, its run may still be going. */
61
+ export declare function isBeingDecided(recordId: string): boolean;
67
62
  export declare function decide(r: ConnectorRecord, person: MiadiPerson, choice: Decision, deps: ApprovalDeps, opts: {
68
63
  instructions?: string;
69
64
  note?: string;
@@ -93,5 +88,14 @@ export declare function isApprovable(r: ConnectorRecord | undefined): r is Conne
93
88
  /** What approving a request does, in one sentence, for the card a person decides on. */
94
89
  export declare function consequenceOf(r: ConnectorRecord): string;
95
90
  export declare function registerApprovalRoutes(app: Express, deps: ApprovalDeps): void;
91
+ /** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
92
+ /**
93
+ * Where a request stands, as every surface of the connector reads it (request-state.ts):
94
+ * the factory queue is read for the two kinds that dispatch a brief.
95
+ */
96
+ export declare function requestProjection(record: ConnectorRecord, opts?: {
97
+ reviewBase?: string;
98
+ factoryDir?: string;
99
+ }): RequestProjection | null;
96
100
  /** How a request is named to a person. */
97
101
  export declare const KIND_LABELS: Record<string, string>;
package/dist/approve.js CHANGED
@@ -10,49 +10,23 @@
10
10
  * is appended as a record that supersedes the request (state, approval, receipt).
11
11
  */
12
12
  import express from "express";
13
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
14
  import { execFileSync } from "node:child_process";
15
- import { homedir } from "node:os";
16
15
  import { join } from "node:path";
17
16
  import { escapeHtml } from "@miadi/node-service-kit/format";
18
17
  import { MiadiError } from "./miadi.js";
19
18
  import { MiadiUnavailable } from "./people.js";
20
19
  import { current, newRecordId } from "./records.js";
21
- const APPROVABLE = new Set([
22
- "run_request",
23
- "review_operation_request",
24
- "deep_diver_request",
25
- "review_episode_link_request",
26
- "work_proposal",
27
- "session_completion_proposal",
28
- "inquiry_episode_request",
29
- ]);
20
+ import { APPROVABLE, projectRequest } from "./request-state.js";
21
+ import { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
22
+ import { steerAttempts, steerSession, tmuxAdapter } from "./steer.js";
23
+ export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
30
24
  /**
31
25
  * What a person may decide on a request. Approving with changes runs it with the
32
26
  * approver's instructions carried into what runs; a revision sends it back to
33
27
  * whoever asked, with instructions, and runs nothing.
34
28
  */
35
29
  export const DECISIONS = ["approve", "approve_with_changes", "request_revision", "decline"];
36
- export function factoryDir(env = process.env) {
37
- return env.MIADI_FACTORY_DIR || join(homedir(), ".miadi", "factory");
38
- }
39
- export function runBriefFile(record) {
40
- return `connector-${record.id.replace(/[^\w-]/g, "-")}.md`;
41
- }
42
- /** Where a run request's brief is: waiting, delivered by the intake, cancelled, or not queued. */
43
- export function readQueueState(record, dir = factoryDir()) {
44
- const file = runBriefFile(record);
45
- const queue = join(dir, "queue");
46
- if (existsSync(join(queue, file)))
47
- return { state: "queued", file };
48
- if (existsSync(join(queue, "delivered", file)))
49
- return { state: "delivered", file };
50
- if (existsSync(join(queue, "cancelled", file)))
51
- return { state: "cancelled", file };
52
- if (existsSync(join(queue, "stale", file)))
53
- return { state: "stale", file, note: "Its seat did not take it in time; the intake moved it aside." };
54
- return { state: record.approval_state === "approved" ? "missing" : "not_queued", file };
55
- }
56
30
  function liveTmux() {
57
31
  try {
58
32
  return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] })
@@ -94,7 +68,15 @@ export function runBrief(record, approver, sessions, instructions) {
94
68
  const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
95
69
  const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
96
70
  const cwd = body.cwd || "/a/src/Miadi-18";
97
- const open = [`to="${attr(to)}"`, 'status="send"', 'type="connector-run"', `record="${attr(record.id)}"`, ...(to.startsWith("new:") ? [`cwd="${attr(cwd)}"`] : [])];
71
+ const open = [
72
+ `to="${attr(to)}"`,
73
+ 'status="send"',
74
+ 'type="connector-run"',
75
+ `record="${attr(record.id)}"`,
76
+ // The provider the connector verified at request time (jgwill/Miadi#893); the intake may honour it or record why not.
77
+ ...(body.resolved_provider ? [`provider="${attr(body.resolved_provider)}"`] : []),
78
+ ...(to.startsWith("new:") ? [`cwd="${attr(cwd)}"`] : []),
79
+ ];
98
80
  const lines = [
99
81
  `<input ${open.join(" ")}>`,
100
82
  `A bounded task requested through the Miadi connector (jgwill/Miadi#815, #816).`,
@@ -229,6 +211,23 @@ export async function executeApproved(record, approver, deps, instructions) {
229
211
  ending: b.ending,
230
212
  note: "Recorded only. The witness seat writes it into the inventory record and closes the session by its own lifecycle.",
231
213
  };
214
+ case "session_steer_request": {
215
+ // The person's approval of this exact steer is its authorization; the
216
+ // grant record it cites still binds the session (jgwill/Miadi#893).
217
+ const grantId = record.refs.find((r) => r.startsWith("run_request:") || r.startsWith("work_proposal:"));
218
+ const grantRecord = grantId ? current(deps.ledger.all()).find((x) => x.id === grantId) : undefined;
219
+ if (!grantRecord)
220
+ return { outcome: "failed", message: "The run whose grant this steer cites is gone." };
221
+ const result = await steerSession({ tmux: deps.tmux ?? tmuxAdapter(), stateDir: deps.config.stateDir, ...(deps.steerSettleMs !== undefined ? { settleMs: deps.steerSettleMs } : {}) }, { record, grantRecord, priorAttempts: steerAttempts(current(deps.ledger.all()), grantRecord.id), authorized: true });
222
+ const delivered = result.status === "accepted" || result.status === "submitted";
223
+ return {
224
+ outcome: delivered ? result.status : "failed",
225
+ steer_status: result.status,
226
+ message: result.message,
227
+ ...(result.target ? { target: result.target } : {}),
228
+ ...(result.artifact ? { artifact: result.artifact } : {}),
229
+ };
230
+ }
232
231
  default:
233
232
  return { outcome: "nothing_to_run" };
234
233
  }
@@ -281,6 +280,10 @@ ${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}
281
280
  export class AlreadyDecided extends Error {
282
281
  }
283
282
  const deciding = new Set();
283
+ /** Whether a decision on this request is being recorded now, its run may still be going. */
284
+ export function isBeingDecided(recordId) {
285
+ return deciding.has(recordId);
286
+ }
284
287
  export async function decide(r, person, choice, deps, opts) {
285
288
  // Checked again here, with no await between the check and the claim: the
286
289
  // page and the card (or two cards) may decide the same request at once.
@@ -421,6 +424,8 @@ export function consequenceOf(r) {
421
424
  return b.new_episode ? "Approving mints a new episode with the inquiry woven in." : `Approving weaves the inquiry into episode ${b.episode ?? "?"}.`;
422
425
  case "session_completion_proposal":
423
426
  return `Approving records the ending (${b.ending ?? "?"}) for the session. No pane is closed.`;
427
+ case "session_steer_request":
428
+ return `Approving types exactly this instruction into the bound agent session (${b.session ?? "the run's session"}), once, and records whether the agent accepted it.`;
424
429
  default:
425
430
  return "Approving runs the request once, as you.";
426
431
  }
@@ -459,9 +464,9 @@ export function registerApprovalRoutes(app, deps) {
459
464
  return void res.status(403).type("html").send(signInToRead(r, `Reading and deciding this request needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
460
465
  }
461
466
  if (!body.decision)
462
- return void res.type("html").send(approvePage(r, { token: writer ? token : undefined }));
467
+ return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined }));
463
468
  if (!writer)
464
- return void res.status(403).type("html").send(approvePage(r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
469
+ return void res.status(403).type("html").send(approvePage(deps, r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
465
470
  if (body.decision === "rerun") {
466
471
  let ran;
467
472
  try {
@@ -469,14 +474,14 @@ export function registerApprovalRoutes(app, deps) {
469
474
  }
470
475
  catch (error) {
471
476
  if (error instanceof AlreadyDecided)
472
- return void res.status(409).type("html").send(approvePage(find(r.id), { token, error: error.message }));
477
+ return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, error: error.message }));
473
478
  throw error;
474
479
  }
475
480
  const failedAgain = ran.receipt.outcome === "failed";
476
481
  return void res.type("html").send(page("Recorded", `<h1>${failedAgain ? "Run again, and it failed again" : "Run again"}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(ran.receipt, null, 2))}</pre>`));
477
482
  }
478
483
  if (r.approval_state !== "awaiting_approval") {
479
- return void res.status(409).type("html").send(approvePage(r, { error: `This request is already ${r.approval_state}.` }));
484
+ return void res.status(409).type("html").send(approvePage(deps, r, { error: `This request is already ${r.approval_state}.` }));
480
485
  }
481
486
  const choice = DECISIONS.includes(String(body.decision)) ? body.decision : "decline";
482
487
  const instructions = String(body.instructions ?? "").trim().slice(0, 20000) || undefined;
@@ -484,7 +489,7 @@ export function registerApprovalRoutes(app, deps) {
484
489
  return void res
485
490
  .status(400)
486
491
  .type("html")
487
- .send(approvePage(r, { token, error: choice === "request_revision" ? "Say what to revise." : "Write the changes to apply." }));
492
+ .send(approvePage(deps, r, { token, error: choice === "request_revision" ? "Say what to revise." : "Write the changes to apply." }));
488
493
  }
489
494
  let decided;
490
495
  try {
@@ -492,13 +497,21 @@ export function registerApprovalRoutes(app, deps) {
492
497
  }
493
498
  catch (error) {
494
499
  if (error instanceof AlreadyDecided)
495
- return void res.status(409).type("html").send(approvePage(find(r.id), { error: error.message }));
500
+ return void res.status(409).type("html").send(approvePage(deps, find(r.id), { error: error.message }));
496
501
  throw error;
497
502
  }
498
503
  const { decision, receipt } = decided;
499
- const failed = receipt.outcome === "failed";
500
- const heading = decision === "approved" ? (failed ? "Approved, but running it failed" : "Approved") : decision === "revision_requested" ? "Sent back for revision" : "Declined";
501
- res.type("html").send(page("Recorded", `<h1>${heading}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(receipt, null, 2))}</pre>`));
504
+ const p = projectionOf(find(r.id), deps);
505
+ const heading = decision === "approved"
506
+ ? p?.stage === "failed"
507
+ ? "Approved, but running it failed"
508
+ : p?.outstanding.length
509
+ ? "Approved, changes still to do"
510
+ : "Approved"
511
+ : decision === "revision_requested"
512
+ ? "Sent back for revision"
513
+ : "Declined";
514
+ res.type("html").send(page("Recorded", `<h1>${heading}</h1>${summary(find(r.id))}${projectionBlock(p, receipt)}`));
502
515
  });
503
516
  // A record's own page, for citations in ChatGPT: its author and anyone with Miadi's read tier may read it.
504
517
  app.get("/records/:id", (req, res) => {
@@ -532,9 +545,12 @@ ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
532
545
  <div class="row"><button class="primary" type="submit">Read</button></div></form>`;
533
546
  }
534
547
  function signInToRead(r, error) {
535
- return page("A Miadi request awaits a decision", `<h1>A request awaits a decision</h1>
536
- <p>${escapeHtml(KIND_LABELS[r.kind] ?? r.kind)}, asked ${escapeHtml(r.created_at.slice(0, 16).replace("T", " "))} UTC. State: ${escapeHtml(r.approval_state ?? "—")}.</p>
537
- <p>Sign in to read it. A person with Miadi's write tier then decides; whoever asked can read it.</p>
548
+ // The label names no one and no instruction: this page is shown before sign-in.
549
+ const open = r.approval_state === "awaiting_approval";
550
+ const label = projectRequest(r, { recording: isBeingDecided(r.id) })?.label ?? r.approval_state ?? "";
551
+ return page(open ? "A Miadi request awaits a decision" : "A Miadi request was decided", `<h1>${open ? "A request awaits a decision" : "A request was decided"}</h1>
552
+ <p>${escapeHtml(KIND_LABELS[r.kind] ?? r.kind)}, asked ${escapeHtml(r.created_at.slice(0, 16).replace("T", " "))} UTC. ${escapeHtml(label)}.</p>
553
+ <p>${open ? "Sign in to read it. A person with Miadi's write tier then decides; whoever asked can read it." : "Sign in to read what was decided and what came of it. Whoever asked, and anyone with Miadi's write tier, can read it."}</p>
538
554
  ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
539
555
  <form method="post">
540
556
  <label for="t">Your Miadi person token</label>
@@ -543,13 +559,41 @@ ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
543
559
  </form>`);
544
560
  }
545
561
  /** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
546
- function approvePage(r, o = {}) {
547
- return page("Approve a Miadi request", `<h1>Approve a request</h1>
548
- <p>${escapeHtml(consequenceOf(r))} Declining records your refusal. Either is kept in the ledger with your name.</p>
562
+ /**
563
+ * Where a request stands, as every surface of the connector reads it (request-state.ts):
564
+ * the factory queue is read for the two kinds that dispatch a brief.
565
+ */
566
+ export function requestProjection(record, opts = {}) {
567
+ const queue = record.kind === "run_request" || record.kind === "work_proposal" ? readQueueState(record, opts.factoryDir ?? factoryDir()) : undefined;
568
+ return projectRequest(record, {
569
+ ...(opts.reviewBase ? { reviewBase: opts.reviewBase } : {}),
570
+ ...(queue ? { queue } : {}),
571
+ recording: isBeingDecided(record.id),
572
+ });
573
+ }
574
+ function projectionOf(r, deps) {
575
+ return requestProjection(r, { reviewBase: deps.config.reviewUrl, factoryDir: deps.factoryDir });
576
+ }
577
+ /** Where a request stands, as a page shows it: the label, each line, the review it produced, and the receipt for audit. */
578
+ function projectionBlock(p, receipt) {
579
+ if (!p)
580
+ return "";
581
+ const lines = p.lines.map((l) => `<li>${escapeHtml(l)}</li>`).join("");
582
+ const review = p.receipt_url ? `<p><a href="${escapeHtml(p.receipt_url)}" rel="noopener">${escapeHtml(p.receipt_url)}</a></p>` : "";
583
+ const audit = receipt ? `<details><summary>Receipt</summary><pre>${escapeHtml(JSON.stringify(receipt, null, 2))}</pre></details>` : "";
584
+ return `<section aria-label="Where it stands"><p><strong>${escapeHtml(p.label)}</strong></p>${lines ? `<ul>${lines}</ul>` : ""}${review}${audit}</section>`;
585
+ }
586
+ /** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
587
+ function approvePage(deps, r, o = {}) {
588
+ const open = r.approval_state === "awaiting_approval";
589
+ // A decision being recorded offers no buttons: a second click would decide the same request again.
590
+ const actionable = open && !isBeingDecided(r.id);
591
+ return page("Approve a Miadi request", `<h1>${open ? "Approve a request" : "A decided request"}</h1>
592
+ ${actionable ? `<p>${escapeHtml(consequenceOf(r))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
549
593
  ${summary(r)}
550
- ${r.completion_receipt ? `<pre>${escapeHtml(JSON.stringify(r.completion_receipt, null, 2))}</pre>` : ""}
594
+ ${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}
551
595
  ${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
552
- ${r.approval_state === "awaiting_approval" && o.token
596
+ ${actionable && o.token
553
597
  ? `<form method="post">
554
598
  <input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
555
599
  <label for="i">Instructions (needed to approve with changes or to send back)</label>
@@ -560,7 +604,7 @@ ${r.approval_state === "awaiting_approval" && o.token
560
604
  <button type="submit" name="decision" value="request_revision">Send back for revision</button>
561
605
  <button type="submit" name="decision" value="decline">Decline</button>
562
606
  </div></form>`
563
- : r.approval_state === "awaiting_approval"
607
+ : actionable
564
608
  ? "<p>A person with Miadi's write tier decides this request.</p>"
565
609
  : isRerunnable(r) && o.token
566
610
  ? `<p>It was approved, and running it failed. When what it calls has been fixed, it can be run again with the same approval and instructions; the ledger records who ran it again.</p>
@@ -578,4 +622,5 @@ export const KIND_LABELS = {
578
622
  review_episode_link_request: "Review into an episode",
579
623
  inquiry_episode_request: "Inquiry into an episode",
580
624
  session_completion_proposal: "Session ending",
625
+ session_steer_request: "Session steer",
581
626
  };
@@ -15,4 +15,4 @@ export declare class DecisionTokens {
15
15
  * and calls `decide_request` through the host. In a host without that bridge it
16
16
  * shows each request's approval page instead.
17
17
  */
18
- export declare const DECISIONS_CARD_HTML = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n:root{color-scheme:light dark;--fg:#1d1b18;--muted:#5b5650;--line:#d9d4cc;--accent:#7a4a00;--ok:#1d6b3a;--err:#b00020;--chip:#f1ede6}\n@media (prefers-color-scheme:dark){:root{--fg:#ece8e1;--muted:#b3ada4;--line:#3a3631;--accent:#ffd700;--ok:#7fd49b;--err:#ff8a80;--chip:#24211d}}\n*{box-sizing:border-box}\nbody{margin:0;color:var(--fg);font:15px/1.45 system-ui,sans-serif;background:transparent}\nmain{padding:12px;display:grid;gap:12px}\n.empty,.note{color:var(--muted)}\narticle{border:1px solid var(--line);border-radius:10px;padding:12px}\nh2{font-size:15px;margin:0 0 4px}\n.kind{display:inline-block;font-size:12px;padding:1px 8px;border-radius:999px;background:var(--chip);color:var(--muted);margin-bottom:6px}\n.meta{font-size:13px;color:var(--muted);margin:0 0 6px}\n.does{margin:6px 0;font-weight:600}\ndetails{margin:6px 0}\nsummary{cursor:pointer;color:var(--muted);font-size:13px}\npre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:6px 0 0;max-height:240px;overflow:auto}\ntextarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line);border-radius:8px;background:transparent;color:var(--fg);font:inherit;margin-top:8px}\n.row{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}\nbutton{padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--fg);font:inherit;cursor:pointer;min-height:40px}\nbutton.primary{border-color:var(--accent);outline:1px solid var(--accent)}\nbutton:focus-visible,a:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton[disabled]{opacity:.55;cursor:default}\na{color:inherit}\n.done{color:var(--ok);font-weight:600}\n.fail{color:var(--err);font-weight:600}\n</style></head><body><main id=\"root\" aria-live=\"polite\"><p class=\"note\">Loading requests\u2026</p></main>\n<script>\n(function(){\n var root=document.getElementById(\"root\");\n var decided={};\n // A redraw while a decision is being recorded must not offer the buttons again.\n var pending={};\n var errors={};\n function el(tag,attrs,text){var e=document.createElement(tag);if(attrs)for(var k in attrs){if(k===\"class\")e.className=attrs[k];else e.setAttribute(k,attrs[k]);}if(text!=null)e.textContent=text;return e;}\n function host(){return window.openai||null;}\n function data(){var h=host();return h&&h.toolOutput?h.toolOutput:null;}\n function tokens(){var h=host();var m=h&&h.toolResponseMetadata;return (m&&m[\"miadi/decisionTokens\"])||{};}\n function render(){\n var d=data();root.textContent=\"\";\n if(!d){root.appendChild(el(\"p\",{class:\"note\"},\"Waiting for the request list.\"));return;}\n var list=d.decisions||[];\n if(!list.length){root.appendChild(el(\"p\",{class:\"empty\"},d.message||\"No request is waiting for a decision.\"));return;}\n if(!d.can_decide_here&&d.reason){root.appendChild(el(\"p\",{class:\"note\"},d.reason));}\n var t=tokens();\n list.forEach(function(r){root.appendChild(card(r,t[r.id]));});\n }\n function card(r,token){\n var a=el(\"article\",{\"aria-label\":r.title});\n a.appendChild(el(\"span\",{class:\"kind\"},r.kind_label||r.kind));\n a.appendChild(el(\"h2\",null,r.title));\n a.appendChild(el(\"p\",{class:\"meta\"},\"Asked by \"+r.by+(r.via?\" through \"+r.via:\"\")+\" \u00B7 \"+new Date(r.created_at).toLocaleString()+(r.circle?\" \u00B7 \"+r.circle+\" circle\":\"\")));\n a.appendChild(el(\"p\",{class:\"does\"},r.consequence));\n if(r.intention)a.appendChild(el(\"p\",null,r.intention));\n var det=el(\"details\");det.appendChild(el(\"summary\",null,\"What exactly is asked\"));det.appendChild(el(\"pre\",null,r.detail||\"\"));a.appendChild(det);\n var status=el(\"p\",{role:\"status\"});\n if(decided[r.id]){status.className=decided[r.id].ok?\"done\":\"fail\";status.textContent=decided[r.id].text;a.appendChild(status);return a;}\n if(pending[r.id]){status.className=\"note\";status.textContent=\"Recording your decision\u2026\";a.appendChild(status);return a;}\n if(!token||!host()||!host().callTool){\n var p=el(\"p\",{class:\"note\"});p.appendChild(document.createTextNode(\"Decide on its approval page: \"));\n var link=el(\"a\",{href:r.approval_url,target:\"_blank\",rel:\"noopener\"},\"open the approval page\");p.appendChild(link);a.appendChild(p);return a;\n }\n var ta=el(\"textarea\",{\"aria-label\":\"Instructions or changes (needed to approve with changes or send back)\",placeholder:\"Instructions or changes (needed to approve with changes or send back)\"});\n a.appendChild(ta);\n var row=el(\"div\",{class:\"row\"});\n var buttons=[[\"approve\",\"Approve\",\"primary\"],[\"approve_with_changes\",\"Approve with my changes\",\"\"],[\"request_revision\",\"Send back for revision\",\"\"],[\"decline\",\"Decline\",\"\"]];\n var btns=[];\n buttons.forEach(function(b){var x=el(\"button\",{type:\"button\",class:b[2]},b[1]);x.addEventListener(\"click\",function(){go(b[0]);});btns.push(x);row.appendChild(x);});\n a.appendChild(row);a.appendChild(status);\n if(errors[r.id]){status.className=\"fail\";status.textContent=errors[r.id];}\n function go(choice){\n var text=ta.value.trim();\n if((choice===\"approve_with_changes\"||choice===\"request_revision\")&&!text){status.className=\"fail\";status.textContent=choice===\"request_revision\"?\"Say what to revise.\":\"Write the changes to apply.\";ta.focus();return;}\n btns.forEach(function(x){x.disabled=true;});status.className=\"note\";status.textContent=\"Recording your decision\u2026\";pending[r.id]=true;delete errors[r.id];\n host().callTool(\"decide_request\",{record_id:r.id,decision:choice,instructions:text||undefined,decision_token:token}).then(function(res){\n var out=(res&&(res.structuredContent||res.structured_content))||null;\n if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}\n var ok=out&&!out.error;\n delete pending[r.id];decided[r.id]={ok:ok,text:ok?(out.summary||\"Recorded.\"):((out&&out.message)||\"Not recorded.\")};render();\n },function(err){delete pending[r.id];errors[r.id]=\"Not recorded: \"+(err&&err.message?err.message:String(err));render();});\n }\n return a;\n }\n window.addEventListener(\"openai:set_globals\",render);\n render();\n})();\n</script></body></html>";
18
+ export declare const DECISIONS_CARD_HTML = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n:root{color-scheme:light dark;--fg:#1d1b18;--muted:#5b5650;--line:#d9d4cc;--accent:#7a4a00;--ok:#1d6b3a;--err:#b00020;--chip:#f1ede6}\n@media (prefers-color-scheme:dark){:root{--fg:#ece8e1;--muted:#b3ada4;--line:#3a3631;--accent:#ffd700;--ok:#7fd49b;--err:#ff8a80;--chip:#24211d}}\n*{box-sizing:border-box}\nbody{margin:0;color:var(--fg);font:15px/1.45 system-ui,sans-serif;background:transparent}\nmain{padding:12px;display:grid;gap:12px}\n.empty,.note{color:var(--muted)}\narticle{border:1px solid var(--line);border-radius:10px;padding:12px}\nh2{font-size:15px;margin:0 0 4px}\n.kind{display:inline-block;font-size:12px;padding:1px 8px;border-radius:999px;background:var(--chip);color:var(--muted);margin-bottom:6px}\n.meta{font-size:13px;color:var(--muted);margin:0 0 6px}\n.does{margin:6px 0;font-weight:600}\ndetails{margin:6px 0}\nsummary{cursor:pointer;color:var(--muted);font-size:13px}\npre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:6px 0 0;max-height:240px;overflow:auto}\ntextarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line);border-radius:8px;background:transparent;color:var(--fg);font:inherit;margin-top:8px}\n.row{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}\nbutton{padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--fg);font:inherit;cursor:pointer;min-height:40px}\nbutton.primary{border-color:var(--accent);outline:1px solid var(--accent)}\nbutton:focus-visible,a:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton[disabled]{opacity:.55;cursor:default}\na{color:inherit}\n.done{color:var(--ok);font-weight:600}\n.fail{color:var(--err);font-weight:600}\n</style></head><body><main id=\"root\" aria-live=\"polite\"><p class=\"note\">Loading requests\u2026</p></main>\n<script>\n(function(){\n var root=document.getElementById(\"root\");\n var decided={};\n // A redraw while a decision is being recorded must not offer the buttons again.\n var pending={};\n var errors={};\n function el(tag,attrs,text){var e=document.createElement(tag);if(attrs)for(var k in attrs){if(k===\"class\")e.className=attrs[k];else e.setAttribute(k,attrs[k]);}if(text!=null)e.textContent=text;return e;}\n function host(){return window.openai||null;}\n function data(){var h=host();return h&&h.toolOutput?h.toolOutput:null;}\n function tokens(){var h=host();var m=h&&h.toolResponseMetadata;return (m&&m[\"miadi/decisionTokens\"])||{};}\n function render(){\n var d=data();root.textContent=\"\";\n if(!d){root.appendChild(el(\"p\",{class:\"note\"},\"Waiting for the request list.\"));return;}\n var list=d.decisions||[];\n if(!list.length){root.appendChild(el(\"p\",{class:\"empty\"},d.message||\"No request is waiting for a decision.\"));return;}\n if(!d.can_decide_here&&d.reason){root.appendChild(el(\"p\",{class:\"note\"},d.reason));}\n var t=tokens();\n list.forEach(function(r){root.appendChild(card(r,t[r.id]));});\n }\n // Where a request stands, as the connector projected it: the label, its lines, and the review it produced.\n function decidedBlock(d){\n var box=el(\"div\",{role:\"status\",class:d.cls});\n if(d.note)box.appendChild(el(\"p\",{class:\"line\"},d.note));\n box.appendChild(el(\"p\",{class:\"label\"},d.label));\n (d.lines||[]).forEach(function(l){box.appendChild(el(\"p\",{class:\"line\"},l));});\n if(d.url){var p=el(\"p\",{class:\"line\"});p.appendChild(el(\"a\",{href:d.url,target:\"_blank\",rel:\"noopener\"},d.url));box.appendChild(p);}\n return box;\n }\n function toolResult(res){\n var out=(res&&(res.structuredContent||res.structured_content))||null;\n if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}\n return out;\n }\n function projectionView(p,note){return {cls:p.stage===\"failed\"?\"fail\":\"done\",stage:p.stage,label:p.label,lines:p.lines||[],url:p.receipt_url||null,note:note||null};}\n function card(r,token){\n var a=el(\"article\",{\"aria-label\":r.title});\n a.appendChild(el(\"span\",{class:\"kind\"},r.kind_label||r.kind));\n a.appendChild(el(\"h2\",null,r.title));\n a.appendChild(el(\"p\",{class:\"meta\"},\"Asked by \"+r.by+(r.via?\" through \"+r.via:\"\")+\" \u00B7 \"+new Date(r.created_at).toLocaleString()+(r.circle?\" \u00B7 \"+r.circle+\" circle\":\"\")));\n a.appendChild(el(\"p\",{class:\"does\"},r.consequence));\n if(r.intention)a.appendChild(el(\"p\",null,r.intention));\n var det=el(\"details\");det.appendChild(el(\"summary\",null,\"What exactly is asked\"));det.appendChild(el(\"pre\",null,r.detail||\"\"));a.appendChild(det);\n var status=el(\"p\",{role:\"status\"});\n if(r.projection&&r.projection.stage!==\"awaiting_decision\"&&(!decided[r.id]||[\"recording\",\"dispatching\"].indexOf(r.projection.stage)===-1)){decided[r.id]=projectionView(r.projection);}\n if(decided[r.id]){\n var shown=decided[r.id];a.appendChild(decidedBlock(shown));\n if([\"recording\",\"dispatching\"].indexOf(shown.stage)!==-1&&host()&&host().callTool){\n var refresh=el(\"button\",{type:\"button\"},\"Refresh status\");\n refresh.addEventListener(\"click\",function(){\n refresh.disabled=true;\n host().callTool(\"get_run_result\",{record_id:r.id}).then(function(res){\n var out=toolResult(res);var p=out&&(out.projection||(out.data&&out.data.projection));\n if(p){decided[r.id]=projectionView(p);delete errors[r.id];}\n else{errors[r.id]=(out&&out.message)||\"Status could not be refreshed.\";}\n render();\n },function(err){errors[r.id]=\"Status could not be refreshed: \"+String(err&&err.message||err);render();});\n });\n a.appendChild(refresh);\n }\n if(errors[r.id])a.appendChild(el(\"p\",{class:\"fail\",role:\"status\"},errors[r.id]));\n return a;\n }\n if(pending[r.id]){status.className=\"note\";status.textContent=\"Recording your decision\u2026\";a.appendChild(status);return a;}\n if(!token||!host()||!host().callTool){\n var p=el(\"p\",{class:\"note\"});p.appendChild(document.createTextNode(\"Decide on its approval page: \"));\n var link=el(\"a\",{href:r.approval_url,target:\"_blank\",rel:\"noopener\"},\"open the approval page\");p.appendChild(link);a.appendChild(p);return a;\n }\n var ta=el(\"textarea\",{\"aria-label\":\"Instructions or changes (needed to approve with changes or send back)\",placeholder:\"Instructions or changes (needed to approve with changes or send back)\"});\n a.appendChild(ta);\n var row=el(\"div\",{class:\"row\"});\n var buttons=[[\"approve\",\"Approve\",\"primary\"],[\"approve_with_changes\",\"Approve with my changes\",\"\"],[\"request_revision\",\"Send back for revision\",\"\"],[\"decline\",\"Decline\",\"\"]];\n var btns=[];\n buttons.forEach(function(b){var x=el(\"button\",{type:\"button\",class:b[2]},b[1]);x.addEventListener(\"click\",function(){go(b[0]);});btns.push(x);row.appendChild(x);});\n a.appendChild(row);a.appendChild(status);\n if(errors[r.id]){status.className=\"fail\";status.textContent=errors[r.id];}\n function go(choice){\n var text=ta.value.trim();\n if((choice===\"approve_with_changes\"||choice===\"request_revision\")&&!text){status.className=\"fail\";status.textContent=choice===\"request_revision\"?\"Say what to revise.\":\"Write the changes to apply.\";ta.focus();return;}\n btns.forEach(function(x){x.disabled=true;});status.className=\"note\";status.textContent=\"Recording your decision\u2026\";pending[r.id]=true;delete errors[r.id];\n host().callTool(\"decide_request\",{record_id:r.id,decision:choice,instructions:text||undefined,decision_token:token}).then(function(res){\n var out=toolResult(res);\n // The connector answers with the request's projection, also when a click finds it decided already.\n var proj=out&&out.projection;\n var already=!!(out&&out.error===\"already_decided\");\n var ok=out&&!out.error;\n delete pending[r.id];\n if(proj){decided[r.id]=projectionView(proj,already?\"This request was already decided. Here is where it stands.\":null);}\n else{decided[r.id]={cls:ok?\"done\":\"fail\",label:ok?(out.summary||\"Recorded.\"):((out&&out.message)||\"Not recorded.\"),lines:[],url:null,note:null};}\n render();\n },function(err){delete pending[r.id];errors[r.id]=\"Not recorded: \"+(err&&err.message?err.message:String(err));render();});\n }\n return a;\n }\n window.addEventListener(\"openai:set_globals\",render);\n render();\n})();\n</script></body></html>";
package/dist/decisions.js CHANGED
@@ -102,6 +102,21 @@ a{color:inherit}
102
102
  var t=tokens();
103
103
  list.forEach(function(r){root.appendChild(card(r,t[r.id]));});
104
104
  }
105
+ // Where a request stands, as the connector projected it: the label, its lines, and the review it produced.
106
+ function decidedBlock(d){
107
+ var box=el("div",{role:"status",class:d.cls});
108
+ if(d.note)box.appendChild(el("p",{class:"line"},d.note));
109
+ box.appendChild(el("p",{class:"label"},d.label));
110
+ (d.lines||[]).forEach(function(l){box.appendChild(el("p",{class:"line"},l));});
111
+ if(d.url){var p=el("p",{class:"line"});p.appendChild(el("a",{href:d.url,target:"_blank",rel:"noopener"},d.url));box.appendChild(p);}
112
+ return box;
113
+ }
114
+ function toolResult(res){
115
+ var out=(res&&(res.structuredContent||res.structured_content))||null;
116
+ if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}
117
+ return out;
118
+ }
119
+ function projectionView(p,note){return {cls:p.stage==="failed"?"fail":"done",stage:p.stage,label:p.label,lines:p.lines||[],url:p.receipt_url||null,note:note||null};}
105
120
  function card(r,token){
106
121
  var a=el("article",{"aria-label":r.title});
107
122
  a.appendChild(el("span",{class:"kind"},r.kind_label||r.kind));
@@ -111,7 +126,25 @@ a{color:inherit}
111
126
  if(r.intention)a.appendChild(el("p",null,r.intention));
112
127
  var det=el("details");det.appendChild(el("summary",null,"What exactly is asked"));det.appendChild(el("pre",null,r.detail||""));a.appendChild(det);
113
128
  var status=el("p",{role:"status"});
114
- if(decided[r.id]){status.className=decided[r.id].ok?"done":"fail";status.textContent=decided[r.id].text;a.appendChild(status);return a;}
129
+ if(r.projection&&r.projection.stage!=="awaiting_decision"&&(!decided[r.id]||["recording","dispatching"].indexOf(r.projection.stage)===-1)){decided[r.id]=projectionView(r.projection);}
130
+ if(decided[r.id]){
131
+ var shown=decided[r.id];a.appendChild(decidedBlock(shown));
132
+ if(["recording","dispatching"].indexOf(shown.stage)!==-1&&host()&&host().callTool){
133
+ var refresh=el("button",{type:"button"},"Refresh status");
134
+ refresh.addEventListener("click",function(){
135
+ refresh.disabled=true;
136
+ host().callTool("get_run_result",{record_id:r.id}).then(function(res){
137
+ var out=toolResult(res);var p=out&&(out.projection||(out.data&&out.data.projection));
138
+ if(p){decided[r.id]=projectionView(p);delete errors[r.id];}
139
+ else{errors[r.id]=(out&&out.message)||"Status could not be refreshed.";}
140
+ render();
141
+ },function(err){errors[r.id]="Status could not be refreshed: "+String(err&&err.message||err);render();});
142
+ });
143
+ a.appendChild(refresh);
144
+ }
145
+ if(errors[r.id])a.appendChild(el("p",{class:"fail",role:"status"},errors[r.id]));
146
+ return a;
147
+ }
115
148
  if(pending[r.id]){status.className="note";status.textContent="Recording your decision…";a.appendChild(status);return a;}
116
149
  if(!token||!host()||!host().callTool){
117
150
  var p=el("p",{class:"note"});p.appendChild(document.createTextNode("Decide on its approval page: "));
@@ -130,10 +163,15 @@ a{color:inherit}
130
163
  if((choice==="approve_with_changes"||choice==="request_revision")&&!text){status.className="fail";status.textContent=choice==="request_revision"?"Say what to revise.":"Write the changes to apply.";ta.focus();return;}
131
164
  btns.forEach(function(x){x.disabled=true;});status.className="note";status.textContent="Recording your decision…";pending[r.id]=true;delete errors[r.id];
132
165
  host().callTool("decide_request",{record_id:r.id,decision:choice,instructions:text||undefined,decision_token:token}).then(function(res){
133
- var out=(res&&(res.structuredContent||res.structured_content))||null;
134
- if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}
166
+ var out=toolResult(res);
167
+ // The connector answers with the request's projection, also when a click finds it decided already.
168
+ var proj=out&&out.projection;
169
+ var already=!!(out&&out.error==="already_decided");
135
170
  var ok=out&&!out.error;
136
- delete pending[r.id];decided[r.id]={ok:ok,text:ok?(out.summary||"Recorded."):((out&&out.message)||"Not recorded.")};render();
171
+ delete pending[r.id];
172
+ if(proj){decided[r.id]=projectionView(proj,already?"This request was already decided. Here is where it stands.":null);}
173
+ else{decided[r.id]={cls:ok?"done":"fail",label:ok?(out.summary||"Recorded."):((out&&out.message)||"Not recorded."),lines:[],url:null,note:null};}
174
+ render();
137
175
  },function(err){delete pending[r.id];errors[r.id]="Not recorded: "+(err&&err.message?err.message:String(err));render();});
138
176
  }
139
177
  return a;
package/dist/index.d.ts CHANGED
@@ -5,4 +5,7 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE, type Person }
5
5
  export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, type ConnectorRecord, type CircleId, type ClaimCategory, type RecordKind, } from "./records.js";
6
6
  export { TOOL_STATUS } from "./tools.js";
7
7
  export { runBrief, readQueueState } from "./approve.js";
8
+ export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, type ProviderAvailability, type ProviderId, type ProviderResolution, type RouterPreflight, } from "./providers.js";
9
+ export { missionStatus, resolveMissionSubject, type MissionStatus, type MissionStatusDeps } from "./mission-status.js";
10
+ export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, type SteerDeps, type SteerResult, type SteerStatus, type SteeringGrant, type TmuxLike, } from "./steer.js";
8
11
  export { VERSION } from "./version.js";
package/dist/index.js CHANGED
@@ -5,4 +5,7 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE } from "./oaut
5
5
  export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, } from "./records.js";
6
6
  export { TOOL_STATUS } from "./tools.js";
7
7
  export { runBrief, readQueueState } from "./approve.js";
8
+ export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, } from "./providers.js";
9
+ export { missionStatus, resolveMissionSubject } from "./mission-status.js";
10
+ export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, } from "./steer.js";
8
11
  export { VERSION } from "./version.js";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Read-only mission status (jgwill/Miadi#893, Phase 1A): where one mission
3
+ * stands, joined from the connector ledger, the factory queue, tmux, Miadi's
4
+ * session view and GitHub — with every stage named by its evidence, so an idle
5
+ * pane or a merge is never read as completion.
6
+ *
7
+ * The caller (tools.ts) decides the viewer's tier first: without the write
8
+ * tier there are no operator details (live panes, session internals); the
9
+ * record's own standing still answers.
10
+ */
11
+ import type { ConnectorRecord } from "./records.js";
12
+ import type { RequestProjection } from "./request-state.js";
13
+ export interface MissionSession {
14
+ /** How this session is bound to the mission: signed_ref (a session: ref on the record), brief_target (the delivered brief's `to`), named (the requested name). */
15
+ association: "signed_ref" | "brief_target" | "named";
16
+ session: string;
17
+ live: "running" | "absent" | "unknown";
18
+ activity?: "busy" | "idle" | "unknown";
19
+ /** Present when Miadi's session view was joined (write tier). */
20
+ miadi?: unknown;
21
+ }
22
+ export interface MissionStageTruth {
23
+ approved: boolean;
24
+ queued: boolean;
25
+ delivered: boolean;
26
+ /** "evidence" only while a bound session is live; an idle pane proves nothing. */
27
+ running: "evidence" | "no_evidence";
28
+ pr_merged: "merged" | "open_or_absent" | "no_pr";
29
+ released: "evidence" | "no_evidence";
30
+ closed: boolean;
31
+ }
32
+ export interface MissionStatus {
33
+ ref: string;
34
+ found: boolean;
35
+ via?: string;
36
+ subject?: {
37
+ id: string;
38
+ kind: string;
39
+ created_at: string;
40
+ by: string;
41
+ circle?: string;
42
+ intention?: string;
43
+ };
44
+ standing?: RequestProjection;
45
+ queue?: {
46
+ state: string;
47
+ };
48
+ sessions?: MissionSession[];
49
+ issues?: {
50
+ ref: string;
51
+ state: string;
52
+ title?: string;
53
+ merged?: boolean;
54
+ }[];
55
+ steering?: {
56
+ grant: "none" | "active" | "expired_grant" | "exhausted";
57
+ attempts_used: number;
58
+ max_attempts?: number;
59
+ };
60
+ stage_truth?: MissionStageTruth;
61
+ next_authorized_action?: string;
62
+ notes: string[];
63
+ }
64
+ export interface MissionStatusDeps {
65
+ queueOf?: (record: ConnectorRecord) => {
66
+ state: string;
67
+ } | undefined;
68
+ projectOf: (record: ConnectorRecord) => RequestProjection | null;
69
+ /** Live tmux session names, when the host can list them (write tier only). */
70
+ tmuxSessions?: () => string[];
71
+ /** Read-only peek: is the named session's first pane mid-turn? (write tier only.) */
72
+ paneBusy?: (session: string) => "busy" | "idle" | "unknown";
73
+ /** Miadi's session view by session:<uuid> (write tier only). */
74
+ sessionOf?: (id: string) => Promise<unknown>;
75
+ issueOf?: (owner: string, repo: string, n: string) => Promise<{
76
+ state: string;
77
+ title?: string;
78
+ merged?: boolean;
79
+ }>;
80
+ now?: () => Date;
81
+ }
82
+ /** The record a mission reference resolves to, and how it was found. */
83
+ export declare function resolveMissionSubject(ref: string, records: ConnectorRecord[]): {
84
+ record: ConnectorRecord;
85
+ via: string;
86
+ } | null;
87
+ export declare function missionStatus(ref: string, records: ConnectorRecord[], deps: MissionStatusDeps): Promise<MissionStatus>;