@miadi/connector 0.1.12 → 0.1.14

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/tools.js CHANGED
@@ -16,10 +16,12 @@ import { reviewIdOf, videoIdOf } from "./reviews.js";
16
16
  import { SCOPE_APPROVE, SCOPE_CONTRIBUTE } from "./oauth.js";
17
17
  import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current, newRecordId, } from "./records.js";
18
18
  import { VERSION } from "./version.js";
19
- import { AlreadyDecided, DECISIONS, KIND_LABELS, consequenceOf, decide, isApprovable, readQueueState, requestProjection } from "./approve.js";
19
+ import { AlreadyDecided, DECISIONS, KIND_LABELS, StaleVersion, consequenceOf, decide, isApprovable, isBeingDecided, readQueueState, requestProjection } from "./approve.js";
20
+ import { RevisionRefused, requestHistory, revisionRecord, supersedeRecords, versionOf } from "./revisions.js";
20
21
  import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
21
- import { discoverProviders, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
22
- import { missionStatus } from "./mission-status.js";
22
+ import { discoverProviders, jevConfigOf, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
23
+ import { authorizesActions, missionStatus, primaryIssueOf, resolveMissionSubject } from "./mission-status.js";
24
+ import { candidatesFor, CRITICALITIES, jevAdapter, modelPreflight, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, TASK_TYPES, } from "./model-preflight.js";
23
25
  import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steerSession } from "./steer.js";
24
26
  import { join } from "node:path";
25
27
  import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
@@ -84,6 +86,29 @@ const WRITE = { readOnlyHint: false, destructiveHint: false, openWorldHint: fals
84
86
  const refs = z.array(z.string()).max(50).default([]).describe("References: miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n, session:<uuid>, a record id, or a URL.");
85
87
  const circle = z.enum(CIRCLE_IDS);
86
88
  const claim = z.enum(CLAIM_CATEGORIES).describe("observed, hypothesis, desired_future, fiction or verified_implementation.");
89
+ const primaryIssue = z
90
+ .string()
91
+ .regex(/^[\w.-]+\/[\w.-]+#\d+$/)
92
+ .optional()
93
+ .describe("owner/repo#n: the one issue this mission implements. Other refs stay related. Without it, mission_status can only infer the mission from the first issue in refs (jgwill/Miadi#913).");
94
+ /** Refs with the declared primary issue first, never duplicated. */
95
+ function withPrimary(refs, primary) {
96
+ return primary ? [primary, ...refs.filter((r) => r !== primary)] : refs;
97
+ }
98
+ /**
99
+ * Why GitHub did not answer a reference (jgwill/Miadi#913 B). A 404 without a
100
+ * token is a private repository as often as a missing issue, so nothing here is
101
+ * ever reported as not found.
102
+ */
103
+ export function githubAccess(status, hasToken) {
104
+ if (status === 404)
105
+ return hasToken ? "not_visible_to_token" : "no_github_token";
106
+ if (status === 401)
107
+ return "token_rejected";
108
+ if (status === 403 || status === 429)
109
+ return "forbidden_or_rate_limited";
110
+ return `github_${status}`;
111
+ }
87
112
  function episodeRef(input) {
88
113
  // miadi-chronicle://316, miadi-chronicle:316, chronicle:<folder>, a folder, or a number
89
114
  return encodeURIComponent(input.replace(/^miadi-chronicle:(\/\/)?/, "").replace(/^chronicle:/, "").trim());
@@ -227,7 +252,7 @@ export function registerTools(server, ctx) {
227
252
  signal: AbortSignal.timeout(15_000),
228
253
  });
229
254
  if (!res.ok)
230
- return { ref: r, kind: "github_issue", state: res.status === 404 ? "not_found_or_private" : `github_${res.status}`, cited_by: cited };
255
+ return { ref: r, kind: "github_issue", state: "unverified", access: githubAccess(res.status, Boolean(process.env.GITHUB_TOKEN)), cited_by: cited };
231
256
  const i = await res.json();
232
257
  return {
233
258
  ref: r,
@@ -614,18 +639,20 @@ export function registerTools(server, ctx) {
614
639
  return { state: "not_found", record_id };
615
640
  return {
616
641
  record: r,
642
+ version: versionOf(r),
643
+ ...(versionOf(r) > 1 ? { history: "Earlier versions: get_request_history." } : {}),
617
644
  approval_url: r.approval_state === "awaiting_approval" ? approvalUrl(r.id) : undefined,
618
645
  queue: r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined,
619
646
  // Where the request stands, in the words every surface uses (jgwill/Miadi#889).
620
647
  projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
621
648
  ...(r.approval_state === "revision_requested"
622
- ? { revision: { instructions: r.completion_receipt?.instructions, next: `Submit a revised request that cites ${r.id} in refs.` } }
649
+ ? { revision: { instructions: r.completion_receipt?.instructions, next: `Revise it in place: revise_pending_request with request_id ${r.id} and expected_version ${versionOf(r)}.` } }
623
650
  : {}),
624
651
  };
625
652
  }));
626
653
  server.registerTool("mission_status", {
627
654
  title: "Where a mission stands",
628
- description: "One mission, joined and verified: its source request and decision, the sessions bound to it (live or not), its issues and PRs, what is approved, queued, delivered, running, merged, released or closed — each by its evidence, never inferred from an idle pane — and the next authorized action. Operator details need Miadi's write tier. jgwill/Miadi#893.",
655
+ description: "One mission, joined and verified: its source request and decision, the sessions bound to it (live or not), its issues and PRs, what is approved, queued, delivered, running, merged, released or closed — each by its evidence, never inferred from an idle pane — and the next authorized action. An issue or session resolves only through the request that holds it as primary; several are ambiguous_mission, citations alone are related_only, and an inference authorizes nothing (identity.authorizes_actions). Released needs a release receipt with artifact, target, production check and a fresh time; closed needs an approved session ending that cites the mission. Operator details need Miadi's write tier. jgwill/Miadi#893, #913.",
629
656
  inputSchema: {
630
657
  ref: z
631
658
  .string()
@@ -667,10 +694,11 @@ export function registerTools(server, ctx) {
667
694
  }
668
695
  const res = await f(`https://api.github.com/repos/${owner}/${repo}/issues/${n}`, { headers, signal: AbortSignal.timeout(15_000) });
669
696
  if (!res.ok)
670
- return { state: res.status === 404 ? "not_found_or_private" : `github_${res.status}` };
697
+ return { state: "unverified", access: githubAccess(res.status, Boolean(process.env.GITHUB_TOKEN)) };
671
698
  const i = await res.json();
672
699
  return { state: i.state, title: i.title };
673
700
  },
701
+ liveVersionOf: (pkg) => (pkg === "@miadi/connector" ? VERSION : undefined),
674
702
  });
675
703
  }));
