unfazed 0.1.0 → 0.3.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/dist/unfazed.d.ts CHANGED
@@ -1,8 +1,35 @@
1
1
  import { UnfazedClient, type ClientOptions, type RequestOptions } from "./client.js";
2
- import type { CreateDeploymentInput, DeploymentFieldsFragment, ListDeploymentsInput, ServiceMode, ServiceRuntime } from "./generated/graphql.js";
2
+ import type { CreateDeploymentInput, DeploymentFieldsFragment, ListDeploymentsInput, RunFieldsFragment, RunStatus, ServiceMode, ServiceRuntime, SdkCreateWorkspaceMutation } from "./generated/graphql.js";
3
3
  import { type WaitOptions } from "./wait.js";
4
4
  /** A deployment as the workflow layer returns it. */
5
5
  export type Deployment = DeploymentFieldsFragment;
6
+ /** An agent run as the workflow layer returns it. */
7
+ export type Run = RunFieldsFragment;
8
+ /**
9
+ * A workspace as {@link Unfazed.createWorkspace} and
10
+ * {@link Unfazed.ensureWorkspace} return it.
11
+ *
12
+ * Derived from the generated mutation type rather than hand-written, so the
13
+ * two documents selecting these fields cannot drift apart without a compile
14
+ * error — which is exactly what makes `ensureWorkspace` able to return either
15
+ * a listed or a freshly created workspace under one type.
16
+ */
17
+ export type Workspace = SdkCreateWorkspaceMutation["createWorkspace"]["workspace"];
18
+ /** Options for {@link Unfazed.deleteWorkspace}. */
19
+ export type DeleteWorkspaceOptions = {
20
+ /**
21
+ * Also delete every deployment snapshotted from the workspace, stopping
22
+ * their service VMs. **Defaults to `true`** — see the method docs for why.
23
+ */
24
+ withDeployments?: boolean;
25
+ /** Resolve `false` instead of throwing when the workspace is not visible. */
26
+ ignoreNotFound?: boolean;
27
+ } & RequestOptions;
28
+ /** Options for {@link Unfazed.deleteDeployment}. */
29
+ export type DeleteDeploymentOptions = {
30
+ /** Resolve `false` instead of throwing when the deployment is not visible. */
31
+ ignoreNotFound?: boolean;
32
+ } & RequestOptions;
6
33
  /** A file record returned by any write/list operation. */
