wicked-crew 0.8.7 → 0.8.9

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.
Files changed (80) hide show
  1. package/dist/api/chat-transcripts.d.ts +3 -19
  2. package/dist/api/chat-transcripts.d.ts.map +1 -1
  3. package/dist/api/chat-transcripts.js +8 -32
  4. package/dist/api/chat-transcripts.js.map +1 -1
  5. package/dist/api/delivery-index.d.ts.map +1 -1
  6. package/dist/api/delivery-index.js +8 -3
  7. package/dist/api/delivery-index.js.map +1 -1
  8. package/dist/api/roster-standing.d.ts +2 -0
  9. package/dist/api/roster-standing.d.ts.map +1 -1
  10. package/dist/api/roster-standing.js +3 -0
  11. package/dist/api/roster-standing.js.map +1 -1
  12. package/dist/api/routes.d.ts +45 -1
  13. package/dist/api/routes.d.ts.map +1 -1
  14. package/dist/api/routes.js +165 -12
  15. package/dist/api/routes.js.map +1 -1
  16. package/dist/api/server.d.ts.map +1 -1
  17. package/dist/api/server.js +4 -0
  18. package/dist/api/server.js.map +1 -1
  19. package/dist/api/unit-output.d.ts +17 -0
  20. package/dist/api/unit-output.d.ts.map +1 -1
  21. package/dist/api/unit-output.js +47 -0
  22. package/dist/api/unit-output.js.map +1 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +7 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/adapter.d.ts +3 -2
  27. package/dist/core/adapter.d.ts.map +1 -1
  28. package/dist/core/adapter.js +15 -11
  29. package/dist/core/adapter.js.map +1 -1
  30. package/dist/core/bridge-path.d.ts +24 -3
  31. package/dist/core/bridge-path.d.ts.map +1 -1
  32. package/dist/core/bridge-path.js +96 -6
  33. package/dist/core/bridge-path.js.map +1 -1
  34. package/dist/core/deliver-text.d.ts +10 -1
  35. package/dist/core/deliver-text.d.ts.map +1 -1
  36. package/dist/core/deliver-text.js +63 -2
  37. package/dist/core/deliver-text.js.map +1 -1
  38. package/dist/core/deliver-triage.d.ts +21 -1
  39. package/dist/core/deliver-triage.d.ts.map +1 -1
  40. package/dist/core/deliver-triage.js +72 -2
  41. package/dist/core/deliver-triage.js.map +1 -1
  42. package/dist/core/deliver.d.ts +19 -0
  43. package/dist/core/deliver.d.ts.map +1 -1
  44. package/dist/core/deliver.js +55 -8
  45. package/dist/core/deliver.js.map +1 -1
  46. package/dist/core/host-paths.d.ts +33 -0
  47. package/dist/core/host-paths.d.ts.map +1 -0
  48. package/dist/core/host-paths.js +95 -0
  49. package/dist/core/host-paths.js.map +1 -0
  50. package/dist/core/linked-issues.d.ts +20 -2
  51. package/dist/core/linked-issues.d.ts.map +1 -1
  52. package/dist/core/linked-issues.js +41 -6
  53. package/dist/core/linked-issues.js.map +1 -1
  54. package/dist/core/worker-env.d.ts +31 -0
  55. package/dist/core/worker-env.d.ts.map +1 -0
  56. package/dist/core/worker-env.js +34 -0
  57. package/dist/core/worker-env.js.map +1 -0
  58. package/dist/interactive/bridge-pool.d.ts +27 -2
  59. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  60. package/dist/interactive/bridge-pool.js +40 -3
  61. package/dist/interactive/bridge-pool.js.map +1 -1
  62. package/dist/projects/routes.d.ts +2 -2
  63. package/dist/skills/runtime.d.ts +56 -1
  64. package/dist/skills/runtime.d.ts.map +1 -1
  65. package/dist/skills/runtime.js +135 -2
  66. package/dist/skills/runtime.js.map +1 -1
  67. package/dist/skills/store.d.ts +27 -0
  68. package/dist/skills/store.d.ts.map +1 -1
  69. package/dist/skills/store.js +63 -0
  70. package/dist/skills/store.js.map +1 -1
  71. package/dist/studio/assets/index-DofYURLG.js +564 -0
  72. package/dist/studio/assets/index-fSSul5Hu.css +32 -0
  73. package/dist/studio/editors/wicked-page/editor.json +1 -1
  74. package/dist/studio/editors/wicked-page/index.html +1 -1
  75. package/dist/studio/index.html +2 -2
  76. package/dist/studio/testid-inventory.json +942 -4529
  77. package/endpoint-manifest.json +33 -1
  78. package/package.json +3 -3
  79. package/dist/studio/assets/index-CUNMURZ4.js +0 -581
  80. package/dist/studio/assets/index-DPqNRzrT.css +0 -32
