@miadi/connector 0.1.15 → 0.1.16

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,273 @@
1
+ const REASON_TEXT = {
2
+ cited_only: "It cites the mission in refs but does not name it in its mission field.",
3
+ no_binding: "The mission has no bound session to compare the submitting session with.",
4
+ not_bound_session: "It ends a session this mission is not bound to.",
5
+ submitter_below_write_tier: "It was submitted below Miadi's write tier; a result is verified only when the write tier or the factory's writer token submits it.",
6
+ };
7
+ const MAX_OUTPUTS = 30;
8
+ /** GitHub calls one answer may make: the anonymous limit is 60 an hour (#922 R9, R19). */
9
+ const MAX_GITHUB_CHECKS = 8;
10
+ const SHA = /^[0-9a-f]{7,40}$/;
11
+ // An owner or repository is one safe path segment: never "." or "..", so a ref cannot reach another API path (#922 R9).
12
+ const SEG = "((?!\\.\\.?(?:/|$))[\\w-][\\w.-]*)";
13
+ const COMMIT_URL = new RegExp(`^https://github\\.com/${SEG}/${SEG}/commit/([0-9a-f]{7,40})/?$`);
14
+ const BLOB_URL = new RegExp(`^https://github\\.com/${SEG}/${SEG}/blob/([^/]+)/(.+)$`);
15
+ const ISSUE_URL = /^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/(?:pull|issues)\/(\d+)\/?$/;
16
+ const ISSUE_REF = /^([\w.-]+)\/([\w.-]+)#(\d+)$/;
17
+ const REVIEW_REF = /^miadi-review:(\/\/)?[0-9a-f-]{36}$/;
18
+ const EPISODE_FILE = /^miadi-chronicle:\/\/\d+\/.+/;
19
+ const RECORD_ID = /^[a-z_]+:[0-9a-f][0-9a-f-]{7,}$/;
20
+ const SESSION_REF = /^session:/;
21
+ const str = (v) => (typeof v === "string" ? v.trim() : "");
22
+ const newestFirst = (a, b) => b.created_at.localeCompare(a.created_at);
23
+ function decisionOf(r) {
24
+ switch (r.approval_state) {
25
+ case "approved":
26
+ return "accepted";
27
+ case "declined":
28
+ return "declined";
29
+ case "revision_requested":
30
+ return "revision_requested";
31
+ default:
32
+ return "pending_approval";
33
+ }
34
+ }
35
+ /** What one reference is, before anything is read. */
36
+ export function classifyOutput(ref) {
37
+ let m = COMMIT_URL.exec(ref);
38
+ if (m)
39
+ return { ref, type: "commit", repository: `${m[1]}/${m[2]}`, commit: m[3], read: ref };
40
+ m = BLOB_URL.exec(ref);
41
+ if (m) {
42
+ const path = m[4].split("#")[0];
43
+ if (path.split("/").some((x) => x === "." || x === ".." || x === ""))
44
+ return { ref, type: "link", read: ref };
45
+ let gitRef;
46
+ try {
47
+ gitRef = decodeURIComponent(m[3]);
48
+ }
49
+ catch {
50
+ return { ref, type: "link", read: ref }; // a malformed escape is a link, never a failed answer (#922 R14)
51
+ }
52
+ return { ref, type: "file", repository: `${m[1]}/${m[2]}`, git_ref: gitRef, path, read: ref };
53
+ }
54
+ m = ISSUE_URL.exec(ref) ?? ISSUE_REF.exec(ref);
55
+ if (m)
56
+ return { ref, type: "issue_or_pr", repository: `${m[1]}/${m[2]}`, read: `mission_status or get_context with ${m[1]}/${m[2]}#${m[3]}` };
57
+ if (REVIEW_REF.test(ref))
58
+ return { ref, type: "review", read: "review_lookup" };
59
+ if (EPISODE_FILE.test(ref))
60
+ return { ref, type: "episode_file", read: "read_episode_file" };
61
+ if (RECORD_ID.test(ref))
62
+ return { ref, type: "record", read: "get_run_result or list_records" };
63
+ if (/^https?:\/\//.test(ref))
64
+ return { ref, type: "link", read: ref };
65
+ return { ref, type: "unrecognized" };
66
+ }
67
+ async function checkOutput(out, ctx) {
68
+ const checked_at = ctx.now.toISOString();
69
+ if (out.type === "unrecognized")
70
+ return { state: "unrecognized", checked_at, note: "Not a reference the connector resolves." };
71
+ if (out.type === "record") {
72
+ return ctx.records.some((r) => r.id === out.ref)
73
+ ? { state: "verified", checked_at }
74
+ : { state: "not_found", checked_at, note: "The connector ledger holds no record with this id." };
75
+ }
76
+ if (out.type !== "commit" && out.type !== "file")
77
+ return { state: "not_checked", checked_at };
78
+ if (!ctx.githubGet)
79
+ return { state: "unverified", access: "github_not_consulted", checked_at };
80
+ if (ctx.budget.left <= 0)
81
+ return { state: "not_checked", checked_at, note: `One answer makes at most ${MAX_GITHUB_CHECKS} GitHub calls.` };
82
+ ctx.budget.left--;
83
+ const repo = out.repository;
84
+ if (out.type === "commit") {
85
+ const res = await ctx.githubGet(`/repos/${repo}/commits/${out.commit}`).catch(() => ({ ok: false, status: 0, access: "unreachable" }));
86
+ if (!res.ok)
87
+ return { state: "unverified", access: res.access, checked_at };
88
+ const sha = str(res.json?.sha);
89
+ const committed = str(res.json?.commit?.committer?.date) || str(res.json?.commit?.author?.date);
90
+ if (out.declared_sha && sha && !sha.startsWith(out.declared_sha) && !out.declared_sha.startsWith(sha)) {
91
+ return { state: "changed_since_submission", sha, checked_at, note: `The agent declared ${out.declared_sha}; the URL resolves to ${sha}.` };
92
+ }
93
+ return { state: "verified", ...(sha ? { sha } : {}), ...(committed ? { committed_at: committed } : {}), checked_at };
94
+ }
95
+ // A file at a ref.
96
+ const path = out.path.split("/").map(encodeURIComponent).join("/");
97
+ const ref = encodeURIComponent(out.git_ref);
98
+ const res = await ctx.githubGet(`/repos/${repo}/contents/${path}?ref=${ref}`).catch(() => ({ ok: false, status: 0, access: "unreachable" }));
99
+ if (!res.ok) {
100
+ return {
101
+ state: "unverified",
102
+ access: res.access,
103
+ checked_at,
104
+ ...(res.status === 404 ? { note: "A branch name holding a slash cannot be told from the path in a GitHub URL; it is read as its first segment." } : {}),
105
+ };
106
+ }
107
+ const sha = str(res.json?.sha);
108
+ const size = typeof res.json?.size === "number" ? res.json.size : undefined;
109
+ const moves = !SHA.test(out.git_ref) || out.git_ref.length < 40;
110
+ const base = { state: "verified", ...(sha ? { sha } : {}), ...(size !== undefined ? { size } : {}), ...(moves ? { ref_moves: true } : {}), checked_at };
111
+ if (out.declared_sha && sha && out.declared_sha !== sha) {
112
+ return { ...base, state: "changed_since_submission", note: `The agent declared blob ${out.declared_sha}; ${out.git_ref} now holds ${sha}.` };
113
+ }
114
+ if (moves && !out.declared_sha && ctx.budget.left > 0) {
115
+ // A branch moves: the file's latest commit says whether it changed after the agent submitted it.
116
+ ctx.budget.left--;
117
+ const hist = await ctx.githubGet(`/repos/${repo}/commits?path=${path}&sha=${ref}&per_page=1`).catch(() => ({ ok: false, status: 0, access: "unreachable" }));
118
+ const last = hist.ok && Array.isArray(hist.json) ? str(hist.json[0]?.commit?.committer?.date) : "";
119
+ if (last) {
120
+ base.last_changed_at = last;
121
+ if (Date.parse(last) > Date.parse(ctx.submittedAt)) {
122
+ return { ...base, state: "changed_since_submission", note: `The file changed on ${out.git_ref} at ${last}, after the submission at ${ctx.submittedAt}.` };
123
+ }
124
+ }
125
+ else {
126
+ base.note = "The submission declared no hash and the file's history could not be read; this is the content now.";
127
+ }
128
+ }
129
+ return base;
130
+ }
131
+ /**
132
+ * The result a mission's agent submitted, checked. `boundSessions` are the Claude
133
+ * Code session ids the mission is bound to (empty when the viewer may not see them).
134
+ */
135
+ export async function missionResult(mission, records, boundSessions, deps = {}) {
136
+ const now = deps.now?.() ?? new Date();
137
+ const notes = [];
138
+ const proposals = records.filter((r) => r.kind === "session_completion_proposal" && r.id !== mission.id).sort(newestFirst);
139
+ const submissions = [];
140
+ for (const r of proposals) {
141
+ const b = (r.body ?? {});
142
+ const names = str(b.mission) === mission.id;
143
+ const cites = r.refs.includes(mission.id) || r.evidence_refs.includes(mission.id);
144
+ if (!names && !cites)
145
+ continue;
146
+ const sessionId = str(b.session_id).replace(/^session:/, "");
147
+ let association;
148
+ let code;
149
+ if (!names) {
150
+ association = "cited_only";
151
+ code = "cited_only";
152
+ }
153
+ else if (!boundSessions.size) {
154
+ association = "unverified_association";
155
+ code = "no_binding";
156
+ }
157
+ else if (!sessionId || !boundSessions.has(sessionId)) {
158
+ association = "unverified_association";
159
+ code = "not_bound_session";
160
+ }
161
+ else if (!(deps.submittedWithWriteTier ?? submittedWithWriteTier)(r)) {
162
+ association = "unverified_association";
163
+ code = "submitter_below_write_tier";
164
+ }
165
+ else {
166
+ association = "verified";
167
+ }
168
+ const receipt = (r.completion_receipt ?? {});
169
+ submissions.push({
170
+ r,
171
+ record: r.id,
172
+ submitted_at: r.created_at,
173
+ submitted_by: { name: r.actor.name, person_id: r.actor.person_id },
174
+ association,
175
+ ...(code ? { association_code: code, association_reason: REASON_TEXT[code] } : {}),
176
+ ...(sessionId ? { session_id: sessionId } : {}),
177
+ ...(str(b.ending) ? { ending: str(b.ending) } : {}),
178
+ decision: decisionOf(r),
179
+ ...(str(receipt.decided_by?.name) ? { decided_by: str(receipt.decided_by.name) } : {}),
180
+ ...(str(receipt.decided_at) ? { decided_at: str(receipt.decided_at) } : {}),
181
+ });
182
+ }
183
+ const strip = ({ r: _r, ...s }) => s;
184
+ const standing = submissions.find((s) => s.association === "verified") ?? submissions.find((s) => s.association === "unverified_association");
185
+ if (!standing) {
186
+ if (submissions.length)
187
+ notes.push(`${submissions.length} record(s) cite this mission without naming it as their mission; none is read as its result.`);
188
+ return { state: "no_result_submitted", verified: false, submissions: submissions.map(strip), completion: "none", notes };
189
+ }
190
+ const verified = standing.association === "verified";
191
+ if (!verified)
192
+ notes.push(`The standing submission is an unverified association: ${standing.association_reason} Its decision is not read as the mission's completion.`);
193
+ const others = submissions.filter((s) => s !== standing && s.association !== "cited_only");
194
+ if (others.length)
195
+ notes.push(`${others.length} other submission(s) name this mission; the newest verified one stands, every one is listed in submissions.`);
196
+ const body = (standing.r.body ?? {});
197
+ const structured = (body.result && typeof body.result === "object" ? body.result : {});
198
+ const declared = new Map();
199
+ for (const o of structured.outputs ?? [])
200
+ if (str(o?.ref))
201
+ declared.set(str(o.ref), { label: str(o.label) || undefined, git_sha: str(o.git_sha) || undefined });
202
+ const refs = [...new Set([...declared.keys(), ...standing.r.evidence_refs])].filter((x) => !SESSION_REF.test(x) && x !== mission.id);
203
+ if (refs.length > MAX_OUTPUTS)
204
+ notes.push(`${refs.length} outputs were submitted; the first ${MAX_OUTPUTS} are listed.`);
205
+ const outputs = [];
206
+ const budget = { left: MAX_GITHUB_CHECKS };
207
+ for (const ref of refs.slice(0, MAX_OUTPUTS)) {
208
+ const d = declared.get(ref);
209
+ const base = { ...classifyOutput(ref), ...(d?.label ? { label: d.label } : {}), ...(d?.git_sha && SHA.test(d.git_sha) ? { declared_sha: d.git_sha } : {}) };
210
+ outputs.push({ ...base, check: await checkOutput(base, { records, githubGet: deps.githubGet, submittedAt: standing.submitted_at, now, budget }) });
211
+ }
212
+ if (outputs.some((o) => o.check.state === "unverified"))
213
+ notes.push("Some outputs could not be read by the connector; they are unverified, not missing.");
214
+ if (outputs.some((o) => o.check.state === "changed_since_submission"))
215
+ notes.push("Some outputs changed after the agent submitted them; read them at the submitted commit.");
216
+ const t = structured.tests;
217
+ const num = (v) => (typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : undefined);
218
+ const tests = t && typeof t === "object" && (num(t.passed) !== undefined || num(t.failed) !== undefined || str(t.command))
219
+ ? {
220
+ state: "agent_claim",
221
+ ...(str(t.command) ? { command: str(t.command) } : {}),
222
+ ...(num(t.passed) !== undefined ? { passed: num(t.passed) } : {}),
223
+ ...(num(t.failed) !== undefined ? { failed: num(t.failed) } : {}),
224
+ ...(num(t.skipped) !== undefined ? { skipped: num(t.skipped) } : {}),
225
+ ...(str(t.note) ? { note: str(t.note) } : {}),
226
+ }
227
+ : { state: "not_structured", note: "The submission records no structured test receipt; the summary is the agent's own account of its tests." };
228
+ const blockers = Array.isArray(structured.blockers) ? structured.blockers.map(str).filter(Boolean) : [];
229
+ return {
230
+ state: "submitted",
231
+ verified,
232
+ current: {
233
+ ...strip(standing),
234
+ summary: str(structured.summary) || str(body.reason),
235
+ summary_claim: "agent_claim",
236
+ outputs,
237
+ tests,
238
+ blockers,
239
+ ...(standing.decision === "pending_approval" && deps.approvalUrl ? { approval_url: deps.approvalUrl(standing.record) } : {}),
240
+ },
241
+ submissions: submissions.map(strip),
242
+ completion: verified ? standing.decision : "none",
243
+ notes,
244
+ };
245
+ }
246
+ /** Endings made before 0.1.16 carry no recorded tier; from this time on every ending carries one (#922 R18). */
247
+ export const TIER_RECORDED_SINCE = "2026-11-05T00:00:00.000Z";
248
+ /**
249
+ * Whether a submission was made with Miadi's write tier. The factory's writer token is write tier
250
+ * by construction. Since 0.1.16 the connector records the submitter's tier on every ending. An
251
+ * ending without it is trusted only when it is older than TIER_RECORDED_SINCE: nobody can add a
252
+ * tier to it now, and a newer one without a tier was not written by this connector.
253
+ */
254
+ export function submittedWithWriteTier(r) {
255
+ if (r.actor.person_id === "system:writer")
256
+ return true;
257
+ const body = (r.body ?? {});
258
+ if (!("submitted_with_tier" in body))
259
+ return r.created_at < TIER_RECORDED_SINCE;
260
+ return body.submitted_with_tier === "write";
261
+ }
262
+ /** The result as a viewer below the write tier reads it: no session ids, which are operator data. */
263
+ export function redactResult(result) {
264
+ const clean = (x) => {
265
+ const { session_id: _s, ...rest } = x;
266
+ return rest;
267
+ };
268
+ return {
269
+ ...result,
270
+ ...(result.current ? { current: clean(result.current) } : {}),
271
+ submissions: result.submissions.map(clean),
272
+ };
273
+ }
@@ -21,15 +21,50 @@
21
21
  */
22
22
  import type { ConnectorRecord } from "./records.js";
23
23
  import type { RequestProjection } from "./request-state.js";
24
+ import type { FactoryReceipt } from "./factory-receipt.js";
25
+ import { type GithubGet, type MissionResult } from "./mission-result.js";
24
26
  export interface MissionSession {
25
- /** 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). */
26
- association: "signed_ref" | "brief_target" | "named";
27
+ /**
28
+ * How this session is bound to the mission: factory_receipt (the factory runner's delivery
29
+ * receipt names the record, the tmux session, the pane and the Claude Code session id that
30
+ * took the brief), signed_ref (a session: ref on the record), brief_target (the delivered
31
+ * brief's `to`), named (the requested name).
32
+ */
33
+ association: "factory_receipt" | "signed_ref" | "brief_target" | "named";
27
34
  session: string;
28
35
  live: "running" | "absent" | "unknown";
36
+ /** Busy or idle is the seat's turn state, never completion. */
29
37
  activity?: "busy" | "idle" | "unknown";
38
+ /** miadi_registry: Miadi's session registry; pane_peek: a read-only capture of the bound pane. */
39
+ activity_source?: "miadi_registry" | "pane_peek";
40
+ /** When the registry last saw that activity. */
41
+ activity_at?: string;
42
+ /** The Claude Code session id (factory_receipt). */
43
+ session_id?: string;
44
+ /** The tmux pane id the brief was typed into (factory_receipt). */
45
+ pane_id?: string;
46
+ delivered_at?: string;
47
+ /**
48
+ * verified: the factory receipt and Miadi's session registry agree on the session id, tmux session
49
+ * and pane. receipt_only: only the receipt says so (the registry is unavailable or shows no live
50
+ * seat). unverified_association: the registry places that session elsewhere.
51
+ */
52
+ binding?: "verified" | "receipt_only" | "unverified_association";
53
+ binding_reason?: string;
54
+ /** The fields the receipt and the registry were compared on; a field one side lacks is not compared. */
55
+ binding_compared?: string[];
30
56
  /** Present when Miadi's session view was joined (write tier). */
31
57
  miadi?: unknown;
32
58
  }
59
+ /** The provider the mission asked for, the one resolved on the host, and the one that ran. */
60
+ export interface MissionProvider {
61
+ requested: string | null;
62
+ resolved: string | null;
63
+ actual: string | null;
64
+ actual_source?: "factory_receipt" | "miadi_registry";
65
+ /** False when the agent that ran is not the one requested or resolved; nothing was re-run because of it. */
66
+ match: boolean | "unknown";
67
+ }
33
68
  /** What the connector could establish about a GitHub reference. `unverified` is never `not_found`: without access the connector cannot tell. */
34
69
  export interface IssueLookup {
35
70
  state: string;
@@ -87,26 +122,32 @@ export interface MissionStageTruth {
87
122
  */
88
123
  released: "verified" | "receipt_recorded" | "stale" | "live_mismatch" | "claimed_unverified" | "no_evidence";
89
124
  release_evidence?: ReleaseEvidence;
125
+ /** The agent submitted a result naming this mission (a completion proposal). Its own claim; see result. */
126
+ result_submitted: boolean;
127
+ /** The standing submission names the mission, ends a bound session and was made with the write tier. */
128
+ result_verified: boolean;
129
+ /** The person's decision on the standing submission's ending; none without one. Accepted is not closed unless the ending exits. */
130
+ completion: MissionResult["completion"];
90
131
  /** True only with an approved ending (exit or complete_then_exit) naming this mission in its `mission` field, for one of its sessions. A completed request is not a closure. */
91
132
  closed: boolean;
92
133
  closure_evidence?: ClosureEvidence;
93
134
  }
94
- export type MissionResolution = "exact" | "declared_primary" | "inferred_primary" | "ambiguous_mission" | "related_only";
135
+ export type MissionResolution = "exact" | "declared_primary" | "factory_bound" | "inferred_primary" | "ambiguous_mission" | "related_only";
95
136
  export interface MissionCandidate {
96
137
  id: string;
97
138
  kind: string;
98
139
  created_at: string;
99
- /** primary: the reference is this record's primary issue. related: an issue it cites and does not implement. cited: a session it cites. */
100
- relation: "primary" | "related" | "cited";
101
- /** declared: body.primary_issue. first_issue_ref: inferred from the first issue in refs. cited_ref: the reference is in its refs. */
102
- provenance: "declared" | "first_issue_ref" | "cited_ref";
140
+ /** primary: the reference is this record's primary issue. related: an issue it cites and does not implement. cited: a session it cites. bound: the session the factory delivered it to. */
141
+ relation: "primary" | "related" | "cited" | "bound";
142
+ /** declared: body.primary_issue. first_issue_ref: inferred from the first issue in refs. cited_ref: the reference is in its refs. factory_receipt: the factory delivered it to that session. */
143
+ provenance: "declared" | "first_issue_ref" | "cited_ref" | "factory_receipt";
103
144
  primary_issue?: string;
104
145
  }
105
146
  export interface MissionIdentity {
106
147
  resolution: MissionResolution;
107
148
  /** A declared primary that another request also holds by inference: reported, not authorizing. */
108
149
  contested?: boolean;
109
- /** True only for exact and an uncontested declared_primary: a steer, release or closure may name this mission. */
150
+ /** True only for exact, factory_bound and an uncontested declared_primary: a steer, release or closure may name this mission. */
110
151
  authorizes_actions: boolean;
111
152
  primary_issue?: {
112
153
  ref: string;
@@ -133,6 +174,7 @@ export interface MissionStatus {
133
174
  state: string;
134
175
  };
135
176
  sessions?: MissionSession[];
177
+ provider?: MissionProvider;
136
178
  issues?: {
137
179
  ref: string;
138
180
  relation: "primary" | "related";
@@ -147,6 +189,21 @@ export interface MissionStatus {
147
189
  max_attempts?: number;
148
190
  };
149
191
  stage_truth?: MissionStageTruth;
192
+ /**
193
+ * The furthest stage with evidence, read first: awaiting_decision, recording, sent_back, declined,
194
+ * approved, queued, delivered, running, result_submitted_unverified, result_submitted_awaiting_completion_approval,
195
+ * result_submitted_completion_declined, result_submitted_revision_requested, completion_accepted,
196
+ * merged, released, closed. stage_truth holds each stage on its own.
197
+ */
198
+ mission_stage?: string;
199
+ /** Decisions only a person takes that this mission waits on, each with where it is taken. */
200
+ pending_human_gates?: {
201
+ gate: "run_approval" | "completion_approval" | "unverified_completion_approval" | "steer_approval";
202
+ record: string;
203
+ approval_url?: string;
204
+ }[];
205
+ /** What the agent returned: summary, outputs with their checks, tests, blockers and the completion decision (jgwill/Miadi#921). */
206
+ result?: MissionResult;
150
207
  next_authorized_action?: string;
151
208
  notes: string[];
152
209
  }
@@ -157,8 +214,27 @@ export interface MissionStatusDeps {
157
214
  projectOf: (record: ConnectorRecord) => RequestProjection | null;
158
215
  /** Live tmux session names, when the host can list them (write tier only). */
159
216
  tmuxSessions?: () => string[];
160
- /** Read-only peek: is the named session's first pane mid-turn? (write tier only.) */
161
- paneBusy?: (session: string) => "busy" | "idle" | "unknown";
217
+ /** Read-only peek: is the named session's pane mid-turn? The pane id when the binding names one, else the first pane. (write tier only.) */
218
+ paneBusy?: (session: string, paneId?: string) => "busy" | "idle" | "unknown";
219
+ /** Whether the tmux session still holds the pane id (write tier only); undefined when it cannot be listed. */
220
+ panePresent?: (session: string, paneId: string) => boolean | undefined;
221
+ /** The factory runner's delivery receipts for one record, newest first (write tier only). */
222
+ factoryReceiptsOf?: (recordId: string) => FactoryReceipt[];
223
+ /** The records the factory delivered to one Claude Code session id (write tier only). */
224
+ recordsBoundTo?: (sessionId: string) => string[];
225
+ /** GitHub reads for a result's outputs, as the connector can make them. */
226
+ githubGet?: GithubGet;
227
+ /**
228
+ * The Claude Code session ids the factory delivered a record to, for every tier: what a result
229
+ * and a closure are checked against never depends on who is reading (#922 R5). Never shown.
230
+ */
231
+ boundSessionIdsOf?: (recordId: string) => string[];
232
+ /** The unredacted ledger, for checking results and closures; what is shown still comes from `records`. */
233
+ trustedRecords?: ConnectorRecord[];
234
+ /** The viewer holds the write tier: session ids in the result and the closure are shown. Default true. */
235
+ operator?: boolean;
236
+ /** The approval page of a record. */
237
+ approvalUrl?: (id: string) => string;
162
238
  /** Miadi's session view by session:<uuid> (write tier only). */
163
239
  sessionOf?: (id: string) => Promise<unknown>;
164
240
  issueOf?: (owner: string, repo: string, n: string) => Promise<IssueLookup>;
@@ -181,7 +257,7 @@ export interface ResolvedSubject {
181
257
  contested?: boolean;
182
258
  candidates: MissionCandidate[];
183
259
  }
184
- /** Whether a resolved reference may be acted on: an exact id, or a declared primary nobody else holds. */
260
+ /** Whether a resolved reference may be acted on: an exact id, the one record the factory delivered to a session, or a declared primary nobody else holds. */
185
261
  export declare function authorizesActions(found: Pick<ResolvedSubject, "resolution" | "contested">): boolean;
186
262
  /**
187
263
  * The record a mission reference resolves to, how it was found, and every other
@@ -189,5 +265,9 @@ export declare function authorizesActions(found: Pick<ResolvedSubject, "resoluti
189
265
  * Expects the folded ledger (`current()`), so a superseding record never stands
190
266
  * as a mission of its own.
191
267
  */
192
- export declare function resolveMissionSubject(ref: string, records: ConnectorRecord[]): ResolvedSubject | null;
268
+ export declare function resolveMissionSubject(ref: string, records: ConnectorRecord[], opts?: {
269
+ recordsBoundTo?: (sessionId: string) => string[];
270
+ }): ResolvedSubject | null;
193
271
  export declare function missionStatus(ref: string, records: ConnectorRecord[], deps: MissionStatusDeps): Promise<MissionStatus>;
272
+ /** The furthest stage the evidence reaches. Each stage keeps its own truth in stage_truth. */
273
+ export declare function missionStage(standing: RequestProjection | null, t: MissionStageTruth): string;