@grida/fx 0.1.0 → 0.2.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.
@@ -0,0 +1,107 @@
1
+ /** Saved evidence and independent observation; neither owns execution or a viewer. */
2
+ import type { InspectOptions } from "./api.js";
3
+ import type { Graph, Inspection, RunEvent } from "./documents.js";
4
+ import { type CallOptions, type Exit } from "./engine.js";
5
+ import { FxError } from "./errors.js";
6
+ /** The complete recorded event envelope. Unknown events and fields remain readable. */
7
+ export interface ObservedRunEvent extends RunEvent {
8
+ kind: "fx-run-events-v1";
9
+ invocation_id: string;
10
+ plan: string;
11
+ offset_ms: number;
12
+ }
13
+ export interface RunSnapshot {
14
+ kind: "fx-run-snapshot-v1";
15
+ cursor: string;
16
+ plan: Graph;
17
+ events: ObservedRunEvent[];
18
+ [field: string]: unknown;
19
+ }
20
+ export interface RunEventBatch {
21
+ kind: "fx-run-event-batch-v1";
22
+ cursor: string;
23
+ events: ObservedRunEvent[];
24
+ has_more: boolean;
25
+ [field: string]: unknown;
26
+ }
27
+ export interface RunObservationFailure {
28
+ kind: "fx-run-observation-error-v1";
29
+ code: string;
30
+ message: string;
31
+ [field: string]: unknown;
32
+ }
33
+ /** A reader failure with the engine's stable code and complete public error document. */
34
+ export declare class RunObservationError extends FxError {
35
+ readonly code: string;
36
+ readonly document: RunObservationFailure;
37
+ constructor(document: RunObservationFailure, done: Exit);
38
+ }
39
+ export interface RunReadOptions {
40
+ /** Stops this reader only; it never cancels the separately owned workflow execution. */
41
+ readonly signal?: AbortSignal;
42
+ }
43
+ export interface RunExportOptions extends RunReadOptions {
44
+ /** Include recorded values after deliberate disclosure review. Default: false. */
45
+ readonly includeValues?: boolean;
46
+ /** Include recorded diagnostic messages. Default: false. */
47
+ readonly includeDiagnostics?: boolean;
48
+ }
49
+ /** The engine's wire receipt. Counts cover included artifacts, excluding manifest.json. */
50
+ export interface RunExportResult {
51
+ kind: "fx-run-export-result-v1";
52
+ manifest_sha256: string;
53
+ output: string;
54
+ files: number;
55
+ bytes: number;
56
+ omitted_artifacts: {
57
+ digest: string;
58
+ reason: string;
59
+ }[];
60
+ [field: string]: unknown;
61
+ }
62
+ export interface RunExportFailure {
63
+ kind: "fx-run-export-error-v1";
64
+ code: string;
65
+ message: string;
66
+ [field: string]: unknown;
67
+ }
68
+ export declare class RunExportError extends FxError {
69
+ readonly code: string;
70
+ readonly document: RunExportFailure;
71
+ constructor(document: RunExportFailure, done: Exit);
72
+ }
73
+ export interface RunEventsOptions extends RunReadOptions {
74
+ /** Opaque acknowledged cursor; omit to read from the beginning. */
75
+ readonly after?: string;
76
+ /** Maximum events per batch, 1–1024 (default: 256). */
77
+ readonly limit?: number;
78
+ }
79
+ export interface RunFollowOptions extends RunEventsOptions {
80
+ /** Poll only after draining readable batches (default: 1000 milliseconds). */
81
+ readonly pollIntervalMs?: number;
82
+ }
83
+ /** One resolved run folder and its sampled inspection. Reads stay pinned to this folder.
84
+ * Inspection and later snapshots are independent samples; only each snapshot and its cursor
85
+ * share one consistent prefix. No exit code or process-liveness claim is synthesized. */
86
+ export declare class RunRecord {
87
+ readonly runDir: string;
88
+ readonly inspection: Inspection;
89
+ private readonly context;
90
+ constructor(runDir: string, inspection: Inspection, context?: CallOptions);
91
+ /** Capture the saved plan, complete readable event prefix and matching attachment cursor. */
92
+ snapshot(options?: RunReadOptions): Promise<RunSnapshot>;
93
+ /** Export terminal, inactive evidence to a new directory, without execution or publication.
94
+ * Relative destinations resolve against the caller's current directory. Values and
95
+ * diagnostics are withheld by default; included media still needs publication review. */
96
+ export(destination: string, options?: RunExportOptions): Promise<RunExportResult>;
97
+ /** Read bounded events after a cursor, or from the beginning when after is omitted. */
98
+ events(options?: RunEventsOptions): Promise<RunEventBatch>;
99
+ /** Follow nonempty batches continuously, including later resumes, until break or abort.
100
+ * Terminal events end an invocation, not this reader, and do not certify owner cleanup.
101
+ * Start after snapshot.cursor to attach without missing events. Reader errors surface;
102
+ * no missing-run waits, automatic reattachment or execution retries are performed. */
103
+ follow(options?: RunFollowOptions): AsyncGenerator<RunEventBatch>;
104
+ }
105
+ /** Resolve a folder or recorded selector once, without planning, executing or starting a service.
106
+ * Later reads use the pinned absolute folder. The loading signal is not retained by the record. */
107
+ export declare function loadRun(target: string, options?: InspectOptions): Promise<RunRecord>;
package/dist/record.js ADDED
@@ -0,0 +1,246 @@
1
+ /** Saved evidence and independent observation; neither owns execution or a viewer. */
2
+ import { resolve } from "node:path";
3
+ import { call, failure } from "./engine.js";
4
+ import { FxError } from "./errors.js";
5
+ /** A reader failure with the engine's stable code and complete public error document. */
6
+ export class RunObservationError extends FxError {
7
+ code;
8
+ document;
9
+ constructor(document, done) {
10
+ super(document.message, {
11
+ exitCode: done.status, signal: done.signal, stdout: done.stdout,
12
+ stderr: done.stderr, args: done.args,
13
+ });
14
+ this.name = "RunObservationError";
15
+ this.code = document.code;
16
+ this.document = document;
17
+ }
18
+ }
19
+ export class RunExportError extends FxError {
20
+ code;
21
+ document;
22
+ constructor(document, done) {
23
+ super(document.message, {
24
+ exitCode: done.status, signal: done.signal, stdout: done.stdout,
25
+ stderr: done.stderr, args: done.args,
26
+ });
27
+ this.name = "RunExportError";
28
+ this.code = document.code;
29
+ this.document = document;
30
+ }
31
+ }
32
+ function object(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ function text(value) {
36
+ return typeof value === "string" && value !== "";
37
+ }
38
+ function cursor(value) {
39
+ return text(value) && !value.includes("\0");
40
+ }
41
+ function event(value) {
42
+ return object(value) && value.kind === "fx-run-events-v1" && text(value.event)
43
+ && text(value.invocation_id) && typeof value.plan === "string"
44
+ && /^[a-f0-9]{64}$/.test(value.plan) && Number.isSafeInteger(value.offset_ms)
45
+ && value.offset_ms >= 0;
46
+ }
47
+ function exportResult(done) {
48
+ let value;
49
+ try {
50
+ value = JSON.parse(done.stdout);
51
+ }
52
+ catch {
53
+ throw failure("export", done);
54
+ }
55
+ if (done.status === 2 && object(value) && value.kind === "fx-run-export-error-v1"
56
+ && text(value.code) && text(value.message)) {
57
+ throw new RunExportError(value, done);
58
+ }
59
+ if (done.status !== 0 || !object(value) || value.kind !== "fx-run-export-result-v1"
60
+ || typeof value.manifest_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.manifest_sha256)
61
+ || !cursor(value.output) || !Number.isSafeInteger(value.files) || value.files < 0
62
+ || !Number.isSafeInteger(value.bytes) || value.bytes < 0
63
+ || !Array.isArray(value.omitted_artifacts) || !value.omitted_artifacts.every((item) => object(item) && typeof item.digest === "string" && /^[a-f0-9]{64}$/.test(item.digest) && text(item.reason))) {
64
+ throw failure("export", done);
65
+ }
66
+ return value;
67
+ }
68
+ function response(done, snapshot, options = {}) {
69
+ let value;
70
+ try {
71
+ value = JSON.parse(done.stdout);
72
+ }
73
+ catch {
74
+ throw failure("observe", done);
75
+ }
76
+ if (done.status === 2 && object(value) && value.kind === "fx-run-observation-error-v1"
77
+ && text(value.code) && text(value.message)) {
78
+ throw new RunObservationError(value, done);
79
+ }
80
+ if (done.status !== 0 || !object(value) || !cursor(value.cursor)
81
+ || !Array.isArray(value.events) || !value.events.every(event)) {
82
+ throw failure("observe", done);
83
+ }
84
+ if (snapshot) {
85
+ if (value.kind !== "fx-run-snapshot-v1" || !object(value.plan)
86
+ || value.plan.kind !== "fx-graph-v1")
87
+ throw failure("observe", done);
88
+ return value;
89
+ }
90
+ if (value.kind !== "fx-run-event-batch-v1" || typeof value.has_more !== "boolean"
91
+ || value.events.length > (options.limit ?? 256)
92
+ || (value.events.length === 0 && (value.has_more || (options.after !== undefined && value.cursor !== options.after)))
93
+ || (value.events.length > 0 && value.cursor === options.after)) {
94
+ throw failure("observe", done);
95
+ }
96
+ return value;
97
+ }
98
+ function eventArgs(options) {
99
+ const args = [];
100
+ if (options.after !== undefined) {
101
+ if (!text(options.after) || options.after.includes("\0")) {
102
+ throw new TypeError("after is a nonempty opaque cursor without NUL");
103
+ }
104
+ args.push(`--after=${options.after}`);
105
+ }
106
+ if (options.limit !== undefined) {
107
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1024) {
108
+ throw new TypeError("limit is an integer from 1 to 1024");
109
+ }
110
+ args.push(`--limit=${options.limit}`);
111
+ }
112
+ return args;
113
+ }
114
+ function sleep(milliseconds, signal) {
115
+ signal?.throwIfAborted();
116
+ return new Promise((done, reject) => {
117
+ const finish = () => { signal?.removeEventListener("abort", aborted); done(); };
118
+ const timer = setTimeout(finish, milliseconds);
119
+ const aborted = () => {
120
+ clearTimeout(timer);
121
+ signal?.removeEventListener("abort", aborted);
122
+ reject(signal?.reason);
123
+ };
124
+ signal?.addEventListener("abort", aborted, { once: true });
125
+ });
126
+ }
127
+ /** One resolved run folder and its sampled inspection. Reads stay pinned to this folder.
128
+ * Inspection and later snapshots are independent samples; only each snapshot and its cursor
129
+ * share one consistent prefix. No exit code or process-liveness claim is synthesized. */
130
+ export class RunRecord {
131
+ runDir;
132
+ inspection;
133
+ context;
134
+ constructor(runDir, inspection, context = {}) {
135
+ this.runDir = resolve(context.cwd ?? process.cwd(), runDir);
136
+ this.inspection = inspection;
137
+ this.context = {
138
+ cwd: resolve(context.cwd ?? process.cwd()),
139
+ ...(context.env !== undefined ? { env: { ...context.env } } : {}),
140
+ };
141
+ }
142
+ /** Capture the saved plan, complete readable event prefix and matching attachment cursor. */
143
+ async snapshot(options = {}) {
144
+ const done = await call(["observe", this.runDir, "--snapshot"], {
145
+ ...this.context, ...(options.signal !== undefined ? { signal: options.signal } : {}),
146
+ });
147
+ return response(done, true);
148
+ }
149
+ /** Export terminal, inactive evidence to a new directory, without execution or publication.
150
+ * Relative destinations resolve against the caller's current directory. Values and
151
+ * diagnostics are withheld by default; included media still needs publication review. */
152
+ async export(destination, options = {}) {
153
+ if (!cursor(destination))
154
+ throw new TypeError("destination is a nonempty filesystem path without NUL");
155
+ for (const option of [options.includeValues, options.includeDiagnostics]) {
156
+ if (option !== undefined && typeof option !== "boolean")
157
+ throw new TypeError("disclosure options are true or false");
158
+ }
159
+ const args = ["export", this.runDir, `--out=${resolve(destination)}`, "--json"];
160
+ if (options.includeValues === true)
161
+ args.push("--include-values");
162
+ if (options.includeDiagnostics === true)
163
+ args.push("--include-diagnostics");
164
+ const done = await call(args, {
165
+ ...this.context, ...(options.signal !== undefined ? { signal: options.signal } : {}),
166
+ });
167
+ return exportResult(done);
168
+ }
169
+ /** Read bounded events after a cursor, or from the beginning when after is omitted. */
170
+ async events(options = {}) {
171
+ const args = eventArgs(options);
172
+ const done = await call(["observe", this.runDir, ...args], {
173
+ ...this.context, ...(options.signal !== undefined ? { signal: options.signal } : {}),
174
+ });
175
+ return response(done, false, options);
176
+ }
177
+ /** Follow nonempty batches continuously, including later resumes, until break or abort.
178
+ * Terminal events end an invocation, not this reader, and do not certify owner cleanup.
179
+ * Start after snapshot.cursor to attach without missing events. Reader errors surface;
180
+ * no missing-run waits, automatic reattachment or execution retries are performed. */
181
+ async *follow(options = {}) {
182
+ eventArgs(options);
183
+ const interval = options.pollIntervalMs ?? 1000;
184
+ if (!Number.isFinite(interval) || interval <= 0 || interval > 2_147_483_647) {
185
+ throw new TypeError("pollIntervalMs is positive, finite and at most 2147483647");
186
+ }
187
+ let after = options.after;
188
+ for (;;) {
189
+ options.signal?.throwIfAborted();
190
+ const batch = await this.events({
191
+ ...(after !== undefined ? { after } : {}),
192
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
193
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
194
+ });
195
+ // A slow consumer or one mutating its received document cannot change our position.
196
+ after = batch.cursor;
197
+ const more = batch.has_more;
198
+ if (batch.events.length > 0)
199
+ yield batch;
200
+ if (!more)
201
+ await sleep(interval, options.signal);
202
+ }
203
+ }
204
+ }
205
+ /** Resolve a folder or recorded selector once, without planning, executing or starting a service.
206
+ * Later reads use the pinned absolute folder. The loading signal is not retained by the record. */
207
+ export async function loadRun(target, options = {}) {
208
+ if (!text(target) || target.startsWith("-") || target.includes("\0")) {
209
+ throw new TypeError("target is a nonempty saved-run selector without a leading '-' or NUL");
210
+ }
211
+ if (options.verify !== undefined && typeof options.verify !== "boolean") {
212
+ throw new TypeError("verify is true or false");
213
+ }
214
+ const cwd = resolve(options.cwd ?? process.cwd());
215
+ const done = await call(["inspect", target, "--json", ...(options.verify === true ? ["--verify"] : [])], {
216
+ cwd,
217
+ ...(options.env !== undefined ? { env: options.env } : {}),
218
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
219
+ });
220
+ if (done.status !== 0 && !(options.verify === true && done.status === 1)) {
221
+ throw failure("inspect", done);
222
+ }
223
+ let inspection;
224
+ try {
225
+ inspection = JSON.parse(done.stdout);
226
+ }
227
+ catch {
228
+ throw failure("inspect", done);
229
+ }
230
+ if (!object(inspection) || !object(inspection.run))
231
+ throw failure("inspect", done);
232
+ if (done.status === 1) {
233
+ const verification = inspection.verification;
234
+ if (!object(verification) || verification.verified !== false
235
+ || !Array.isArray(verification.problems) || verification.problems.length === 0
236
+ || !verification.problems.every(text))
237
+ throw failure("inspect", done);
238
+ }
239
+ const folder = inspection.run.folder;
240
+ if (!text(folder) || folder.includes("\0")) {
241
+ throw failure("inspect", done);
242
+ }
243
+ return new RunRecord(resolve(cwd, folder), inspection, {
244
+ cwd, ...(options.env !== undefined ? { env: options.env } : {}),
245
+ });
246
+ }
package/dist/request.d.ts CHANGED
@@ -29,6 +29,10 @@ export interface RunOptions extends PlanOptions {
29
29
  /** The run folder, relative to `cwd` (`--run`); a folder holding a run of the same plan is
30
30
  * resumed. Default: a new folder under the project's runs folder. */
31
31
  readonly runDir?: string;
32
+ /** Create a named run (`--name`); an existing name is refused by the engine. */
33
+ readonly name?: string;
34
+ /** Continue an existing named run (`--resume`) with the same target and inputs. */
35
+ readonly resume?: string;
32
36
  /** Copy outputs out after the run (`--deliver OUTPUT=PATH`): `{ output: path }`, with `{key}`
33
37
  * in the path for each element of a collection. */
34
38
  readonly deliver?: Readonly<Record<string, string>>;
package/dist/request.js CHANGED
@@ -59,6 +59,16 @@ export function planningArgs(request, inputsFile) {
59
59
  /** The options of `run` alone. */
60
60
  export function runArgs(options) {
61
61
  const args = [];
62
+ const selected = ["runDir", "name", "resume"];
63
+ if (selected.filter((name) => options[name] !== undefined).length > 1) {
64
+ throw new TypeError("runDir, name and resume are mutually exclusive");
65
+ }
66
+ for (const name of selected) {
67
+ const value = options[name];
68
+ if (value !== undefined && (typeof value !== "string" || value === "" || value.includes("\0"))) {
69
+ throw new TypeError(`${name} is a nonempty string without NUL`);
70
+ }
71
+ }
62
72
  if (options.live !== undefined && typeof options.live !== "boolean") {
63
73
  throw new TypeError(`live is true or false, not ${JSON.stringify(options.live)}`);
64
74
  }
@@ -69,11 +79,12 @@ export function runArgs(options) {
69
79
  args.push(`--yes-up-to=${amountText("yesUpTo", options.yesUpTo)}`);
70
80
  }
71
81
  if (options.runDir !== undefined) {
72
- if (typeof options.runDir !== "string" || options.runDir === "") {
73
- throw new TypeError("runDir is a folder path");
74
- }
75
82
  args.push(`--run=${options.runDir}`);
76
83
  }
84
+ if (options.name !== undefined)
85
+ args.push(`--name=${options.name}`);
86
+ if (options.resume !== undefined)
87
+ args.push(`--resume=${options.resume}`);
77
88
  for (const [name, path] of Object.entries(options.deliver ?? {})) {
78
89
  if (typeof path !== "string" || name === "" || path === "") {
79
90
  throw new TypeError(`deliver.${name} is a path`);
package/dist/runs.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ /** A project's runs through the engine's public command line (spec/store.md §8 and §9): listing
2
+ * them, and removing named ones. The engine finds the project from cwd, selects, locks and
3
+ * removes; this module only reads what it prints. */
4
+ import { type CallOptions, type Exit } from "./engine.js";
5
+ import { FxError } from "./errors.js";
6
+ /** Where a run sits: `allocated` (`<runs>/<id>/<YYYY-MM-DD>-<n>`), `named`, `explicit`
7
+ * (elsewhere in the runs tree) or `external` (outside it, known from the run index or catalog). */
8
+ export type RunPlacement = "allocated" | "named" | "explicit" | "external";
9
+ /** A run's state as the listing reads its log. `empty`: no plan.json at a run's position;
10
+ * `removing`: a removal's tombstone; `missing`: an external folder that is gone. Recorded
11
+ * `unfinished` is not evidence that a process is alive. */
12
+ export type RunState = "planned" | "unfinished" | "succeeded" | "failed" | "incomplete" | "cancelled" | "empty" | "removing" | "missing";
13
+ /** One run of fx-run-list-v1. Wire fields retain lower_snake_case names. */
14
+ export interface RunEntry {
15
+ /** The run folder as the engine printed it: relative to cwd, or absolute. */
16
+ readonly folder: string;
17
+ /** `folder` resolved once against cwd. */
18
+ readonly runDir: string;
19
+ /** The recorded workflow id, or the one its position gives; null when neither is known. */
20
+ readonly workflow: string | null;
21
+ /** The recorded workflow source; null for a folder without a plan. */
22
+ readonly source: string | null;
23
+ /** The run's name: recorded, or given by a named position. */
24
+ readonly name: string | null;
25
+ readonly placement: RunPlacement;
26
+ readonly state: RunState;
27
+ /** The first start's creation time (RFC 3339); null for a folder without a plan. */
28
+ readonly created_at: string | null;
29
+ /** What the run was charged; null before it ended. */
30
+ readonly charged_usd: number | null;
31
+ readonly stand_in: boolean;
32
+ /** Why an incomplete run stopped. */
33
+ readonly stopped?: string;
34
+ }
35
+ /** What removing one run did, or would do in a preview. */
36
+ export interface RunRemovalEntry extends RunEntry {
37
+ /** `would_*`: a preview. `forgotten`: a missing external folder's index and catalog entries
38
+ * removed. `refused`: nothing of this run was removed. `partial`: the run is gone as a run but
39
+ * its tombstone remains; removing it again finishes it. */
40
+ readonly outcome: "would_remove" | "would_forget" | "removed" | "forgotten" | "refused" | "partial";
41
+ /** Present when refused or partial. */
42
+ readonly code?: "active" | "holds_pick" | "changed" | "not_removable" | "io";
43
+ /** Present when refused or partial. */
44
+ readonly message?: string;
45
+ /** Bytes of files whose every link was inside the run folder. */
46
+ readonly freed_bytes: number;
47
+ /** Bytes of files still linked from elsewhere (the store, other runs); not freed. */
48
+ readonly cache_bytes: number;
49
+ }
50
+ /** Why a selection passed a run over. Explicit runs are never passed over, so a removal by
51
+ * this SDK normally reports none. */
52
+ export type RunRemovalSkip = "named" | "explicit" | "external" | "holds_pick" | "young" | "holds_runs";
53
+ /** fx-run-removal-v1: what a removal did, or would do when `applied` is false. */
54
+ export interface RunRemoval {
55
+ /** False for a preview: nothing was removed. */
56
+ readonly applied: boolean;
57
+ readonly runs: readonly RunRemovalEntry[];
58
+ /** How many runs a selection passed over, by reason. */
59
+ readonly skipped: Readonly<Partial<Record<RunRemovalSkip, number>>>;
60
+ readonly freed_bytes: number;
61
+ readonly cache_bytes: number;
62
+ }
63
+ /** The fx-run-removal-v1 error document: a RUN the engine would not resolve. Nothing was removed. */
64
+ export interface RunRemovalFailure {
65
+ readonly kind: "fx-run-removal-v1";
66
+ readonly applied: false;
67
+ readonly error: {
68
+ readonly code: "invalid_target" | "ambiguous_target" | "not_a_run";
69
+ readonly message: string;
70
+ readonly run: string | null;
71
+ };
72
+ }
73
+ /** A removal the engine refused before removing anything, with its stable code, the RUN it
74
+ * names and the complete error document. Usage mistakes and malformed output reject with
75
+ * ordinary FxError; a refused or partial run does not reject (read each entry's `outcome`). */
76
+ export declare class RunRemovalError extends FxError {
77
+ readonly code: RunRemovalFailure["error"]["code"];
78
+ readonly run: string | null;
79
+ readonly result: RunRemovalFailure;
80
+ constructor(result: RunRemovalFailure, done: Exit);
81
+ }
82
+ /** Options of listRuns. */
83
+ export interface ListRunsOptions extends CallOptions {
84
+ /** Only the runs of this workflow id. */
85
+ readonly workflow?: string;
86
+ }
87
+ /** Options of removeRuns. */
88
+ export interface RemoveRunsOptions extends CallOptions {
89
+ /** Report what would be removed and remove nothing (default: false). */
90
+ readonly preview?: boolean;
91
+ }
92
+ /** The runs of the project found from cwd, newest first. Reads only: no lock, no writes.
93
+ * Folders the engine could not read are left out. */
94
+ export declare function listRuns(options?: ListRunsOptions): Promise<RunEntry[]>;
95
+ /** Remove exactly the named runs: run folders (relative to cwd) or `WORKFLOW_ID/NAME` of named
96
+ * runs; select them with {@link listRuns} first. Every RUN is resolved before anything is removed,
97
+ * and one that is not a run rejects with {@link RunRemovalError}. A run that is active, or the last
98
+ * holding a pick, is refused without rejecting: read each entry's `outcome`. Aborting a removal
99
+ * may leave a run `partial`; removing it again finishes it. */
100
+ export declare function removeRuns(runs: readonly string[], options?: RemoveRunsOptions): Promise<RunRemoval>;