676
704
  server.registerTool("provider_capabilities", {
@@ -689,6 +717,63 @@ export function registerTools(server, ctx) {
689
717
  note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted.",
690
718
  };
691
719
  }));
720
+ server.registerTool("mission_model_preflight", {
721
+ title: "Which provider and model fits a mission, and how that is known",
722
+ description: "A typed, versioned, read-only preflight for one mission (jgwill/Miadi#913): the candidate providers with their concrete model when the host shows one, sign-in evidence (config directory only), quota unknown, policy eligibility (the approved provider is kept; Claude Code only when named), a review floor, and a recommendation or an abstention with its reasons. It says where the choice came from: static_fallback, jev or a synthetic fixture. Jev is asked only with consult_jev and a Jev key in the connector's environment; that call is charged, and it carries no task text, mission id or repository name. A reported launch on another provider than the approved one fails closed as provider_mismatch. It authorizes nothing. Needs Miadi's write tier.",
723
+ inputSchema: {
724
+ ref: z.string().min(1).describe("The mission: run_request:<id> or work_proposal:<id> (an issue or session ref resolves only through its primary relation)."),
725
+ task_type: z.enum(TASK_TYPES),
726
+ criticality: z.enum(CRITICALITIES).default("normal"),
727
+ consult_jev: z.boolean().default(false).describe("Ask Jev to choose among the eligible options. Charged by Jev; off by default."),
728
+ launch: z
729
+ .object({
730
+ provider: z.enum(["codex", "pi", "copilot", "claude"]),
731
+ model: z.string().max(120).optional(),
732
+ evidence: z.string().min(1).max(500).describe("What shows it: a process line, a pane header."),
733
+ })
734
+ .optional()
735
+ .describe("What actually launched, as observed. A provider other than the approved one fails closed."),
736
+ },
737
+ annotations: READ,
738
+ }, wrap(async (a, extra) => {
739
+ const { person } = who(extra);
740
+ requireTier(person, "write");
741
+ const found = resolveMissionSubject(String(a.ref).trim(), records());
742
+ if (!found?.record) {
743
+ return {
744
+ contract: PREFLIGHT_CONTRACT,
745
+ version: PREFLIGHT_VERSION,
746
+ state: "mission_unresolved",
747
+ ref: a.ref,
748
+ identity: found ? { resolution: found.resolution, candidates: found.candidates } : { resolution: "not_found", candidates: [] },
749
+ note: "Name one mission by its record id; no model is chosen for an unidentified mission.",
750
+ };
751
+ }
752
+ const record = found.record;
753
+ const body = record.body;
754
+ const provider = (typeof body.resolved_provider === "string" ? body.resolved_provider : typeof body.provider === "string" ? body.provider : null);
755
+ const basis = record.approval_state === "approved" ? "approved" : "requested";
756
+ const candidates = candidatesFor(discoverProviders(ctx.providersProbe), provider, ctx.modelOf, basis);
757
+ const jevConfig = jevConfigOf(process.env);
758
+ const jev = a.consult_jev ? (ctx.jevAdapter ?? (jevConfig ? jevAdapter(jevConfig, ctx.approval?.fetchImpl ?? fetch) : undefined)) : undefined;
759
+ const configured = Boolean(jevConfig);
760
+ const primary = primaryIssueOf(record);
761
+ return modelPreflight({
762
+ mission: {
763
+ id: record.id,
764
+ resolution: found.resolution,
765
+ authorizes_actions: authorizesActions(found),
766
+ ...(primary ? { primary_issue: primary.ref } : {}),
767
+ },
768
+ approved_provider: provider,
769
+ provider_basis: basis,
770
+ task: a.task_type,
771
+ criticality: a.criticality,
772
+ candidates,
773
+ ...(a.launch ? { launch: a.launch } : {}),
774
+ ...(jev ? { adapter: jev } : { routerWhenAbsent: configured ? "not_requested" : "router_unconfigured" }),
775
+ });
776
+ }));
692
777
  // search / fetch: the pair ChatGPT uses for connectors in search and deep research.