@@ -3,6 +3,7 @@ import { PlanSchema, toLaunchPlan } from './plan-schema.js';
3
3
  import { z } from 'zod';
4
4
  import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
5
5
  import { randomUUID } from 'node:crypto';
6
+ import { homedir } from 'node:os';
6
7
  import { readFileSync, existsSync } from 'node:fs';
7
8
  import { promises as fsp } from 'node:fs';
8
9
  import { isAbsolute, join, relative, resolve } from 'node:path';
@@ -14,7 +15,7 @@ import { detectRefusal } from './gate-cache.js';
14
15
  import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
15
16
  import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
16
17
  import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
17
- import { outputUnavailableReason, resolveUnit, unitKeysFor } from './unit-output.js';
18
+ import { attemptUnavailableReason, capturedAttempts, outputUnavailableReason, resolveUnit, storedAttempt, unitKeysFor } from './unit-output.js';
18
19
  import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
19
20
  import { callEstateTool, EstateMcpError, memoryStoreInfo } from '../core/estate-mcp-client.js';
20
21
  import { SeatHealthTracker } from './seat-health.js';
@@ -148,15 +149,28 @@ function message(err) {
148
149
  * the caller comparing their JSON against the source, so the unknown keys are named in `error`
149
150
  * itself, where a human and a `curl | jq .error` both see it without reading `details`.
150
151
  */
151
- function invalidBody(err, what) {
152
+ export function invalidBody(err, what) {
152
153
  const unknown = err.issues.flatMap((i) => (i.code === 'unrecognized_keys' ? i.keys : []));
153
154
  const error = unknown.length > 0
154
155
  ? `${what}: unknown field${unknown.length > 1 ? 's' : ''} ${unknown
155
156
  .map((k) => `\`${k}\``)
156
157
  .join(', ')} — this endpoint does not accept ${unknown.length > 1 ? 'them' : 'it'}, and ignoring ${unknown.length > 1 ? 'them' : 'it'} would run a different request than you sent`
157
- : what;
158
+ : namedIssues(what, err.issues);
158
159
  return { error, details: err.issues };
159
160
  }
161
+ /**
162
+ * crew#662: a schema rejection that is NOT an unknown key names the field and what was expected —
163
+ * `Invalid request body: \`revisesPr\` — Expected number, received boolean`. The bare
164
+ * "Invalid request body" sent a caller off to misdiagnose the contract. Up to three issues are
165
+ * spelled out; the rest are counted (all of them stay in `details`).
166
+ */
167
+ function namedIssues(what, issues) {
168
+ if (issues.length === 0)
169
+ return what;
170
+ const shown = issues.slice(0, 3).map((i) => (i.path.length > 0 ? `\`${i.path.join('.')}\` — ${i.message}` : i.message));
171
+ const more = issues.length > 3 ? `; and ${issues.length - 3} more (see details)` : '';
172
+ return `${what}: ${shown.join('; ')}${more}`;
173
+ }
160
174
  /** First value of a possibly-repeated query param, WITHOUT trimming — `''` is PRESERVED because a
161
175
  * blank `scope`/`scope_prefix` is meaningful to estate (root / whole-subtree), unlike an omitted
162
176
  * one. Returns undefined only when the param is absent. */
@@ -313,6 +327,11 @@ const OnboardBodySchema = z
313
327
  // compile time, that every body the published contract (`wicked-crew-api-types`) lets a client
314
328
  // send is a body these schemas accept — the request-direction half of the drift guard (task #84).
315
329
  export { PlanSchema };
330
+ /** crew#825: the issue refs a launch leaves out of the linked-issue expansion. */
331
+ const ExcludeLinkedIssuesSchema = z
332
+ .array(z.string().regex(/^\s*(?:\d+|(?:(?:[\w.-]+\/)?[\w.-]+)?#\d+)\s*$/, 'an issue ref: 541, #541, repo#541 or owner/repo#541'))
333
+ .max(50)
334
+ .optional();
316
335
  export const LaunchSchema = z.object({
317
336
  problem: z.string().min(1),
318
337
  sessionId: z.string().min(1).optional(),
@@ -375,6 +394,11 @@ export const LaunchSchema = z.object({
375
394
  * launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
376
395
  * (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
377
396
  actor: z.string().min(1).max(256).optional(),
397
+ /** crew#825 (api-types additive) — issue refs the intent names that the daemon must NOT read and
398
+ * append (`541`, `#541`, `repo#541`, `owner/repo#541`): a reference in passing is not a request to inline
399
+ * the issue. They are answered as `linkedIssues[].excluded`. Send only when
400
+ * `GET /health.capabilities.linkedIssuesExclude === true`. */
401
+ excludeLinkedIssues: ExcludeLinkedIssuesSchema,
378
402
  }).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
379
403
  message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
380
404
  path: ['plan'],
@@ -461,6 +485,11 @@ export const OpenTerminalSchema = z.object({
461
485
  // default; the ungoverned operator shell must opt in explicitly).
462
486
  governed: z.boolean().optional(),
463
487
  }).strict();
488
+ /** `POST /seats/:cli/login|logout` body (crew#615): the PTY size, both optional. */
489
+ export const SeatSessionSchema = z.object({
490
+ cols: z.number().int().positive().optional(),
491
+ rows: z.number().int().positive().optional(),
492
+ }).strict();
464
493
  const ResizeTerminalSchema = z.object({
465
494
  cols: z.number().int().positive(),
466
495
  rows: z.number().int().positive(),
@@ -764,7 +793,8 @@ runtime = {}) {
764
793
  /** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
765
794
  const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
766
795
  /** crew#627: the intent's linked issues, read under the daemon's identity at launch. */
767
- const readLinkedIssues = runtime.resolveLinkedIssues ?? ((p, root, ref) => resolveLinkedIssues(p, root, ref));
796
+ const readLinkedIssues = runtime.resolveLinkedIssues ??
797
+ ((p, root, ref, exclude) => resolveLinkedIssues(p, root, ref, undefined, exclude));
768
798
  /** crew#550 P-7: the follow-ups the run's evaluator units flagged, read from their captured output.
769
799
  * A unit whose output cannot be read contributes nothing (the section then says what WAS read);
770
800
  * an adapter without transcripts (a directly-driven route set) reads as "not read" — absent. */
@@ -1000,6 +1030,8 @@ runtime = {}) {
1000
1030
  : { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false }),
1001
1031
  // C1: `AgentSession.chat_id` is served from the daemon's own launch index, whatever the engine.
1002
1032
  runChatId: true,
1033
+ // crew#825: `LaunchRunBody.excludeLinkedIssues` + `POST /linked-issues/preview` are served.
1034
+ linkedIssuesExclude: true,
1003
1035
  // ASK-C1 (DES-ASK-TEAM-CHAT-001 §5.1): an ask starts a path — the whole contract is the
1004
1036
  // pinned core-ts floor the adapter reports; an older studio reads ABSENT as "every helper
1005
1037
  // answers", a newer one renders the one PA voice.
@@ -1586,6 +1618,33 @@ runtime = {}) {
1586
1618
  const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, body.data);
1587
1619
  return reply.code(201).send({ runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
1588
1620
  });
1621
+ // crew#825: what a workflow launch WOULD append — the same reading `POST /runs` does, before Send,
1622
+ // so the composer shows "N linked issues will be appended (≈K chars)" with a way to leave one out
1623
+ // (`excludeLinkedIssues`). Read-only: nothing launches, nothing is stored.
1624
+ const LinkedIssuesPreviewSchema = z
1625
+ .object({
1626
+ problem: z.string().min(1),
1627
+ repoRef: z.string().min(1).optional(),
1628
+ excludeLinkedIssues: ExcludeLinkedIssuesSchema,
1629
+ })
1630
+ .strict();
1631
+ app.post(`${V}/linked-issues/preview`, { config: { manifest: { requestType: 'LinkedIssuesPreviewBody', responseType: 'LinkedIssuesPreviewResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
1632
+ const parsed = LinkedIssuesPreviewSchema.safeParse(req.body);
1633
+ if (!parsed.success)
1634
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1635
+ const b = parsed.data;
1636
+ // The launch's own best-effort contract (codex on this PR): a resolver fault launches on the
1637
+ // intent alone, so the preview answers "nothing appended" and says why — never a 500.
1638
+ try {
1639
+ const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
1640
+ const linked = await readLinkedIssues(b.problem, root, b.repoRef, b.excludeLinkedIssues ?? []);
1641
+ return { issues: linked.issues, appendedChars: linked.block === null ? 0 : linked.block.length + 2 };
1642
+ }
1643
+ catch (err) {
1644
+ req.log.warn(`linked-issue preview failed; a launch would run on the intent alone: ${message(err)}`);
1645
+ return { issues: [], appendedChars: 0, error: `linked issues could not be read: ${message(err)} — a launch now runs on the intent alone` };
1646
+ }
1647
+ });
1589
1648
  // Launch a run (replaces POST /sessions). `clisJson` defaults to the roster;
1590
1649
  // `sessionId` is minted if the client omits it.
1591
1650
  app.post(`${V}/runs`, {
@@ -1798,11 +1857,12 @@ runtime = {}) {
1798
1857
  // fault (the repo list unreadable, say) launches the run on its intent alone and is logged.
1799
1858
  try {
1800
1859
  const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
1801
- const linked = await readLinkedIssues(b.problem, root, b.repoRef);
1802
- if (linked.block !== null) {
1860
+ const linked = await readLinkedIssues(b.problem, root, b.repoRef, b.excludeLinkedIssues ?? []);
1861
+ if (linked.block !== null)
1803
1862
  input.problem = `${b.problem.trimEnd()}\n\n${linked.block}`;
1863
+ // crew#825: an all-excluded intent appends nothing but still answers what it left out.
1864
+ if (linked.issues.length > 0)
1804
1865
  linkedIssues = linked.issues;
1805
- }
1806
1866
  }
1807
1867
  catch (err) {
1808
1868
  req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
@@ -1899,7 +1959,11 @@ runtime = {}) {
1899
1959
  // planner (one unit, the brief verbatim). Say so in the answer, with how to get work done,
1900
1960
  // instead of letting it pass for a launch of real work.
1901
1961
  const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
1902
- return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText });
1962
+ // crew#874: the run uses the PUBLISHED generation; when the installed garden is not the one it
1963
+ // was built from, the launch answer says so (a recorded proof can disclose which garden ran).
1964
+ const skillsAhead = runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
1965
+ const skillsWarning = skillsAhead === null ? {} : { skillsWarning: { kind: skillsAhead.kind, severity: skillsAhead.severity, message: skillsAhead.message } };
1966
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText, ...skillsWarning });
1903
1967
  }
1904
1968
  catch (err) {
1905
1969
  const msg = message(err);
@@ -3009,7 +3073,9 @@ runtime = {}) {
3009
3073
  refused: chatScopes.refusedOf(id) ?? null,
3010
3074
  ...(askPaths.view(id) !== undefined ? { path: askPaths.view(id) } : {}),
3011
3075
  // DES-L5 (D-13): the transcript so far, append order — `[]` before the first persisted
3012
- // turn; the file goes with the chat on `chatClosed`, so a reclaimed id answers `[]` too.
3076
+ // turn. The file goes with the chat on `chatClosed` UNLESS a run promoted from it is still
3077
+ // live (crew#619 retention — dropped when that run terminals), so a reclaimed id answers
3078
+ // `[]` only once no promoted run holds the old file.
3013
3079
  ...(chatTranscripts !== undefined ? { messages: chatTranscripts.read(id) } : {}),
3014
3080
  };
3015
3081
  }
@@ -3085,12 +3151,48 @@ runtime = {}) {
3085
3151
  units: unitKeysFor(run),
3086
3152
  });
3087
3153
  }
3154
+ const rawAttempt = req.query.attempt;
3155
+ const attemptArg = Array.isArray(rawAttempt) ? rawAttempt[0] : rawAttempt;
3156
+ const wanted = attemptArg === undefined || attemptArg === '' ? undefined : /^\d{1,9}$/.test(attemptArg) ? Number(attemptArg) : NaN;
3157
+ if (wanted !== undefined && !Number.isSafeInteger(wanted)) {
3158
+ return reply.code(400).send({ error: '`attempt` must be a non-negative integer' });
3159
+ }
3088
3160
  // Keyed off the unit RECORD (the same derivation the evidence bundle uses), not off the
3089
3161
  // caller's path segment — the two disagreeing is what made a resolvable unit unreadable.
3090
3162
  const output = await adapter.workOutput(coreUnitId(id, unit));
3091
- if (output !== null)
3092
- return reply.send({ output });
3093
- return reply.send({ output: null, outputUnavailable: outputUnavailableReason(unit) });
3163
+ // crew#848: which attempt those bytes are. The engine keeps one transcript per unit (the last
3164
+ // attempt that folded ok); the durable log has one `unitOutputCaptured` per attempt.
3165
+ const events = typeof adapter.runEvents === 'function' ? await adapter.runEvents(id).catch(() => null) : null;
3166
+ const attempts = capturedAttempts((events ?? []), unit.ord);
3167
+ const served = output !== null ? storedAttempt(attempts, output) : null;
3168
+ const latest = attempts[attempts.length - 1];
3169
+ const meta = {
3170
+ ...(served !== null ? { attempt: served } : {}),
3171
+ ...(latest !== undefined ? { latestAttempt: latest.attempt } : {}),
3172
+ ...(events !== null ? { attempts } : {}),
3173
+ };
3174
+ if (served !== null)
3175
+ void reply.header('x-wicked-unit-attempt', String(served));
3176
+ if (wanted !== undefined) {
3177
+ const rec = attempts.find((a) => a.attempt === wanted);
3178
+ if (rec === undefined) {
3179
+ return reply.code(404).send({
3180
+ error: `Unit ${unit.ord} of run ${id} has no attempt ${wanted} on record`,
3181
+ attempts: attempts.map((a) => a.attempt),
3182
+ });
3183
+ }
3184
+ if (output !== null && served === wanted)
3185
+ return reply.send({ output, ...meta });
3186
+ return reply.send({ output: null, ...meta, attempt: wanted, outputUnavailable: attemptUnavailableReason(unit, rec, served) });
3187
+ }
3188
+ if (output !== null) {
3189
+ // A later attempt that left no transcript: the stored bytes are SUPERSEDED — say so, and why.
3190
+ if (served !== null && latest !== undefined && latest.attempt > served) {
3191
+ return reply.send({ output, ...meta, superseded: true, latestUnavailable: attemptUnavailableReason(unit, latest, served) });
3192
+ }
3193
+ return reply.send({ output, ...meta });
3194
+ }
3195
+ return reply.send({ output: null, ...meta, outputUnavailable: outputUnavailableReason(unit) });
3094
3196
  });
3095
3197
  // DC-S7 (§4.6): what the seats considered, set aside and cited for one chat turn or one unit
3096
3198
  // attempt. Operator+ (the rules are the project's doctrine). A cited rule is "unchecked", never
@@ -4617,6 +4719,57 @@ runtime = {}) {
4617
4719
  return reply.code(400).send({ error: message(err) });
4618
4720
  }
