@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 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", { title: b.new_episode.title, ...(b.new_episode.goal ? { goal: b.new_episode.goal } : {}), inquiry }, 120_000);
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.7",
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",