693
778
  server.registerTool("search", {
694
779
  title: "Search Miadi",
@@ -969,12 +1054,13 @@ export function registerTools(server, ctx) {
969
1054
  }));
970
1055
  server.registerTool("propose_work", {
971
1056
  title: "Propose work to a circle",
972
- description: "Propose a bounded piece of work: what should exist when it is done, the circle accountable, the sources. Nothing starts until a person decides. Approving it starts the work as a factory run, with any changes the person writes; the person may instead send it back for revision with instructions (approval_state revision_requested, instructions in completion_receipt): then submit a revised proposal citing the old id in refs.",
1057
+ description: "Propose a bounded piece of work: what should exist when it is done, the circle accountable, the sources. Nothing starts until a person decides. Approving it starts the work as a factory run, with any changes the person writes; the person may instead send it back for revision with instructions (approval_state revision_requested, instructions in completion_receipt): then revise it in place with revise_pending_request (same id). To change a proposal still awaiting a decision, revise it the same way rather than proposing again.",
973
1058
  inputSchema: {
974
1059
  title: z.string().min(1).max(200),
975
1060
  desired_outcome: z.string().min(1).max(6000),
976
1061
  circle,
977
1062
  refs,
1063
+ primary_issue: primaryIssue,
978
1064
  repo: z.string().max(120).optional(),
979
1065
  ...base,
980
1066
  },
@@ -984,13 +1070,13 @@ export function registerTools(server, ctx) {
984
1070
  requireContribute(auth);
985
1071
  const r = append("work_proposal", person, auth, {
986
1072
  circle: a.circle,
987
- refs: a.refs,
1073
+ refs: withPrimary(a.refs, a.primary_issue),
988
1074
  intention: a.intention,
989
1075
  role: a.role,
990
1076
  idempotency_key: a.idempotency_key,
991
1077
  state: "pending",
992
1078
  approval_state: "awaiting_approval",
993
- body: { title: a.title, desired_outcome: a.desired_outcome, repo: a.repo },
1079
+ body: { title: a.title, desired_outcome: a.desired_outcome, repo: a.repo, ...(a.primary_issue ? { primary_issue: a.primary_issue } : {}) },
994
1080
  });
995
1081
  return { ...r, approval_url: approvalUrl(r.id) };
996
1082
  }));
