@miadi/connector 0.1.8 → 0.1.10
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 +12 -2
- package/dist/approve.d.ts +18 -14
- package/dist/approve.js +95 -50
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.js +42 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mission-status.d.ts +87 -0
- package/dist/mission-status.js +144 -0
- package/dist/providers.d.ts +61 -0
- package/dist/providers.js +120 -0
- package/dist/queue-state.d.ts +15 -0
- package/dist/queue-state.js +24 -0
- package/dist/records.d.ts +4 -1
- package/dist/records.js +3 -0
- package/dist/request-state.d.ts +69 -0
- package/dist/request-state.js +180 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +16 -1
- package/dist/steer.d.ts +71 -0
- package/dist/steer.js +229 -0
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +218 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
|
|
|
20
20
|
|
|
21
21
|
| kind | tools |
|
|
22
22
|
|---|---|
|
|
23
|
-
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `search` `fetch` |
|
|
23
|
+
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `search` `fetch` |
|
|
24
24
|
| contributions | `append_observation` `open_inquiry` `link_records` `propose_narrative_beat` `record_evaluation` `register_source_intent` `record_handoff` `update_handoff` `prepare_calendar_event` `save_research` |
|
|
25
|
-
| requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
|
|
25
|
+
| requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_session_steer` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
|
|
26
26
|
| decisions in the conversation | `show_decisions` (cards), `decide_request` (called by the card only) |
|
|
27
27
|
|
|
28
28
|
- **Reads** need Miadi's read tier and stay within it. A person Miadi gives no API tier reaches only `get_capabilities`. Sessions need the write tier, because a session's name and pane are operator data. For the same reason, below the write tier a record someone else made shows no session, working folder or seat, and no long bodies.
|
|
@@ -38,6 +38,16 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
|
|
|
38
38
|
|
|
39
39
|
The model that asked cannot approve its own request.
|
|
40
40
|
|
|
41
|
+
`get_run_result` returns one `projection` of the decision, dispatch/completion state, receipt link and outstanding instructions. The in-chat card, approval page and Miadi's research page use that same read model. Asking `show_decisions` for an already-decided `record_id` shows its receipt without issuing another decision token. An in-flight card can refresh status without repeating the operation. A completed basic review with unapplied approval instructions remains open as **changes outstanding**, not falsely finished.
|
|
42
|
+
|
|
43
|
+
### Following and steering a mission
|
|
44
|
+
|
|
45
|
+
`mission_status` joins one mission read-only: its source request and decision, the sessions bound to it (each with how it is bound — a signed `session:` ref, the delivered brief's target, or the requested name), its issues and PRs, its steering grant, and where it stands stage by stage — approved, queued, delivered, running, merged, released, closed — each by its evidence, so an idle pane or a merge is never read as completion. Live panes and Miadi's session view are joined only for the write tier.
|
|
46
|
+
|
|
47
|
+
`request_session_steer` sends one authorized follow-up to one existing live agent, within the steering grant an approved run carries (`steering: { max_attempts, expires_at, pane }` on `request_authorized_run`). It peeks at the bound pane — the grant's pane or the session's first pane, never whichever pane is active — refuses a busy agent or a pane that is not an agent, types the exact instruction literally, and re-peeks to verify the agent accepted it (`accepted`) or only holds it at the prompt (`submitted`). Every attempt leaves a durable artifact with the instruction's sha256, its grant, its target and both captures. A steer the grant does not cover waits for one explicit scoped human decision on its `approval_url`, which runs exactly that instruction once.
|
|
48
|
+
|
|
49
|
+
`request_authorized_run` may name a `provider` (`codex`, `pi`, `copilot`, `claude`). The connector verifies it is installed on the host before recording anything; an unknown or unavailable provider answers `provider_unavailable` and the mission stays unstarted. With no provider named, the first installed provider in the preference order is resolved and named in the brief; Claude Code is never chosen silently — it runs only when named. `provider_capabilities` (write tier) reports what the host has installed, the sign-in evidence, the preference order, and the read-only Jev routing preflight (`unconfigured` without `MIADI_JEV_URL`). A provider name inside a task's text selects nothing.
|
|
50
|
+
|
|
41
51
|
### Deciding inside the conversation
|
|
42
52
|
|
|
43
53
|
In a client that renders MCP Apps (ChatGPT among them), `show_decisions` draws one card per request awaiting a decision: what is asked, by whom, and what approving does. The person decides on the card with their own click: Approve, Approve with my changes, Send back for revision, or Decline. The card then shows the receipt.
|
package/dist/approve.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ import type { Ledger } from "./store.js";
|
|
|
15
15
|
import type { MiadiClient } from "./miadi.js";
|
|
16
16
|
import { type MiadiPerson } from "./people.js";
|
|
17
17
|
import { type ConnectorRecord } from "./records.js";
|
|
18
|
+
import { type RequestProjection } from "./request-state.js";
|
|
19
|
+
import { type TmuxLike } from "./steer.js";
|
|
20
|
+
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
18
21
|
export interface ApprovalDeps {
|
|
19
22
|
config: ConnectorConfig;
|
|
20
23
|
ledger: Ledger<ConnectorRecord>;
|
|
@@ -24,6 +27,10 @@ export interface ApprovalDeps {
|
|
|
24
27
|
/** The factory directory holding queue/ (default ~/.miadi/factory). */
|
|
25
28
|
factoryDir?: string;
|
|
26
29
|
tmuxSessions?: () => string[];
|
|
30
|
+
/** The steering surface (jgwill/Miadi#893); tests inject a fake, production uses tmux. */
|
|
31
|
+
tmux?: TmuxLike;
|
|
32
|
+
/** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
|
|
33
|
+
steerSettleMs?: number;
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
36
|
* What a person may decide on a request. Approving with changes runs it with the
|
|
@@ -32,20 +39,6 @@ export interface ApprovalDeps {
|
|
|
32
39
|
*/
|
|
33
40
|
export declare const DECISIONS: readonly ["approve", "approve_with_changes", "request_revision", "decline"];
|
|
34
41
|
export type Decision = (typeof DECISIONS)[number];
|
|
35
|
-
export declare function factoryDir(env?: NodeJS.ProcessEnv): string;
|
|
36
|
-
export declare function runBriefFile(record: {
|
|
37
|
-
id: string;
|
|
38
|
-
}): string;
|
|
39
|
-
/** Where a run request's brief is: waiting, delivered by the intake, cancelled, or not queued. */
|
|
40
|
-
export declare function readQueueState(record: ConnectorRecord, dir?: string): {
|
|
41
|
-
state: string;
|
|
42
|
-
file: string;
|
|
43
|
-
note?: undefined;
|
|
44
|
-
} | {
|
|
45
|
-
state: string;
|
|
46
|
-
file: string;
|
|
47
|
-
note: string;
|
|
48
|
-
};
|
|
49
42
|
/** The task a work proposal asks for, written from the proposal's own fields. */
|
|
50
43
|
export declare function proposalTask(record: ConnectorRecord): string;
|
|
51
44
|
export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson, sessions: string[], instructions?: string): {
|
|
@@ -64,6 +57,8 @@ export declare function executeApproved(record: ConnectorRecord, approver: Miadi
|
|
|
64
57
|
/** A request is decided once: a second decision while one runs, or after it, is refused. */
|
|
65
58
|
export declare class AlreadyDecided extends Error {
|
|
66
59
|
}
|
|
60
|
+
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
61
|
+
export declare function isBeingDecided(recordId: string): boolean;
|
|
67
62
|
export declare function decide(r: ConnectorRecord, person: MiadiPerson, choice: Decision, deps: ApprovalDeps, opts: {
|
|
68
63
|
instructions?: string;
|
|
69
64
|
note?: string;
|
|
@@ -93,5 +88,14 @@ export declare function isApprovable(r: ConnectorRecord | undefined): r is Conne
|
|
|
93
88
|
/** What approving a request does, in one sentence, for the card a person decides on. */
|
|
94
89
|
export declare function consequenceOf(r: ConnectorRecord): string;
|
|
95
90
|
export declare function registerApprovalRoutes(app: Express, deps: ApprovalDeps): void;
|
|
91
|
+
/** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
|
|
92
|
+
/**
|
|
93
|
+
* Where a request stands, as every surface of the connector reads it (request-state.ts):
|
|
94
|
+
* the factory queue is read for the two kinds that dispatch a brief.
|
|
95
|
+
*/
|
|
96
|
+
export declare function requestProjection(record: ConnectorRecord, opts?: {
|
|
97
|
+
reviewBase?: string;
|
|
98
|
+
factoryDir?: string;
|
|
99
|
+
}): RequestProjection | null;
|
|
96
100
|
/** How a request is named to a person. */
|
|
97
101
|
export declare const KIND_LABELS: Record<string, string>;
|
package/dist/approve.js
CHANGED
|
@@ -10,49 +10,23 @@
|
|
|
10
10
|
* is appended as a record that supersedes the request (state, approval, receipt).
|
|
11
11
|
*/
|
|
12
12
|
import express from "express";
|
|
13
|
-
import {
|
|
13
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
15
|
import { join } from "node:path";
|
|
17
16
|
import { escapeHtml } from "@miadi/node-service-kit/format";
|
|
18
17
|
import { MiadiError } from "./miadi.js";
|
|
19
18
|
import { MiadiUnavailable } from "./people.js";
|
|
20
19
|
import { current, newRecordId } from "./records.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"review_episode_link_request",
|
|
26
|
-
"work_proposal",
|
|
27
|
-
"session_completion_proposal",
|
|
28
|
-
"inquiry_episode_request",
|
|
29
|
-
]);
|
|
20
|
+
import { APPROVABLE, projectRequest } from "./request-state.js";
|
|
21
|
+
import { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
22
|
+
import { steerAttempts, steerSession, tmuxAdapter } from "./steer.js";
|
|
23
|
+
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
30
24
|
/**
|
|
31
25
|
* What a person may decide on a request. Approving with changes runs it with the
|
|
32
26
|
* approver's instructions carried into what runs; a revision sends it back to
|
|
33
27
|
* whoever asked, with instructions, and runs nothing.
|
|
34
28
|
*/
|
|
35
29
|
export const DECISIONS = ["approve", "approve_with_changes", "request_revision", "decline"];
|
|
36
|
-
export function factoryDir(env = process.env) {
|
|
37
|
-
return env.MIADI_FACTORY_DIR || join(homedir(), ".miadi", "factory");
|
|
38
|
-
}
|
|
39
|
-
export function runBriefFile(record) {
|
|
40
|
-
return `connector-${record.id.replace(/[^\w-]/g, "-")}.md`;
|
|
41
|
-
}
|
|
42
|
-
/** Where a run request's brief is: waiting, delivered by the intake, cancelled, or not queued. */
|
|
43
|
-
export function readQueueState(record, dir = factoryDir()) {
|
|
44
|
-
const file = runBriefFile(record);
|
|
45
|
-
const queue = join(dir, "queue");
|
|
46
|
-
if (existsSync(join(queue, file)))
|
|
47
|
-
return { state: "queued", file };
|
|
48
|
-
if (existsSync(join(queue, "delivered", file)))
|
|
49
|
-
return { state: "delivered", file };
|
|
50
|
-
if (existsSync(join(queue, "cancelled", file)))
|
|
51
|
-
return { state: "cancelled", file };
|
|
52
|
-
if (existsSync(join(queue, "stale", file)))
|
|
53
|
-
return { state: "stale", file, note: "Its seat did not take it in time; the intake moved it aside." };
|
|
54
|
-
return { state: record.approval_state === "approved" ? "missing" : "not_queued", file };
|
|
55
|
-
}
|
|
56
30
|
function liveTmux() {
|
|
57
31
|
try {
|
|
58
32
|
return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] })
|
|
@@ -94,7 +68,15 @@ export function runBrief(record, approver, sessions, instructions) {
|
|
|
94
68
|
const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
|
|
95
69
|
const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
|
|
96
70
|
const cwd = body.cwd || "/a/src/Miadi-18";
|
|
97
|
-
const open = [
|
|
71
|
+
const open = [
|
|
72
|
+
`to="${attr(to)}"`,
|
|
73
|
+
'status="send"',
|
|
74
|
+
'type="connector-run"',
|
|
75
|
+
`record="${attr(record.id)}"`,
|
|
76
|
+
// The provider the connector verified at request time (jgwill/Miadi#893); the intake may honour it or record why not.
|
|
77
|
+
...(body.resolved_provider ? [`provider="${attr(body.resolved_provider)}"`] : []),
|
|
78
|
+
...(to.startsWith("new:") ? [`cwd="${attr(cwd)}"`] : []),
|
|
79
|
+
];
|
|
98
80
|
const lines = [
|
|
99
81
|
`<input ${open.join(" ")}>`,
|
|
100
82
|
`A bounded task requested through the Miadi connector (jgwill/Miadi#815, #816).`,
|
|
@@ -229,6 +211,23 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
229
211
|
ending: b.ending,
|
|
230
212
|
note: "Recorded only. The witness seat writes it into the inventory record and closes the session by its own lifecycle.",
|
|
231
213
|
};
|
|
214
|
+
case "session_steer_request": {
|
|
215
|
+
// The person's approval of this exact steer is its authorization; the
|
|
216
|
+
// grant record it cites still binds the session (jgwill/Miadi#893).
|
|
217
|
+
const grantId = record.refs.find((r) => r.startsWith("run_request:") || r.startsWith("work_proposal:"));
|
|
218
|
+
const grantRecord = grantId ? current(deps.ledger.all()).find((x) => x.id === grantId) : undefined;
|
|
219
|
+
if (!grantRecord)
|
|
220
|
+
return { outcome: "failed", message: "The run whose grant this steer cites is gone." };
|
|
221
|
+
const result = await steerSession({ tmux: deps.tmux ?? tmuxAdapter(), stateDir: deps.config.stateDir, ...(deps.steerSettleMs !== undefined ? { settleMs: deps.steerSettleMs } : {}) }, { record, grantRecord, priorAttempts: steerAttempts(current(deps.ledger.all()), grantRecord.id), authorized: true });
|
|
222
|
+
const delivered = result.status === "accepted" || result.status === "submitted";
|
|
223
|
+
return {
|
|
224
|
+
outcome: delivered ? result.status : "failed",
|
|
225
|
+
steer_status: result.status,
|
|
226
|
+
message: result.message,
|
|
227
|
+
...(result.target ? { target: result.target } : {}),
|
|
228
|
+
...(result.artifact ? { artifact: result.artifact } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
232
231
|
default:
|
|
233
232
|
return { outcome: "nothing_to_run" };
|
|
234
233
|
}
|
|
@@ -281,6 +280,10 @@ ${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}
|
|
|
281
280
|
export class AlreadyDecided extends Error {
|
|
282
281
|
}
|
|
283
282
|
const deciding = new Set();
|
|
283
|
+
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
284
|
+
export function isBeingDecided(recordId) {
|
|
285
|
+
return deciding.has(recordId);
|
|
286
|
+
}
|
|
284
287
|
export async function decide(r, person, choice, deps, opts) {
|
|
285
288
|
// Checked again here, with no await between the check and the claim: the
|
|
286
289
|
// page and the card (or two cards) may decide the same request at once.
|
|
@@ -421,6 +424,8 @@ export function consequenceOf(r) {
|
|
|
421
424
|
return b.new_episode ? "Approving mints a new episode with the inquiry woven in." : `Approving weaves the inquiry into episode ${b.episode ?? "?"}.`;
|
|
422
425
|
case "session_completion_proposal":
|
|
423
426
|
return `Approving records the ending (${b.ending ?? "?"}) for the session. No pane is closed.`;
|
|
427
|
+
case "session_steer_request":
|
|
428
|
+
return `Approving types exactly this instruction into the bound agent session (${b.session ?? "the run's session"}), once, and records whether the agent accepted it.`;
|
|
424
429
|
default:
|
|
425
430
|
return "Approving runs the request once, as you.";
|
|
426
431
|
}
|
|
@@ -459,9 +464,9 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
459
464
|
return void res.status(403).type("html").send(signInToRead(r, `Reading and deciding this request needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
|
|
460
465
|
}
|
|
461
466
|
if (!body.decision)
|
|
462
|
-
return void res.type("html").send(approvePage(r, { token: writer ? token : undefined }));
|
|
467
|
+
return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined }));
|
|
463
468
|
if (!writer)
|
|
464
|
-
return void res.status(403).type("html").send(approvePage(r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
|
|
469
|
+
return void res.status(403).type("html").send(approvePage(deps, r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
|
|
465
470
|
if (body.decision === "rerun") {
|
|
466
471
|
let ran;
|
|
467
472
|
try {
|
|
@@ -469,14 +474,14 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
469
474
|
}
|
|
470
475
|
catch (error) {
|
|
471
476
|
if (error instanceof AlreadyDecided)
|
|
472
|
-
return void res.status(409).type("html").send(approvePage(find(r.id), { token, error: error.message }));
|
|
477
|
+
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, error: error.message }));
|
|
473
478
|
throw error;
|
|
474
479
|
}
|
|
475
480
|
const failedAgain = ran.receipt.outcome === "failed";
|
|
476
481
|
return void res.type("html").send(page("Recorded", `<h1>${failedAgain ? "Run again, and it failed again" : "Run again"}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(ran.receipt, null, 2))}</pre>`));
|
|
477
482
|
}
|
|
478
483
|
if (r.approval_state !== "awaiting_approval") {
|
|
479
|
-
return void res.status(409).type("html").send(approvePage(r, { error: `This request is already ${r.approval_state}.` }));
|
|
484
|
+
return void res.status(409).type("html").send(approvePage(deps, r, { error: `This request is already ${r.approval_state}.` }));
|
|
480
485
|
}
|
|
481
486
|
const choice = DECISIONS.includes(String(body.decision)) ? body.decision : "decline";
|
|
482
487
|
const instructions = String(body.instructions ?? "").trim().slice(0, 20000) || undefined;
|
|
@@ -484,7 +489,7 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
484
489
|
return void res
|
|
485
490
|
.status(400)
|
|
486
491
|
.type("html")
|
|
487
|
-
.send(approvePage(r, { token, error: choice === "request_revision" ? "Say what to revise." : "Write the changes to apply." }));
|
|
492
|
+
.send(approvePage(deps, r, { token, error: choice === "request_revision" ? "Say what to revise." : "Write the changes to apply." }));
|
|
488
493
|
}
|
|
489
494
|
let decided;
|
|
490
495
|
try {
|
|
@@ -492,13 +497,21 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
492
497
|
}
|
|
493
498
|
catch (error) {
|
|
494
499
|
if (error instanceof AlreadyDecided)
|
|
495
|
-
return void res.status(409).type("html").send(approvePage(find(r.id), { error: error.message }));
|
|
500
|
+
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { error: error.message }));
|
|
496
501
|
throw error;
|
|
497
502
|
}
|
|
498
503
|
const { decision, receipt } = decided;
|
|
499
|
-
const
|
|
500
|
-
const heading = decision === "approved"
|
|
501
|
-
|
|
504
|
+
const p = projectionOf(find(r.id), deps);
|
|
505
|
+
const heading = decision === "approved"
|
|
506
|
+
? p?.stage === "failed"
|
|
507
|
+
? "Approved, but running it failed"
|
|
508
|
+
: p?.outstanding.length
|
|
509
|
+
? "Approved, changes still to do"
|
|
510
|
+
: "Approved"
|
|
511
|
+
: decision === "revision_requested"
|
|
512
|
+
? "Sent back for revision"
|
|
513
|
+
: "Declined";
|
|
514
|
+
res.type("html").send(page("Recorded", `<h1>${heading}</h1>${summary(find(r.id))}${projectionBlock(p, receipt)}`));
|
|
502
515
|
});
|
|
503
516
|
// A record's own page, for citations in ChatGPT: its author and anyone with Miadi's read tier may read it.
|
|
504
517
|
app.get("/records/:id", (req, res) => {
|
|
@@ -532,9 +545,12 @@ ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
|
532
545
|
<div class="row"><button class="primary" type="submit">Read</button></div></form>`;
|
|
533
546
|
}
|
|
534
547
|
function signInToRead(r, error) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
// The label names no one and no instruction: this page is shown before sign-in.
|
|
549
|
+
const open = r.approval_state === "awaiting_approval";
|
|
550
|
+
const label = projectRequest(r, { recording: isBeingDecided(r.id) })?.label ?? r.approval_state ?? "";
|
|
551
|
+
return page(open ? "A Miadi request awaits a decision" : "A Miadi request was decided", `<h1>${open ? "A request awaits a decision" : "A request was decided"}</h1>
|
|
552
|
+
<p>${escapeHtml(KIND_LABELS[r.kind] ?? r.kind)}, asked ${escapeHtml(r.created_at.slice(0, 16).replace("T", " "))} UTC. ${escapeHtml(label)}.</p>
|
|
553
|
+
<p>${open ? "Sign in to read it. A person with Miadi's write tier then decides; whoever asked can read it." : "Sign in to read what was decided and what came of it. Whoever asked, and anyone with Miadi's write tier, can read it."}</p>
|
|
538
554
|
${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
539
555
|
<form method="post">
|
|
540
556
|
<label for="t">Your Miadi person token</label>
|
|
@@ -543,13 +559,41 @@ ${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
|
543
559
|
</form>`);
|
|
544
560
|
}
|
|
545
561
|
/** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Where a request stands, as every surface of the connector reads it (request-state.ts):
|
|
564
|
+
* the factory queue is read for the two kinds that dispatch a brief.
|
|
565
|
+
*/
|
|
566
|
+
export function requestProjection(record, opts = {}) {
|
|
567
|
+
const queue = record.kind === "run_request" || record.kind === "work_proposal" ? readQueueState(record, opts.factoryDir ?? factoryDir()) : undefined;
|
|
568
|
+
return projectRequest(record, {
|
|
569
|
+
...(opts.reviewBase ? { reviewBase: opts.reviewBase } : {}),
|
|
570
|
+
...(queue ? { queue } : {}),
|
|
571
|
+
recording: isBeingDecided(record.id),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function projectionOf(r, deps) {
|
|
575
|
+
return requestProjection(r, { reviewBase: deps.config.reviewUrl, factoryDir: deps.factoryDir });
|
|
576
|
+
}
|
|
577
|
+
/** Where a request stands, as a page shows it: the label, each line, the review it produced, and the receipt for audit. */
|
|
578
|
+
function projectionBlock(p, receipt) {
|
|
579
|
+
if (!p)
|
|
580
|
+
return "";
|
|
581
|
+
const lines = p.lines.map((l) => `<li>${escapeHtml(l)}</li>`).join("");
|
|
582
|
+
const review = p.receipt_url ? `<p><a href="${escapeHtml(p.receipt_url)}" rel="noopener">${escapeHtml(p.receipt_url)}</a></p>` : "";
|
|
583
|
+
const audit = receipt ? `<details><summary>Receipt</summary><pre>${escapeHtml(JSON.stringify(receipt, null, 2))}</pre></details>` : "";
|
|
584
|
+
return `<section aria-label="Where it stands"><p><strong>${escapeHtml(p.label)}</strong></p>${lines ? `<ul>${lines}</ul>` : ""}${review}${audit}</section>`;
|
|
585
|
+
}
|
|
586
|
+
/** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
|
|
587
|
+
function approvePage(deps, r, o = {}) {
|
|
588
|
+
const open = r.approval_state === "awaiting_approval";
|
|
589
|
+
// A decision being recorded offers no buttons: a second click would decide the same request again.
|
|
590
|
+
const actionable = open && !isBeingDecided(r.id);
|
|
591
|
+
return page("Approve a Miadi request", `<h1>${open ? "Approve a request" : "A decided request"}</h1>
|
|
592
|
+
${actionable ? `<p>${escapeHtml(consequenceOf(r))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
|
|
549
593
|
${summary(r)}
|
|
550
|
-
${
|
|
594
|
+
${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}
|
|
551
595
|
${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
|
|
552
|
-
${
|
|
596
|
+
${actionable && o.token
|
|
553
597
|
? `<form method="post">
|
|
554
598
|
<input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
|
|
555
599
|
<label for="i">Instructions (needed to approve with changes or to send back)</label>
|
|
@@ -560,7 +604,7 @@ ${r.approval_state === "awaiting_approval" && o.token
|
|
|
560
604
|
<button type="submit" name="decision" value="request_revision">Send back for revision</button>
|
|
561
605
|
<button type="submit" name="decision" value="decline">Decline</button>
|
|
562
606
|
</div></form>`
|
|
563
|
-
:
|
|
607
|
+
: actionable
|
|
564
608
|
? "<p>A person with Miadi's write tier decides this request.</p>"
|
|
565
609
|
: isRerunnable(r) && o.token
|
|
566
610
|
? `<p>It was approved, and running it failed. When what it calls has been fixed, it can be run again with the same approval and instructions; the ledger records who ran it again.</p>
|
|
@@ -578,4 +622,5 @@ export const KIND_LABELS = {
|
|
|
578
622
|
review_episode_link_request: "Review into an episode",
|
|
579
623
|
inquiry_episode_request: "Inquiry into an episode",
|
|
580
624
|
session_completion_proposal: "Session ending",
|
|
625
|
+
session_steer_request: "Session steer",
|
|
581
626
|
};
|
package/dist/decisions.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export declare class DecisionTokens {
|
|
|
15
15
|
* and calls `decide_request` through the host. In a host without that bridge it
|
|
16
16
|
* shows each request's approval page instead.
|
|
17
17
|
*/
|
|
18
|
-
export declare const DECISIONS_CARD_HTML = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n:root{color-scheme:light dark;--fg:#1d1b18;--muted:#5b5650;--line:#d9d4cc;--accent:#7a4a00;--ok:#1d6b3a;--err:#b00020;--chip:#f1ede6}\n@media (prefers-color-scheme:dark){:root{--fg:#ece8e1;--muted:#b3ada4;--line:#3a3631;--accent:#ffd700;--ok:#7fd49b;--err:#ff8a80;--chip:#24211d}}\n*{box-sizing:border-box}\nbody{margin:0;color:var(--fg);font:15px/1.45 system-ui,sans-serif;background:transparent}\nmain{padding:12px;display:grid;gap:12px}\n.empty,.note{color:var(--muted)}\narticle{border:1px solid var(--line);border-radius:10px;padding:12px}\nh2{font-size:15px;margin:0 0 4px}\n.kind{display:inline-block;font-size:12px;padding:1px 8px;border-radius:999px;background:var(--chip);color:var(--muted);margin-bottom:6px}\n.meta{font-size:13px;color:var(--muted);margin:0 0 6px}\n.does{margin:6px 0;font-weight:600}\ndetails{margin:6px 0}\nsummary{cursor:pointer;color:var(--muted);font-size:13px}\npre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:6px 0 0;max-height:240px;overflow:auto}\ntextarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line);border-radius:8px;background:transparent;color:var(--fg);font:inherit;margin-top:8px}\n.row{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}\nbutton{padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--fg);font:inherit;cursor:pointer;min-height:40px}\nbutton.primary{border-color:var(--accent);outline:1px solid var(--accent)}\nbutton:focus-visible,a:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton[disabled]{opacity:.55;cursor:default}\na{color:inherit}\n.done{color:var(--ok);font-weight:600}\n.fail{color:var(--err);font-weight:600}\n</style></head><body><main id=\"root\" aria-live=\"polite\"><p class=\"note\">Loading requests\u2026</p></main>\n<script>\n(function(){\n var root=document.getElementById(\"root\");\n var decided={};\n // A redraw while a decision is being recorded must not offer the buttons again.\n var pending={};\n var errors={};\n function el(tag,attrs,text){var e=document.createElement(tag);if(attrs)for(var k in attrs){if(k===\"class\")e.className=attrs[k];else e.setAttribute(k,attrs[k]);}if(text!=null)e.textContent=text;return e;}\n function host(){return window.openai||null;}\n function data(){var h=host();return h&&h.toolOutput?h.toolOutput:null;}\n function tokens(){var h=host();var m=h&&h.toolResponseMetadata;return (m&&m[\"miadi/decisionTokens\"])||{};}\n function render(){\n var d=data();root.textContent=\"\";\n if(!d){root.appendChild(el(\"p\",{class:\"note\"},\"Waiting for the request list.\"));return;}\n var list=d.decisions||[];\n if(!list.length){root.appendChild(el(\"p\",{class:\"empty\"},d.message||\"No request is waiting for a decision.\"));return;}\n if(!d.can_decide_here&&d.reason){root.appendChild(el(\"p\",{class:\"note\"},d.reason));}\n var t=tokens();\n list.forEach(function(r){root.appendChild(card(r,t[r.id]));});\n }\n function card(r,token){\n var a=el(\"article\",{\"aria-label\":r.title});\n a.appendChild(el(\"span\",{class:\"kind\"},r.kind_label||r.kind));\n a.appendChild(el(\"h2\",null,r.title));\n a.appendChild(el(\"p\",{class:\"meta\"},\"Asked by \"+r.by+(r.via?\" through \"+r.via:\"\")+\" \u00B7 \"+new Date(r.created_at).toLocaleString()+(r.circle?\" \u00B7 \"+r.circle+\" circle\":\"\")));\n a.appendChild(el(\"p\",{class:\"does\"},r.consequence));\n if(r.intention)a.appendChild(el(\"p\",null,r.intention));\n var det=el(\"details\");det.appendChild(el(\"summary\",null,\"What exactly is asked\"));det.appendChild(el(\"pre\",null,r.detail||\"\"));a.appendChild(det);\n var status=el(\"p\",{role:\"status\"});\n if(decided[r.id]){
|
|
18
|
+
export declare const DECISIONS_CARD_HTML = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n:root{color-scheme:light dark;--fg:#1d1b18;--muted:#5b5650;--line:#d9d4cc;--accent:#7a4a00;--ok:#1d6b3a;--err:#b00020;--chip:#f1ede6}\n@media (prefers-color-scheme:dark){:root{--fg:#ece8e1;--muted:#b3ada4;--line:#3a3631;--accent:#ffd700;--ok:#7fd49b;--err:#ff8a80;--chip:#24211d}}\n*{box-sizing:border-box}\nbody{margin:0;color:var(--fg);font:15px/1.45 system-ui,sans-serif;background:transparent}\nmain{padding:12px;display:grid;gap:12px}\n.empty,.note{color:var(--muted)}\narticle{border:1px solid var(--line);border-radius:10px;padding:12px}\nh2{font-size:15px;margin:0 0 4px}\n.kind{display:inline-block;font-size:12px;padding:1px 8px;border-radius:999px;background:var(--chip);color:var(--muted);margin-bottom:6px}\n.meta{font-size:13px;color:var(--muted);margin:0 0 6px}\n.does{margin:6px 0;font-weight:600}\ndetails{margin:6px 0}\nsummary{cursor:pointer;color:var(--muted);font-size:13px}\npre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:6px 0 0;max-height:240px;overflow:auto}\ntextarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line);border-radius:8px;background:transparent;color:var(--fg);font:inherit;margin-top:8px}\n.row{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}\nbutton{padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--fg);font:inherit;cursor:pointer;min-height:40px}\nbutton.primary{border-color:var(--accent);outline:1px solid var(--accent)}\nbutton:focus-visible,a:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton[disabled]{opacity:.55;cursor:default}\na{color:inherit}\n.done{color:var(--ok);font-weight:600}\n.fail{color:var(--err);font-weight:600}\n</style></head><body><main id=\"root\" aria-live=\"polite\"><p class=\"note\">Loading requests\u2026</p></main>\n<script>\n(function(){\n var root=document.getElementById(\"root\");\n var decided={};\n // A redraw while a decision is being recorded must not offer the buttons again.\n var pending={};\n var errors={};\n function el(tag,attrs,text){var e=document.createElement(tag);if(attrs)for(var k in attrs){if(k===\"class\")e.className=attrs[k];else e.setAttribute(k,attrs[k]);}if(text!=null)e.textContent=text;return e;}\n function host(){return window.openai||null;}\n function data(){var h=host();return h&&h.toolOutput?h.toolOutput:null;}\n function tokens(){var h=host();var m=h&&h.toolResponseMetadata;return (m&&m[\"miadi/decisionTokens\"])||{};}\n function render(){\n var d=data();root.textContent=\"\";\n if(!d){root.appendChild(el(\"p\",{class:\"note\"},\"Waiting for the request list.\"));return;}\n var list=d.decisions||[];\n if(!list.length){root.appendChild(el(\"p\",{class:\"empty\"},d.message||\"No request is waiting for a decision.\"));return;}\n if(!d.can_decide_here&&d.reason){root.appendChild(el(\"p\",{class:\"note\"},d.reason));}\n var t=tokens();\n list.forEach(function(r){root.appendChild(card(r,t[r.id]));});\n }\n // Where a request stands, as the connector projected it: the label, its lines, and the review it produced.\n function decidedBlock(d){\n var box=el(\"div\",{role:\"status\",class:d.cls});\n if(d.note)box.appendChild(el(\"p\",{class:\"line\"},d.note));\n box.appendChild(el(\"p\",{class:\"label\"},d.label));\n (d.lines||[]).forEach(function(l){box.appendChild(el(\"p\",{class:\"line\"},l));});\n if(d.url){var p=el(\"p\",{class:\"line\"});p.appendChild(el(\"a\",{href:d.url,target:\"_blank\",rel:\"noopener\"},d.url));box.appendChild(p);}\n return box;\n }\n function toolResult(res){\n var out=(res&&(res.structuredContent||res.structured_content))||null;\n if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}\n return out;\n }\n function projectionView(p,note){return {cls:p.stage===\"failed\"?\"fail\":\"done\",stage:p.stage,label:p.label,lines:p.lines||[],url:p.receipt_url||null,note:note||null};}\n function card(r,token){\n var a=el(\"article\",{\"aria-label\":r.title});\n a.appendChild(el(\"span\",{class:\"kind\"},r.kind_label||r.kind));\n a.appendChild(el(\"h2\",null,r.title));\n a.appendChild(el(\"p\",{class:\"meta\"},\"Asked by \"+r.by+(r.via?\" through \"+r.via:\"\")+\" \u00B7 \"+new Date(r.created_at).toLocaleString()+(r.circle?\" \u00B7 \"+r.circle+\" circle\":\"\")));\n a.appendChild(el(\"p\",{class:\"does\"},r.consequence));\n if(r.intention)a.appendChild(el(\"p\",null,r.intention));\n var det=el(\"details\");det.appendChild(el(\"summary\",null,\"What exactly is asked\"));det.appendChild(el(\"pre\",null,r.detail||\"\"));a.appendChild(det);\n var status=el(\"p\",{role:\"status\"});\n if(r.projection&&r.projection.stage!==\"awaiting_decision\"&&(!decided[r.id]||[\"recording\",\"dispatching\"].indexOf(r.projection.stage)===-1)){decided[r.id]=projectionView(r.projection);}\n if(decided[r.id]){\n var shown=decided[r.id];a.appendChild(decidedBlock(shown));\n if([\"recording\",\"dispatching\"].indexOf(shown.stage)!==-1&&host()&&host().callTool){\n var refresh=el(\"button\",{type:\"button\"},\"Refresh status\");\n refresh.addEventListener(\"click\",function(){\n refresh.disabled=true;\n host().callTool(\"get_run_result\",{record_id:r.id}).then(function(res){\n var out=toolResult(res);var p=out&&(out.projection||(out.data&&out.data.projection));\n if(p){decided[r.id]=projectionView(p);delete errors[r.id];}\n else{errors[r.id]=(out&&out.message)||\"Status could not be refreshed.\";}\n render();\n },function(err){errors[r.id]=\"Status could not be refreshed: \"+String(err&&err.message||err);render();});\n });\n a.appendChild(refresh);\n }\n if(errors[r.id])a.appendChild(el(\"p\",{class:\"fail\",role:\"status\"},errors[r.id]));\n return a;\n }\n if(pending[r.id]){status.className=\"note\";status.textContent=\"Recording your decision\u2026\";a.appendChild(status);return a;}\n if(!token||!host()||!host().callTool){\n var p=el(\"p\",{class:\"note\"});p.appendChild(document.createTextNode(\"Decide on its approval page: \"));\n var link=el(\"a\",{href:r.approval_url,target:\"_blank\",rel:\"noopener\"},\"open the approval page\");p.appendChild(link);a.appendChild(p);return a;\n }\n var ta=el(\"textarea\",{\"aria-label\":\"Instructions or changes (needed to approve with changes or send back)\",placeholder:\"Instructions or changes (needed to approve with changes or send back)\"});\n a.appendChild(ta);\n var row=el(\"div\",{class:\"row\"});\n var buttons=[[\"approve\",\"Approve\",\"primary\"],[\"approve_with_changes\",\"Approve with my changes\",\"\"],[\"request_revision\",\"Send back for revision\",\"\"],[\"decline\",\"Decline\",\"\"]];\n var btns=[];\n buttons.forEach(function(b){var x=el(\"button\",{type:\"button\",class:b[2]},b[1]);x.addEventListener(\"click\",function(){go(b[0]);});btns.push(x);row.appendChild(x);});\n a.appendChild(row);a.appendChild(status);\n if(errors[r.id]){status.className=\"fail\";status.textContent=errors[r.id];}\n function go(choice){\n var text=ta.value.trim();\n if((choice===\"approve_with_changes\"||choice===\"request_revision\")&&!text){status.className=\"fail\";status.textContent=choice===\"request_revision\"?\"Say what to revise.\":\"Write the changes to apply.\";ta.focus();return;}\n btns.forEach(function(x){x.disabled=true;});status.className=\"note\";status.textContent=\"Recording your decision\u2026\";pending[r.id]=true;delete errors[r.id];\n host().callTool(\"decide_request\",{record_id:r.id,decision:choice,instructions:text||undefined,decision_token:token}).then(function(res){\n var out=toolResult(res);\n // The connector answers with the request's projection, also when a click finds it decided already.\n var proj=out&&out.projection;\n var already=!!(out&&out.error===\"already_decided\");\n var ok=out&&!out.error;\n delete pending[r.id];\n if(proj){decided[r.id]=projectionView(proj,already?\"This request was already decided. Here is where it stands.\":null);}\n else{decided[r.id]={cls:ok?\"done\":\"fail\",label:ok?(out.summary||\"Recorded.\"):((out&&out.message)||\"Not recorded.\"),lines:[],url:null,note:null};}\n render();\n },function(err){delete pending[r.id];errors[r.id]=\"Not recorded: \"+(err&&err.message?err.message:String(err));render();});\n }\n return a;\n }\n window.addEventListener(\"openai:set_globals\",render);\n render();\n})();\n</script></body></html>";
|
package/dist/decisions.js
CHANGED
|
@@ -102,6 +102,21 @@ a{color:inherit}
|
|
|
102
102
|
var t=tokens();
|
|
103
103
|
list.forEach(function(r){root.appendChild(card(r,t[r.id]));});
|
|
104
104
|
}
|
|
105
|
+
// Where a request stands, as the connector projected it: the label, its lines, and the review it produced.
|
|
106
|
+
function decidedBlock(d){
|
|
107
|
+
var box=el("div",{role:"status",class:d.cls});
|
|
108
|
+
if(d.note)box.appendChild(el("p",{class:"line"},d.note));
|
|
109
|
+
box.appendChild(el("p",{class:"label"},d.label));
|
|
110
|
+
(d.lines||[]).forEach(function(l){box.appendChild(el("p",{class:"line"},l));});
|
|
111
|
+
if(d.url){var p=el("p",{class:"line"});p.appendChild(el("a",{href:d.url,target:"_blank",rel:"noopener"},d.url));box.appendChild(p);}
|
|
112
|
+
return box;
|
|
113
|
+
}
|
|
114
|
+
function toolResult(res){
|
|
115
|
+
var out=(res&&(res.structuredContent||res.structured_content))||null;
|
|
116
|
+
if(!out&&res&&res.content&&res.content[0]&&res.content[0].text){try{out=JSON.parse(res.content[0].text);}catch(e){out={message:res.content[0].text};}}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function projectionView(p,note){return {cls:p.stage==="failed"?"fail":"done",stage:p.stage,label:p.label,lines:p.lines||[],url:p.receipt_url||null,note:note||null};}
|
|
105
120
|
function card(r,token){
|
|
106
121
|
var a=el("article",{"aria-label":r.title});
|
|
107
122
|
a.appendChild(el("span",{class:"kind"},r.kind_label||r.kind));
|
|
@@ -111,7 +126,25 @@ a{color:inherit}
|
|
|
111
126
|
if(r.intention)a.appendChild(el("p",null,r.intention));
|
|
112
127
|
var det=el("details");det.appendChild(el("summary",null,"What exactly is asked"));det.appendChild(el("pre",null,r.detail||""));a.appendChild(det);
|
|
113
128
|
var status=el("p",{role:"status"});
|
|
114
|
-
if(
|
|
129
|
+
if(r.projection&&r.projection.stage!=="awaiting_decision"&&(!decided[r.id]||["recording","dispatching"].indexOf(r.projection.stage)===-1)){decided[r.id]=projectionView(r.projection);}
|
|
130
|
+
if(decided[r.id]){
|
|
131
|
+
var shown=decided[r.id];a.appendChild(decidedBlock(shown));
|
|
132
|
+
if(["recording","dispatching"].indexOf(shown.stage)!==-1&&host()&&host().callTool){
|
|
133
|
+
var refresh=el("button",{type:"button"},"Refresh status");
|
|
134
|
+
refresh.addEventListener("click",function(){
|
|
135
|
+
refresh.disabled=true;
|
|
136
|
+
host().callTool("get_run_result",{record_id:r.id}).then(function(res){
|
|
137
|
+
var out=toolResult(res);var p=out&&(out.projection||(out.data&&out.data.projection));
|
|
138
|
+
if(p){decided[r.id]=projectionView(p);delete errors[r.id];}
|
|
139
|
+
else{errors[r.id]=(out&&out.message)||"Status could not be refreshed.";}
|
|
140
|
+
render();
|
|
141
|
+
},function(err){errors[r.id]="Status could not be refreshed: "+String(err&&err.message||err);render();});
|
|
142
|
+
});
|
|
143
|
+
a.appendChild(refresh);
|
|
144
|
+
}
|
|
145
|
+
if(errors[r.id])a.appendChild(el("p",{class:"fail",role:"status"},errors[r.id]));
|
|
146
|
+
return a;
|
|
147
|
+
}
|
|
115
148
|
if(pending[r.id]){status.className="note";status.textContent="Recording your decision…";a.appendChild(status);return a;}
|
|
116
149
|
if(!token||!host()||!host().callTool){
|
|
117
150
|
var p=el("p",{class:"note"});p.appendChild(document.createTextNode("Decide on its approval page: "));
|
|
@@ -130,10 +163,15 @@ a{color:inherit}
|
|
|
130
163
|
if((choice==="approve_with_changes"||choice==="request_revision")&&!text){status.className="fail";status.textContent=choice==="request_revision"?"Say what to revise.":"Write the changes to apply.";ta.focus();return;}
|
|
131
164
|
btns.forEach(function(x){x.disabled=true;});status.className="note";status.textContent="Recording your decision…";pending[r.id]=true;delete errors[r.id];
|
|
132
165
|
host().callTool("decide_request",{record_id:r.id,decision:choice,instructions:text||undefined,decision_token:token}).then(function(res){
|
|
133
|
-
var out=(res
|
|
134
|
-
|
|
166
|
+
var out=toolResult(res);
|
|
167
|
+
// The connector answers with the request's projection, also when a click finds it decided already.
|
|
168
|
+
var proj=out&&out.projection;
|
|
169
|
+
var already=!!(out&&out.error==="already_decided");
|
|
135
170
|
var ok=out&&!out.error;
|
|
136
|
-
delete pending[r.id];
|
|
171
|
+
delete pending[r.id];
|
|
172
|
+
if(proj){decided[r.id]=projectionView(proj,already?"This request was already decided. Here is where it stands.":null);}
|
|
173
|
+
else{decided[r.id]={cls:ok?"done":"fail",label:ok?(out.summary||"Recorded."):((out&&out.message)||"Not recorded."),lines:[],url:null,note:null};}
|
|
174
|
+
render();
|
|
137
175
|
},function(err){delete pending[r.id];errors[r.id]="Not recorded: "+(err&&err.message?err.message:String(err));render();});
|
|
138
176
|
}
|
|
139
177
|
return a;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,7 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE, type Person }
|
|
|
5
5
|
export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, type ConnectorRecord, type CircleId, type ClaimCategory, type RecordKind, } from "./records.js";
|
|
6
6
|
export { TOOL_STATUS } from "./tools.js";
|
|
7
7
|
export { runBrief, readQueueState } from "./approve.js";
|
|
8
|
+
export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, type ProviderAvailability, type ProviderId, type ProviderResolution, type RouterPreflight, } from "./providers.js";
|
|
9
|
+
export { missionStatus, resolveMissionSubject, type MissionStatus, type MissionStatusDeps } from "./mission-status.js";
|
|
10
|
+
export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, type SteerDeps, type SteerResult, type SteerStatus, type SteeringGrant, type TmuxLike, } from "./steer.js";
|
|
8
11
|
export { VERSION } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,7 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE } from "./oaut
|
|
|
5
5
|
export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, } from "./records.js";
|
|
6
6
|
export { TOOL_STATUS } from "./tools.js";
|
|
7
7
|
export { runBrief, readQueueState } from "./approve.js";
|
|
8
|
+
export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, } from "./providers.js";
|
|
9
|
+
export { missionStatus, resolveMissionSubject } from "./mission-status.js";
|
|
10
|
+
export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, } from "./steer.js";
|
|
8
11
|
export { VERSION } from "./version.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only mission status (jgwill/Miadi#893, Phase 1A): where one mission
|
|
3
|
+
* stands, joined from the connector ledger, the factory queue, tmux, Miadi's
|
|
4
|
+
* session view and GitHub — with every stage named by its evidence, so an idle
|
|
5
|
+
* pane or a merge is never read as completion.
|
|
6
|
+
*
|
|
7
|
+
* The caller (tools.ts) decides the viewer's tier first: without the write
|
|
8
|
+
* tier there are no operator details (live panes, session internals); the
|
|
9
|
+
* record's own standing still answers.
|
|
10
|
+
*/
|
|
11
|
+
import type { ConnectorRecord } from "./records.js";
|
|
12
|
+
import type { RequestProjection } from "./request-state.js";
|
|
13
|
+
export interface MissionSession {
|
|
14
|
+
/** How this session is bound to the mission: signed_ref (a session: ref on the record), brief_target (the delivered brief's `to`), named (the requested name). */
|
|
15
|
+
association: "signed_ref" | "brief_target" | "named";
|
|
16
|
+
session: string;
|
|
17
|
+
live: "running" | "absent" | "unknown";
|
|
18
|
+
activity?: "busy" | "idle" | "unknown";
|
|
19
|
+
/** Present when Miadi's session view was joined (write tier). */
|
|
20
|
+
miadi?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface MissionStageTruth {
|
|
23
|
+
approved: boolean;
|
|
24
|
+
queued: boolean;
|
|
25
|
+
delivered: boolean;
|
|
26
|
+
/** "evidence" only while a bound session is live; an idle pane proves nothing. */
|
|
27
|
+
running: "evidence" | "no_evidence";
|
|
28
|
+
pr_merged: "merged" | "open_or_absent" | "no_pr";
|
|
29
|
+
released: "evidence" | "no_evidence";
|
|
30
|
+
closed: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface MissionStatus {
|
|
33
|
+
ref: string;
|
|
34
|
+
found: boolean;
|
|
35
|
+
via?: string;
|
|
36
|
+
subject?: {
|
|
37
|
+
id: string;
|
|
38
|
+
kind: string;
|
|
39
|
+
created_at: string;
|
|
40
|
+
by: string;
|
|
41
|
+
circle?: string;
|
|
42
|
+
intention?: string;
|
|
43
|
+
};
|
|
44
|
+
standing?: RequestProjection;
|
|
45
|
+
queue?: {
|
|
46
|
+
state: string;
|
|
47
|
+
};
|
|
48
|
+
sessions?: MissionSession[];
|
|
49
|
+
issues?: {
|
|
50
|
+
ref: string;
|
|
51
|
+
state: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
merged?: boolean;
|
|
54
|
+
}[];
|
|
55
|
+
steering?: {
|
|
56
|
+
grant: "none" | "active" | "expired_grant" | "exhausted";
|
|
57
|
+
attempts_used: number;
|
|
58
|
+
max_attempts?: number;
|
|
59
|
+
};
|
|
60
|
+
stage_truth?: MissionStageTruth;
|
|
61
|
+
next_authorized_action?: string;
|
|
62
|
+
notes: string[];
|
|
63
|
+
}
|
|
64
|
+
export interface MissionStatusDeps {
|
|
65
|
+
queueOf?: (record: ConnectorRecord) => {
|
|
66
|
+
state: string;
|
|
67
|
+
} | undefined;
|
|
68
|
+
projectOf: (record: ConnectorRecord) => RequestProjection | null;
|
|
69
|
+
/** Live tmux session names, when the host can list them (write tier only). */
|
|
70
|
+
tmuxSessions?: () => string[];
|
|
71
|
+
/** Read-only peek: is the named session's first pane mid-turn? (write tier only.) */
|
|
72
|
+
paneBusy?: (session: string) => "busy" | "idle" | "unknown";
|
|
73
|
+
/** Miadi's session view by session:<uuid> (write tier only). */
|
|
74
|
+
sessionOf?: (id: string) => Promise<unknown>;
|
|
75
|
+
issueOf?: (owner: string, repo: string, n: string) => Promise<{
|
|
76
|
+
state: string;
|
|
77
|
+
title?: string;
|
|
78
|
+
merged?: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
now?: () => Date;
|
|
81
|
+
}
|
|
82
|
+
/** The record a mission reference resolves to, and how it was found. */
|
|
83
|
+
export declare function resolveMissionSubject(ref: string, records: ConnectorRecord[]): {
|
|
84
|
+
record: ConnectorRecord;
|
|
85
|
+
via: string;
|
|
86
|
+
} | null;
|
|
87
|
+
export declare function missionStatus(ref: string, records: ConnectorRecord[], deps: MissionStatusDeps): Promise<MissionStatus>;
|