4619
4721
  });
4722
+ // crew#615: sign a seat in or out without the client composing the command. The daemon runs the
4723
+ // ENGINE's own `login_invocation` / `logout_invocation` for that seat (already prefixed with the
4724
+ // seat's configuration root; the daemon never synthesizes one) in a governed PTY, and answers
4725
+ // the terminal id — the device URL or the CLI's prompt streams over `/ws/terminals/:id`. The
4726
+ // seat's credential probe is re-run when the terminal exits, so the roster's `signed_in` follows
4727
+ // the outcome instead of waiting out the probe's TTL.
4728
+ for (const action of ['login', 'logout']) {
4729
+ app.post(`${V}/seats/:cli/${action}`, { config: { manifest: { requestType: 'SeatSessionBody', responseType: 'SeatSessionResponse', statusCodes: [201, 400, 404] } } }, async (req, reply) => {
4730
+ const { cli } = req.params;
4731
+ const parsed = SeatSessionSchema.safeParse(req.body ?? {});
4732
+ if (!parsed.success) {
4733
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
4734
+ }
4735
+ const seat = rosterWithStanding().find((s) => s.key === cli);
4736
+ if (seat === undefined)
4737
+ return reply.code(404).send({ error: `unknown seat: ${cli}` });
4738
+ const field = action === 'login' ? seat.login_invocation : seat.logout_invocation;
4739
+ const invocation = typeof field === 'string' ? field.trim() : '';
4740
+ if (invocation === '') {
4741
+ return reply.code(404).send({
4742
+ error: `seat ${cli} has no ${action} command: the engine roster carries no ${action}_invocation for it (its CLI documents none)`,
4743
+ });
4744
+ }
4745
+ // Subscribe BEFORE the spawn: a short command can exit before `openTerminal` resolves, and
4746
+ // its exit must still re-probe and release the listener (codex review).
4747
+ let termId = null;
4748
+ const exitedEarly = new Set();
4749
+ const done = () => {
4750
+ stop();
4751
+ rosterWithStanding.reprobe?.(cli);
4752
+ };
4753
+ const stop = adapter.onEvent((event) => {
4754
+ if (event.type !== 'terminalExited' || typeof event.id !== 'string')
4755
+ return;
4756
+ if (termId === null)
4757
+ exitedEarly.add(event.id);
4758
+ else if (event.id === termId)
4759
+ done();
4760
+ });
4761
+ try {
4762
+ termId = await adapter.openTerminal(homedir(), ['sh', '-lc', invocation], parsed.data.cols ?? 100, parsed.data.rows ?? 30, true);
4763
+ }
4764
+ catch (err) {
4765
+ stop();
4766
+ return reply.code(400).send({ error: message(err) });
4767
+ }
4768
+ if (exitedEarly.has(termId))
4769
+ done();
4770
+ return reply.code(201).send({ terminalId: termId, cli, action });
4771
+ });
4772
+ }
4620
4773
  // Resize a live terminal's PTY.
4621
4774
  app.post(`${V}/terminals/:id/resize`, async (req, reply) => {
4622
4775
  const { id } = req.params;