@@ -1027,6 +1113,11 @@ export function registerTools(server, ctx) {
1027
1113
  session_id: z.string().min(8),
1028
1114
  ending: z.enum(["exit", "complete_then_exit", "record_only"]),
1029
1115
  reason: z.string().min(1).max(4000),
1116
+ mission: z
1117
+ .string()
1118
+ .regex(/^(run_request|work_proposal):[\w-]+$/)
1119
+ .optional()
1120
+ .describe("The mission this ending closes, by record id. Only an approved exit or complete_then_exit that names the mission here closes it in mission_status (jgwill/Miadi#913)."),
1030
1121
  evidence_refs: refs,
1031
1122
  },
1032
1123
  annotations: WRITE,
@@ -1036,24 +1127,25 @@ export function registerTools(server, ctx) {
1036
1127
  const id = a.session_id.replace(/^session:/, "");
1037
1128
  const r = append("session_completion_proposal", person, auth, {
1038
1129
  circle: "continuity",
1039
- refs: [`session:${id}`],
1130
+ refs: a.mission ? [`session:${id}`, a.mission] : [`session:${id}`],
1040
1131
  evidence_refs: a.evidence_refs,
1041
1132
  approval_state: "awaiting_approval",
1042
- idempotency_key: `${id}:${a.ending}`,
1043
- body: { session_id: id, ending: a.ending, reason: a.reason },
1133
+ idempotency_key: a.mission ? `${id}:${a.ending}:${a.mission}` : `${id}:${a.ending}`,
1134
+ body: { session_id: id, ending: a.ending, reason: a.reason, ...(a.mission ? { mission: a.mission } : {}) },
1044
1135
  });
1045
1136
  return { ...r, approval_url: approvalUrl(r.id) };
1046
1137
  }));
1047
1138
  // ── requests: recorded, approved by a person on the approval page, then run ──
1048
1139
  server.registerTool("request_authorized_run", {
1049
1140
  title: "Request a bounded agent run",
1050
- description: "Ask for an agent session to do one bounded task. The request waits for a person with Miadi's write tier to approve it on the returned approval_url; once approved, a brief is queued for the factory intake, which opens or reaches the session within minutes. Read it back with get_run_result.",
1141
+ description: "Ask for an agent session to do one bounded task. The request waits for a person with Miadi's write tier to approve it on the returned approval_url; once approved, a brief is queued for the factory intake, which opens or reaches the session within minutes. Read it back with get_run_result. This is for a new mission only: to change a request that still awaits a decision, call revise_pending_request with its id, so the person keeps one card to decide.",
1051
1142
  inputSchema: {
1052
1143
  task: z.string().min(1).max(12000).describe("What the session does and what it returns."),
1053
1144
  circle,
1054
1145
  refs,
1146
+ primary_issue: primaryIssue,
1055
1147
  session: z.string().regex(/^[A-Za-z0-9._-]{3,80}$/).optional().describe("An existing tmux session to hand it to, or a name for a new one."),
1056
- cwd: z.string().max(300).optional().describe("Working folder for a new session (default the Miadi checkout)."),
1148
+ cwd: z.string().max(300).optional().describe("Working folder for a new session (default: the factory runner's defaultCwd on the host)."),
1057
1149
  provider: z
1058
1150
  .enum(["codex", "pi", "copilot", "claude"])
1059
1151
  .optional()
@@ -1081,7 +1173,7 @@ export function registerTools(server, ctx) {
1081
1173
  }
1082
1174
  const r = append("run_request", person, auth, {
1083
1175
  circle: a.circle,
1084
- refs: a.refs,
1176
+ refs: withPrimary(a.refs, a.primary_issue),
1085
1177
  intention: a.intention,
1086
1178
  role: a.role,
1087
1179
  idempotency_key: a.idempotency_key,
@@ -1093,6 +1185,7 @@ export function registerTools(server, ctx) {
1093
1185
  cwd: a.cwd,
1094
1186
  provider: a.provider,
1095
1187
  resolved_provider: resolution.provider.id,
1188
+ ...(a.primary_issue ? { primary_issue: a.primary_issue } : {}),
1096
1189
  ...(a.steering ? { steering: a.steering } : {}),
1097
1190
  },
1098
1191
  });
@@ -1102,6 +1195,178 @@ export function registerTools(server, ctx) {
1102
1195
  approval_url: approvalUrl(r.id),
1103
1196
  };
1104
1197
  }));
