@miadi/connector 0.1.20 → 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.
- package/README.md +58 -4
- package/dist/approve.d.ts +11 -1
- package/dist/approve.js +97 -9
- package/dist/bin.js +3 -0
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +96 -3
- package/dist/draft-review.d.ts +96 -0
- package/dist/draft-review.js +267 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mission-standard.d.ts +65 -0
- package/dist/mission-standard.js +178 -0
- package/dist/mission-status.d.ts +13 -1
- package/dist/mission-status.js +10 -4
- package/dist/model-preflight.d.ts +1 -1
- package/dist/records.d.ts +1 -1
- package/dist/records.js +2 -0
- package/dist/request-state.js +6 -1
- package/dist/research-digest.d.ts +30 -0
- package/dist/research-digest.js +104 -0
- package/dist/research-shelf.d.ts +173 -0
- package/dist/research-shelf.js +297 -0
- package/dist/runtime-steer.d.ts +182 -0
- package/dist/runtime-steer.js +543 -0
- package/dist/server.d.ts +20 -1
- package/dist/server.js +7 -11
- package/dist/steer.d.ts +21 -7
- package/dist/steer.js +115 -10
- package/dist/tools.d.ts +32 -1
- package/dist/tools.js +326 -46
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type JevAdapter } from "./model-preflight.js";
|
|
2
|
+
import { type MissionStandard, type SettledBy, type StandardSource } from "./mission-standard.js";
|
|
3
|
+
import type { ConnectorRecord } from "./records.js";
|
|
4
|
+
/** Two rounds of report and revision, then the open aspects go on the card (D4). */
|
|
5
|
+
export declare const MAX_ROUNDS = 2;
|
|
6
|
+
export interface Finding {
|
|
7
|
+
aspect: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** "code", or "jev 0.17" with the number that raised it. */
|
|
10
|
+
checked_by: string;
|
|
11
|
+
/** The words in the mission this is about. Jev picks the sentence for a finding it raised. */
|
|
12
|
+
span?: string;
|
|
13
|
+
/** A fact the check found, such as the mission running on the same issue. */
|
|
14
|
+
note?: string;
|
|
15
|
+
why: string;
|
|
16
|
+
ask: string;
|
|
17
|
+
/** What the drafting agent does with the answer. Not shown to the person deciding. */
|
|
18
|
+
fix?: string;
|
|
19
|
+
settled_by: SettledBy;
|
|
20
|
+
}
|
|
21
|
+
/** What a requester may read of another person's mission: as list_records shows it. A session names a seat, and it stays out (jgwill/Miadi#922 R2). */
|
|
22
|
+
export interface RunningMission {
|
|
23
|
+
id: string;
|
|
24
|
+
intention?: string;
|
|
25
|
+
primary_issue?: string;
|
|
26
|
+
approved_at?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DraftReview {
|
|
29
|
+
/** The ledger's key: one report per record and version. */
|
|
30
|
+
id: string;
|
|
31
|
+
record_id: string;
|
|
32
|
+
version: number;
|
|
33
|
+
at: string;
|
|
34
|
+
standard: {
|
|
35
|
+
name: string;
|
|
36
|
+
version: number;
|
|
37
|
+
source: MissionStandard["source"];
|
|
38
|
+
note?: string;
|
|
39
|
+
};
|
|
40
|
+
needs_revision: Finding[];
|
|
41
|
+
/** What this report did not judge. A short list of findings is not a clean bill. */
|
|
42
|
+
not_checked: {
|
|
43
|
+
aspect: string;
|
|
44
|
+
name: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
}[];
|
|
47
|
+
running_missions: RunningMission[];
|
|
48
|
+
preferences: string[];
|
|
49
|
+
round: number;
|
|
50
|
+
jev: {
|
|
51
|
+
state: "ran" | "unconfigured" | "unavailable";
|
|
52
|
+
model?: string;
|
|
53
|
+
uncalibrated?: true;
|
|
54
|
+
paid_input_tokens?: number;
|
|
55
|
+
message?: string;
|
|
56
|
+
};
|
|
57
|
+
next_action: string;
|
|
58
|
+
}
|
|
59
|
+
/** Ids, paths, hashes, URLs and repository names out of what goes to Jev. */
|
|
60
|
+
export declare function redactForJev(text: string): string;
|
|
61
|
+
/** 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. */
|
|
62
|
+
export declare function namesExactPlace(delivery: string): boolean;
|
|
63
|
+
/** Runs and proposals a person approved in the last 48 hours that have not finished. */
|
|
64
|
+
export declare function runningMissions(records: ConnectorRecord[], selfId: string, now?: number): (RunningMission & {
|
|
65
|
+
issues: string[];
|
|
66
|
+
})[];
|
|
67
|
+
/** The mission cut into sentences to choose among: Jev picks, it does not write (Episode 564, read gate F3). */
|
|
68
|
+
export declare function sentencesOf(text: string, limit?: number): string[];
|
|
69
|
+
export interface ReviewInput {
|
|
70
|
+
record: ConnectorRecord;
|
|
71
|
+
records: ConnectorRecord[];
|
|
72
|
+
standard: MissionStandard;
|
|
73
|
+
adapter?: JevAdapter;
|
|
74
|
+
now?: number;
|
|
75
|
+
}
|
|
76
|
+
/** One report for one version of one mission. Jev's failure leaves the code's findings and names what went unchecked. */
|
|
77
|
+
export declare function reviewDraft({ record, records, standard, adapter, now }: ReviewInput): Promise<DraftReview>;
|
|
78
|
+
export declare const reportId: (recordId: string, version: number) => string;
|
|
79
|
+
export interface DraftReviewService {
|
|
80
|
+
/** The stored report for this version, or a new one. Never throws. */
|
|
81
|
+
review(record: ConnectorRecord, records: ConnectorRecord[]): Promise<DraftReview | {
|
|
82
|
+
state: "unavailable";
|
|
83
|
+
message: string;
|
|
84
|
+
}>;
|
|
85
|
+
/** The report already made for this version, with no call out. */
|
|
86
|
+
stored(record: ConnectorRecord): DraftReview | undefined;
|
|
87
|
+
standard(): Promise<MissionStandard>;
|
|
88
|
+
jevConfigured: boolean;
|
|
89
|
+
}
|
|
90
|
+
export declare function draftReviewService(o: {
|
|
91
|
+
stateDir: string;
|
|
92
|
+
env?: NodeJS.ProcessEnv;
|
|
93
|
+
fetchImpl?: typeof fetch;
|
|
94
|
+
adapter?: JevAdapter;
|
|
95
|
+
source?: StandardSource;
|
|
96
|
+
}): DraftReviewService;
|
|
@@ -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
|
+
}
|
package/dist/mission-status.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { ConnectorRecord } from "./records.js";
|
|
23
23
|
import type { RequestProjection } from "./request-state.js";
|
|
24
|
+
import type { SteerDelivery } from "./runtime-steer.js";
|
|
24
25
|
import type { FactoryReceipt } from "./factory-receipt.js";
|
|
25
26
|
import { type GithubGet, type MissionResult } from "./mission-result.js";
|
|
26
27
|
export interface MissionSession {
|
|
@@ -184,9 +185,18 @@ export interface MissionStatus {
|
|
|
184
185
|
access?: string;
|
|
185
186
|
}[];
|
|
186
187
|
steering?: {
|
|
187
|
-
grant: "none" | "active" | "expired_grant" | "exhausted";
|
|
188
|
+
grant: "none" | "active" | "expired_grant" | "exhausted" | "revoked";
|
|
188
189
|
attempts_used: number;
|
|
189
190
|
max_attempts?: number;
|
|
191
|
+
/** Each correction sent under this mission and where it stands, by its own evidence (jgwill/Miadi#930; write tier). */
|
|
192
|
+
steers?: {
|
|
193
|
+
id: string;
|
|
194
|
+
at: string;
|
|
195
|
+
by: string;
|
|
196
|
+
state: string;
|
|
197
|
+
route: string;
|
|
198
|
+
delivery: SteerDelivery;
|
|
199
|
+
}[];
|
|
190
200
|
};
|
|
191
201
|
stage_truth?: MissionStageTruth;
|
|
192
202
|
/**
|
|
@@ -216,6 +226,8 @@ export interface MissionStatusDeps {
|
|
|
216
226
|
tmuxSessions?: () => string[];
|
|
217
227
|
/** Read-only peek: is the named session's pane mid-turn? The pane id when the binding names one, else the first pane. (write tier only.) */
|
|
218
228
|
paneBusy?: (session: string, paneId?: string) => "busy" | "idle" | "unknown";
|
|
229
|
+
/** Where one steer stands, read from the runtime's inbox and the session's transcript (write tier only). */
|
|
230
|
+
steerDeliveryOf?: (steer: ConnectorRecord) => SteerDelivery | null;
|
|
219
231
|
/** Whether the tmux session still holds the pane id (write tier only); undefined when it cannot be listed. */
|
|
220
232
|
panePresent?: (session: string, paneId: string) => boolean | undefined;
|
|
221
233
|
/** The factory runner's delivery receipts for one record, newest first (write tier only). */
|