@miadi/connector 0.1.14 → 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.
- package/README.md +5 -1
- package/dist/approve.js +24 -4
- package/dist/bin.js +12 -1
- package/dist/factory-receipt.d.ts +28 -0
- package/dist/factory-receipt.js +69 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/mission-result.d.ts +142 -0
- package/dist/mission-result.js +273 -0
- package/dist/mission-status.d.ts +92 -12
- package/dist/mission-status.js +214 -13
- package/dist/request-state.js +1 -1
- package/dist/steer.d.ts +34 -2
- package/dist/steer.js +111 -15
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +176 -19
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/mission-status.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
26
|
-
|
|
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
|
|
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[]
|
|
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;
|