@miadi/connector 0.1.7 → 0.1.8
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/dist/approve.d.ts +18 -0
- package/dist/approve.js +91 -2
- package/package.json +1 -1
package/dist/approve.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson,
|
|
|
52
52
|
to: string;
|
|
53
53
|
brief: string;
|
|
54
54
|
};
|
|
55
|
+
/** A request's refs as episode references: an `owner/repo#n` becomes its issue link, anything else stays as written. */
|
|
56
|
+
export declare function episodeReferences(record: ConnectorRecord): string[];
|
|
55
57
|
export declare function executeApproved(record: ConnectorRecord, approver: MiadiPerson, deps: ApprovalDeps, instructions?: string): Promise<Record<string, unknown>>;
|
|
56
58
|
/**
|
|
57
59
|
* Record a person's decision on a request and, when approved, run it once as
|
|
@@ -71,6 +73,22 @@ export declare function decide(r: ConnectorRecord, person: MiadiPerson, choice:
|
|
|
71
73
|
state: ConnectorRecord["state"];
|
|
72
74
|
receipt: Record<string, unknown>;
|
|
73
75
|
}>;
|
|
76
|
+
/** Whether a request was approved and its run failed, so a person may run it again. */
|
|
77
|
+
export declare function isRerunnable(r: ConnectorRecord | undefined): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Run an approved request again when its run failed (state `blocked`): a door
|
|
80
|
+
* it called was down or refused, and was fixed since. It runs as the person
|
|
81
|
+
* who asks, with the instructions it was approved with. The approval stays the
|
|
82
|
+
* decider's; the new receipt names who ran it again and keeps the failed
|
|
83
|
+
* outcome it replaces. jgwill/Miadi#884
|
|
84
|
+
*/
|
|
85
|
+
export declare function rerun(r: ConnectorRecord, person: MiadiPerson, deps: ApprovalDeps, opts: {
|
|
86
|
+
via: string;
|
|
87
|
+
}): Promise<{
|
|
88
|
+
decision: "approved";
|
|
89
|
+
state: ConnectorRecord["state"];
|
|
90
|
+
receipt: Record<string, unknown>;
|
|
91
|
+
}>;
|
|
74
92
|
export declare function isApprovable(r: ConnectorRecord | undefined): r is ConnectorRecord;
|
|
75
93
|
/** What approving a request does, in one sentence, for the card a person decides on. */
|
|
76
94
|
export declare function consequenceOf(r: ConnectorRecord): string;
|
package/dist/approve.js
CHANGED
|
@@ -133,6 +133,13 @@ function plainName(name) {
|
|
|
133
133
|
const n = name.replace(/[^\w .'-]/g, "").trim().slice(0, 40);
|
|
134
134
|
return /^[^\W_]/.test(n) ? n : "Miadi connector";
|
|
135
135
|
}
|
|
136
|
+
/** A request's refs as episode references: an `owner/repo#n` becomes its issue link, anything else stays as written. */
|
|
137
|
+
export function episodeReferences(record) {
|
|
138
|
+
return (record.refs ?? []).map((ref) => {
|
|
139
|
+
const m = /^([\w.-]+)\/([\w.-]+)#(\d+)$/.exec(ref);
|
|
140
|
+
return m ? `https://github.com/${m[1]}/${m[2]}/issues/${m[3]}` : ref;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
136
143
|
export async function executeApproved(record, approver, deps, instructions) {
|
|
137
144
|
const f = deps.fetchImpl ?? fetch;
|
|
138
145
|
const b = record.body;
|
|
@@ -198,7 +205,9 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
198
205
|
const inquiry = { artefact: b.artefact, ...(b.issue ? { issue: b.issue } : {}) };
|
|
199
206
|
try {
|
|
200
207
|
if (b.new_episode) {
|
|
201
|
-
const res = await deps.miadi.request(approver.token, "POST", "/api/chronicle/episodes",
|
|
208
|
+
const res = await deps.miadi.request(approver.token, "POST", "/api/chronicle/episodes",
|
|
209
|
+
// The mint needs at least one reference: the request's own refs, an owner/repo#n made a link (jgwill/Miadi#884).
|
|
210
|
+
{ title: b.new_episode.title, ...(b.new_episode.goal ? { goal: b.new_episode.goal } : {}), inquiry, references: episodeReferences(record) }, 120_000);
|
|
202
211
|
return { outcome: "minted_and_woven", response: res };
|
|
203
212
|
}
|
|
204
213
|
const res = await deps.miadi.request(approver.token, "POST", `/api/chronicle/episodes/${encodeURIComponent(b.episode)}/inquiry`, inquiry, 120_000);
|
|
@@ -287,6 +296,68 @@ export async function decide(r, person, choice, deps, opts) {
|
|
|
287
296
|
deciding.delete(r.id);
|
|
288
297
|
}
|
|
289
298
|
}
|
|
299
|
+
/** Whether a request was approved and its run failed, so a person may run it again. */
|
|
300
|
+
export function isRerunnable(r) {
|
|
301
|
+
return !!r && r.approval_state === "approved" && r.state === "blocked";
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Run an approved request again when its run failed (state `blocked`): a door
|
|
305
|
+
* it called was down or refused, and was fixed since. It runs as the person
|
|
306
|
+
* who asks, with the instructions it was approved with. The approval stays the
|
|
307
|
+
* decider's; the new receipt names who ran it again and keeps the failed
|
|
308
|
+
* outcome it replaces. jgwill/Miadi#884
|
|
309
|
+
*/
|
|
310
|
+
export async function rerun(r, person, deps, opts) {
|
|
311
|
+
const live = current(deps.ledger.all()).find((x) => x.id === r.id);
|
|
312
|
+
if (!isRerunnable(live) || deciding.has(r.id)) {
|
|
313
|
+
throw new AlreadyDecided(deciding.has(r.id) ? "This request is being run." : `This request is ${live?.approval_state ?? "gone"} and ${live?.state ?? "unknown"}; only an approved request whose run failed is run again.`);
|
|
314
|
+
}
|
|
315
|
+
deciding.add(r.id);
|
|
316
|
+
try {
|
|
317
|
+
const prior = (live.completion_receipt ?? {});
|
|
318
|
+
const instructions = typeof prior.instructions === "string" ? prior.instructions : undefined;
|
|
319
|
+
let receipt = {
|
|
320
|
+
decided_by: prior.decided_by,
|
|
321
|
+
decided_at: prior.decided_at,
|
|
322
|
+
decision: prior.decision,
|
|
323
|
+
...(instructions ? { instructions } : {}),
|
|
324
|
+
rerun_by: { person_id: person.id, name: person.name },
|
|
325
|
+
rerun_at: new Date().toISOString(),
|
|
326
|
+
via: opts.via,
|
|
327
|
+
replaces: { outcome: prior.outcome, ...(prior.status !== undefined ? { status: prior.status } : {}), ...(prior.response !== undefined ? { response: prior.response } : {}) },
|
|
328
|
+
};
|
|
329
|
+
let state = "completed";
|
|
330
|
+
try {
|
|
331
|
+
const outcome = await executeApproved(live, person, deps, instructions);
|
|
332
|
+
receipt = { ...receipt, ...outcome };
|
|
333
|
+
if (outcome.outcome === "failed")
|
|
334
|
+
state = "blocked";
|
|
335
|
+
if (outcome.outcome === "queued")
|
|
336
|
+
state = "in_progress";
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
receipt = { ...receipt, outcome: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
340
|
+
state = "blocked";
|
|
341
|
+
}
|
|
342
|
+
deps.ledger.append({
|
|
343
|
+
id: newRecordId(r.kind),
|
|
344
|
+
kind: r.kind,
|
|
345
|
+
created_at: new Date().toISOString(),
|
|
346
|
+
actor: { person_id: person.id, name: person.name, client_id: opts.via },
|
|
347
|
+
refs: r.refs,
|
|
348
|
+
evidence_refs: [],
|
|
349
|
+
supersedes: r.id,
|
|
350
|
+
approval_state: "approved",
|
|
351
|
+
state,
|
|
352
|
+
completion_receipt: receipt,
|
|
353
|
+
body: { decision: "rerun" },
|
|
354
|
+
});
|
|
355
|
+
return { decision: "approved", state, receipt };
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
deciding.delete(r.id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
290
361
|
async function decideOnce(r, person, choice, deps, opts) {
|
|
291
362
|
const { instructions } = opts;
|
|
292
363
|
const decision = choice === "approve" || choice === "approve_with_changes" ? "approved" : choice === "request_revision" ? "revision_requested" : "declined";
|
|
@@ -391,6 +462,19 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
391
462
|
return void res.type("html").send(approvePage(r, { token: writer ? token : undefined }));
|
|
392
463
|
if (!writer)
|
|
393
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"}.` }));
|
|
465
|
+
if (body.decision === "rerun") {
|
|
466
|
+
let ran;
|
|
467
|
+
try {
|
|
468
|
+
ran = await rerun(r, person, deps, { via: String(body.via ?? "approval-page").slice(0, 60) });
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (error instanceof AlreadyDecided)
|
|
472
|
+
return void res.status(409).type("html").send(approvePage(find(r.id), { token, error: error.message }));
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
const failedAgain = ran.receipt.outcome === "failed";
|
|
476
|
+
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
|
+
}
|
|
394
478
|
if (r.approval_state !== "awaiting_approval") {
|
|
395
479
|
return void res.status(409).type("html").send(approvePage(r, { error: `This request is already ${r.approval_state}.` }));
|
|
396
480
|
}
|
|
@@ -478,7 +562,12 @@ ${r.approval_state === "awaiting_approval" && o.token
|
|
|
478
562
|
</div></form>`
|
|
479
563
|
: r.approval_state === "awaiting_approval"
|
|
480
564
|
? "<p>A person with Miadi's write tier decides this request.</p>"
|
|
481
|
-
:
|
|
565
|
+
: isRerunnable(r) && o.token
|
|
566
|
+
? `<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>
|
|
567
|
+
<form method="post">
|
|
568
|
+
<input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
|
|
569
|
+
<div class="row"><button class="primary" type="submit" name="decision" value="rerun">Run it again</button></div></form>`
|
|
570
|
+
: ""}`);
|
|
482
571
|
}
|
|
483
572
|
/** How a request is named to a person. */
|
|
484
573
|
export const KIND_LABELS = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "The Miadi connector: one remote MCP service through which a person signed in with their Miadi person token, from ChatGPT or any MCP client, reads sessions, reviews, episodes and circles and records observations, inquiries, handoffs and proposals with their name on each record.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Miadi",
|