@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.
@@ -0,0 +1,144 @@
1
+ import { steerAttempts, steeringGrantOf, checkGrant } from "./steer.js";
2
+ const ISSUE_REF = /^([\w.-]+)\/([\w.-]+)#(\d+)$/;
3
+ const SESSION_REF = /^session:([0-9a-f-]{36})$/;
4
+ /** The record a mission reference resolves to, and how it was found. */
5
+ export function resolveMissionSubject(ref, records) {
6
+ const exact = records.find((r) => r.id === ref);
7
+ if (exact)
8
+ return { record: exact, via: "record_id" };
9
+ const session = SESSION_REF.exec(ref);
10
+ if (session) {
11
+ const citing = records
12
+ .filter((r) => (r.kind === "run_request" || r.kind === "work_proposal") && r.refs.includes(ref))
13
+ .sort((a, b) => b.created_at.localeCompare(a.created_at));
14
+ if (citing.length)
15
+ return { record: citing[0], via: "session_ref" };
16
+ return null;
17
+ }
18
+ if (ISSUE_REF.test(ref)) {
19
+ const citing = records
20
+ .filter((r) => (r.kind === "run_request" || r.kind === "work_proposal") && (r.refs.includes(ref) || r.evidence_refs.includes(ref)))
21
+ .sort((a, b) => b.created_at.localeCompare(a.created_at));
22
+ if (citing.length)
23
+ return { record: citing[0], via: "issue_ref" };
24
+ return null;
25
+ }
26
+ return null;
27
+ }
28
+ function nextAction(record, standing, queue) {
29
+ if (!standing)
30
+ return "Read the record itself; it is not an approvable request.";
31
+ switch (standing.stage) {
32
+ case "awaiting_decision":
33
+ return "A person with Miadi's write tier decides on its approval page or a decisions card.";
34
+ case "recording":
35
+ return "A decision is being recorded; read it again shortly.";
36
+ case "declined":
37
+ case "sent_back":
38
+ return standing.stage === "declined" ? "Nothing runs; a new request is needed." : "Submit a revised request citing the old id.";
39
+ case "dispatching":
40
+ return queue?.state === "delivered"
41
+ ? "The brief is with its session. Follow it, or steer it with request_session_steer inside the approved grant."
42
+ : "Wait for the factory intake to deliver the brief.";
43
+ case "failed":
44
+ return standing.rerun.available ? "A write-tier person may run it again with the same approval." : "Read the failure and decide.";
45
+ case "completed":
46
+ return standing.outstanding.length
47
+ ? "The approved changes are still outstanding; they need their own authorized step."
48
+ : "The request is done. A mission closes when its session's ending is decided, not when this request completes.";
49
+ }
50
+ }
51
+ export async function missionStatus(ref, records, deps) {
52
+ const notes = [];
53
+ const found = resolveMissionSubject(ref, records);
54
+ if (!found)
55
+ return { ref, found: false, notes: ["No mission record resolves from this reference."] };
56
+ const { record, via } = found;
57
+ const standing = deps.projectOf(record) ?? undefined;
58
+ const queue = deps.queueOf?.(record);
59
+ const now = deps.now?.() ?? new Date();
60
+ // The sessions this mission names, each with how it is bound.
61
+ const sessions = [];
62
+ const live = deps.tmuxSessions ? new Set(deps.tmuxSessions()) : undefined;
63
+ const briefTo = (record.completion_receipt ?? {}).to;
64
+ const named = String(record.body.session ?? "") || undefined;
65
+ const seen = new Set();
66
+ const addSession = (session, association) => {
67
+ const key = `${association}:${session}`;
68
+ if (seen.has(session))
69
+ return;
70
+ seen.add(session);
71
+ sessions.push({
72
+ association,
73
+ session,
74
+ live: live ? (live.has(session) ? "running" : "absent") : "unknown",
75
+ ...(deps.paneBusy && live?.has(session) ? { activity: deps.paneBusy(session) } : {}),
76
+ });
77
+ void key;
78
+ };
79
+ if (typeof briefTo === "string" && briefTo)
80
+ addSession(briefTo.replace(/^new:/, ""), "brief_target");
81
+ if (named)
82
+ addSession(named, "named");
83
+ for (const r of record.refs) {
84
+ const m = SESSION_REF.exec(r);
85
+ if (m) {
86
+ const miadi = deps.sessionOf ? await deps.sessionOf(m[1]).catch(() => ({ state: "unavailable" })) : undefined;
87
+ sessions.push({ association: "signed_ref", session: r, live: "unknown", ...(miadi ? { miadi } : {}) });
88
+ }
89
+ }
90
+ if (!live)
91
+ notes.push("Live session state was not joined (the viewer's tier or the host does not allow it).");
92
+ const issues = [];
93
+ if (deps.issueOf) {
94
+ for (const r of [...record.refs, ...record.evidence_refs]) {
95
+ const m = ISSUE_REF.exec(r);
96
+ if (!m)
97
+ continue;
98
+ const issue = await deps
99
+ .issueOf(m[1], m[2], m[3])
100
+ .catch(() => ({ state: "unavailable" }));
101
+ issues.push({ ref: r, state: issue.state, ...(issue.title ? { title: issue.title } : {}), ...(issue.merged !== undefined ? { merged: issue.merged } : {}) });
102
+ }
103
+ }
104
+ const grant = steeringGrantOf(record);
105
+ const attempts = steerAttempts(records, record.id);
106
+ const grantCheck = grant ? checkGrant(grant, attempts, now) : null;
107
+ const grantState = grantCheck === null ? "none" : grantCheck === "ok" ? "active" : grantCheck === "expired_grant" ? "expired_grant" : "exhausted";
108
+ const receipt = (record.completion_receipt ?? {});
109
+ const anyLive = sessions.some((s) => s.live === "running");
110
+ const prs = issues.filter((i) => i.merged !== undefined);
111
+ const stageTruth = {
112
+ approved: record.approval_state === "approved",
113
+ queued: queue?.state === "queued" || queue?.state === "delivered",
114
+ delivered: queue?.state === "delivered",
115
+ running: anyLive ? "evidence" : "no_evidence",
116
+ pr_merged: prs.length ? (prs.every((p) => p.merged) ? "merged" : "open_or_absent") : "no_pr",
117
+ released: receipt.published || receipt.version ? "evidence" : "no_evidence",
118
+ closed: standing?.stage === "completed" && standing?.standing === "done",
119
+ };
120
+ if (stageTruth.running === "no_evidence" && record.approval_state === "approved") {
121
+ notes.push("No bound session shows live; an idle or absent pane is not proof of completion or failure.");
122
+ }
123
+ return {
124
+ ref,
125
+ found: true,
126
+ via,
127
+ subject: {
128
+ id: record.id,
129
+ kind: record.kind,
130
+ created_at: record.created_at,
131
+ by: record.actor.name,
132
+ ...(record.circle ? { circle: record.circle } : {}),
133
+ ...(record.intention ? { intention: record.intention } : {}),
134
+ },
135
+ ...(standing ? { standing } : {}),
136
+ ...(queue ? { queue } : {}),
137
+ sessions,
138
+ issues,
139
+ steering: { grant: grantState, attempts_used: attempts, ...(grant?.max_attempts !== undefined ? { max_attempts: grant.max_attempts } : {}) },
140
+ stage_truth: stageTruth,
141
+ next_authorized_action: nextAction(record, standing ?? null, queue),
142
+ notes,
143
+ };
144
+ }
@@ -0,0 +1,61 @@
1
+ export type ProviderId = "codex" | "pi" | "copilot" | "claude";
2
+ export interface ProviderSpec {
3
+ id: ProviderId;
4
+ cli: string;
5
+ name: string;
6
+ /** False when a provider may run only on an explicit, authorized request. */
7
+ selectable_by_default: boolean;
8
+ /** Config paths whose existence suggests a sign-in; contents are never read. */
9
+ auth_evidence: string[];
10
+ }
11
+ /** Preference order when a run request names no provider (jgwill/Miadi#893). */
12
+ export declare const PROVIDERS: ProviderSpec[];
13
+ export interface ProviderAvailability {
14
+ id: ProviderId;
15
+ name: string;
16
+ cli: string;
17
+ installed: boolean;
18
+ version?: string;
19
+ auth_evidence: "found" | "none";
20
+ selectable_by_default: boolean;
21
+ }
22
+ export interface DiscoveryProbe {
23
+ which?: (cli: string) => string | null;
24
+ versionOf?: (cli: string) => string | undefined;
25
+ exists?: (path: string) => boolean;
26
+ home?: string;
27
+ }
28
+ export declare function discoverProviders(probe?: DiscoveryProbe, specs?: ProviderSpec[]): ProviderAvailability[];
29
+ export type ProviderResolution = {
30
+ state: "ok";
31
+ provider: ProviderAvailability;
32
+ explicit: boolean;
33
+ } | {
34
+ state: "provider_unknown" | "provider_unavailable" | "provider_needs_explicit_selection";
35
+ message: string;
36
+ available: ProviderAvailability[];
37
+ };
38
+ /**
39
+ * Resolve the provider a run will ask the intake for. A requested provider must
40
+ * be known and installed; with no request the first installed provider in the
41
+ * preference order that may be selected by default wins — Claude Code only ever
42
+ * by an explicit request.
43
+ */
44
+ export declare function resolveProvider(requested: string | undefined, found: ProviderAvailability[]): ProviderResolution;
45
+ export type RouterPreflight = {
46
+ state: "unconfigured";
47
+ note: string;
48
+ } | {
49
+ state: "reachable";
50
+ url: string;
51
+ } | {
52
+ state: "router_unavailable";
53
+ url: string;
54
+ message: string;
55
+ };
56
+ /**
57
+ * The read-only Jev routing preflight (jgwill/Miadi#893, section E): is a Jev
58
+ * decision-aide endpoint configured and reachable from this host? Nothing is
59
+ * sent but a health probe; no key is read here and no source text ever leaves.
60
+ */
61
+ export declare function routerPreflight(env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Promise<RouterPreflight>;
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ import type { ConnectorRecord } from "./records.js";
2
+ export declare function factoryDir(env?: NodeJS.ProcessEnv): string;
3
+ export declare function runBriefFile(record: {
4
+ id: string;
5
+ }): string;
6
+ /** Evidence only: never creates a queue, dispatches work, or changes a request. */
7
+ export declare function readQueueState(record: ConnectorRecord, dir?: string): {
8
+ state: string;
9
+ file: string;
10
+ note?: undefined;
11
+ } | {
12
+ state: string;
13
+ file: string;
14
+ note: string;
15
+ };
@@ -0,0 +1,24 @@
1
+ /** Shared, read-only factory queue evidence for every request-state surface. */
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ export function factoryDir(env = process.env) {
6
+ return env.MIADI_FACTORY_DIR || join(homedir(), ".miadi", "factory");
7
+ }
8
+ export function runBriefFile(record) {
9
+ return `connector-${record.id.replace(/[^\w-]/g, "-")}.md`;
10
+ }
11
+ /** Evidence only: never creates a queue, dispatches work, or changes a request. */
12
+ export function readQueueState(record, dir = factoryDir()) {
13
+ const file = runBriefFile(record);
14
+ const queue = join(dir, "queue");
15
+ if (existsSync(join(queue, file)))
16
+ return { state: "queued", file };
17
+ if (existsSync(join(queue, "delivered", file)))
18
+ return { state: "delivered", file };
19
+ if (existsSync(join(queue, "cancelled", file)))
20
+ return { state: "cancelled", file };
21
+ if (existsSync(join(queue, "stale", file)))
22
+ return { state: "stale", file, note: "Its seat did not take it in time; the intake moved it aside." };
23
+ return { state: record.approval_state === "approved" ? "missing" : "not_queued", file };
24
+ }
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"];
@@ -87,4 +87,7 @@ export declare function newRecordId(kind: RecordKind): string;
87
87
  * Fold the ledger: a record superseded by a later one shows the later one's
88
88
  * state, approval and receipt, keeping its own id and history.
89
89
  */
90
+ export { projectRequest, APPROVABLE_KINDS, APPROVABLE } from "./request-state.js";
91
+ export { readQueueState, runBriefFile } from "./queue-state.js";
92
+ export type { RequestProjection, RequestStage, RequestStanding, RequestDecision, ProjectionContext } from "./request-state.js";
90
93
  export declare function current(records: ConnectorRecord[]): ConnectorRecord[];
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"];
@@ -76,6 +77,8 @@ export function newRecordId(kind) {
76
77
  * Fold the ledger: a record superseded by a later one shows the later one's
77
78
  * state, approval and receipt, keeping its own id and history.
78
79
  */
80
+ export { projectRequest, APPROVABLE_KINDS, APPROVABLE } from "./request-state.js";
81
+ export { readQueueState, runBriefFile } from "./queue-state.js";
79
82
  export function current(records) {
80
83
  const byId = new Map();
81
84
  for (const r of records) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Where a request stands, read once for every surface that shows it: the
3
+ * decision card in the conversation, the approval page, get_run_result and
4
+ * Miadi's /research (jgwill/Miadi#889, Mission A). A surface shows the
5
+ * projection's `label` and `lines`. It does not read the record's state,
6
+ * approval state or receipt by itself.
7
+ *
8
+ * Stages, from the ledger's folded record (see `current()`):
9
+ * - awaiting_decision: awaiting_approval, nothing decided
10
+ * - declined, sent_back: decided, nothing ran
11
+ * - dispatching: approved, and its brief is queued or running (state in_progress)
12
+ * - completed: approved, and its run finished (state completed)
13
+ * - failed: approved, and its run failed (state blocked); the same approval may run it again
14
+ *
15
+ * Approving with changes keeps the changes with the decision. A kind whose run
16
+ * does not carry instructions (approve.ts runBrief carries them only for runs and
17
+ * work proposals) lists them as outstanding. The projection never says they ran.
18
+ */
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", "session_steer_request"];
21
+ export declare const APPROVABLE: ReadonlySet<string>;
22
+ /** `recording`: a decision was taken a moment ago and its run may still be going; it is not the ledger's state. */
23
+ export type RequestStage = "awaiting_decision" | "recording" | "declined" | "sent_back" | "dispatching" | "completed" | "failed";
24
+ export type RequestStanding = "waiting" | "open" | "done" | "stopped";
25
+ export interface RequestDecision {
26
+ /** approve, approve_with_changes, request_revision or decline, as the person chose. */
27
+ choice: string;
28
+ by: string;
29
+ by_id?: string;
30
+ at?: string;
31
+ via?: string;
32
+ instructions?: string;
33
+ rerun_by?: string;
34
+ }
35
+ export interface RequestProjection {
36
+ ref: string;
37
+ kind: string;
38
+ stage: RequestStage;
39
+ standing: RequestStanding;
40
+ /** Plain words for where it stands. Names, times and instructions are not in it, so it is safe before sign-in. */
41
+ label: string;
42
+ decision?: RequestDecision;
43
+ /** The receipt's outcome: queued, done, failed, linked, woven, minted_and_woven, marked_ready, decided, nothing_to_run. */
44
+ outcome?: string;
45
+ failure?: string;
46
+ /** The review the request produced or changed, at the review service, when the receipt names one. */
47
+ receipt_url?: string;
48
+ /** Approved and failed: a write-tier person may run it again with the same approval and instructions. */
49
+ rerun: {
50
+ available: boolean;
51
+ by?: string;
52
+ };
53
+ /** Work the approver asked for that did not run. Empty when nothing is outstanding. */
54
+ outstanding: string[];
55
+ /** Every sentence a surface shows under the label, names and instructions included. */
56
+ lines: string[];
57
+ }
58
+ export interface ProjectionContext {
59
+ /** The review service's public address, for receipt_url. Default MIADI_REVIEW_BASE_URL, else the public service. */
60
+ reviewBase?: string;
61
+ /** The factory queue state of a brief (queued, delivered, missing, not_queued), when the caller read it. */
62
+ queue?: {
63
+ state: string;
64
+ };
65
+ /** A decision on this request is being recorded now (its run may still be going). */
66
+ recording?: boolean;
67
+ }
68
+ /** The projection of an approvable request, or null for a record that is not one. */
69
+ export declare function projectRequest(record: ConnectorRecord, ctx?: ProjectionContext): RequestProjection | null;
@@ -0,0 +1,180 @@
1
+ export const APPROVABLE_KINDS = [
2
+ "run_request",
3
+ "review_operation_request",
4
+ "deep_diver_request",
5
+ "review_episode_link_request",
6
+ "work_proposal",
7
+ "session_completion_proposal",
8
+ "inquiry_episode_request",
9
+ "session_steer_request",
10
+ ];
11
+ export const APPROVABLE = new Set(APPROVABLE_KINDS);
12
+ /** The kinds whose approved run carries the approver's instructions into what it does. */
13
+ const CARRIES_INSTRUCTIONS = new Set(["run_request", "work_proposal"]);
14
+ const CHOICE_WORDS = {
15
+ approve: "Approved",
16
+ approve_with_changes: "Approved with changes",
17
+ request_revision: "Sent back for revision",
18
+ decline: "Declined",
19
+ covered_by_grant: "Covered by the approved run's steering grant",
20
+ };
21
+ const OUTCOME_WORDS = {
22
+ queued: "queued for the factory intake",
23
+ done: "done",
24
+ linked: "filed in the episode",
25
+ woven: "woven into the episode",
26
+ minted_and_woven: "a new episode minted with it",
27
+ marked_ready: "marked ready for Deep Diver",
28
+ decided: "recorded",
29
+ nothing_to_run: "nothing to run",
30
+ accepted: "accepted by the session",
31
+ submitted: "typed into the session, acceptance unconfirmed",
32
+ failed: "failed",
33
+ };
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.";
35
+ const text = (v) => (typeof v === "string" ? v.trim() : "");
36
+ function reviewBaseOf(ctx) {
37
+ const base = ctx.reviewBase ?? process.env.MIADI_REVIEW_BASE_URL ?? "https://miadi-review-service.vercel.app";
38
+ return base.replace(/\/$/, "");
39
+ }
40
+ /** The projection of an approvable request, or null for a record that is not one. */
41
+ export function projectRequest(record, ctx = {}) {
42
+ if (!APPROVABLE.has(record.kind))
43
+ return null;
44
+ const receipt = (record.completion_receipt ?? {});
45
+ const choice = text(receipt.decision) || undefined;
46
+ const instructions = text(receipt.instructions) || undefined;
47
+ const decision = record.approval_state === "awaiting_approval" || !choice
48
+ ? undefined
49
+ : {
50
+ choice,
51
+ by: text(receipt.decided_by?.name) || "someone",
52
+ ...(receipt.decided_by?.person_id ? { by_id: String(receipt.decided_by.person_id) } : {}),
53
+ ...(text(receipt.decided_at) ? { at: text(receipt.decided_at) } : {}),
54
+ ...(text(receipt.via) ? { via: text(receipt.via) } : {}),
55
+ ...(instructions ? { instructions } : {}),
56
+ ...(text(receipt.rerun_by?.name) ? { rerun_by: text(receipt.rerun_by.name) } : {}),
57
+ };
58
+ const base = { ref: record.id, kind: record.kind, rerun: { available: false }, outstanding: [] };
59
+ if (record.approval_state === "awaiting_approval" && ctx.recording) {
60
+ return finish({ ...base, stage: "recording", standing: "open", label: "Being recorded" }, [
61
+ "A decision on this request is being recorded. It shows here when it is done.",
62
+ ]);
63
+ }
64
+ if (record.approval_state === "awaiting_approval") {
65
+ return finish({ ...base, stage: "awaiting_decision", standing: "waiting", label: "Awaiting a decision" }, [
66
+ "Waiting for a person with Miadi's write tier to decide.",
67
+ ]);
68
+ }
69
+ if (record.approval_state === "declined") {
70
+ return finish({ ...base, stage: "declined", standing: "stopped", decision, label: "Declined" }, decisionLines(decision));
71
+ }
72
+ if (record.approval_state === "revision_requested") {
73
+ const lines = decisionLines(decision);
74
+ if (instructions)
75
+ lines.push(`Instructions for whoever asked: ${instructions}`);
76
+ return finish({ ...base, stage: "sent_back", standing: "waiting", decision, label: "Sent back for revision" }, lines);
77
+ }
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)
82
+ return null;
83
+ const changes = choice === "approve_with_changes";
84
+ const outcome = text(receipt.outcome) || undefined;
85
+ const outcomeWord = outcome ? (OUTCOME_WORDS[outcome] ?? outcome.replace(/_/g, " ")) : undefined;
86
+ const rerunBy = decision?.rerun_by;
87
+ if (record.state === "blocked") {
88
+ const resp = (receipt.response ?? {});
89
+ const status = typeof receipt.status === "number" ? receipt.status : undefined;
90
+ const failure = text(receipt.message) || text(resp.message) || text(resp.error) || (status !== undefined ? `the door answered ${status}` : "the door it called refused");
91
+ const outstanding = outstandingFor(record, decision);
92
+ const lines = [...decisionLines(decision), `Failure: ${failure}`];
93
+ if (receipt.outcome)
94
+ lines.push(`Outcome: ${outcomeWord}.`);
95
+ lines.push(...outstanding);
96
+ return finish({
97
+ ...base,
98
+ stage: "failed",
99
+ standing: "stopped",
100
+ decision,
101
+ label: `${changes ? "Approved with changes" : "Approved"}, but running it failed`,
102
+ outcome: outcome ?? "failed",
103
+ failure,
104
+ rerun: { available: true, ...(rerunBy ? { by: rerunBy } : {}) },
105
+ outstanding,
106
+ }, lines);
107
+ }
108
+ if (record.state === "in_progress") {
109
+ const delivered = ctx.queue?.state === "delivered";
110
+ const outstanding = [];
111
+ if (ctx.queue?.state === "missing")
112
+ outstanding.push("The brief is missing from the factory queue, so the intake cannot deliver it.");
113
+ if (ctx.queue?.state === "cancelled" || ctx.queue?.state === "stale") {
114
+ outstanding.push(`The factory intake set the brief aside (${ctx.queue.state}), so nothing is running for it.`);
115
+ }
116
+ const lines = [...decisionLines(decision), outcomeWord ? `Outcome: ${outcomeWord}.` : "Outcome: queued for the factory intake."];
117
+ if (delivered)
118
+ lines.push("Delivered to an agent session.");
119
+ lines.push(...outstanding);
120
+ return finish({
121
+ ...base,
122
+ stage: "dispatching",
123
+ standing: "open",
124
+ decision,
125
+ label: `${changes ? "Approved with changes" : "Approved"}, ${delivered ? "delivered to a session" : "dispatching"}`,
126
+ outcome,
127
+ outstanding,
128
+ }, lines);
129
+ }
130
+ const outstanding = outstandingFor(record, decision);
131
+ const receiptUrl = reviewUrlOf(record, receipt, ctx);
132
+ const lines = [...decisionLines(decision)];
133
+ if (outcomeWord)
134
+ lines.push(`Outcome: ${outcomeWord}.`);
135
+ if (receiptUrl)
136
+ lines.push(`Review: ${receiptUrl}`);
137
+ lines.push(...outstanding);
138
+ return finish({
139
+ ...base,
140
+ stage: "completed",
141
+ standing: outstanding.length ? "open" : "done",
142
+ decision,
143
+ label: outstanding.length
144
+ ? "Approved with changes, changes outstanding"
145
+ : `${changes ? "Approved with changes" : "Approved"}${outcomeWord ? `, ${outcomeWord}` : ""}`,
146
+ outcome,
147
+ ...(receiptUrl ? { receipt_url: receiptUrl } : {}),
148
+ outstanding,
149
+ }, lines);
150
+ }
151
+ function finish(p, lines) {
152
+ return { ...p, lines: lines.filter((l) => l.length > 0) };
153
+ }
154
+ function decisionLines(decision) {
155
+ if (!decision)
156
+ return [];
157
+ const when = decision.at ? ` at ${decision.at}` : "";
158
+ const through = decision.via ? ` (through ${decision.via})` : "";
159
+ const out = [`${CHOICE_WORDS[decision.choice] ?? decision.choice} by ${decision.by}${when}${through}.`];
160
+ if (decision.instructions)
161
+ out.push(`Instructions: ${decision.instructions}`);
162
+ if (decision.rerun_by)
163
+ out.push(`Run again by ${decision.rerun_by}.`);
164
+ return out;
165
+ }
166
+ function outstandingFor(record, decision) {
167
+ if (decision?.choice !== "approve_with_changes" || !decision.instructions)
168
+ return [];
169
+ if (CARRIES_INSTRUCTIONS.has(record.kind))
170
+ return [];
171
+ return [NOT_CARRIED];
172
+ }
173
+ function reviewUrlOf(record, receipt, ctx) {
174
+ if (record.kind !== "review_operation_request" && record.kind !== "deep_diver_request")
175
+ return undefined;
176
+ if (receipt.outcome !== "done" && receipt.outcome !== "marked_ready")
177
+ return undefined;
178
+ const id = text(receipt.response?.id) || text(record.body.review_id);
179
+ return id ? `${reviewBaseOf(ctx)}/review/${encodeURIComponent(id)}` : undefined;
180
+ }