wicked-crew 0.7.39 → 0.7.40

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 (38) hide show
  1. package/dist/api/chat-scope.d.ts +37 -1
  2. package/dist/api/chat-scope.d.ts.map +1 -1
  3. package/dist/api/chat-scope.js +131 -7
  4. package/dist/api/chat-scope.js.map +1 -1
  5. package/dist/api/routes.d.ts +19 -10
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +168 -19
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/api/run-timing-index.d.ts +10 -0
  10. package/dist/api/run-timing-index.d.ts.map +1 -1
  11. package/dist/api/run-timing-index.js +24 -0
  12. package/dist/api/run-timing-index.js.map +1 -1
  13. package/dist/api/server.d.ts.map +1 -1
  14. package/dist/api/server.js +33 -9
  15. package/dist/api/server.js.map +1 -1
  16. package/dist/core/adapter.d.ts +9 -2
  17. package/dist/core/adapter.d.ts.map +1 -1
  18. package/dist/core/adapter.js +19 -4
  19. package/dist/core/adapter.js.map +1 -1
  20. package/dist/projects/graph.d.ts +11 -0
  21. package/dist/projects/graph.d.ts.map +1 -1
  22. package/dist/projects/graph.js +12 -0
  23. package/dist/projects/graph.js.map +1 -1
  24. package/dist/skills/phase-skill-gaps.d.ts +108 -0
  25. package/dist/skills/phase-skill-gaps.d.ts.map +1 -0
  26. package/dist/skills/phase-skill-gaps.js +199 -0
  27. package/dist/skills/phase-skill-gaps.js.map +1 -0
  28. package/dist/skills/runtime.d.ts +1 -1
  29. package/dist/skills/runtime.d.ts.map +1 -1
  30. package/dist/skills/runtime.js +2 -1
  31. package/dist/skills/runtime.js.map +1 -1
  32. package/dist/studio/assets/{index-Drfcsy8b.css → index-Bg7Z3BR-.css} +1 -1
  33. package/dist/studio/assets/index-BvOFMjf9.js +554 -0
  34. package/dist/studio/index.html +2 -2
  35. package/dist/studio/testid-inventory.json +40 -10
  36. package/endpoint-manifest.json +1 -1
  37. package/package.json +2 -2
  38. package/dist/studio/assets/index-BRDAN35i.js +0 -548
@@ -20,13 +20,13 @@ import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
20
20
  import { chatSeatAdmission } from './seat-standing.js';
21
21
  import { rosterWithStandingFactory } from './roster-standing.js';
22
22
  import { ChatTurnIndex } from './chat-turns.js';
23
- import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
23
+ import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
24
24
  import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
25
25
  import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
26
26
  import { DocRunIndex } from '../interactive/doc-run-index.js';
27
27
  import { listInteractiveDocs } from '../interactive/docs-index.js';
28
28
  import { TestSetIndex } from '../qe/test-sets.js';
29
- import { estateExe, resolveProjectGraphBinding } from '../projects/graph.js';
29
+ import { estateExe, parseEstateTotals, resolveProjectGraphBinding } from '../projects/graph.js';
30
30
  import { registerProjectRoutes } from '../projects/routes.js';
31
31
  import { registerCampaignRoutes } from '../campaigns/routes.js';
32
32
  import { registerGovernanceWikiRoutes } from './governance-wiki.js';
@@ -35,6 +35,7 @@ import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js
35
35
  import { registerTestingRoutes } from './testing.js';
36
36
  import { registerSkillsRoutes } from './skills.js';
37
37
  import { disabledSkillsHealth } from '../skills/runtime.js';
38
+ import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
38
39
  import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
39
40
  import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
40
41
  import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
