@miadi/connector 0.1.21 → 0.1.22

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.
@@ -0,0 +1,267 @@
1
+ /**
2
+ * The report a drafted mission gets against the mission standard (jgwill/Miadi#939).
3
+ *
4
+ * It lists aspects to revise, each with why, the question to settle and who can
5
+ * settle it. It carries no overall verdict: in Episode 564's test a reviewer said
6
+ * `revise` on ten missions of ten, so a verdict says nothing. It advises and
7
+ * blocks nothing, a person decides whatever it says, and a failure here never
8
+ * refuses a request.
9
+ *
10
+ * What leaves the host: the mission's text and its delivery field, with ids,
11
+ * paths, hashes, URLs and repository names removed, to Jev when the connector
12
+ * holds a key. The tests that set the bars read text in the same form. Reports
13
+ * are kept beside the ledger, one line per record and version, so a person's
14
+ * decision on a version can be read against the aspects open on it.
15
+ */
16
+ import { join } from "node:path";
17
+ import { jevConfigOf } from "./providers.js";
18
+ import { jevAdapter } from "./model-preflight.js";
19
+ import { standardSource } from "./mission-standard.js";
20
+ import { versionOf } from "./revisions.js";
21
+ import { Ledger } from "./store.js";
22
+ /** Two rounds of report and revision, then the open aspects go on the card (D4). */
23
+ export const MAX_ROUNDS = 2;
24
+ const RUNNING_WINDOW_MS = 48 * 3600 * 1000;
25
+ const JEV_TIMEOUT_MS = 8000;
26
+ const ISSUE_REF = /^[\w.-]+\/[\w.-]+#\d+$/;
27
+ const EPISODE_REF = /^miadi-chronicle:/;
28
+ /** Ids, paths, hashes, URLs and repository names out of what goes to Jev. */
29
+ export function redactForJev(text) {
30
+ return String(text ?? "")
31
+ .replace(/https?:\/\/\S+/g, "[url]")
32
+ .replace(/\b(run_request|work_proposal|review_operation_request|session_steer_request|research_artifact|inquiry|observation|source_intent|session):[0-9a-f-]{6,}/gi, "[id]")
33
+ .replace(/\bmiadi-[a-z]+:\/*[\w./:-]+/gi, "[id]")
34
+ .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "[id]")
35
+ .replace(/\b[0-9a-f]{12,64}\b/gi, "[hash]")
36
+ .replace(/\b[\w.-]+\/[\w.-]+#\d+\b/g, "[issue]")
37
+ .replace(/#\d+\b/g, "[issue]")
38
+ .replace(/(?:^|(?<=[\s`"'(]))(?:\/|~\/|\.\/)[\w@.+-]+(?:\/[\w@.+-]*)*/g, "[path]")
39
+ .replace(/\b(?!and\/or\b)[\w.-]+\/[\w.-]+(?:\/[\w./-]*)?/g, "[path]")
40
+ .replace(/%\d+\b/g, "[pane]");
41
+ }
42
+ /** A delivery code can vouch for, with no model: an address, an issue, a uuid or a path of more than one folder. "Or" means a choice, which code cannot judge. */
43
+ export function namesExactPlace(delivery) {
44
+ if (/\bor\b/i.test(delivery))
45
+ return false;
46
+ return (/\bmiadi-[a-z]+:/.test(delivery) ||
47
+ /(^|\s)[\w.-]+\/[\w.-]+#\d+\b/.test(delivery) ||
48
+ /#\d+\b/.test(delivery) ||
49
+ /\bepisode\s+\d+/i.test(delivery) ||
50
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i.test(delivery) ||
51
+ /(^|\s)(\/|~\/|\.\/)[\w@.+-]+/.test(delivery) ||
52
+ /[\w@.+-]+\/[\w@.+-]+\/[\w@.+-]/.test(delivery));
53
+ }
54
+ const issuesOf = (r) => {
55
+ const primary = r.body.primary_issue;
56
+ return [...new Set([...(primary ? [primary] : []), ...(r.refs ?? []).filter((x) => ISSUE_REF.test(x))])];
57
+ };
58
+ /** Runs and proposals a person approved in the last 48 hours that have not finished. */
59
+ export function runningMissions(records, selfId, now = Date.now()) {
60
+ const out = [];
61
+ for (const r of records) {
62
+ if (r.id === selfId || (r.kind !== "run_request" && r.kind !== "work_proposal"))
63
+ continue;
64
+ if (r.approval_state !== "approved" || r.state === "completed" || r.state === "blocked")
65
+ continue;
66
+ // A mission carrying a private reading is read only by its owner and operators, and a report is read by others.
67
+ if (r.body.reading)
68
+ continue;
69
+ const decidedAt = r.completion_receipt?.decided_at;
70
+ if (!decidedAt || now - Date.parse(decidedAt) > RUNNING_WINDOW_MS)
71
+ continue;
72
+ const body = r.body;
73
+ out.push({
74
+ id: r.id,
75
+ ...(r.intention || body.title ? { intention: (r.intention ?? body.title).slice(0, 240) } : {}),
76
+ ...(body.primary_issue ? { primary_issue: body.primary_issue } : {}),
77
+ approved_at: decidedAt,
78
+ issues: issuesOf(r),
79
+ });
80
+ }
81
+ return out;
82
+ }
83
+ const finding = (a, checked_by, span) => ({
84
+ aspect: a.code,
85
+ name: a.name,
86
+ checked_by,
87
+ ...(span ? { span: span.slice(0, 300) } : {}),
88
+ why: a.why,
89
+ ask: a.ask,
90
+ ...(a.fix ? { fix: a.fix } : {}),
91
+ settled_by: a.settled_by,
92
+ });
93
+ /** The mission cut into sentences to choose among: Jev picks, it does not write (Episode 564, read gate F3). */
94
+ export function sentencesOf(text, limit = 40) {
95
+ return text
96
+ .split(/\n+|(?<=[.!?;])\s+/)
97
+ .map((s) => s.trim())
98
+ .filter((s) => s.length >= 20)
99
+ .slice(0, limit);
100
+ }
101
+ const missionText = (r) => {
102
+ const b = r.body;
103
+ return b.task ?? [b.title, b.desired_outcome].filter(Boolean).join("\n\n");
104
+ };
105
+ function nextAction(r, version, needs) {
106
+ const ours = needs.filter((f) => f.settled_by !== "person");
107
+ if (version > MAX_ROUNDS)
108
+ return `This mission has had ${MAX_ROUNDS} rounds of review. Call show_decisions: what is still open goes on the person's card. Revise again only if the person asks.`;
109
+ if (ours.length)
110
+ return `For each ask: answer it from your conversation with the person, or put it to them, and do what its fix says. Then call revise_pending_request with request_id ${r.id} and expected_version ${version}, and show_decisions. If the person wants it as it stands, call show_decisions: what is open goes on their card.`;
111
+ if (needs.length)
112
+ return "Call show_decisions: only the person can answer what is open, and it goes on their card.";
113
+ return "Nothing was flagged on the aspects checked. That is not a verdict: not_checked lists what this report did not judge. Call show_decisions.";
114
+ }
115
+ /** One report for one version of one mission. Jev's failure leaves the code's findings and names what went unchecked. */
116
+ export async function reviewDraft({ record, records, standard, adapter, now = Date.now() }) {
117
+ const version = versionOf(record);
118
+ const body = record.body;
119
+ const delivery = typeof body.delivery === "string" ? body.delivery.trim() : "";
120
+ const personWords = typeof body.person_words === "string" ? body.person_words.trim() : "";
121
+ const text = missionText(record);
122
+ const aspect = (code) => standard.aspects.find((a) => a.code === code);
123
+ const handledInCode = new Set(["S1", "S3", "S6"]);
124
+ // What goes to Jev: the delivery on its own when code cannot vouch for it, the mission for everything else.
125
+ const s1 = aspect("S1");
126
+ const askDelivery = Boolean(s1?.jev && delivery && !namesExactPlace(delivery));
127
+ const onTask = standard.aspects.filter((a) => a.jev && !handledInCode.has(a.code) && !(a.code === "S5" && personWords));
128
+ const asks = [];
129
+ const paid = [];
130
+ let jev = adapter ? { state: "ran", model: standard.jev_model, uncalibrated: true } : { state: "unconfigured" };
131
+ const decide = async (state, list) => {
132
+ const questions = Object.fromEntries(list.map((a) => [a.jev.id, { type: "noul", instructions: a.jev.instructions, criteria: a.jev.criteria }]));
133
+ const answer = await adapter.decide({ state, model: standard.jev_model, questions });
134
+ const tokens = Number(answer.billing?.paid_input_tokens_used);
135
+ if (Number.isFinite(tokens))
136
+ paid.push(tokens);
137
+ return answer.answers;
138
+ };
139
+ if (adapter) {
140
+ if (askDelivery)
141
+ asks.push(decide({ delivery: redactForJev(delivery) }, [s1]));
142
+ if (onTask.length)
143
+ asks.push(decide({ task: redactForJev(text) }, onTask));
144
+ }
145
+ const settled = await Promise.allSettled(asks);
146
+ const answers = Object.assign({}, ...settled.map((s) => (s.status === "fulfilled" ? s.value : {})));
147
+ const failed = settled.find((s) => s.status === "rejected");
148
+ if (failed)
149
+ jev = { state: "unavailable", message: failed.reason instanceof Error ? failed.reason.message : String(failed.reason) };
150
+ if (paid.length)
151
+ jev.paid_input_tokens = paid.reduce((a, b) => a + b, 0);
152
+ const scoreOf = (id) => {
153
+ const v = answers[id]?.noul;
154
+ return typeof v === "number" && v >= 0 && v <= 1 ? v : null;
155
+ };
156
+ const needs = [];
157
+ const notChecked = [];
158
+ const running = runningMissions(records, record.id, now);
159
+ for (const a of standard.aspects) {
160
+ if (a.code === "S1") {
161
+ if (!delivery)
162
+ needs.push(finding(a, "code"));
163
+ else if (askDelivery) {
164
+ const p = scoreOf(a.jev.id);
165
+ if (p === null)
166
+ notChecked.push({ aspect: a.code, name: a.name, reason: "Jev did not answer, and code cannot vouch for this delivery." });
167
+ else if (p < a.jev.bar)
168
+ needs.push(finding(a, `jev ${p.toFixed(2)}`, delivery));
169
+ }
170
+ }
171
+ else if (a.code === "S3") {
172
+ const mine = issuesOf(record);
173
+ const shared = running.filter((m) => m.issues.some((i) => mine.includes(i)) && !text.includes(m.id.split(":")[1]?.slice(0, 8) ?? m.id));
174
+ if (shared.length)
175
+ needs.push({ ...finding(a, "code"), note: `Running on the same issue: ${shared.map((m) => `${m.id.replace(/^([a-z_]+:[0-9a-f]{8}).*$/, "$1")}${m.intention ? ` (${m.intention.slice(0, 90)})` : ""}`).join(", ")}.` });
176
+ }
177
+ else if (a.code === "S6") {
178
+ const refs = record.refs ?? [];
179
+ const issues = refs.filter((x) => ISSUE_REF.test(x));
180
+ const declared = Boolean(record.body.primary_issue);
181
+ if ((!issues.length && !declared && !refs.some((x) => EPISODE_REF.test(x))) || (issues.length > 1 && !declared))
182
+ needs.push(finding(a, "code"));
183
+ }
184
+ else if (a.jev) {
185
+ if (a.code === "S5" && personWords)
186
+ continue;
187
+ const p = scoreOf(a.jev.id);
188
+ if (p === null)
189
+ notChecked.push({ aspect: a.code, name: a.name, reason: adapter ? "Jev did not answer." : "The connector holds no Jev key." });
190
+ else if (a.jev.fires === "above" ? p >= a.jev.bar : p < a.jev.bar)
191
+ needs.push(finding(a, `jev ${p.toFixed(2)}`));
192
+ }
193
+ else if (!a.not_checked) {
194
+ notChecked.push({ aspect: a.code, name: a.name, reason: "This version of the connector runs no check for it." });
195
+ }
196
+ if (a.not_checked)
197
+ notChecked.push({ aspect: a.code, name: a.name, reason: a.not_checked });
198
+ }
199
+ // For a finding Jev raised on the mission, the sentence it is about. A failure leaves the finding without one.
200
+ const raised = needs.flatMap((f) => {
201
+ const a = aspect(f.aspect);
202
+ return a?.jev && !handledInCode.has(a.code) ? [{ f, a }] : [];
203
+ });
204
+ const sentences = adapter && raised.length ? sentencesOf(text) : [];
205
+ if (sentences.length > 1) {
206
+ const criteria = Object.fromEntries(sentences.map((s, i) => [`s${i + 1}`, redactForJev(s).slice(0, 300)]));
207
+ const questions = Object.fromEntries(raised.map(({ a }) => [`${a.jev.id}_where`, { type: "choice", instructions: `Which sentence of \`task\` is the clearest case of this: ${a.jev.criteria.true}`, criteria }]));
208
+ try {
209
+ const answer = await adapter.decide({ state: { task: redactForJev(text) }, model: standard.jev_model, questions });
210
+ const tokens = Number(answer.billing?.paid_input_tokens_used);
211
+ if (Number.isFinite(tokens))
212
+ jev.paid_input_tokens = (jev.paid_input_tokens ?? 0) + tokens;
213
+ for (const { f, a } of raised) {
214
+ const pick = answer.answers[`${a.jev.id}_where`]?.choice;
215
+ const index = typeof pick === "string" && /^s\d+$/.test(pick) ? Number(pick.slice(1)) - 1 : -1;
216
+ if (sentences[index])
217
+ f.span = sentences[index].slice(0, 300);
218
+ }
219
+ }
220
+ catch {
221
+ /* the finding stands without its sentence */
222
+ }
223
+ }
224
+ return {
225
+ id: reportId(record.id, version),
226
+ record_id: record.id,
227
+ version,
228
+ at: new Date(now).toISOString(),
229
+ standard: { name: standard.name, version: standard.version, source: standard.source, ...(standard.note ? { note: standard.note } : {}) },
230
+ needs_revision: needs,
231
+ not_checked: notChecked,
232
+ running_missions: running.map(({ issues: _issues, ...m }) => m),
233
+ preferences: standard.preferences[record.actor.person_id] ?? [],
234
+ round: Math.min(version, MAX_ROUNDS + 1),
235
+ jev,
236
+ next_action: nextAction(record, version, needs),
237
+ };
238
+ }
239
+ export const reportId = (recordId, version) => `${recordId}@${version}`;
240
+ export function draftReviewService(o) {
241
+ const env = o.env ?? process.env;
242
+ const fetchImpl = o.fetchImpl ?? fetch;
243
+ const reports = new Ledger(join(o.stateDir, "draft-reviews.jsonl"));
244
+ const source = o.source ?? standardSource(env, fetchImpl);
245
+ const config = jevConfigOf(env);
246
+ const adapter = o.adapter ?? (config ? jevAdapter(config, fetchImpl, JEV_TIMEOUT_MS) : undefined);
247
+ const stored = (record) => {
248
+ const id = reportId(record.id, versionOf(record));
249
+ return reports.all().find((r) => r.id === id);
250
+ };
251
+ return {
252
+ stored,
253
+ standard: () => source.get(),
254
+ jevConfigured: Boolean(adapter),
255
+ async review(record, records) {
256
+ try {
257
+ const prior = stored(record);
258
+ if (prior)
259
+ return prior;
260
+ return reports.append(await reviewDraft({ record, records, standard: await source.get(), adapter }));
261
+ }
262
+ catch (error) {
263
+ return { state: "unavailable", message: `The report could not be made (${error instanceof Error ? error.message : String(error)}). The request is recorded and awaits its decision as usual.` };
264
+ }
265
+ },
266
+ };
267
+ }
package/dist/index.d.ts CHANGED
@@ -11,4 +11,6 @@ export { PROVIDERS, discoverProviders, jevConfigOf, resolveProvider, routerPrefl
11
11
  export { missionStage, missionStatus, primaryIssueOf, resolveMissionSubject, type MissionIdentity, type MissionProvider, type MissionSession, type MissionStatus, type MissionStatusDeps, } from "./mission-status.js";
12
12
  export { modelPreflight, candidatesFor, buildJevRequest, decisionState, jevAdapter, reviewFloor, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, JEV_CONFIDENCE_FLOOR, type ModelPreflight, type ModelCandidate, type JevAdapter, type JevRequest, } from "./model-preflight.js";
13
13
  export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, composingText, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, type SteerDeps, type SteerResult, type SteerStatus, type SteeringGrant, type TmuxLike, } from "./steer.js";
