@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/README.md +15 -3
- package/dist/approve.d.ts +8 -1
- package/dist/approve.js +39 -7
- package/dist/decisions.d.ts +4 -2
- package/dist/decisions.js +18 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mission-status.d.ts +121 -15
- package/dist/mission-status.js +199 -27
- package/dist/model-preflight.d.ts +144 -0
- package/dist/model-preflight.js +323 -0
- package/dist/providers.d.ts +29 -3
- package/dist/providers.js +32 -11
- package/dist/queue-state.js +2 -2
- package/dist/records.d.ts +13 -4
- package/dist/records.js +11 -3
- package/dist/request-state.d.ts +9 -1
- package/dist/request-state.js +37 -1
- package/dist/revisions.d.ts +110 -0
- package/dist/revisions.js +245 -0
- package/dist/server.d.ts +7 -3
- package/dist/server.js +4 -1
- package/dist/tools.d.ts +13 -2
- package/dist/tools.js +299 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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:
|
|
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: `
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.1.14";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
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.
|
|
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",
|