@@ -83,17 +84,10 @@ const STATUS_ORDER = {
83
84
  function sortActionableFirst(views) {
84
85
  return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
85
86
  }
86
- /**
87
- * The whole-graph counts from `wicked-estate stats --db` stdout (`nodes=N edges=M files=F …`, the
88
- * first line estate prints — `main.rs`'s `stats` summary). `undefined` when the line does not parse
89
- * (an older/newer estate, an error message): the caller leaves `CodeGraphData.totals` ABSENT.
90
- */
91
- export function parseEstateTotals(stdout) {
92
- const m = /^nodes=(\d+) edges=(\d+) files=(\d+)/m.exec(stdout);
93
- if (m === null)
94
- return undefined;
95
- return { nodes: Number(m[1]), edges: Number(m[2]), files: Number(m[3]) };
96
- }
87
+ // parseEstateTotals lives in projects/graph.ts — shared with chat-scope.ts (crew#642) to avoid
88
+ // a circular import (routes.ts → chat-scope.ts). Re-exported here for backward compat
89
+ // (tests import it from routes.js; see top-of-file import for local use).
90
+ export { parseEstateTotals };
97
91
  function codeGraphDbOr503(repo, reply) {
98
92
  try {
99
93
  return codeGraphDb(repo);
@@ -526,7 +520,82 @@ export const ChatOpenSchema = z.object({
526
520
  /** File the chat into a project (`crew.chat` membership) — and, with no `repoRefs`, scope it to
527
521
  * every registered `crew.repo` member of that project (crew#502). */
528
522
  projectId: z.string().min(1).optional(),
523
+ /** The scope the caller NAMES (studio#323 R4) — omitted ⇒ the legacy inference. Shape rules
524
+ * (which kinds need / refuse `projectId` / `repoRefs`) are the resolver's (`chat-scope.ts`). */
525
+ scopeKind: z.enum(['system', 'everything', 'project', 'repo', 'repos', 'none']).optional(),
529
526
  }).strict();
527
+ /**
528
+ * The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
529
+ * and the 202 cannot drift.
530
+ *
531
+ * The property this field states is **"this chat has one seat; it cannot disagree with itself"**,
532
+ * and that is true of every one-warm-seat chat. The first cut also required `refused.length > 0`,
533
+ * so the case an operator is most likely to create — a chat deliberately opened with a single seat
534
+ * — returned `ok: true` with no signal at all, which is the exact failure mode #641 was filed
535
+ * about. It also made the 202 go quiet after a restart, when `refusedOf` no longer knows the
536
+ * refusals, even though the chat is just as single-seated as it was a minute earlier.
537
+ *
538
+ * `refused` therefore rides along as EVIDENCE when there is any — it never decides whether the
539
+ * caller is told.
540
+ *
541
+ * THREE states, not two (review of #658). The refusal list has a third possibility that `[]` cannot
542
+ * express: this daemon does not KNOW. `chatScopes.refusedOf` answers `undefined` for a chat this
543
+ * daemon did not open — the engine keeps the warm session across a restart, the in-memory index does
544
+ * not — and the first cut collapsed that to `[]` with `?? []`, which made the 202 tell an operator
545
+ * *"It was opened with a single seat — no other seat was refused"* about a chat that was opened with
546
+ * two seats, one of which was refused. Both halves false, stated confidently. It is the same
547
+ * distinction #651 got right for chat-promotion provenance (`.catch(() => null)`, never `[]`):
548
+ * unreadable and empty are different answers, and only one of them may be asserted.
549
+ *
550
+ * TWO MOMENTS, and only one route witnessed each (third review of #658). `refused` describes the
551
+ * daemon's record — what the OPEN (or the last re-seat) observed; `warmRoster` describes NOW. Any
552
+ * sentence built from both fabricates the instant they diverge, and they diverge routinely: a seat
553
+ * that blows its turn budget is RELEASED by the engine (`chat-turns.ts`), a re-seat clears an entry
554
+ * from the record, a restart drops the record entirely. A chat opened with `['claude','opencode']`
555
+ * — both warm, nothing refused, nothing unknown — whose `opencode` is later released would have been
556
+ * told *"It was opened with a single seat — no other seat was refused"*: false in both halves, with
557
+ * no `undefined` anywhere near it. So `at` names the moment the caller is asking about:
558
+ *
559
+ * • `'open'` — the 201. This route just performed the open, so it is the ONLY one with standing to
560
+ * describe it: refusals known and empty ⇒ it may say the chat was opened with a single seat.
561
+ * • `'turn'` — the 202. It knows the roster NOW and the refusal record; it knows nothing about how
562
+ * the chat came to look like this. Refusals known and empty ⇒ it says NOTHING about opening.
563
+ * It still names refusals that are on record, and still says so when the record is unavailable —
564
+ * both are statements about the record it holds, not about a moment it did not see.
565
+ *
566
+ * `refusalsKnown` carries the known/unknown distinction for a machine reader, because `refused: []`
567
+ * is exactly as ambiguous on the wire as the sentence was in prose.
568
+ */
569
+ function singleSeatDisclosure(warmed,
570
+ /** `undefined` = this daemon has no refusal record for the chat (it did not open it). */
571
+ refused,
572
+ /** Which moment the caller is asking about — see the note above; only `'open'` may describe one. */
573
+ at) {
574
+ const known = refused !== undefined;
575
+ const list = refused ?? [];
576
+ const refusalClause = list.length > 0
577
+ ? `Refused: ${list.map((r) => `${r.cliKey} (${r.reason})`).join('; ')}. `
578
+ : known
579
+ ? at === 'open'
580
+ ? 'It was opened with a single seat — no other seat was refused. '
581
+ : // A turn has no standing to say how the chat was opened: this seat may be the survivor of
582
+ // a two-seat chat whose other seat was released mid-session. Say nothing.
583
+ ''
584
+ : 'This daemon holds no refusal record for this chat — it was opened before a restart — so ' +
585
+ 'whether another seat was refused is UNKNOWN: the empty refused[] means the record is gone, ' +
586
+ 'not that nothing was refused. ';
587
+ return {
588
+ degraded: true,
589
+ warmed,
590
+ refused: list,
591
+ refusalsKnown: known,
592
+ message:
593
+ // "warm" on a turn: the claim is about the roster as it stands, not about how it got there.
594
+ `This chat has one ${at === 'open' ? 'seat' : 'warm seat'} (${warmed}); it cannot disagree with itself. ` +
595
+ refusalClause +
596
+ 'The single-seat root cause is tracked as wicked-core#563; this chat makes it visible.',
597
+ };
598
+ }
530
599
  export function registerRoutes(app, adapter, gateCache, elicitationCache,
531
600
  // Defaulted so a caller that never arms the bus seam (tests drive this
532
601
  // function directly) gets the same behavior as an unarmed daemon: an empty
@@ -673,6 +742,10 @@ runtime = {}) {
673
742
  const guidance = guidanceIndex.guidanceFor(view.session.id);
674
743
  if (guidance !== undefined)
675
744
  view.session.guidance = guidance;
745
+ // crew#661: the declared skills this run's phases ran WITHOUT (degrade-and-disclose) — ABSENT when none.
746
+ const skillGaps = runtime.runSkillGaps?.gapsFor(view.session.id);
747
+ if (skillGaps !== undefined)
748
+ view.session.skill_gaps = skillGaps;
676
749
  // Run launch time (home command-center run metrics): unix SECONDS from the `run.launched`
677
750
  // audit entry, ABSENT when the daemon has no launch record for this run (onboarding/campaign
678
751
  // runs launched off POST /runs, pre-field runs) — never fabricated, so a bucketed KPI can
@@ -696,6 +769,13 @@ runtime = {}) {
696
769
  view.session.delivery = state.delivery;
697
770
  if (state.deliverUrl !== undefined)
698
771
  view.session.deliverUrl = state.deliverUrl;
772
+ // crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
773
+ const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
774
+ if (chatSeatCount !== undefined)
775
+ view.session.chat_seat_count = chatSeatCount;
776
+ const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
777
+ if (chatGrounded !== undefined)
778
+ view.session.chat_grounded = chatGrounded;
699
779
  return view;
700
780
  };
701
781
  // Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
@@ -735,9 +815,12 @@ runtime = {}) {
735
815
  // `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
736
816
  // `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
737
817
  // and show it — exactly the state-home blocker's precedent above.
818
+ // crew#661: a drafting seam that armed WITHOUT its declared skill rides `warnings` as well — its
819
+ // runs proceed degraded, so "status ok, no warnings" would be the silent answer the issue names.
738
820
  const warnings = [
739
821
  ...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
740
822
  ...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
823
+ ...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
741
824
  ];
742
825
  return {
743
826
  status: 'ok',
@@ -790,7 +873,9 @@ runtime = {}) {
790
873
  // The skills seam's last outcome (skills keystone): published / fallback / blocked /
791
874
  // config-error, with the `skills.*` findings the ladder produced — the operator's one
792
875
  // read-only answer to "why do launches refuse the snapshot".
793
- skills: runtime.skills?.health() ?? disabledSkillsHealth(),
876
+ // crew#661: plus every subsystem whose phases ARMED without a declared skill (`phaseSkillGaps`,
877
+ // one `skills.phase-skill` finding each) — the degraded runs' operator-facing record.
878
+ skills: withPhaseSkillGaps(runtime.skills?.health() ?? disabledSkillsHealth(), runtime.phaseSkills),
794
879
  // Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
795
880
  // letters — with a `governance.deadletter` finding the moment the outbox holds one.
796
881
  governance,
@@ -1312,6 +1397,23 @@ runtime = {}) {
1312
1397
  }
1313
1398
  try {
1314
1399
  const runId = await adapter.launchRun(input);
1400
+ // crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
1401
+ // participated and whether the chat was grounded — so an intent authored by a single
1402
+ // ungrounded model is distinguishable from one that survived a disagreement.
1403
+ let chatSeatCount;
1404
+ let chatGrounded;
1405
+ // Both stay ABSENT unless the chat actually resolves: a closed or unknown chatId must read as
1406
+ // "provenance unavailable", not as "promoted from an ungrounded zero-seat chat". A valid chat
1407
+ // always has at least one warm seat (`crew-api-types` ChatOpenResponse), so 0 is not a value
1408
+ // `chat_seat_count` can honestly hold.
1409
+ if (b.chatId !== undefined) {
1410
+ const promotedFrom = await adapter.chatSeats(b.chatId).catch(() => null);
1411
+ const scope = chatScopes.engineOf(b.chatId);
1412
+ if (promotedFrom !== null && promotedFrom.length > 0 && scope !== undefined) {
1413
+ chatSeatCount = promotedFrom.length;
1414
+ chatGrounded = scope.codeGraphDb != null;
1415
+ }
1416
+ }
1315
1417
  // Who launched it — the engine's LaunchOptions carries no actor field
1316
1418
  // (checked, wicked-core-ts 0.6.0), so the crew-side trail is the system
1317
1419
  // of record for run provenance (task #88).
@@ -1344,10 +1446,17 @@ runtime = {}) {
1344
1446
  // crew#619: the chat↔run link is durable so the retention maps can be rehydrated after a
1345
1447
  // daemon restart (rehydration reads `run.launched` entries and keeps non-terminal entries).
1346
1448
  ...(b.chatId !== undefined ? { chatId: b.chatId } : {}),
1449
+ // crew#641/#642 (item 5): chat promotion provenance — seat count and grounding at launch.
1450
+ ...(chatSeatCount !== undefined ? { chatSeatCount } : {}),
1451
+ ...(chatGrounded !== undefined ? { chatGrounded } : {}),
1347
1452
  // crew#632: launch surface and caller identity — human-readable provenance on the audit trail.
1348
1453
  ...(b.channel !== undefined ? { channel: b.channel } : {}),
1349
1454
  ...(b.actor !== undefined ? { actor: b.actor } : {}),
1350
1455
  });
1456
+ // Stamp the index live so the field is served without waiting for a daemon restart.
1457
+ if (chatSeatCount !== undefined && chatGrounded !== undefined) {
1458
+ runTimingIndex.setChatPromotion(runId, chatSeatCount, chatGrounded);
1459
+ }
1351
1460
  if (b.retryOf !== undefined)
1352
1461
  retryIndex.set(runId, b.retryOf);
1353
1462
  if (revisesPr !== undefined)
@@ -1826,6 +1935,12 @@ runtime = {}) {
1826
1935
  }
1827
1936
  const b = parsed.data;
1828
1937
  const chatId = b.chatId ?? randomUUID();
1938
+ const requestedRefs = [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])];
1939
+ // A named scope's SHAPE first (studio#323 R4, codex on #664): a body whose kind refuses its own
1940
+ // projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below.
1941
+ const shape = namedKindRefusal(b.scopeKind, b.projectId, [...new Set(requestedRefs)]);
1942
+ if (shape !== null)
1943
+ return reply.code(400).send({ error: shape.error });
1829
1944
  // Validate the project BEFORE opening seats: a chat has no launch record for the engine to