1198
+ // ── one mission, many versions, one decision (jgwill/Miadi#919) ────────────
1199
+ const revisionOut = (r) => {
1200
+ const live = all().find((x) => x.id === r.id) ?? r;
1201
+ return {
1202
+ request_id: live.id,
1203
+ version: versionOf(live),
1204
+ approval_url: approvalUrl(live.id),
1205
+ projection: requestProjection(live, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
1206
+ };
1207
+ };
1208
+ server.registerTool("revise_pending_request", {
1209
+ title: "Revise a request that still awaits a decision",
1210
+ description: "Change an agent run or a work proposal that waits for a decision, or that its approver sent back for revision, without making a new request. The request keeps its id, its approval page and its one card; the new content becomes the next version and every earlier version stays readable with get_request_history. Pass the version you are revising as expected_version: a request revised or decided since then is refused. Give only the fields that change. A request already approved, queued or declined is not revised: a change after a decision is a new request or a steer. Call show_decisions afterwards so the person sees the one current card.",
1211
+ inputSchema: {
1212
+ request_id: z.string().min(1).describe("The request's id, run_request:<uuid> or work_proposal:<uuid>."),
1213
+ expected_version: z.number().int().min(1).describe("The version you read and are revising (projection.version, 1 for a request never revised)."),
1214
+ reason: z.string().min(1).max(2000).describe("Why it changes, in the person's words when they gave them."),
1215
+ changes: z
1216
+ .object({
1217
+ task: z.string().min(1).max(12000).optional().describe("Agent run: what the session does and returns."),
1218
+ title: z.string().min(1).max(200).optional().describe("Work proposal: its title."),
1219
+ desired_outcome: z.string().min(1).max(6000).optional().describe("Work proposal: what should exist when it is done."),
1220
+ repo: z.string().max(120).optional(),
1221
+ intention: z.string().max(2000).optional(),
1222
+ circle: circle.optional(),
1223
+ refs: z.array(z.string()).max(50).optional().describe("The whole new list of refs, replacing the old one."),
1224
+ primary_issue: primaryIssue,
1225
+ session: z.string().regex(/^[A-Za-z0-9._-]{3,80}$/).optional(),
1226
+ cwd: z.string().max(300).optional(),
1227
+ provider: z.enum(["codex", "pi", "copilot", "claude"]).optional().describe("Verified installed on the connector host again, as request_authorized_run does."),
1228
+ steering: z
1229
+ .object({ max_attempts: z.number().int().min(1).max(20), expires_at: z.string().optional(), pane: z.string().regex(/^\d+\.\d+$/).optional() })
1230
+ .optional(),
1231
+ })
1232
+ .describe("Only the fields that change."),
1233
+ idempotency_key: z.string().max(200).optional().describe("Repeat the same key to get the same revision back instead of a second one."),
1234
+ },
1235
+ annotations: WRITE,
1236
+ }, wrap(async (a, extra) => {
1237
+ const { person, auth } = who(extra);
1238
+ requireContribute(auth);
1239
+ const raw = ledger.all();
1240
+ if (a.idempotency_key) {
1241
+ const prior = raw.find((r) => r.supersedes === a.request_id && r.revision && r.idempotency_key === a.idempotency_key && r.actor.person_id === person.id);
1242
+ if (prior)
1243
+ return { ...revisionOut({ ...prior, id: a.request_id }), revised: prior.revision, repeated: true };
1244
+ }
1245
+ const live = all().find((r) => r.id === a.request_id);
1246
+ const c = (a.changes ?? {});
1247
+ const body = { ...(live?.body ?? {}) };
1248
+ let provider;
1249
+ if (live?.kind === "run_request") {
1250
+ for (const k of ["task", "session", "cwd"])
1251
+ if (c[k] !== undefined)
1252
+ body[k] = c[k];
1253
+ if (c.steering !== undefined)
1254
+ body.steering = c.steering;
1255
+ if (c.provider !== undefined) {
1256
+ const resolution = resolveProvider(c.provider, discoverProviders(ctx.providersProbe));
1257
+ if (resolution.state === "provider_unknown")
1258
+ throw new ToolRefusal("invalid_request", resolution.message, { available: resolution.available });
1259
+ if (resolution.state !== "ok")
1260
+ return { state: resolution.state, message: resolution.message, available: resolution.available, note: "Nothing was recorded; the request stays at its version." };
1261
+ body.provider = c.provider;
1262
+ body.resolved_provider = resolution.provider.id;
1263
+ provider = { requested: c.provider, resolved: resolution.provider.id, explicit: resolution.explicit, version: resolution.provider.version };
1264
+ }
1265
+ }
1266
+ else if (live?.kind === "work_proposal") {
1267
+ for (const k of ["title", "desired_outcome", "repo"])
1268
+ if (c[k] !== undefined)
1269
+ body[k] = c[k];
1270
+ }
1271
+ if (c.primary_issue !== undefined)
1272
+ body.primary_issue = c.primary_issue;
1273
+ const nextRefs = withPrimary(c.refs ?? live?.refs ?? [], c.primary_issue ?? body.primary_issue);
1274
+ let record;
1275
+ try {
1276
+ record = revisionRecord(live, { intention: c.intention ?? live?.intention, circle: c.circle ?? live?.circle, refs: nextRefs, body }, {
1277
+ id: newRecordId(live?.kind ?? "run_request"),
1278
+ person: { id: person.id, name: person.name, writer: person.api_tier === "write" },
1279
+ actor: actorOf(person, auth),
1280
+ expectedVersion: a.expected_version,
1281
+ reason: a.reason,
1282
+ beingDecided: isBeingDecided(a.request_id),
1283
+ idempotency_key: a.idempotency_key,
1284
+ });
1285
+ }
1286
+ catch (error) {
1287
+ if (error instanceof RevisionRefused) {
1288
+ throw new ToolRefusal(error.code, error.message, { ...(error.detail ?? {}), ...(live ? revisionOut(live) : {}) });
1289
+ }
1290
+ throw error;
1291
+ }
1292
+ ledger.append(record);
1293
+ return {
1294
+ ...revisionOut(record.supersedes ? { ...record, id: record.supersedes } : record),
1295
+ revised: record.revision,
1296
+ ...(provider ? { provider } : {}),
1297
+ note: `One request, one approval: its page and card now show version ${record.revision.version}. Earlier versions stay readable with get_request_history.`,
1298
+ };
1299
+ }));
1300
+ server.registerTool("get_request_history", {
1301
+ title: "Every version of a request",
1302
+ description: "Every version of an agent run or work proposal, oldest first, with who changed it, when, why, which fields changed and each version's content hash, and the decisions taken on it. Earlier requests joined to it as its earlier versions (supersede_pending_requests) are included. A request retired that way names the request that stands.",
1303
+ inputSchema: { request_id: z.string().min(1) },
1304
+ annotations: READ,
1305
+ }, wrap(async ({ request_id }, extra) => {
1306
+ const { person } = who(extra);
1307
+ const live = all().find((r) => r.id === request_id);
1308
+ if (!live || !isApprovable(live))
1309
+ return { state: "not_found", request_id };
1310
+ if (person.api_tier !== "write" && live.actor.person_id !== person.id) {
1311
+ throw new ToolRefusal("forbidden", "A request's history is read by whoever asked and by people with Miadi's write tier.");
1312
+ }
1313
+ const raw = ledger.all();
1314
+ const versions = requestHistory(request_id, raw, all()).map((v) => {
1315
+ const content = v.content;
1316
+ const shownBody = redact({ ...live, body: (content.body ?? {}) }).body;
1317
+ return { ...v, content: { ...content, body: shownBody } };
1318
+ });
1319
+ const decisions = raw
1320
+ .filter((r) => r.supersedes && versions.some((v) => v.request_id === r.supersedes) && !r.revision && !r.lineage && r.completion_receipt)
1321
+ .map((r) => {
1322
+ const rc = r.completion_receipt;
1323
+ return { request_id: r.supersedes, at: rc.decided_at ?? r.created_at, decision: rc.decision, by: rc.decided_by?.name, version: rc.version, superseded_by: rc.superseded_by, instructions: rc.instructions, reason: rc.reason };
1324
+ });
1325
+ const receipt = (live.completion_receipt ?? {});
1326
+ return {
1327
+ request_id,
1328
+ version: versionOf(live),
1329
+ versions,
1330
+ decisions,
1331
+ ...(receipt.decision === "superseded" ? { superseded_by: receipt.superseded_by, note: `This request was retired as an earlier version of ${receipt.superseded_by}; read that one's history for every version.` } : {}),
1332
+ projection: requestProjection(live, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
1333
+ };
1334
+ }));
1335
+ server.registerTool("supersede_pending_requests", {
1336
+ title: "Join earlier requests to the one that stands",
1337
+ description: "For requests made before revise_pending_request existed, one new request per revision: name the request that stands and, by exact id, the earlier requests that are its earlier versions. Each named request must still await a decision and be asked by the same person; each is retired (it can no longer be approved, and its card and page say which request to decide instead) and becomes an earlier version of the standing request, readable with get_request_history. All are checked before anything is written. Never infer which requests are duplicates: retire only ids the person named or confirmed. A request already approved or queued is refused, never cancelled.",
1338
+ inputSchema: {
1339
+ request_id: z.string().min(1).describe("The request that stands, the one the person will decide."),
1340
+ supersede: z.array(z.string().min(1)).min(1).max(10).describe("The exact ids of the earlier requests it replaces."),
1341
+ reason: z.string().min(1).max(2000).describe("Why they are one mission, in the person's words when they gave them."),
1342
+ },
1343
+ annotations: WRITE,
1344
+ }, wrap(async (a, extra) => {
1345
+ const { person, auth } = who(extra);
1346
+ requireContribute(auth);
1347
+ let out;
1348
+ try {
1349
+ out = supersedeRecords(all(), a.request_id, a.supersede, {
1350
+ person: { id: person.id, name: person.name, writer: person.api_tier === "write" },
1351
+ actor: actorOf(person, auth),
1352
+ reason: a.reason,
1353
+ beingDecided: isBeingDecided,
1354
+ newId: newRecordId,
1355
+ });
1356
+ }
1357
+ catch (error) {
1358
+ if (error instanceof RevisionRefused)
1359
+ throw new ToolRefusal(error.code, error.message, error.detail);
1360
+ throw error;
1361
+ }
1362
+ for (const r of out)
1363
+ ledger.append(r);
1364
+ return {
1365
+ ...revisionOut({ ...out[out.length - 1], id: a.request_id }),
1366
+ retired: a.supersede,
1367
+ note: "The standing request is now the one card to decide; the retired requests are its earlier versions (get_request_history).",
1368
+ };
1369
+ }));
1105
1370
  server.registerTool("request_session_steer", {
1106
1371
  title: "Send one authorized follow-up to a running agent",
1107
1372
  description: "Steer one existing live agent session within an approved run's steering grant (jgwill/Miadi#893): the exact instruction is typed into the mission's bound pane — never whichever pane is active — after a peek, and a re-peek verifies the agent accepted it. When the approved run carries no steering grant covering this, the steer waits for one explicit scoped human decision on the returned approval_url. Every attempt leaves a durable artifact with its hash and both captures.",
@@ -1449,7 +1714,8 @@ export function registerTools(server, ctx) {
1449
1714
  id: r.id,
1450
1715
  kind: r.kind,
1451
1716
  kind_label: KIND_LABELS[r.kind] ?? r.kind,
1452
- title: titleOf(r),
1717
+ title: versionOf(r) > 1 ? `${titleOf(r)} · version ${versionOf(r)}` : titleOf(r),
1718
+ version: versionOf(r),
1453
1719
  by: r.actor.name,
1454
1720
  via: r.actor.client_name,
1455
1721
  created_at: r.created_at,
@@ -1461,7 +1727,8 @@ export function registerTools(server, ctx) {
1461
1727
  projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
1462
1728
  }));
1463
1729
  const actionable = decisions.filter((d) => d.projection?.stage === "awaiting_decision");
1464
- const tokens = canDecide ? Object.fromEntries(actionable.map((r) => [r.id, ctx.decisionTokens.issue(r.id, person.id, auth.clientId)])) : {};
1730
+ // Each token is bound to the version its card shows: a card left open while the request is revised cannot approve it (#919).
1731
+ const tokens = canDecide ? Object.fromEntries(actionable.map((r) => [r.id, ctx.decisionTokens.issue(r.id, person.id, auth.clientId, undefined, r.version)])) : {};
1465
1732
  const structured = {
1466
1733
  decisions,
1467
1734
  count: decisions.length,
@@ -1504,11 +1771,14 @@ export function registerTools(server, ctx) {
1504
1771
  const r = all().find((x) => x.id === a.record_id);
1505
1772
  if (!isApprovable(r))
1506
1773
  throw new ToolRefusal("not_found", "No request with that id.");
1507
- const check = ctx.decisionTokens.verify(a.decision_token, r.id, person.id, auth.clientId);
1508
- if (check !== "ok")
1509
- throw new ToolRefusal(check === "expired" ? "token_expired" : "invalid_token", check === "expired" ? "This card is too old; show the requests again." : "This decision did not come from the person's card.");
1510
1774
  // A stale card or a second client gets where the request stands, not a bare refusal (jgwill/Miadi#889).
1511
1775
  const projectionNow = () => requestProjection(all().find((x) => x.id === r.id) ?? r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir });
1776
+ const check = ctx.decisionTokens.verify(a.decision_token, r.id, person.id, auth.clientId, undefined, versionOf(r));
1777
+ if (check === "stale") {
1778
+ throw new ToolRefusal("stale_version", `This card showed an earlier version; the request is now at version ${versionOf(r)}. Show the requests again and decide version ${versionOf(r)}.`, { projection: projectionNow() });
1779
+ }
1780
+ if (check !== "ok")
1781
+ throw new ToolRefusal(check === "expired" ? "token_expired" : "invalid_token", check === "expired" ? "This card is too old; show the requests again." : "This decision did not come from the person's card.");
1512
1782
  if (r.approval_state !== "awaiting_approval") {
1513
1783
  throw new ToolRefusal("already_decided", `This request is already ${r.approval_state}.`, { projection: projectionNow() });
1514
1784
  }
@@ -1522,9 +1792,12 @@ export function registerTools(server, ctx) {
1522
1792
  decided = await decide(r, person, a.decision, ctx.approval, {
1523
1793
  instructions,
1524
1794
  via: `card:${String(client?.client_name ?? auth.clientId).slice(0, 50)}`,
1795
+ expectedVersion: versionOf(r),
1525
1796
  });
1526
1797
  }
1527
1798
  catch (error) {
1799
+ if (error instanceof StaleVersion)
1800
+ throw new ToolRefusal("stale_version", error.message, { projection: projectionNow() });
1528
1801
  // A decision recorded a moment earlier, by another client, is reported with where it stands.
1529
1802
  if (error instanceof AlreadyDecided)
1530
1803
  throw new ToolRefusal("already_decided", error.message, { projection: projectionNow() });
@@ -1537,7 +1810,7 @@ export function registerTools(server, ctx) {
1537
1810
  : decision === "revision_requested"
1538
1811
  ? `Sent back for revision by ${person.name}.`
1539
1812
  : `Declined by ${person.name}.`;
1540
- const out = { record_id: r.id, decision, state, summary, receipt, projection };
1813
+ const out = { record_id: r.id, version: versionOf(r), decision, state, summary, receipt, projection };
1541
1814
  return { structuredContent: out, content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
1542
1815
  }
1543
1816
  catch (error) {
@@ -1613,6 +1886,7 @@ export const TOOL_STATUS = {
1613
1886
  list_records: "live",
1614
1887
  get_run_result: "live",
1615
1888
  mission_status: "live (operator details need the Miadi write tier)",
1889
+ mission_model_preflight: "live, Miadi write tier (static policy; Jev only with consult_jev and a Jev key on the host, otherwise router_unconfigured)",
1616
1890
  provider_capabilities: "live, Miadi write tier",
1617
1891
  search: "live",
1618
1892
  fetch: "live",
@@ -1628,6 +1902,9 @@ export const TOOL_STATUS = {
1628
1902
  prepare_calendar_event: "live, contribute (draft and .ics only)",
1629
1903
  propose_session_completion: "live, contribute, needs_approval (decision recorded, nothing closed)",
1630
1904
  request_authorized_run: "live, contribute, needs_approval (queues a factory brief once approved; may name a verified provider and a bounded steering grant)",
1905
+ revise_pending_request: "live, contribute (a waiting or sent-back run or proposal gets its next version; same id, same card)",
1906
+ get_request_history: "live (every version of a request, oldest first; whoever asked, or the write tier)",
1907
+ supersede_pending_requests: "live, contribute (retires named earlier requests still waiting and joins them as earlier versions of the one that stands)",
1631
1908
  request_session_steer: "live, contribute, write tier (runs inside an approved run's steering grant, else one scoped human decision)",
1632
1909
  request_review_operation: "live, contribute, needs_approval",
1633
1910
  request_deep_diver: "live, contribute, needs_approval",
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.12";
1
+ export declare const VERSION = "0.1.14";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.12";
1
+ export const VERSION = "0.1.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/connector",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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",