14
+ export { reviewDraft, redactForJev, namesExactPlace, runningMissions, draftReviewService, MAX_ROUNDS, type DraftReview, type DraftReviewService, type Finding } from "./draft-review.js";
15
+ export { BUILT_IN_STANDARD, parseStandard, standardSource, STANDARD_NAME, type MissionStandard, type StandardAspect, type StandardSource } from "./mission-standard.js";
14
16
  export { VERSION } from "./version.js";
package/dist/index.js CHANGED
@@ -11,4 +11,6 @@ export { PROVIDERS, discoverProviders, jevConfigOf, resolveProvider, routerPrefl
11
11
  export { missionStage, missionStatus, primaryIssueOf, resolveMissionSubject, } from "./mission-status.js";
12
12
  export { modelPreflight, candidatesFor, buildJevRequest, decisionState, jevAdapter, reviewFloor, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, JEV_CONFIDENCE_FLOOR, } from "./model-preflight.js";
13
13
  export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, composingText, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, } from "./steer.js";
14
+ export { reviewDraft, redactForJev, namesExactPlace, runningMissions, draftReviewService, MAX_ROUNDS } from "./draft-review.js";
15
+ export { BUILT_IN_STANDARD, parseStandard, standardSource, STANDARD_NAME } from "./mission-standard.js";
14
16
  export { VERSION } from "./version.js";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The mission standard (jgwill/Miadi#939): what a drafted mission owes before a
3
+ * person decides on it, as aspects with the words a report returns.
4
+ *
5
+ * The standard is a document, so it changes without a connector release: a text
6
+ * prompt named `mission-standard`, label `production`, in the prompt store's
7
+ * Engineering domain, holding JSON in the shape below. The built-in copy is what
8
+ * runs when the store has no keys, cannot be reached or answers outside the
9
+ * contract, and a report says which one it used. A person's standing preferences
10
+ * live only in the store: this package is published.
11
+ *
12
+ * The bars are uncalibrated. They were read from the tests in Episode 564
13
+ * (`mission-draft-review-261105/`) and move with a person's rulings.
14
+ */
15
+ export declare const STANDARD_NAME = "mission-standard";
16
+ /** Who can settle an aspect: the agent holding the conversation, only the person, or a local agent reading the running system. */
17
+ export type SettledBy = "drafter" | "person" | "local_agent";
18
+ export interface JevCheck {
19
+ id: string;
20
+ instructions: string;
21
+ criteria: {
22
+ true: string;
23
+ false: string;
24
+ };
25
+ /** A finding comes from a probability above or below the bar. */
26
+ fires: "above" | "below";
27
+ bar: number;
28
+ }
29
+ export interface StandardAspect {
30
+ code: string;
31
+ name: string;
32
+ requires: string;
33
+ why: string;
34
+ /** The question to settle, in words the person deciding can read. */
35
+ ask: string;
36
+ /** What the drafting agent does with the answer. It goes to that agent and not on the card. */
37
+ fix?: string;
38
+ settled_by: SettledBy;
39
+ jev?: JevCheck;
40
+ /** Why this version of the connector does not check it, or checks only part of it. */
41
+ not_checked?: string;
42
+ }
43
+ export interface MissionStandard {
44
+ name: string;
45
+ version: number;
46
+ source: "built_in" | "prompt_store";
47
+ /** What happened to the prompt store, when the built-in copy or an older read is in use. */
48
+ note?: string;
49
+ jev_model: string;
50
+ aspects: StandardAspect[];
51
+ /** Standing preferences by person id, returned to the agent drafting for that person. */
52
+ preferences: Record<string, string[]>;
53
+ }
54
+ export declare const BUILT_IN_STANDARD: MissionStandard;
55
+ /** The stored document, or an error saying what is outside the contract. A model alias is refused: one pinned version per standard. */
56
+ export declare function parseStandard(text: string, version: number): MissionStandard;
57
+ export interface StandardSource {
58
+ get(): Promise<MissionStandard>;
59
+ }
60
+ /**
61
+ * Reads the standard from the prompt store, once per ten minutes. A failed read
62
+ * keeps the last good version and says so, and with no good version the built-in
63
+ * copy answers and says why: a report never hides which standard judged the mission.
64
+ */
65
+ export declare function standardSource(env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch, now?: () => number): StandardSource;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The mission standard (jgwill/Miadi#939): what a drafted mission owes before a
3
+ * person decides on it, as aspects with the words a report returns.
4
+ *
5
+ * The standard is a document, so it changes without a connector release: a text
6
+ * prompt named `mission-standard`, label `production`, in the prompt store's
7
+ * Engineering domain, holding JSON in the shape below. The built-in copy is what
8
+ * runs when the store has no keys, cannot be reached or answers outside the
9
+ * contract, and a report says which one it used. A person's standing preferences
10
+ * live only in the store: this package is published.
11
+ *
12
+ * The bars are uncalibrated. They were read from the tests in Episode 564
13
+ * (`mission-draft-review-261105/`) and move with a person's rulings.
14
+ */
15
+ export const STANDARD_NAME = "mission-standard";
16
+ const CACHE_MS = 10 * 60 * 1000;
17
+ const STORE_TIMEOUT_MS = 3000;
18
+ const DEFAULT_STORE_URL = "https://cloud.langfuse.com";
19
+ export const BUILT_IN_STANDARD = {
20
+ name: STANDARD_NAME,
21
+ version: 1,
22
+ source: "built_in",
23
+ jev_model: "jev-1.14.0",
24
+ preferences: {},
25
+ aspects: [
26
+ {
27
+ code: "S1",
28
+ name: "where the result lands",
29
+ requires: "One exact place the result lands: a path, a branch, a pull request, an issue, a review or an episode, in the delivery field.",
30
+ why: "Without one exact place the worker chooses, and the result may land where the person cannot open it.",
31
+ ask: "Which exact place receives the result?",
32
+ fix: "Put it in the delivery field.",
33
+ settled_by: "drafter",
34
+ jev: {
35
+ id: "delivery_exact",
36
+ instructions: "Does `delivery` name one exact place the result lands (a path, a branch, a pull request, an issue, a review or an episode)? A bracketed placeholder such as [path] or [issue] stands for a real one.",
37
+ criteria: { true: "It names one exact place.", false: "It names a kind of place, or a choice among places, or no place." },
38
+ fires: "below",
39
+ bar: 0.3,
40
+ },
41
+ },
42
+ {
43
+ code: "S2",
44
+ name: "who may release, deploy or start a session",
45
+ requires: "Each act that reaches production or starts a session names who performs it and what authorizes it.",
46
+ why: "A worker reads an unstated permission as its own, or stops at a step nobody can perform.",
47
+ ask: "Who performs this act, and does approving this mission authorize it?",
48
+ settled_by: "person",
49
+ not_checked: "The local reviewer checks this aspect, and it does not run yet (jgwill/Miadi#939 A3).",
50
+ },
51
+ {
52
+ code: "S3",
53
+ name: "running work on the same ground",
54
+ requires: "The mission says whether it waits for running work on its ground, builds on it or keeps clear of it.",
55
+ why: "Two missions on one issue overwrite or undo each other, and the person is left to notice.",
56
+ ask: "Does this mission wait for that work, build on it or keep clear of it?",
57
+ fix: "Say which in the mission.",
58
+ settled_by: "drafter",
59
+ not_checked: "Only shared issues are compared. The local reviewer judges other overlap, and it does not run yet (jgwill/Miadi#939 A3).",
60
+ },
61
+ {
62
+ code: "S4",
63
+ name: "a mechanism the mission dictates",
64
+ requires: "The end result, unless the person asked for the mechanism.",
65
+ why: "A dictated mechanism stops the worker from finding that the work belongs elsewhere.",
66
+ ask: "Was this mechanism asked for?",
67
+ fix: "Quote the person in person_words, or state the result instead.",
68
+ settled_by: "drafter",
69
+ jev: {
70
+ id: "names_mechanism",
71
+ instructions: "Does `task` tell the worker which mechanism to build (named components, queues, watchers, storage, workers)?",
72
+ criteria: { true: "It dictates the mechanism.", false: "It leaves the mechanism to the worker." },
73
+ fires: "above",
74
+ bar: 0.8,
75
+ },
76
+ },
77
+ {
78
+ code: "S5",
79
+ name: "the person's words",
80
+ requires: "The person's own words for any rule, correction or decision credited to them, in person_words.",
81
+ why: "Only this conversation holds what the person said, and a misstated rule carries into the work as if it were their decision.",
82
+ ask: "What was said, word for word?",
83
+ fix: "Put it in person_words.",
84
+ settled_by: "drafter",
85
+ jev: {
86
+ id: "credits_person",
87
+ instructions: "Does `task` present a rule, correction or decision as coming from the person who asked (the author, the creator, the requester or a named person)?",
88
+ criteria: { true: "It credits a rule, correction or decision to the person.", false: "It credits nothing to the person." },
89
+ fires: "above",
90
+ bar: 0.8,
91
+ },
92
+ },
93
+ {
94
+ code: "S6",
95
+ name: "what holds the work",
96
+ requires: "An episode or an issue that holds the work, and primary_issue when several issues are named.",
97
+ why: "Without a holder the result has no home, and mission_status has to guess which issue the mission implements.",
98
+ ask: "Which episode or issue holds this work, and which one issue does it implement?",
99
+ fix: "Name them in refs and primary_issue.",
100
+ settled_by: "drafter",
101
+ },
102
+ ],
103
+ };
104
+ const isText = (v) => typeof v === "string" && v.trim().length > 0;
105
+ function checkOf(raw) {
106
+ if (raw === undefined)
107
+ return undefined;
108
+ if (!isText(raw?.id) || !isText(raw?.instructions) || !isText(raw?.criteria?.true) || !isText(raw?.criteria?.false))
109
+ throw new Error("a Jev check needs id, instructions and both criteria");
110
+ if (raw.fires !== "above" && raw.fires !== "below")
111
+ throw new Error(`check ${raw.id}: fires must be above or below`);
112
+ if (typeof raw.bar !== "number" || raw.bar <= 0 || raw.bar >= 1)
113
+ throw new Error(`check ${raw.id}: bar must be between 0 and 1`);
114
+ return { id: raw.id, instructions: raw.instructions, criteria: { true: raw.criteria.true, false: raw.criteria.false }, fires: raw.fires, bar: raw.bar };
115
+ }
116
+ /** The stored document, or an error saying what is outside the contract. A model alias is refused: one pinned version per standard. */
117
+ export function parseStandard(text, version) {
118
+ const raw = JSON.parse(text);
119
+ if (!isText(raw?.jev_model) || /^jev-(latest|preview)$/.test(raw.jev_model))
120
+ throw new Error("jev_model must be a pinned version, never an alias");
121
+ if (!Array.isArray(raw?.aspects) || raw.aspects.length === 0)
122
+ throw new Error("aspects must be a list");
123
+ const aspects = raw.aspects.map((a) => {
124
+ for (const k of ["code", "name", "requires", "why", "ask"])
125
+ if (!isText(a?.[k]))
126
+ throw new Error(`an aspect needs ${k}`);
127
+ if (!["drafter", "person", "local_agent"].includes(a.settled_by))
128
+ throw new Error(`aspect ${a.code}: settled_by is drafter, person or local_agent`);
129
+ const jev = checkOf(a.jev);
130
+ return { code: a.code, name: a.name, requires: a.requires, why: a.why, ask: a.ask, ...(isText(a.fix) ? { fix: a.fix } : {}), settled_by: a.settled_by, ...(jev ? { jev } : {}), ...(isText(a.not_checked) ? { not_checked: a.not_checked } : {}) };
131
+ });
132
+ const preferences = {};
133
+ for (const [person, list] of Object.entries(raw.preferences ?? {})) {
134
+ if (!Array.isArray(list) || !list.every(isText))
135
+ throw new Error(`preferences of ${person} must be a list of text`);
136
+ preferences[person] = list;
137
+ }
138
+ return { name: STANDARD_NAME, version, source: "prompt_store", jev_model: raw.jev_model, aspects, preferences };
139
+ }
140
+ /**
141
+ * Reads the standard from the prompt store, once per ten minutes. A failed read
142
+ * keeps the last good version and says so, and with no good version the built-in
143
+ * copy answers and says why: a report never hides which standard judged the mission.
144
+ */
145
+ export function standardSource(env = process.env, fetchImpl = fetch, now = Date.now) {
146
+ let good = null;
147
+ const publicKey = env.ENG_LANGFUSE_PUBLIC_KEY;
148
+ const secretKey = env.ENG_LANGFUSE_SECRET_KEY;
149
+ const base = (env.ENG_LANGFUSE_BASE_URL || DEFAULT_STORE_URL).replace(/\/+$/, "");
150
+ return {
151
+ async get() {
152
+ if (!publicKey || !secretKey)
153
+ return { ...BUILT_IN_STANDARD, note: "The connector holds no prompt store keys." };
154
+ if (good && now() - good.at < CACHE_MS)
155
+ return good.standard;
156
+ try {
157
+ const res = await fetchImpl(`${base}/api/public/v2/prompts/${STANDARD_NAME}?label=production`, {
158
+ headers: { authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`, accept: "application/json" },
159
+ signal: AbortSignal.timeout(STORE_TIMEOUT_MS),
160
+ });
161
+ if (!res.ok)
162
+ throw new Error(`the prompt store answered ${res.status}`);
163
+ const body = await res.json();
164
+ if (typeof body?.prompt !== "string" || typeof body?.version !== "number")
165
+ throw new Error("the prompt store's answer holds no text prompt");
166
+ const standard = parseStandard(body.prompt, body.version);
167
+ good = { at: now(), standard };
168
+ return standard;
169
+ }
170
+ catch (error) {
171
+ const why = error instanceof Error ? error.message : String(error);
172
+ if (good)
173
+ return { ...good.standard, note: `Read earlier: the last read failed (${why}).` };
174
+ return { ...BUILT_IN_STANDARD, note: `The prompt store did not answer with a usable standard (${why}).` };
175
+ }
176
+ },
177
+ };
178
+ }
@@ -113,7 +113,7 @@ export interface JevRequest {
113
113
  state: Record<string, unknown>;
114
114
  model: string;
115
115
  questions: Record<string, {
116
- type: "choice";
116
+ type: "choice" | "noul";
117
117
  instructions: string;
118
118
  criteria: Record<string, string>;
119
119
  }>;
package/dist/server.d.ts CHANGED
@@ -18,6 +18,7 @@ import type { ConnectorConfig } from "./config.js";
18
18
  import type { TmuxLike } from "./steer.js";
19
19
  import type { DiscoveryProbe, ProviderId } from "./providers.js";
20
20
  import type { JevAdapter } from "./model-preflight.js";
21
+ import type { StandardSource } from "./mission-standard.js";
21
22
  export interface ConnectorDeps {
22
23
  identify: (token: string) => Promise<MiadiPerson | null>;
23
24
  fetchImpl?: typeof fetch;
@@ -31,6 +32,10 @@ export interface ConnectorDeps {
31
32
  /** A provider's configured default model and a Jev decider (jgwill/Miadi#913); tests inject fakes. */
32
33
  modelOf?: (provider: ProviderId) => string | null;
33
34
  jevAdapter?: JevAdapter;
35
+ /** The environment the draft review reads its keys from (default process.env); tests pass their own (jgwill/Miadi#939). */
36
+ draftReviewEnv?: NodeJS.ProcessEnv;
37
+ /** Where the mission standard comes from (default the prompt store, then the built-in copy); tests pass their own. */
38
+ standardSource?: StandardSource;
34
39
  /** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
35
40
  steerSettleMs?: number;
36
41
  /** Where sessions and their steering inboxes live (jgwill/Miadi#930). */
@@ -45,4 +50,4 @@ export declare function createConnectorApp(config: ConnectorConfig, deps: Connec
45
50
  provider: MiadiOAuthProvider;
46
51
  ledger: Ledger<ConnectorRecord>;
47
52
  };
48
- export declare const INSTRUCTIONS = "Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).\n\nStart with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.\n\nEvery tool is offered on every connection. A client that loads tools on demand shows only those its last tool search matched: a tool you do not see is not missing. Search for it by its exact name, get_capabilities first, before telling the person a tool is absent. The main reads are get_capabilities, get_context, search, fetch, list_records, list_sessions, get_session, review_lookup, read_episode_file and show_decisions.\n\nRules every tool keeps:\n- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.\n- Requests (runs, work proposals, review operations, Deep Diver, review-to-episode links, inquiry-to-episode weaves, session completion) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person decides. After making a request, call show_decisions: it shows the request as a card in the conversation, where the person decides with their own click (or follows its approval page). Never claim a decision the card has not reported. The person may approve, approve with changes (their instructions are carried into what runs), send it back for revision with instructions (approval_state revision_requested; read the instructions with get_run_result and revise it in place with revise_pending_request), or decline.\n- A mission is revised, never re-requested: to change a run or work proposal that still awaits a decision, call revise_pending_request with its id and the version you read (expected_version). It keeps one id, one approval page and one card; get_request_history shows every version. A new request_authorized_run or propose_work is for a new mission only. Requests made earlier as separate versions are joined with supersede_pending_requests, naming their exact ids, only when the person names or confirms them.\n- A research report (a Deep Research report) is delivered with save_research exactly as written, never summarised or shortened, with its sources; a long report goes in parts. Check the bytes and first and last lines the answer returns against what you sent.\n- An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.\n- Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.";
53
+ export declare const INSTRUCTIONS = "Miadi is a relational software factory: agent sessions, Miadi reviews of screenwalks, Chronicle episodes, talking circles and the five accountable circles (Story, Stewardship, Engineering, Evaluation, Continuity) across three domains (Creative, Ceremonial, Engineering).\n\nStart with get_capabilities: it says who you are signed in as, which tools are live, and which return not_implemented.\n\nEvery tool is offered on every connection. A client that loads tools on demand shows only those its last tool search matched: a tool you do not see is not missing. Search for it by its exact name, get_capabilities first, before telling the person a tool is absent. The main reads are get_capabilities, get_context, search, fetch, list_records, list_sessions, get_session, review_lookup, read_episode_file and show_decisions.\n\nRules every tool keeps:\n- Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.\n- Requests (runs, work proposals, review operations, Deep Diver, review-to-episode links, inquiry-to-episode weaves, session completion) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person decides. After making a request, call show_decisions: it shows the request as a card in the conversation, where the person decides with their own click (or follows its approval page). Never claim a decision the card has not reported. The person may approve, approve with changes (their instructions are carried into what runs), send it back for revision with instructions (approval_state revision_requested; read the instructions with get_run_result and revise it in place with revise_pending_request), or decline.\n- A new or revised run or work proposal answers with draft_review: the aspects of the mission standard to revise, each with why, the question to settle and who can settle it. Read it before show_decisions. It carries no verdict, it advises and blocks nothing, and next_action says what to do. Answer what your conversation holds, ask the person the rest, then revise. preferences lists the person's standing preferences for you to apply. Fill delivery with the one exact place the result lands, and person_words with the person's own words, verbatim, for anything credited to them.\n- A mission is revised, never re-requested: to change a run or work proposal that still awaits a decision, call revise_pending_request with its id and the version you read (expected_version). It keeps one id, one approval page and one card; get_request_history shows every version. A new request_authorized_run or propose_work is for a new mission only. Requests made earlier as separate versions are joined with supersede_pending_requests, naming their exact ids, only when the person names or confirms them.\n- A research report (a Deep Research report) is delivered with save_research exactly as written, never summarised or shortened, with its sources; a long report goes in parts. Check the bytes and first and last lines the answer returns against what you sent.\n- An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.\n- Use the miadi-review:<uuid>, miadi-chronicle://<n>, owner/repo#n and session:<uuid> reference forms.";
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ import { JsonFile, Ledger, ensureStateDir, statePaths } from "./store.js";
11
11
  import { registerTools } from "./tools.js";
12
12
  import { MiadiClient } from "./miadi.js";
13
13
  import { ReviewClient } from "./reviews.js";
14
+ import { draftReviewService } from "./draft-review.js";
14
15
  import { VERSION } from "./version.js";
15
16
  export function createConnectorApp(config, deps) {
16
17
  ensureStateDir(config.stateDir);
@@ -66,8 +67,10 @@ export function createConnectorApp(config, deps) {
66
67
  app.post("/authorize/confirm", confirmLimit, express.urlencoded({ extended: false, limit: "16kb" }), (req, res, next) => {
67
68
  provider.confirm(req, res).catch(next);
68
69
  });
70
+ const draftReview = draftReviewService({ stateDir: config.stateDir, fetchImpl: deps.fetchImpl, adapter: deps.jevAdapter, env: deps.draftReviewEnv, source: deps.standardSource });
69
71
  const approvalDeps = {
70
72
  config,
73
+ draftReview,
71
74
  ledger,
72
75
  miadi,
73
76
  identify: deps.identify,
@@ -116,6 +119,7 @@ export function createConnectorApp(config, deps) {
116
119
  providersProbe: deps.providersProbe,
117
120
  modelOf: deps.modelOf,
118
121
  jevAdapter: deps.jevAdapter,
122
+ draftReview,
119
123
  steerSettleMs: deps.steerSettleMs,
120
124
  ...(deps.steeringRuntime ? { steeringRuntime: deps.steeringRuntime } : {}),
121
125
  };
@@ -155,6 +159,7 @@ Every tool is offered on every connection. A client that loads tools on demand s
155
159
  Rules every tool keeps:
156
160
  - Every record names the person who made it, with a claim category: observed, hypothesis, desired_future, fiction or verified_implementation. Keep fiction and desired futures apart from what was observed.
157
161
  - Requests (runs, work proposals, review operations, Deep Diver, review-to-episode links, inquiry-to-episode weaves, session completion) are recorded as awaiting_approval. Nothing runs, publishes or closes until a person decides. After making a request, call show_decisions: it shows the request as a card in the conversation, where the person decides with their own click (or follows its approval page). Never claim a decision the card has not reported. The person may approve, approve with changes (their instructions are carried into what runs), send it back for revision with instructions (approval_state revision_requested; read the instructions with get_run_result and revise it in place with revise_pending_request), or decline.
162
+ - A new or revised run or work proposal answers with draft_review: the aspects of the mission standard to revise, each with why, the question to settle and who can settle it. Read it before show_decisions. It carries no verdict, it advises and blocks nothing, and next_action says what to do. Answer what your conversation holds, ask the person the rest, then revise. preferences lists the person's standing preferences for you to apply. Fill delivery with the one exact place the result lands, and person_words with the person's own words, verbatim, for anything credited to them.
158
163
  - A mission is revised, never re-requested: to change a run or work proposal that still awaits a decision, call revise_pending_request with its id and the version you read (expected_version). It keeps one id, one approval page and one card; get_request_history shows every version. A new request_authorized_run or propose_work is for a new mission only. Requests made earlier as separate versions are joined with supersede_pending_requests, naming their exact ids, only when the person names or confirms them.
159
164
  - A research report (a Deep Research report) is delivered with save_research exactly as written, never summarised or shortened, with its sources; a long report goes in parts. Check the bytes and first and last lines the answer returns against what you sent.
160
165
  - An unknown id is answered as not found, never invented. A missing backing service is answered as unavailable.
package/dist/tools.d.ts CHANGED
@@ -8,6 +8,7 @@ import { type ConnectorRecord } from "./records.js";
8
8
  import { type ApprovalDeps } from "./approve.js";
9
9
  import { type DecisionTokens } from "./decisions.js";
10
10
  import { type DiscoveryProbe, type ProviderId } from "./providers.js";
11
+ import type { DraftReviewService } from "./draft-review.js";
11
12
  import { type JevAdapter } from "./model-preflight.js";
12
13
  import { type RuntimeDeps } from "./runtime-steer.js";
13
14
  import { type TmuxLike } from "./steer.js";
@@ -34,6 +35,8 @@ export interface ToolContext {
34
35
  modelOf?: (provider: ProviderId) => string | null;
35
36
  /** A Jev decider; tests inject a synthetic fixture. Absent, the connector's env decides (jgwill/Miadi#913). */
36
37
  jevAdapter?: JevAdapter;
38
+ /** The report a drafted mission gets against the mission standard (jgwill/Miadi#939). */
39
+ draftReview?: DraftReviewService;
37
40
  /** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
38
41
  steerSettleMs?: number;
39
42
  }