1830
1945
  // attach against atomically (chats are an in-memory seat pool), so the route validates
1831
1946
  // up-front and attaches right after open — the one non-atomic attach, documented in the ADR
@@ -1866,8 +1981,14 @@ runtime = {}) {
1866
1981
  const resolution = await resolveChatScope({
1867
1982
  chatId,
1868
1983
  ...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
1869
- repoRefs: [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])],
1870
- }, { ...chatScopeDeps(adapter), scratchBase: chatScopes.base, log: (m) => req.log.warn(m) });
1984
+ repoRefs: requestedRefs,
1985
+ ...(b.scopeKind !== undefined ? { kind: b.scopeKind } : {}),
1986
+ }, {
1987
+ ...chatScopeDeps(adapter),
1988
+ scratchBase: chatScopes.base,
1989
+ log: (m) => req.log.warn(m),
1990
+ ...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
1991
+ });
1871
1992
  if (!resolution.ok) {
1872
1993
  chatScopes.release(chatId, token);
1873
1994
  return reply.code(resolution.status).send({
@@ -1895,7 +2016,10 @@ runtime = {}) {
1895
2016
  // here is NAMED on the response (`refused`) and in the thread (`chatSeatRefused`), so a
1896
2017
  // person can see why pi is missing. Explicit `clis` are passed through as asked; the engine
1897
2018
  // refuses per seat with its reason, which is copied into `refused` too.
1898
- const scoped = scope.kind !== 'none';
2019
+ // A chat is SCOPED — held to read-only roots — when it reads repositories by its kind. `none`
2020
+ // and `system` (studio#323 R4: the platform itself, no repository) read none, so they take the
2021
+ // unscoped admission and open on an engine that predates chat scope.
2022
+ const scoped = scope.kind !== 'none' && scope.kind !== 'system';
1899
2023
  const refused = [];
1900
2024
  // The standing roster, read ONCE: the default admission below and the engine-drop
1901
2025
  // attribution after `chatOpen` both consult it (F-A45-011).
@@ -2067,12 +2191,17 @@ runtime = {}) {
2067
2191
  ...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
2068
2192
  });
2069
2193
  }
