@jr2/cli 0.6.0 → 0.7.0
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/package.json +3 -3
- package/src/build.ts +4 -4
- package/src/caller-tokens.ts +108 -0
- package/src/cli.ts +33 -2
- package/src/client.ts +224 -23
- package/src/command-table.ts +269 -20
- package/src/commands/caller.ts +115 -0
- package/src/commands/completion.ts +236 -0
- package/src/commands/gates.ts +23 -0
- package/src/commands/hooks.ts +31 -0
- package/src/commands/init.ts +2 -3
- package/src/commands/logs.ts +107 -31
- package/src/commands/run.ts +77 -27
- package/src/commands/runs.ts +17 -78
- package/src/commands/send.ts +1 -1
- package/src/commands/status.ts +121 -6
- package/src/commands/steer.ts +47 -0
- package/src/commands/up.ts +373 -70
- package/src/commands/watch.ts +57 -0
- package/src/commands/whoami.ts +22 -0
- package/src/commands/workflows.ts +58 -0
- package/src/commands/workspace.ts +316 -0
- package/src/deploy.ts +233 -12
- package/src/instance-check.ts +47 -1
- package/src/instance.ts +16 -5
- package/src/kube.ts +37 -3
- package/src/mount-wait.ts +82 -0
- package/src/output.ts +3 -0
- package/src/sse.ts +1 -1
- package/src/workspaces.ts +307 -0
package/src/commands/runs.ts
CHANGED
|
@@ -1,96 +1,35 @@
|
|
|
1
|
-
// `jr2 runs` (ADR-0009): list
|
|
2
|
-
//
|
|
1
|
+
// `jr2 runs` (ADR-0009): list runs as JSON on stdout, newest start first: the live runs by default,
|
|
2
|
+
// the history with `--status ended|all|<list>`, one Workflow with `--workflow`, and the runs that
|
|
3
|
+
// started in a time with `--since` (`2h`, `7d`, or an ISO-8601 time). An ended run is listed while
|
|
4
|
+
// its row is kept (`runs.retain`, ADR-0065); a `drifted` run is listed with its reason (ADR-0030).
|
|
5
|
+
// Each item is the Orchestrator's summary, and the verb reads every page of `GET /runs`.
|
|
3
6
|
//
|
|
4
7
|
// A run with a Workspace in `placing` (ADR-0064) carries `placing`: the child paths of those
|
|
5
8
|
// Workspaces, in `RunWaiting.child`'s form. It is the at-a-glance marker for a run that waits for
|
|
6
9
|
// capacity — why it waits is `jr2 status <runId>`'s `waiting`. A run with none has no `placing` key.
|
|
7
|
-
// A dispatched run (ADR-0071) carries `dispatchedBy
|
|
8
|
-
//
|
|
10
|
+
// A dispatched run (ADR-0071) carries `dispatchedBy`, as the Orchestrator sends it: the run that
|
|
11
|
+
// started it, the caller's slot and the chain's depth, so the chain reads off the listing.
|
|
9
12
|
//
|
|
10
|
-
// The
|
|
11
|
-
// stderr so the result on stdout stays the live runs: one line per Workflow with its origin
|
|
12
|
-
// (`machine`, `chart file`, `chart`), a Chart wrapper's Charts indented under it, and `unrunnable`
|
|
13
|
-
// with its reason. `--workflows` prints that listing as the result instead. No verb puts or deletes
|
|
14
|
-
// a runtime Chart in this step: `PUT`/`DELETE /workflows/<wrapper>/charts/<name>` is for an MCP or curl.
|
|
13
|
+
// The Workflows that a run can start are `jr2 workflows`, not this verb.
|
|
15
14
|
|
|
16
|
-
import
|
|
17
|
-
import { JR2Client, type RunStatus, type WorkflowListing, type WorkflowOrigin } from "../client.ts";
|
|
15
|
+
import { JR2Client } from "../client.ts";
|
|
18
16
|
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
19
|
-
import {
|
|
17
|
+
import { result, type Io } from "../output.ts";
|
|
20
18
|
import { parseCommand } from "../command-table.ts";
|
|
21
19
|
|
|
22
20
|
export async function runs(args: string[], io: Io): Promise<number> {
|
|
23
21
|
const { values } = parseCommand("runs", args);
|
|
24
22
|
const target = await resolveTarget(io, targetOptions(values));
|
|
25
23
|
try {
|
|
26
|
-
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
activity(io, renderWorkflows(await client.workflows()));
|
|
35
|
-
} catch (e) {
|
|
36
|
-
activity(io, `workflows: unknown — ${e instanceof Error ? e.message : String(e)}`);
|
|
37
|
-
}
|
|
38
|
-
result(io, live);
|
|
24
|
+
const client = new JR2Client(target.url, io.fetch, target.token, target.onBehalfOf);
|
|
25
|
+
const listed = await client.allRuns({
|
|
26
|
+
...(typeof values.status === "string" ? { status: values.status } : {}),
|
|
27
|
+
...(typeof values.workflow === "string" ? { workflow: values.workflow } : {}),
|
|
28
|
+
...(typeof values.since === "string" ? { since: values.since } : {}),
|
|
29
|
+
});
|
|
30
|
+
result(io, listed);
|
|
39
31
|
return 0;
|
|
40
32
|
} finally {
|
|
41
33
|
target.close?.();
|
|
42
34
|
}
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
/** How a listing names an origin (ADR-0070): a runtime Chart is plainly a `chart`. */
|
|
46
|
-
const ORIGIN_LABEL: Record<WorkflowOrigin, string> = { machine: "machine", file: "chart file", runtime: "chart" };
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The Workflow listing as lines: each Workflow with its origin, a Chart wrapper's Open body and
|
|
50
|
-
* whether its door takes a Chart one-shot, its Charts indented under it, and `unrunnable` with the
|
|
51
|
-
* reason. A Chart the listing also gives at the top level (or whose wrapper it does not list) prints
|
|
52
|
-
* once: under its wrapper when the wrapper is listed, else at the top level.
|
|
53
|
-
*/
|
|
54
|
-
export function renderWorkflows(listing: readonly WorkflowListing[]): string {
|
|
55
|
-
const underWrapper = new Set<string>();
|
|
56
|
-
for (const w of listing) for (const c of w.wrapper?.charts ?? []) underWrapper.add(chartFullName(w.name, c.name));
|
|
57
|
-
const line = (indent: string, name: string, label: string, unrunnable?: string): string =>
|
|
58
|
-
`${indent}${name} (${label})${unrunnable ? ` unrunnable: ${unrunnable}` : ""}`;
|
|
59
|
-
const out = ["workflows:"];
|
|
60
|
-
for (const w of listing) {
|
|
61
|
-
if (w.origin !== "machine" && underWrapper.has(w.name)) continue;
|
|
62
|
-
const wrapper = w.wrapper;
|
|
63
|
-
const label = wrapper
|
|
64
|
-
? `${ORIGIN_LABEL[w.origin]}, Chart wrapper: body "${wrapper.body}"${wrapper.oneShot ? ", one-shot" : ""}`
|
|
65
|
-
: ORIGIN_LABEL[w.origin];
|
|
66
|
-
out.push(line(" ", w.name, label, w.unrunnable));
|
|
67
|
-
for (const c of wrapper?.charts ?? []) {
|
|
68
|
-
out.push(line(" ", chartFullName(w.name, c.name), ORIGIN_LABEL[c.origin], c.unrunnable));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (out.length === 1) out.push(" none registered — `jr2 up` publishes the Assembly");
|
|
72
|
-
return out.join("\n");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** A Chart's Workflow name, `<wrapper>/<name>` (ADR-0070), whether the listing gives it short or whole. */
|
|
76
|
-
function chartFullName(wrapper: string, name: string): string {
|
|
77
|
-
return name.includes("/") ? name : `${wrapper}/${name}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The run as `jr2 runs` lists it: `dispatchedBy` after its id when a `dispatch` Effect started it
|
|
81
|
-
* (ADR-0071), and `placing` after that when any Workspace in it is in `placing`. */
|
|
82
|
-
export function markListed(run: RunStatus): RunStatus & { placing?: string[] } {
|
|
83
|
-
const placing = [...(run.value === "placing" ? [""] : []), ...placingIn(run.children ?? [], [])];
|
|
84
|
-
if (!placing.length && !run.dispatchedBy) return run;
|
|
85
|
-
const { runId, dispatchedBy, ...rest } = run;
|
|
86
|
-
return { runId, ...(dispatchedBy ? { dispatchedBy } : {}), ...(placing.length ? { placing } : {}), ...rest };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The paths of every child in `placing`, at any depth — read off the state values alone, which
|
|
90
|
-
* are public (a Workspace's value is the flat state key). */
|
|
91
|
-
function placingIn(children: RunChild[], path: string[]): string[] {
|
|
92
|
-
return children.flatMap((child) => {
|
|
93
|
-
const here = [...path, child.id];
|
|
94
|
-
return [...(child.value === "placing" ? [here.join("/")] : []), ...placingIn(child.children, here)];
|
|
95
|
-
});
|
|
96
|
-
}
|
package/src/commands/send.ts
CHANGED
|
@@ -43,7 +43,7 @@ export async function send(args: string[], io: Io): Promise<number> {
|
|
|
43
43
|
}
|
|
44
44
|
const target = await resolveTarget(io, targetOptions(values));
|
|
45
45
|
try {
|
|
46
|
-
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
46
|
+
const client = new JR2Client(target.url, io.fetch, target.token, target.onBehalfOf);
|
|
47
47
|
const ref = await resolveRunId(client, given);
|
|
48
48
|
if (!ref.ok) {
|
|
49
49
|
activity(io, ref.message);
|
package/src/commands/status.ts
CHANGED
|
@@ -25,8 +25,17 @@
|
|
|
25
25
|
//
|
|
26
26
|
// A failed run (ADR-0073) says its reason beside the result: `failed: <reason>`. The result's
|
|
27
27
|
// `output` holds the whole Failure, `{ reason, detail? }`.
|
|
28
|
+
//
|
|
29
|
+
// A run says beside the result who started it (ADR-0078): `started by: <via>/<name>`, with the
|
|
30
|
+
// name jr2 shows and the party the Caller said it acts for. A run that a `dispatch` started names
|
|
31
|
+
// its first Caller: the verb walks `startedBy` up the chain of runs to the first one that a Caller
|
|
32
|
+
// started, and names the runs between. A run that jr2 started says why (`started by: jr2 (boot)`).
|
|
33
|
+
// A cancelled run says who cancelled it: `cancelled by: <via>/<name>`. A standing run (ADR-0017)
|
|
34
|
+
// says who resumed it (`resumed by:`) and, once a hand-over ended it, which run took over
|
|
35
|
+
// (`handed over to:`). Bare `jr2 status` prints each standing Workflow: live, paused with the number
|
|
36
|
+
// of items it holds, or waiting out a back-off.
|
|
28
37
|
|
|
29
|
-
import type { AssemblyView, RepoStatus } from "@jr2/orchestrator/internal";
|
|
38
|
+
import type { AssemblyView, CallerRecord, RepoStatus, StandingView } from "@jr2/orchestrator/internal";
|
|
30
39
|
import { JR2Client, type FetchLike, type RunStatus, workflowPath } from "../client.ts";
|
|
31
40
|
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
32
41
|
import { activity, result, type Io } from "../output.ts";
|
|
@@ -41,7 +50,7 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
41
50
|
const given = positionals.length === 0 ? undefined : positionals[0]!;
|
|
42
51
|
const target = await resolveTarget(io, targetOptions(values));
|
|
43
52
|
try {
|
|
44
|
-
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
53
|
+
const client = new JR2Client(target.url, io.fetch, target.token, target.onBehalfOf);
|
|
45
54
|
if (given === undefined) return await instanceStatus(client, io, target.url, values.json === true);
|
|
46
55
|
const ref = await resolveRunId(client, given);
|
|
47
56
|
if (!ref.ok) {
|
|
@@ -55,6 +64,9 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
55
64
|
}
|
|
56
65
|
result(io, s);
|
|
57
66
|
const notes = [
|
|
67
|
+
await startedByActivity(client, s),
|
|
68
|
+
cancelledByActivity(s),
|
|
69
|
+
...standingRunLines(s),
|
|
58
70
|
failureActivity(s),
|
|
59
71
|
dispatchedActivity(s),
|
|
60
72
|
...dispatchedLines(s),
|
|
@@ -76,6 +88,70 @@ export function failureActivity(s: Pick<RunStatus, "status" | "output">): string
|
|
|
76
88
|
return typeof reason === "string" ? `failed: ${reason}` : undefined;
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
/** A Caller as one line names it (ADR-0078): `<via>/<name>`, the name jr2 shows, and the party it
|
|
92
|
+
* said it acts for. */
|
|
93
|
+
export function callerText(c: CallerRecord): string {
|
|
94
|
+
return (
|
|
95
|
+
`${c.via}/${c.name}` +
|
|
96
|
+
(c.shown !== undefined && c.shown !== c.name ? ` (${c.shown})` : "") +
|
|
97
|
+
(c.onBehalfOf !== undefined ? ` on behalf of ${c.onBehalfOf}` : "")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The most runs the `started by:` walk reads: a chain of dispatches longer than this is named as
|
|
102
|
+
* cut, never read without end. */
|
|
103
|
+
const STARTED_BY_HOPS = 16;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The `started by:` line (ADR-0078): the run's first Caller. A run that a `dispatch` started is
|
|
107
|
+
* walked up its `startedBy` chain, one read per run, to the first run that a Caller or jr2 started;
|
|
108
|
+
* the line names the runs between, nearest first. A run of the chain that the Caller may not read,
|
|
109
|
+
* or that is no longer known, ends the walk, and the line says so. Undefined for a run that records
|
|
110
|
+
* no starter.
|
|
111
|
+
*/
|
|
112
|
+
export async function startedByActivity(
|
|
113
|
+
client: Pick<JR2Client, "read">,
|
|
114
|
+
s: Pick<RunStatus, "startedBy">,
|
|
115
|
+
): Promise<string | undefined> {
|
|
116
|
+
let by = s.startedBy;
|
|
117
|
+
if (!by) return undefined;
|
|
118
|
+
const through: string[] = [];
|
|
119
|
+
const via = () => (through.length ? `, through ${through.join(" ← ")}` : "");
|
|
120
|
+
while (by.kind === "run") {
|
|
121
|
+
if (through.length >= STARTED_BY_HOPS) {
|
|
122
|
+
return `started by: not read — the chain is longer than ${STARTED_BY_HOPS} runs${via()}`;
|
|
123
|
+
}
|
|
124
|
+
const hop = `run ${by.run} (${by.slot})`;
|
|
125
|
+
let next: RunStatus | undefined;
|
|
126
|
+
try {
|
|
127
|
+
next = await client.read(by.run);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return `started by: not read — ${hop}: ${e instanceof Error ? e.message : String(e)}${via()}`;
|
|
130
|
+
}
|
|
131
|
+
if (!next) return `started by: not known — ${hop} is no longer known${via()}`;
|
|
132
|
+
through.push(hop);
|
|
133
|
+
if (!next.startedBy) return `started by: not recorded${via()}`;
|
|
134
|
+
by = next.startedBy;
|
|
135
|
+
}
|
|
136
|
+
if (by.kind === "jr2") return `started by: jr2 (${by.why})${via()}`;
|
|
137
|
+
const { kind: _kind, ...caller } = by;
|
|
138
|
+
return `started by: ${callerText(caller)}${via()}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A standing run's lines (ADR-0017): the Caller whose `jr2 run` resumed it, and the run that took
|
|
142
|
+
* over from it at a hand-over. */
|
|
143
|
+
export function standingRunLines(s: Pick<RunStatus, "resumedBy" | "handedOverTo">): string[] {
|
|
144
|
+
return [
|
|
145
|
+
...(s.resumedBy ? [`resumed by: ${callerText(s.resumedBy)}`] : []),
|
|
146
|
+
...(s.handedOverTo ? [`handed over to: ${s.handedOverTo}`] : []),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The `cancelled by:` line for a run a Caller cancelled (ADR-0078). */
|
|
151
|
+
export function cancelledByActivity(s: Pick<RunStatus, "cancelledBy">): string | undefined {
|
|
152
|
+
return s.cancelledBy ? `cancelled by: ${callerText(s.cancelledBy)}` : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
79
155
|
/** The most `dispatched:` lines `jr2 status` prints: a watcher dispatches on every pass, and the
|
|
80
156
|
* result carries the whole list. */
|
|
81
157
|
const DISPATCHED_LINES = 10;
|
|
@@ -105,7 +181,16 @@ export function worktreeLines(s: RunStatus): string[] {
|
|
|
105
181
|
export function dispatchedActivity(s: RunStatus): string | undefined {
|
|
106
182
|
const by = s.dispatchedBy;
|
|
107
183
|
if (!by) return undefined;
|
|
108
|
-
|
|
184
|
+
// A Pool of runs (ADR-0017) names the item's key, and the Hook delivery that carried it.
|
|
185
|
+
const pool =
|
|
186
|
+
by.key !== undefined
|
|
187
|
+
? `, key ${by.key}${
|
|
188
|
+
by.hook
|
|
189
|
+
? `, hook ${by.hook.name}, delivery ${by.hook.delivery}${by.hook.onBehalfOf ? `, on behalf of ${by.hook.onBehalfOf}` : ""}`
|
|
190
|
+
: ""
|
|
191
|
+
}`
|
|
192
|
+
: "";
|
|
193
|
+
return `dispatched by: ${by.run} (${by.slot}), depth ${by.depth}${by.instance ? `, from Orchestrator ${by.instance}` : ""}${pool}`;
|
|
109
194
|
}
|
|
110
195
|
|
|
111
196
|
/** The `chart: bound` line for a run that binds a Chart (ADR-0070), or undefined for one that binds
|
|
@@ -144,17 +229,19 @@ async function instanceStatus(client: JR2Client, io: Io, url: string, json: bool
|
|
|
144
229
|
|
|
145
230
|
/** `GET /assembly` (ADR-0069): open, like `GET /workflows`, so it needs no token. An answer that
|
|
146
231
|
* cannot be read is reported as the reason, never as a failure of the verb. */
|
|
147
|
-
async function readAssembly(fetchImpl: FetchLike, url: string): Promise<
|
|
232
|
+
async function readAssembly(fetchImpl: FetchLike, url: string): Promise<InstanceAssembly | { error: string }> {
|
|
148
233
|
try {
|
|
149
234
|
const res = await fetchImpl(`${url.replace(/\/+$/, "")}/assembly`);
|
|
150
235
|
if (!res.ok) return { error: `GET /assembly answered HTTP ${res.status}` };
|
|
151
236
|
// Read loosely: a `held` that is absent or not a list reads as none held, so a report never
|
|
152
237
|
// fails on the shape of one field (ADR-0048: a report is not a failure of the asking).
|
|
153
238
|
const view = (await res.json()) as Partial<AssemblyView>;
|
|
239
|
+
const standing = (view as { standing?: unknown }).standing;
|
|
154
240
|
return {
|
|
155
241
|
kitVersion: view.kitVersion ?? "",
|
|
156
242
|
current: view.current ?? null,
|
|
157
243
|
held: Array.isArray(view.held) ? view.held : [],
|
|
244
|
+
...(Array.isArray(standing) ? { standing: standing as StandingView[] } : {}),
|
|
158
245
|
};
|
|
159
246
|
} catch (e) {
|
|
160
247
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
@@ -168,7 +255,7 @@ async function readAssembly(fetchImpl: FetchLike, url: string): Promise<Assembly
|
|
|
168
255
|
* The current Assembly prints first, whatever order `held` uses; the other held Assemblies follow
|
|
169
256
|
* in the server's order.
|
|
170
257
|
*/
|
|
171
|
-
function assemblyLines(view:
|
|
258
|
+
function assemblyLines(view: InstanceAssembly | { error: string } | undefined): string {
|
|
172
259
|
if (view === undefined) return "";
|
|
173
260
|
if ("error" in view) return `assembly: unknown — ${view.error}\n`;
|
|
174
261
|
const current = view.current;
|
|
@@ -187,9 +274,37 @@ function assemblyLines(view: AssemblyView | { error: string } | undefined): stri
|
|
|
187
274
|
if (held.hash === current?.hash) continue;
|
|
188
275
|
out += line(held.hash, held.kitVersion, held.runs.length, false);
|
|
189
276
|
}
|
|
277
|
+
for (const s of view.standing ?? []) out += `${standingLine(s)}\n`;
|
|
190
278
|
return out;
|
|
191
279
|
}
|
|
192
280
|
|
|
281
|
+
/** What `GET /assembly` answers, with its standing Workflows (ADR-0017). */
|
|
282
|
+
export type InstanceAssembly = AssemblyView & { standing?: StandingView[] };
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* One standing Workflow as one line (ADR-0017): its Pool's chain to its callee, and its state. A
|
|
286
|
+
* paused one says why, how many items it holds and how to resume it; one with no live run and no
|
|
287
|
+
* pause waits out the back-off of a hand-over.
|
|
288
|
+
*/
|
|
289
|
+
export function standingLine(s: StandingView): string {
|
|
290
|
+
const items = `${s.held} held item${s.held === 1 ? "" : "s"}`;
|
|
291
|
+
const head = `standing ${s.workflow} → ${s.callee}`;
|
|
292
|
+
if (s.state === "live") return `${head}: live, run ${s.run ?? "?"}, ${items}`;
|
|
293
|
+
if (s.state === "paused") {
|
|
294
|
+
const why =
|
|
295
|
+
s.why === "errors"
|
|
296
|
+
? "4 runs in a row in which no item that the run took started a run; the last one ended error or failed"
|
|
297
|
+
: s.why === "bound"
|
|
298
|
+
? "4 runs in a row in which no item that the run took started a run; the last one took its bound of items"
|
|
299
|
+
: "a cancel";
|
|
300
|
+
return (
|
|
301
|
+
`${head}: paused (${why}), ${items}${s.run ? ` in run ${s.run}` : ""} — \`jr2 run ${s.workflow}\` resumes it, ` +
|
|
302
|
+
"and `--drop-held` resumes it with no held item"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return `${head}: no live run — jr2 starts its next run after the back-off`;
|
|
306
|
+
}
|
|
307
|
+
|
|
193
308
|
/** The human table — the held Assemblies, current first, one line per Repo, one per node under it, then the
|
|
194
309
|
* hints for what was seen. */
|
|
195
310
|
export function renderInstanceStatus({
|
|
@@ -197,7 +312,7 @@ export function renderInstanceStatus({
|
|
|
197
312
|
dataPlane,
|
|
198
313
|
repos,
|
|
199
314
|
}: {
|
|
200
|
-
assembly?:
|
|
315
|
+
assembly?: InstanceAssembly | { error: string };
|
|
201
316
|
dataPlane: boolean;
|
|
202
317
|
repos: RepoStatus[];
|
|
203
318
|
}): string {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// `jr2 steer` (ADR-0002): a Caller's Steering message to one Agent of a live run.
|
|
2
|
+
//
|
|
3
|
+
// jr2 steer <runId|abbrev> <agent> "<text>" send the message
|
|
4
|
+
// jr2 steer <runId|abbrev> list the run's Agent slots
|
|
5
|
+
//
|
|
6
|
+
// The address is the Agent SLOT, as the run's list names it (`<machine path>/<agent>`), never an
|
|
7
|
+
// Instance ID: jr2 computes those, and a fresh Turn gets a new one. An open Turn of the slot hears
|
|
8
|
+
// the message at its next step boundary; otherwise the slot's next Turn in the same Machine
|
|
9
|
+
// instance hears it. The run id resolves first, as for every write (ADR-0009).
|
|
10
|
+
|
|
11
|
+
import { JR2Client } from "../client.ts";
|
|
12
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
13
|
+
import { activity, result, type Io } from "../output.ts";
|
|
14
|
+
import { resolveRunId } from "../run-id.ts";
|
|
15
|
+
import { commandNamed, parseCommand, renderCommand } from "../command-table.ts";
|
|
16
|
+
|
|
17
|
+
export async function steer(args: string[], io: Io): Promise<number> {
|
|
18
|
+
const { values, positionals } = parseCommand("steer", args);
|
|
19
|
+
const [given, agent, text] = positionals;
|
|
20
|
+
if (!given || (agent !== undefined && text === undefined) || positionals.length > 3) {
|
|
21
|
+
activity(io, renderCommand(commandNamed("steer")));
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
const target = await resolveTarget(io, targetOptions(values));
|
|
25
|
+
try {
|
|
26
|
+
const client = new JR2Client(target.url, io.fetch, target.token, target.onBehalfOf);
|
|
27
|
+
const ref = await resolveRunId(client, given);
|
|
28
|
+
if (!ref.ok) {
|
|
29
|
+
activity(io, ref.message);
|
|
30
|
+
return ref.code;
|
|
31
|
+
}
|
|
32
|
+
if (agent === undefined || text === undefined) {
|
|
33
|
+
result(io, await client.agents(ref.runId));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const sent = await client.steer(ref.runId, agent, text);
|
|
37
|
+
activity(
|
|
38
|
+
io,
|
|
39
|
+
sent.open
|
|
40
|
+
? `sent a Steering message to ${sent.agent} on ${ref.runId}: its open Turn hears it at its next step boundary, or its next Turn hears it if this Turn ends first`
|
|
41
|
+
: `sent a Steering message to ${sent.agent} on ${ref.runId}: its next Turn hears it`,
|
|
42
|
+
);
|
|
43
|
+
return 0;
|
|
44
|
+
} finally {
|
|
45
|
+
target.close?.();
|
|
46
|
+
}
|
|
47
|
+
}
|