7
34
  export interface WorkspaceFile {
8
35
  path: string;
@@ -106,10 +133,99 @@ export interface RollbackOptions {
106
133
  * Default 1. Each retry refreshes the guard from a fresh read; the TARGET
107
134
  * release stays pinned, so a retry can never silently roll back to something
108
135
  * other than what was chosen.
136
+ *
137
+ * Ignored when {@link expectedCurrentId} is set — see there.
109
138
  */
110
139
  retries?: number;
140
+ /**
141
+ * The release you believe currently holds the alias. When set, the rollback
142
+ * proceeds only if that is still true.
143
+ *
144
+ * This turns the rollback from "undo whatever is live now" into a checked
145
+ * operation: if the live holder is anything else, you get
146
+ * {@link UnfazedConflictError} BEFORE any mutation is sent, so a rollback
147
+ * script can never act on a world that moved since you inspected it.
148
+ *
149
+ * It also disables the CAS retry, necessarily: a rejected guard means the
150
+ * alias moved, and the alias having moved is precisely the precondition you
151
+ * asked to be enforced. Retrying would refresh the guard to the new holder
152
+ * and roll back the very deploy the precondition existed to protect.
153
+ */
154
+ expectedCurrentId?: string;
155
+ signal?: AbortSignal;
156
+ }
157
+ /** Options for {@link Unfazed.createRun}. */
158
+ export interface CreateRunOptions {
159
+ workspaceId: string;
160
+ /** Delivered to the guest as `/work/prompt.md` and `$UNFZ_RUN_PROMPT` (max 64 KiB). */
161
+ prompt?: string;
162
+ /** A named preset from the image's `/etc/unfazed/harnesses.json`. Mutually exclusive in effect with `command` — set one. */
163
+ harness?: string;
164
+ /** Runs anything else with `sh -c`, instead of a named `harness`. */
165
+ command?: string;
166
+ /** Up to 20 single-line shell commands run in `/work` before the harness; the first failure fails the run. */
167
+ setup?: string[];
168
+ /** Up to 10 `owner/repo:path` GitHub references, cloned into `/work/.claude/skills/<name>` first. */
169
+ skills?: string[];
170
+ /** VM memory. Omit for the platform default. */
171
+ memoryMiB?: number;
172
+ /** Wall-clock cap in seconds. Omit for the platform default; capped at 3600. */
173
+ maxSeconds?: number;
174
+ /** Up to 20 immutable labels for your own correlation (a ticket id, a PR). Echoed back on every read. */
175
+ metadata?: unknown;
176
+ /** Copy the workspace's files into `/work`. Default true. */
177
+ includeFiles?: boolean;
178
+ /**
179
+ * `"human"` routes the harness's guarded actions (permission prompts) to
180
+ * the run's owner: the run shows BLOCKED with the question until it is
181
+ * answered ({@link Unfazed.answerRun}, the dashboard). Omit for the
182
+ * harness's own defaults.
183
+ */
184
+ approval?: "human";
185
+ /**
186
+ * `"plan"` asks the harness to research and write a plan WITHOUT acting
187
+ * (Claude Code runs in `--permission-mode plan`); the plan is the run's
188
+ * transcript. Approve it with {@link Unfazed.followUpRun}. Omit to do the task.
189
+ */
190
+ mode?: "plan";
191
+ /** The run this one continues. Usually set for you by {@link Unfazed.followUpRun}. */
192
+ parentRunId?: string;
193
+ /**
194
+ * Block until the run reaches a terminal status.
195
+ *
196
+ * `createRun` itself already returns RUNNING or — on a boot failure —
197
+ * FAILED with `error` set; `wait: true` additionally polls a RUNNING run
198
+ * through to SUCCEEDED/FAILED/CANCELLED. Either way this resolves with the
199
+ * run, never throws for FAILED: see {@link Unfazed.waitForRun}.
200
+ */
201
+ wait?: boolean;
202
+ /** Backoff/timeout knobs used when `wait` is true. */
203
+ waitOptions?: WaitOptions;
204
+ signal?: AbortSignal;
205
+ }
206
+ /** Options for {@link Unfazed.followUpRun}. */
207
+ export interface FollowUpRunOptions {
208
+ /** What to do next. For a plan run: the go-ahead (and any changes to the plan). */
209
+ prompt: string;
210
+ /** `"plan"` to plan again instead of executing; omit to execute. */
211
+ mode?: "plan";
212
+ /** Override the parent's approval; omit to inherit it. */
213
+ approval?: "human" | "";
214
+ /** Override the parent's wall clock (seconds). */
215
+ maxSeconds?: number;
216
+ metadata?: unknown;
217
+ /** As for {@link CreateRunOptions.wait}. */
218
+ wait?: boolean;
219
+ waitOptions?: WaitOptions;
111
220
  signal?: AbortSignal;
112
221
  }
222
+ /** Filter/paging for {@link Unfazed.listRuns}. */
223
+ export interface ListRunsFilter {
224
+ workspaceId?: string;
225
+ status?: RunStatus;
226
+ limit?: number;
227
+ offset?: number;
228
+ }
113
229
  export interface UnfazedOptions extends ClientOptions {
114
230
  /** Use an existing core client instead of constructing one. */
115
231
  client?: UnfazedClient;
@@ -180,8 +296,55 @@ export declare class Unfazed {
180
296
  /**
181
297
  * Deletes a workspace and everything anchored to it — deployments, env, and
182
298
  * a BOX's persistent data disk. Not recoverable.
299
+ *
300
+ * **The cascade is opt-out, not opt-in.** The server refuses to delete a
301
+ * workspace that has ANY deployment history — including FAILED records, and
302
+ * including deployments whose alias was long since taken over by something
303
+ * else — unless the mutation carries `withDeployments`. Since almost every
304
+ * workspace an agent creates gets deployed at least once, defaulting to
305
+ * `false` would make this method fail on nearly every real workspace, so it
306
+ * defaults to **`true`**: the behaviour this docstring has always promised.
307
+ *
308
+ * Pass `withDeployments: false` for the strict variant, which refuses rather
309
+ * than cascading and is the safer choice when you are deleting a workspace
310
+ * you did not create:
311
+ *
312
+ * ```ts
313
+ * await unfazed.deleteWorkspace(ws.id); // cascade
314
+ * await unfazed.deleteWorkspace(ws.id, { withDeployments: false }); // refuse if deployed
315
+ * await unfazed.deleteWorkspace(id, { ignoreNotFound: true }); // false, not a throw
316
+ * ```
317
+ *
318
+ * With `ignoreNotFound` a workspace that is absent (or belongs to someone
319
+ * else — the server returns one indistinguishable sentinel for both, on
320
+ * purpose) resolves to `false` instead of throwing, which makes teardown in
321
+ * a `finally` block idempotent.
183
322
  */
184
- deleteWorkspace(workspaceId: string, options?: RequestOptions): Promise<boolean>;
323
+ deleteWorkspace(workspaceId: string, options?: DeleteWorkspaceOptions): Promise<boolean>;
324
+ /**
325
+ * Returns the workspace named `name`, creating it only if it does not exist.
326
+ *
327
+ * Workspace names are unique per owner, case-insensitively — a second
328
+ * `createWorkspace("Hello")` after a `createWorkspace("hello")` is an error,
329
+ * not a second workspace. That makes the bare create a poor fit for any
330
+ * script that might run twice; this is the re-runnable form.
331
+ *
332
+ * ```ts
333
+ * const ws = await unfazed.ensureWorkspace("greeter"); // safe to re-run
334
+ * ```
335
+ *
336
+ * Costs one list, plus one create only on a miss. The create is still
337
+ * guarded: if a concurrent caller wins the race, the duplicate-name error is
338
+ * caught and the now-existing workspace is returned instead of thrown.
339
+ */
340
+ ensureWorkspace(name: string, options?: RequestOptions): Promise<{
341
+ id: string;
342
+ name: string;
343
+ ownerId: string;
344
+ projectId: string | null;
345
+ createdAt: string;
346
+ updatedAt: string;
347
+ }>;
185
348
  /**
186
349
  * Writes many files in as few requests as possible.
187
350
  *
@@ -248,13 +411,22 @@ export declare class Unfazed {
248
411
  updatedAt: string;
249
412
  };
250
413
  } | null>;
251
- /** Deletes a workspace file. */
414
+ /**
415
+ * Deletes a workspace file.
416
+ *
417
+ * Returns `false` when there was nothing to delete — an absent path is not
418
+ * an error. See the `ok: false` convention on {@link deleteEnv}.
419
+ */
252
420
  deleteFile(workspaceId: string, path: string, options?: RequestOptions): Promise<boolean>;
253
421
  /**
254
422
  * Deploys the workspace as a SERVICE (an HTTP server).
255
423
  *
256
424
  * With `wait: true` this resolves only once the build, VM boot and health
257
425
  * probe have all passed.
426
+ *
427
+ * Passing the same `alias` again redeploys that alias — a new release that
428
+ * takes over the alias when it goes READY. Omitting `alias` lets the server
429
+ * generate one, which means a different app on every call.
258
430
  */
259
431
  deployService(options: DeployServiceOptions): Promise<Deployment>;
260
432
  /** Deploys the workspace as a STATIC site. */
@@ -263,6 +435,25 @@ export declare class Unfazed {
263
435
  * Deploys a BOX: a persistent always-on Linux VM with a data disk that
264
436
  * survives stops and redeploys. A dev machine, not an HTTP service. The
265
437
  * result's `sshCommand` is how you get in.
438
+ *
439
+ * ⚠️ **Omitting `alias` creates a NEW BOX every single call.** There is no
440
+ * "the workspace's box" to update: with no alias the server generates a
441
+ * fresh random one, so a script run twice leaves you with two box records,
442
+ * not one redeployed box.
443
+ *
444
+ * The data disk, meanwhile, is keyed by WORKSPACE — one disk, shared by
445
+ * every box in the workspace, and it is the box's real identity. Since only
446
+ * one VM can hold that disk read-write, the second box cannot even start
447
+ * while the first is running (`box data disk for workspace ... is attached
448
+ * to VM ...; stop it first`). The result is a pile of dead box records
449
+ * fighting over one disk.
450
+ *
451
+ * **Always pass an explicit `alias` in anything re-runnable.** Redeploying
452
+ * the same alias is the in-place update you actually want.
453
+ *
454
+ * ```ts
455
+ * await unfazed.deployBox({ workspaceId: ws.id, alias: "dev" }); // re-runnable
456
+ * ```
266
457
  */
267
458
  deployBox(options: DeployBoxOptions): Promise<Deployment>;
268
459
  /** Escape hatch: deploy with a raw CreateDeploymentInput. */
@@ -379,37 +570,111 @@ export declare class Unfazed {
379
570
  restartCount: number;
380
571
  lastError: string | null;
381
572
  }>;
382
- /** Deletes a deployment. */
383
- deleteDeployment(deploymentId: string, options?: RequestOptions): Promise<boolean>;
384
- /** Sets a workspace-default env var. Redeploy the alias to apply it. */
573
+ /**
574
+ * Deletes a deployment: stops its VM, releases its alias, drops its release.
575
+ *
576
+ * With `ignoreNotFound` a deployment that is absent — or belongs to another
577
+ * tenant, which the server reports identically so ids cannot be probed —
578
+ * resolves to `false` instead of throwing, making cleanup loops idempotent.
579
+ */
580
+ deleteDeployment(deploymentId: string, options?: DeleteDeploymentOptions): Promise<boolean>;
581
+ /**
582
+ * Sets a workspace-default env var. Redeploy the alias to apply it.
583
+ *
584
+ * A name is either plain or secret, never both: this DELETES a workspace
585
+ * secret of the same name. {@link setSecret} displaces a plain var the same
586
+ * way. Last write wins, and the kind flips silently — so re-running a setup
587
+ * script that calls `setSecret("API_KEY", ...)` after one that called
588
+ * `setEnv("API_KEY", ...)` quietly converts it, and vice versa.
589
+ */
385
590
  setEnv(workspaceId: string, name: string, value: string, options?: RequestOptions): Promise<EnvVar>;
386
- /** Sets a workspace-default secret. The value is never readable again. */
591
+ /**
592
+ * Sets a workspace-default secret. The value is never readable again.
593
+ *
594
+ * Mutually exclusive with {@link setEnv}: this DELETES a plain workspace var
595
+ * of the same name. See {@link setEnv} for the displacement rule.
596
+ */
387
597
  setSecret(workspaceId: string, name: string, value: string, options?: RequestOptions): Promise<EnvVar>;
388
598
  /**
389
599
  * Sets many workspace env vars in one request, via aliased mutations —
390
600
  * the same batching trick as {@link writeFiles}.
391
601
  *
392
602
  * Pass `{ secret: true }` to route them all to `setWorkspaceSecret`.
603
+ *
604
+ * It inherits {@link writeFiles}' failure semantics too: the batch is NOT
605
+ * atomic. GraphQL resolves each aliased field independently, so a rejected
606
+ * name (over the limit, malformed, whatever) leaves the vars before it
607
+ * already committed. The thrown {@link UnfazedGraphQLError} carries `data`
608
+ * with the aliases that succeeded and `errors[].path` naming those that did
609
+ * not — do not assume "it threw" means "nothing was written".
610
+ *
611
+ * Each name still displaces its opposite kind, exactly as {@link setEnv} and
612
+ * {@link setSecret} do one at a time.
393
613
  */
394
614
  setEnvMany(workspaceId: string, vars: Record<string, string>, options?: {
395
615
  secret?: boolean;
396
616
  signal?: AbortSignal;
397
617
  }): Promise<EnvVar[]>;
398
- /** Deletes a workspace-default env var. */
618
+ /**
619
+ * Deletes a workspace-default env var (plain or secret).
620
+ *
621
+ * **The `ok: false` convention.** A `false` here does NOT mean failure — it
622
+ * means "nothing to do, the world is already in the state you asked for".
623
+ * The var did not exist. Every `ok`-returning mutation in this SDK follows
624
+ * the rule ({@link deleteFile}, {@link inheritAppEnv}, {@link removeAppEnv}),
625
+ * so the idempotent-cleanup pattern is simply to ignore the boolean; a real
626
+ * failure throws.
627
+ */
399
628
  deleteEnv(workspaceId: string, name: string, options?: RequestOptions): Promise<boolean>;
400
629
  /** Workspace-default env vars. Secret values are redacted. */
401
630
  listEnv(workspaceId: string, options?: RequestOptions): Promise<EnvVar[]>;
402
- /** Sets an env var for ONE alias, overriding the workspace default. */
631
+ /**
632
+ * Sets an env var for ONE alias, overriding the workspace default.
633
+ *
634
+ * Displaces both of the other states this name could be in for the app: it
635
+ * deletes an app SECRET of the same name, and it CLEARS a REMOVE mask left
636
+ * by {@link removeAppEnv}. So setting a value is also the way to un-block a
637
+ * masked name, and a name is only ever one of plain / secret / masked.
638
+ */
403
639
  setAppEnv(workspaceId: string, alias: string, name: string, value: string, options?: RequestOptions): Promise<EnvVar>;
404
- /** Sets a secret for ONE alias, overriding the workspace default. */
640
+ /**
641
+ * Sets a secret for ONE alias, overriding the workspace default.
642
+ *
643
+ * Like {@link setAppEnv}, this deletes a plain app var of the same name and
644
+ * clears a REMOVE mask for it.
645
+ */
405
646
  setAppSecret(workspaceId: string, alias: string, name: string, value: string, options?: RequestOptions): Promise<EnvVar>;
406
647
  /**
407
648
  * REMOVE: blocks this name for the app entirely — the app override, the
408
649
  * workspace default, and integration enrichment. Not the same as deleting an
409
650
  * override; use {@link inheritAppEnv} for that.
651
+ *
652
+ * This WRITES A MASK, and the mask is a real, persistent row. Two things
653
+ * follow that surprise people:
654
+ *
655
+ * - It is written even when nothing was inherited. Masking a name that no
656
+ * workspace default and no integration ever provided is not a no-op; it
657
+ * leaves a tombstone that keeps blocking the name if a default appears
658
+ * later. Clear it with {@link inheritAppEnv}.
659
+ * - It COSTS A SLOT. A workspace gets 128 env entries total — that budget
660
+ * is shared across the workspace defaults AND every app scope combined,
661
+ * not 128 per scope — and masks count toward it just like values do.
662
+ * Masking names in a loop can exhaust the workspace's env budget.
663
+ *
664
+ * Returns `false` when the exact mask was already present (a second call for
665
+ * the same name), per the {@link deleteEnv} `ok: false` convention. It
666
+ * returns `true` when it had something to change — including converting an
667
+ * existing override into a mask.
410
668
  */
411
669
  removeAppEnv(workspaceId: string, alias: string, name: string, options?: RequestOptions): Promise<boolean>;
412
- /** INHERIT: clears the app override/mask so workspace and integration values apply again. */
670
+ /**
671
+ * INHERIT: clears the app override/mask so workspace and integration values
672
+ * apply again. This is the undo for both {@link setAppEnv} and
673
+ * {@link removeAppEnv}.
674
+ *
675
+ * Returns `false` when there was no override and no mask to clear — nothing
676
+ * to do, not a failure. See {@link deleteEnv}.
677
+ */
413
678
  inheritAppEnv(workspaceId: string, alias: string, name: string, options?: RequestOptions): Promise<boolean>;
414
679
  /** The effective env view for one alias: overrides, inherited and masked. */
415
680
  listAppEnv(workspaceId: string, alias: string, options?: RequestOptions): Promise<EnvVar[]>;
@@ -439,7 +704,183 @@ export declare class Unfazed {
439
704
  * ```ts
440
705
  * await unfazed.rollback({ alias: "my-app" }); // undo last deploy
441
706
  * await unfazed.rollback({ alias: "my-app", to: relId }); // to a specific release
707
+ * await unfazed.rollback({ alias: "my-app", to: relId, expectedCurrentId: liveId });
442
708
  * ```
709
+ *
710
+ * ⚠️ **A bare `rollback({ alias })` is a TOGGLE, not an idempotent step.**
711
+ * It means "restore the most recent READY release that is not the live one",
712
+ * which is a different release each time the alias moves. Run the same
713
+ * script twice and the second run rolls back the rollback — A → B → A → B,
714
+ * forever, with each hop a real alias swap. Nothing errors, because every
715
+ * individual call did exactly what it was asked.
716
+ *
717
+ * In anything re-runnable — a retry loop, a CI job, an agent that might redo
718
+ * a step — pin the outcome instead:
719
+ *
720
+ * - `to:` names the release you want live, so re-running is a no-op-ish
721
+ * convergence rather than a flip (a second run then reports the target
722
+ * already holds the alias instead of moving it);
723
+ * - `expectedCurrentId:` states what you expect to be live now, so a second
724
+ * run fails loudly instead of silently undoing the first.
443
725
  */
444
726
  rollback(options: RollbackOptions): Promise<Deployment>;
727
+ /**
728
+ * Starts an agent run.
729
+ *
730
+ * Resolves once the VM has started (status RUNNING) — or, on a boot
731
+ * failure, once the run has already settled FAILED with `error` set. A
732
+ * boot failure is a normal return value here, not a thrown error, with or
733
+ * without `wait`.
734
+ *
735
+ * ```ts
736
+ * const run = await unfazed.createRun({
737
+ * workspaceId: ws.id,
738
+ * harness: "claude-code",
739
+ * prompt: "Fix the failing test in test/foo.test.ts and open a PR.",
740
+ * wait: true,
741
+ * });
742
+ * console.log(run.status, run.usage.mibSeconds);
743
+ * ```
744
+ *
745
+ * With `wait: true` this additionally polls a RUNNING run through to a
746
+ * terminal status — see {@link waitForRun} for exactly what "terminal"
747
+ * means and how a timeout is reported.
748
+ */
749
+ createRun(options: CreateRunOptions): Promise<Run>;
750
+ /**
751
+ * Polls a run until it leaves QUEUED/RUNNING, throwing only on timeout.
752
+ *
753
+ * Terminal is defined negatively — anything that is not QUEUED or RUNNING —
754
+ * matching the platform's own status machine, so an unknown future status
755
+ * still ends the wait instead of looping forever. **SUCCEEDED, FAILED and
756
+ * CANCELLED are all returned normally**: a run that executed and exited
757
+ * non-zero is a completed answer (read `run.error` / `run.exitCode`), not
758
+ * an exception the way a FAILED deployment is. Only running out of time
759
+ * throws, as {@link UnfazedRunError} with `reason: "TIMEOUT"`.
760
+ */
761
+ waitForRun(runId: string, waitOptions?: WaitOptions): Promise<Run>;
762
+ /** Reads one run by id. Throws if it does not exist or is not visible to you. */
763
+ getRun(runId: string, options?: RequestOptions): Promise<Run>;
764
+ /** Your agent runs, newest first, optionally filtered by workspace/status. */
765
+ listRuns(filter?: ListRunsFilter, options?: RequestOptions): Promise<{
766
+ total: number;
767
+ limit: number;
768
+ offset: number;
769
+ hasMore: boolean;
770
+ nextOffset: number | null;
771
+ items: Array<{
772
+ id: string;
773
+ workspaceID: string;
774
+ ownerId: string;
775
+ status: RunStatus;
776
+ rawStatus: string;
777
+ command: string | null;
778
+ harness: string | null;
779
+ prompt: string | null;
780
+ setup: Array<string>;
781
+ skills: Array<string>;
782
+ approval: string;
783
+ mode: string;
784
+ parentRunID: string | null;
785
+ depth: number;
786
+ memoryMiB: number;
787
+ maxSeconds: number;
788
+ metadata: unknown;
789
+ error: string | null;
790
+ exitCode: number | null;
791
+ vmID: string | null;
792
+ transcriptAvailable: boolean;
793
+ createdAt: string;
794
+ startedAt: string | null;
795
+ settledAt: string | null;
796
+ question: {
797
+ id: string;
798
+ text: string;
799
+ options: Array<string>;
800
+ askedAt: string;
801
+ } | null;
802
+ usage: {
803
+ vmSeconds: number;
804
+ memoryMiB: number;
805
+ mibSeconds: number;
806
+ };
807
+ }>;
808
+ }>;
809
+ /**
810
+ * Stops a run: a QUEUED run is cancelled at once, a RUNNING run's VM is
811
+ * killed and the run settles CANCELLED shortly after (`waitForRun` to
812
+ * observe it). Idempotent on an already-CANCELLED run; a run that has
813
+ * already SUCCEEDED or FAILED cannot be cancelled and the server rejects
814
+ * the call (thrown as {@link UnfazedGraphQLError}) — check `status` first
815
+ * when that is a routine case for you.
816
+ */
817
+ cancelRun(runId: string, options?: RequestOptions): Promise<Run>;
818
+ /**
819
+ * Continues a FINISHED run with a new instruction: a new run in the same
820
+ * workspace that inherits the parent's harness (or command), setup, skills
821
+ * and approval, with the parent's task, outcome and the end of its
822
+ * transcript prepended to `prompt` as context. This is how a plan is
823
+ * approved — `createRun({ mode: "plan" })` writes the plan, `followUpRun`
824
+ * with the go-ahead executes it — and how work continues after a run
825
+ * exited. The new run carries `parentRunID` and `depth`; chains are capped
826
+ * server-side. A live run cannot be followed up (the server refuses):
827
+ * answer it or wait for it.
828
+ *
829
+ * ```ts
830
+ * const plan = await unfazed.createRun({ workspaceId, harness: "claude-code", prompt: "plan the migration", mode: "plan", wait: true });
831
+ * const done = await unfazed.followUpRun(plan.id, { prompt: "go ahead", wait: true });
832
+ * ```
833
+ */
834
+ followUpRun(runId: string, options: FollowUpRunOptions): Promise<Run>;
835
+ /**
836
+ * Answers a BLOCKED run's pending question (`run.question`); the run
837
+ * resumes with the text. For a question with `options`, answer with one
838
+ * of them. Only a BLOCKED run accepts an answer — anything else is
839
+ * refused by the server.
840
+ *
841
+ * A run blocks when the agent inside calls `request_human_input`, or when
842
+ * it was created with `approval: "human"` and its harness asks permission
843
+ * for a guarded action. `waitForRun` keeps waiting through BLOCKED (it is
844
+ * not terminal); poll `getRun` and look at `question` to notice one.
845
+ */
846
+ answerRun(runId: string, answer: string, options?: RequestOptions): Promise<Run>;
847
+ /** A run's own history, oldest first (started, cancel requested, settled). */
848
+ runEvents(runId: string, input?: {
849
+ limit?: number;
850
+ }, options?: RequestOptions): Promise<{
851
+ id: string;
852
+ runID: string;
853
+ type: string;
854
+ severity: import("./index.js").DeploymentEventSeverity;
855
+ message: string;
856
+ metadata: unknown;
857
+ createdAt: string;
858
+ }[]>;
859
+ /**
860
+ * The live tail of a RUNNING run's console. Once the run has settled this
861
+ * comes back empty with `running: false` — read {@link runTranscript}
862
+ * instead, which is the stored, durable copy.
863
+ */
864
+ runConsole(runId: string, input?: {
865
+ tailBytes?: number;
866
+ }, options?: RequestOptions): Promise<{
867
+ runID: string;
868
+ running: boolean;
869
+ text: string;
870
+ }>;
871
+ /**
872
+ * The bounded end of a finished run's stored transcript (secrets
873
+ * redacted). `available` is false until the run settles and the transcript
874
+ * is copied out of the VM's console file — check `run.transcriptAvailable`
875
+ * or poll this after {@link waitForRun} resolves.
876
+ */
877
+ runTranscript(runId: string, input?: {
878
+ tailBytes?: number;
879
+ }, options?: RequestOptions): Promise<{
880
+ runID: string;
881
+ available: boolean;
882
+ text: string;
883
+ totalBytes: number;
884
+ truncated: boolean;
885
+ }>;
445
886
  }