2194
+ // crew#641: a chat with exactly ONE warm seat cannot disagree with itself — disclosed
2195
+ // prominently so the caller does not have to reason about the refused[] array.
2196
+ const warm = seats.filter((s) => s.ok);
2197
+ const singleSeat = warm.length === 1 ? singleSeatDisclosure(warm[0].cliKey, refused, 'open') : undefined;
2070
2198
  return reply.code(201).send({
2071
2199
  chatId,
2072
2200
  seats,
2073
2201
  scope,
2074
2202
  refused,
2075
2203
  ...(projectAttachError !== undefined ? { projectAttachError } : {}),
2204
+ ...(singleSeat !== undefined ? { singleSeat } : {}),
2076
2205
  });
2077
2206
  }
2078
2207
  catch (err) {
@@ -2123,7 +2252,12 @@ runtime = {}) {
2123
2252
  // warm seats — so the turn can be opened BEFORE the engine call, in the same tick as the
2124
2253
  // predicate below: two racing sends can no longer both pass `inFlight` during the await, and a
2125
2254
  // `chatDelta` that lands before `chatSend` resolves is already stamped with its `turn_id`.
2126
- const audience = parsed.data.targets ?? (await adapter.chatSeats(id));
2255
+ // crew#641 (review): the ROSTER and the AUDIENCE are different things and only one of them can
2256
+ // answer "can this chat disagree with itself". Resolved ONCE here: `audience` is who this turn
2257
+ // goes to (the named targets, else everyone warm), `warmRoster` is who is warm at all. A
2258
+ // targeted send to one seat of a two-seat chat is not a degraded chat.
2259
+ const warmRoster = await adapter.chatSeats(id);
2260
+ const audience = parsed.data.targets ?? warmRoster;
2127
2261
  if (audience.length === 0) {
2128
2262
  return reply.code(409).send({ error: `chat '${id}' has no warm seats — open it first` });
2129
2263
  }
@@ -2141,7 +2275,22 @@ runtime = {}) {
2141
2275
  chatTurns.reconcile(id, turn.turnId, seats);
2142
2276
  chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
2143
2277
  }
2144
- return reply.code(202).send({ seats, ...(turn !== null ? { turnId: turn.turnId } : {}) });
2278
+ // crew#641: re-state single-seat degradation on every turn so it is visible in the transcript.
2279
+ // Decided from `warmRoster` — the seats that are WARM — never from `seats`, which is only the
2280
+ // seats this turn reached: a targeted send to one seat of a two-seat chat would otherwise
2281
+ // announce a degradation that does not exist, and a false "you are degraded" sends an operator
2282
+ // after a problem they do not have.
2283
+ // crew#650: `refusedOf` is `undefined` for a chat THIS daemon did not open (a restart drops
2284
+ // the index) — that is "the refusals are unknown", not "the chat gained a seat". The
2285
+ // disclosure is decided by the warm roster alone, and the `undefined` is handed STRAIGHT to
2286
+ // the builder (never `?? []`, review of #658): only it may decide what an unknown record is
2287
+ // allowed to claim.
2288
+ const singleSeat202 = warmRoster.length === 1 ? singleSeatDisclosure(warmRoster[0], chatScopes.refusedOf(id), 'turn') : undefined;
2289
+ return reply.code(202).send({
2290
+ seats,
2291
+ ...(turn !== null ? { turnId: turn.turnId } : {}),
2292
+ ...(singleSeat202 !== undefined ? { singleSeat: singleSeat202 } : {}),
2293
+ });
2145
2294
  }
2146
2295
  catch (err) {
2147
2296
  // Nothing went out: the reservation is retracted so the next send is not refused for it.