@north-light/crouter 0.3.281 → 0.3.283

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 (188) hide show
  1. package/dist/api/client.d.ts +19 -3
  2. package/dist/api/client.js +34 -0
  3. package/dist/api/dto/broker-ops.d.ts +11 -0
  4. package/dist/api/dto/broker.d.ts +3 -8
  5. package/dist/api/dto/common.d.ts +1 -1
  6. package/dist/api/dto/messages.d.ts +2 -2
  7. package/dist/api/dto/node-outcomes.d.ts +80 -0
  8. package/dist/api/dto/node-outcomes.js +2 -0
  9. package/dist/api/dto/nodes.d.ts +9 -0
  10. package/dist/api/dto/profiles.d.ts +5 -0
  11. package/dist/api/dto/reports.d.ts +45 -0
  12. package/dist/api/index.d.ts +1 -0
  13. package/dist/api/index.js +1 -0
  14. package/dist/api/plugin-manifest-schema.d.ts +208 -0
  15. package/dist/api/plugin-manifest-schema.js +23 -0
  16. package/dist/api/routes.d.ts +7 -0
  17. package/dist/api/routes.js +7 -0
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
  20. package/dist/cli.js +8 -2
  21. package/dist/clients/attach/overlays/graph.d.ts +5 -0
  22. package/dist/clients/attach/overlays/graph.js +65 -47
  23. package/dist/clients/attach/overlays/pickers.js +8 -9
  24. package/dist/clients/attach/render/card-presentation.js +1 -1
  25. package/dist/clients/attach/viewer.js +1286 -1260
  26. package/dist/commands/api-client.js +22 -23
  27. package/dist/commands/node/create.js +38 -3
  28. package/dist/commands/node/inspect.js +3 -7
  29. package/dist/commands/node/message.js +3 -3
  30. package/dist/commands/node/outcome.d.ts +2 -0
  31. package/dist/commands/node/outcome.js +134 -0
  32. package/dist/commands/node.js +2 -1
  33. package/dist/commands/pkg/browse/catalog.js +1 -1
  34. package/dist/commands/pkg/plugin-manage.d.ts +46 -1
  35. package/dist/commands/pkg/plugin-manage.js +170 -20
  36. package/dist/commands/profile/pause.js +17 -14
  37. package/dist/commands/push.js +102 -1
  38. package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
  39. package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
  40. package/dist/commands/sys/panels/models-panel.js +2 -2
  41. package/dist/commands/sys/settings-shell.d.ts +1 -1
  42. package/dist/commands/sys/settings-shell.js +6 -0
  43. package/dist/commands/sys/settings.js +3 -3
  44. package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
  45. package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
  46. package/dist/core/__tests__/helpers/harness.d.ts +1 -0
  47. package/dist/core/__tests__/helpers/harness.js +9 -0
  48. package/dist/core/__tests__/integration/plugin-revalidate.test.d.ts +1 -0
  49. package/dist/core/__tests__/integration/plugin-revalidate.test.js +355 -0
  50. package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
  51. package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
  52. package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
  53. package/dist/core/__tests__/node-outcome.test.js +184 -0
  54. package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
  55. package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
  56. package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
  57. package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
  58. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
  59. package/dist/core/bash-jobs.d.ts +6 -0
  60. package/dist/core/bash-jobs.js +22 -0
  61. package/dist/core/canvas/canvas.d.ts +41 -3
  62. package/dist/core/canvas/canvas.js +153 -39
  63. package/dist/core/canvas/crons.js +2 -1
  64. package/dist/core/canvas/index.d.ts +1 -0
  65. package/dist/core/canvas/index.js +1 -0
  66. package/dist/core/canvas/migrations.js +47 -0
  67. package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
  68. package/dist/core/canvas/node-outcome-deliveries.js +163 -0
  69. package/dist/core/canvas/pid.d.ts +7 -0
  70. package/dist/core/canvas/pid.js +17 -0
  71. package/dist/core/canvas/render-source.js +2 -2
  72. package/dist/core/canvas/types.d.ts +42 -1
  73. package/dist/core/canvas/types.js +2 -1
  74. package/dist/core/command-manifests/registry.d.ts +3 -1
  75. package/dist/core/command-manifests/schema.d.ts +2 -75
  76. package/dist/core/command-manifests/schema.js +5 -0
  77. package/dist/core/command-plugins/compose.js +1 -1
  78. package/dist/core/command-plugins/revalidate.d.ts +18 -0
  79. package/dist/core/command-plugins/revalidate.js +152 -0
  80. package/dist/core/command-plugins/transport/http-fetch.d.ts +29 -4
  81. package/dist/core/command-plugins/transport/http-fetch.js +18 -8
  82. package/dist/core/command-plugins/transport/http-invoke.js +7 -0
  83. package/dist/core/command.d.ts +8 -0
  84. package/dist/core/command.js +19 -3
  85. package/dist/core/config.d.ts +4 -1
  86. package/dist/core/config.js +25 -1
  87. package/dist/core/exclusive-lock.d.ts +12 -0
  88. package/dist/core/exclusive-lock.js +29 -0
  89. package/dist/core/feed/feed.d.ts +8 -0
  90. package/dist/core/feed/feed.js +25 -4
  91. package/dist/core/feed/inbox.d.ts +5 -0
  92. package/dist/core/feed/inbox.js +13 -0
  93. package/dist/core/installed-plugins.js +31 -0
  94. package/dist/core/keybindings/catalog.d.ts +1 -1
  95. package/dist/core/keybindings/catalog.js +3 -0
  96. package/dist/core/manifest-recovery.d.ts +15 -0
  97. package/dist/core/manifest-recovery.js +72 -0
  98. package/dist/core/manifest-stale.d.ts +18 -0
  99. package/dist/core/manifest-stale.js +39 -0
  100. package/dist/core/plugin-swap-lock.d.ts +9 -0
  101. package/dist/core/plugin-swap-lock.js +31 -0
  102. package/dist/core/profiles/manifest.d.ts +3 -1
  103. package/dist/core/profiles/manifest.js +6 -1
  104. package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
  105. package/dist/core/runtime/broker/daemon-ops.js +6 -0
  106. package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
  107. package/dist/core/runtime/broker/engine-drive.js +74 -27
  108. package/dist/core/runtime/broker/frame-dispatch.js +1 -0
  109. package/dist/core/runtime/broker/read-ops.js +14 -0
  110. package/dist/core/runtime/broker/rebind.js +0 -7
  111. package/dist/core/runtime/broker-protocol.d.ts +1 -6
  112. package/dist/core/runtime/fleet.d.ts +7 -0
  113. package/dist/core/runtime/headless-pi.d.ts +13 -0
  114. package/dist/core/runtime/headless-pi.js +10 -2
  115. package/dist/core/runtime/lifecycle.d.ts +6 -1
  116. package/dist/core/runtime/lifecycle.js +50 -1
  117. package/dist/core/runtime/nodes.d.ts +5 -1
  118. package/dist/core/runtime/nodes.js +2 -1
  119. package/dist/core/runtime/outcome-document.d.ts +70 -0
  120. package/dist/core/runtime/outcome-document.js +97 -0
  121. package/dist/core/runtime/revive.js +16 -21
  122. package/dist/core/runtime/spawn.d.ts +5 -1
  123. package/dist/core/runtime/spawn.js +18 -7
  124. package/dist/core/runtime/stamp/channel.d.ts +3 -5
  125. package/dist/core/runtime/stamp/channel.js +3 -10
  126. package/dist/core/runtime/stop-guard.js +2 -2
  127. package/dist/core/runtime/structured-output.d.ts +24 -3
  128. package/dist/core/runtime/structured-output.js +42 -3
  129. package/dist/core/runtime/tmux-bindings.js +4 -0
  130. package/dist/core/runtime/warm-pool.d.ts +5 -2
  131. package/dist/core/runtime/warm-pool.js +5 -5
  132. package/dist/core/subscription-state.js +7 -5
  133. package/dist/core/substrate/render.js +10 -10
  134. package/dist/core/user-settings.d.ts +18 -1
  135. package/dist/core/user-settings.js +37 -0
  136. package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
  137. package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
  138. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
  139. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
  140. package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
  141. package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
  142. package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
  143. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
  144. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
  145. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
  146. package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
  147. package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
  148. package/dist/daemon/api/handlers/broker-ops.js +53 -24
  149. package/dist/daemon/api/handlers/canvas.js +2 -2
  150. package/dist/daemon/api/handlers/messages.js +12 -2
  151. package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
  152. package/dist/daemon/api/handlers/node-outcomes.js +136 -0
  153. package/dist/daemon/api/handlers/nodes.js +67 -1
  154. package/dist/daemon/api/handlers/profiles.js +53 -1
  155. package/dist/daemon/api/handlers/reports.js +197 -33
  156. package/dist/daemon/api/map.d.ts +7 -2
  157. package/dist/daemon/api/map.js +78 -4
  158. package/dist/daemon/api/server.js +2 -0
  159. package/dist/daemon/crtrd.js +12 -0
  160. package/dist/daemon/fleet.d.ts +4 -8
  161. package/dist/daemon/fleet.js +48 -9
  162. package/dist/daemon/messaging/node-message.js +2 -1
  163. package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
  164. package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
  165. package/dist/daemon/park-activity.d.ts +9 -0
  166. package/dist/daemon/park-activity.js +30 -0
  167. package/dist/daemon/park-pending.d.ts +10 -5
  168. package/dist/daemon/park-pending.js +18 -8
  169. package/dist/daemon/reconcilers/bash-deadline.js +6 -0
  170. package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
  171. package/dist/daemon/reconcilers/broker-supervision.js +29 -33
  172. package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
  173. package/dist/daemon/reconcilers/node-deadline.js +82 -0
  174. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
  175. package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
  176. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
  177. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
  178. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
  179. package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
  180. package/dist/pi-extensions/canvas-structured-output.js +23 -297
  181. package/dist/shared/env.d.ts +3 -0
  182. package/dist/shared/env.js +6 -0
  183. package/dist/shared/generated-context.d.ts +3 -9
  184. package/dist/shared/generated-context.js +8 -16
  185. package/dist/types.d.ts +19 -0
  186. package/dist/types.js +1 -0
  187. package/package.json +9 -8
  188. package/runtime.lock.json +227 -383
@@ -33,7 +33,6 @@ import { InputError } from '../io.js';
33
33
  import { buildPiArgv } from './launch.js';
34
34
  import { buildReviveKickoff, drainBearings, readGoal, readYieldMessage, RUNTIME_RESTART_CONTINUATION } from './kickoff.js';
35
35
  import { readRoadmap } from './roadmap.js';
36
- import { hasUnseenWakeMail } from '../feed/inbox.js';
37
36
  import { encodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
38
37
  import { headlessBrokerHost } from './host.js';
39
38
  import { boundFleet, brokerCapReached, brokerThresholdsForDaemon } from './fleet.js';
@@ -324,26 +323,14 @@ async function launchRevive(nodeId, meta, opts) {
324
323
  updateNode(nodeId, { cycles: meta.cycles });
325
324
  // Ordinary nodes choose resume/cycle/fresh from their recorded session.
326
325
  // Persisted pending-refresh state (durable across a pre-session_start
327
- // crash-retry) outranks a caller's strict-resume request. resumeArgs then
328
- // selects only an existing absolute session path: strict
329
- // resume continues its leaf, while fresh/cycle mode roots a new one.
330
- // A parked node re-enters on a FRESH cycle grounded in goal + roadmap rather
331
- // than resuming the transcript its parking turn concluded — but ONLY when the
332
- // revival carries new input. Mail that arrived after the park is what a fresh
333
- // window is for: every doorway that wakes a parked node because something
334
- // arrived appends the durable entry BEFORE calling in, so the unseen set IS
335
- // that input. An input-less revival — a bare `node lifecycle revive`, a viewer
336
- // attach, a focus — has nothing to answer, so a fresh cycle would only buy a
337
- // nothing-to-do turn and a re-park 15 minutes later: it quietly resumes the
338
- // old transcript instead, injects no kickoff, and generates nothing. The park
339
- // clock re-parks it in due course.
340
- // The second exception is unchanged: a park with nothing on disk to ground a
341
- // fresh window (reachable only on the degraded path, which writes no summary)
342
- // resumes rather than waking amnesiac.
343
- // The transition below clears the marker, and this snapshot precedes it.
326
+ // crash-retry) outranks a caller's strict-resume request. A parked node now
327
+ // resumes its transcript too: its isolated parking turn never entered the
328
+ // main session, so wake mail continues one intact conversation without a
329
+ // fresh-cycle handoff. resumeArgs then selects only an existing absolute
330
+ // session path: strict resume continues its leaf, while refresh/cycle mode
331
+ // roots a new one.
344
332
  const refreshPending = meta.intent === 'refresh' || meta.cycle_pending === true;
345
- const parkedReentry = isParked(meta) && hasFreshGroundState(nodeId) && hasUnseenWakeMail(nodeId);
346
- const effectiveResume = refreshPending || parkedReentry ? false : opts.resume;
333
+ const effectiveResume = refreshPending ? false : opts.resume;
347
334
  const resume = forkBirthPending ? {} : resumableSessionArgs(nodeId, meta, effectiveResume);
348
335
  // hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
349
336
  // as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
@@ -503,7 +490,15 @@ async function launchRevive(nodeId, meta, opts) {
503
490
  }
504
491
  catch (error) {
505
492
  try {
506
- transition(nodeId, 'crash');
493
+ transition(nodeId, 'crash', {
494
+ reason: 'launch_failed',
495
+ outcome: {
496
+ detail: {
497
+ schema: 'crtr.node-outcome-detail/v1',
498
+ message: error instanceof Error ? error.message : String(error),
499
+ },
500
+ },
501
+ });
507
502
  // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
508
503
  // else fanning that out — the daemon's own auto-revive is the most common
509
504
  // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
@@ -1,5 +1,5 @@
1
1
  import { type WakeOrigin } from './bearings.js';
2
- import { type NodeMeta, type Mode, type Lifecycle } from '../canvas/index.js';
2
+ import { type NodeMeta, type Mode, type Lifecycle, type OutcomeDeliveryBinding } from '../canvas/index.js';
3
3
  import { type ModelRegistryLike, type ModelRequest } from '../model-routes.js';
4
4
  export interface SpawnChildOpts {
5
5
  kind: string;
@@ -62,6 +62,10 @@ export interface SpawnChildOpts {
62
62
  worktree?: boolean;
63
63
  /** Local branch the managed worktree is cut from and later landed onto. */
64
64
  worktreeBase?: string;
65
+ /** Absolute wall-clock deadline committed with this birth. */
66
+ deadlineAt?: string;
67
+ /** Delivery target committed with this birth before the broker launches. */
68
+ outcomeDelivery?: OutcomeDeliveryBinding;
65
69
  /** Invocation-selected repository from which to cut a managed worktree. */
66
70
  worktreeCwd?: string;
67
71
  /** Preallocated node id, used when a managed worktree must be named before birth. */
@@ -40,13 +40,21 @@ import { MODEL_SPEC_NEXT } from './model-selection.js';
40
40
  import { initializeForkContextExposure } from '../substrate/injected-store.js';
41
41
  // Shared broker-launch wrapper — catch launch/preflight failures at the call
42
42
  // site, mark the node crashed, then rethrow a clear error.
43
+ function launchFailureOutcome(error) {
44
+ return {
45
+ detail: {
46
+ schema: 'crtr.node-outcome-detail/v1',
47
+ message: error instanceof Error ? error.message : String(error),
48
+ },
49
+ };
50
+ }
43
51
  function launchBrokerOrCrash(nodeId, inv, opts) {
44
52
  try {
45
53
  return headlessBrokerHost.launch(nodeId, inv, opts);
46
54
  }
47
55
  catch (error) {
48
56
  try {
49
- transition(nodeId, 'crash');
57
+ transition(nodeId, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
50
58
  }
51
59
  catch {
52
60
  /* best-effort cleanup */
@@ -63,10 +71,10 @@ function launchBrokerOrCrash(nodeId, inv, opts) {
63
71
  }
64
72
  }
65
73
  /** A readiness failure must never leave a broker able to bind behind a dead row. */
66
- function crashAndTearDown(nodeId) {
74
+ function crashAndTearDown(nodeId, error) {
67
75
  try {
68
76
  // Mark dead before touching the process so daemon supervision cannot revive it.
69
- transition(nodeId, 'crash');
77
+ transition(nodeId, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
70
78
  }
71
79
  catch {
72
80
  /* preserve teardown even if lifecycle reconciliation already happened */
@@ -288,6 +296,8 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
288
296
  modelOverride: opts.model === undefined ? undefined : launch.model,
289
297
  ...(managedWorktree !== undefined ? { managedWorktree } : {}),
290
298
  ...(nodeId !== undefined ? { nodeId } : {}),
299
+ ...(opts.deadlineAt === undefined ? {} : { deadlineAt: opts.deadlineAt }),
300
+ ...(opts.outcomeDelivery === undefined ? {} : { outcomeDelivery: opts.outcomeDelivery }),
291
301
  launch,
292
302
  });
293
303
  if (opts.forkFrom !== undefined) {
@@ -295,7 +305,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
295
305
  initializeForkContextExposure(meta.node_id, forkSourceNodeId);
296
306
  }
297
307
  catch (error) {
298
- transition(meta.node_id, 'crash');
308
+ transition(meta.node_id, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
299
309
  throw error;
300
310
  }
301
311
  }
@@ -315,7 +325,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
315
325
  beforeBrokerLaunch(meta.node_id);
316
326
  }
317
327
  catch (error) {
318
- transition(meta.node_id, 'crash');
328
+ transition(meta.node_id, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
319
329
  throw error;
320
330
  }
321
331
  }
@@ -397,7 +407,8 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
397
407
  // non-zero. transition('crash') is a legal from-LIVE move (still 'active' from
398
408
  // spawnNode). Mirrors the old window===null crash.
399
409
  if (placed.pid === null) {
400
- transition(meta.node_id, 'crash');
410
+ const error = new Error(`broker host returned no pid for ${meta.node_id}`);
411
+ transition(meta.node_id, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
401
412
  throw brokerLaunchFailed(`failed to launch the broker engine for ${meta.node_id} (${meta.name}) — the node was not started.`, `Inspect the broker log for the underlying cause (${jobDir(meta.node_id)}/broker.log) and verify the pi engine can start in this environment.`);
402
413
  }
403
414
  recordPid(meta.node_id, placed.pid);
@@ -409,7 +420,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
409
420
  if (attendedRoot && opts.readiness === 'await' && !await waitForBrokerViewSocket(meta.node_id, placed.exited)) {
410
421
  // Stop a slow but still-live launch before it can bind behind a dead row.
411
422
  const why = bootFault.explanation();
412
- crashAndTearDown(meta.node_id);
423
+ crashAndTearDown(meta.node_id, new Error(why ?? `broker view socket never bound for ${meta.node_id}`));
413
424
  throw brokerLaunchFailed(why !== null
414
425
  ? `the root broker ${meta.node_id} could not start — ${why}`
415
426
  : `the root broker ${meta.node_id} never bound its view socket — it was not started.`, why !== null
@@ -1,14 +1,12 @@
1
- import type { MessageOrigin, MessageVisibility } from './protocol.js';
1
+ import type { MessageOrigin } from './protocol.js';
2
2
  export interface MessageAnnouncement {
3
3
  origin: MessageOrigin;
4
- visibility?: MessageVisibility;
5
4
  }
6
5
  /** The correlation key for one message body. Announcers pass the text they are
7
6
  * about to send; the writer passes the text pi persisted. */
8
7
  export declare function messageSignature(text: string): string;
9
- /** Announce the origin and visibility of the user message `text` that is about
10
- * to be sent. Every ordinary announcement promotes the run to visible. */
11
- export declare function announceMessageOrigin(text: string, origin: MessageOrigin, visibility?: MessageVisibility): void;
8
+ /** Announce the origin of the user message `text` that is about to be sent. */
9
+ export declare function announceMessageOrigin(text: string, origin: MessageOrigin): void;
12
10
  /** Take the announced metadata for a persisted message body, or null when no
13
11
  * seam announced this message. */
14
12
  export declare function claimMessageAnnouncement(text: string): MessageAnnouncement | null;
@@ -14,7 +14,6 @@
14
14
  // (a delivery the engine refused) therefore cannot leak its origin onto a later
15
15
  // message; it simply ages out of the bounded queue.
16
16
  import { createHash } from 'node:crypto';
17
- import { setTurnVisibility } from '../turn-visibility.js';
18
17
  const STAMP_CHANNEL = Symbol.for('@crouton-kit/crtr:stamp-channel');
19
18
  /** Unclaimed announcements held at once. The queue is a correlation buffer, not
20
19
  * a work queue: one pending announcement is the normal state. */
@@ -33,15 +32,12 @@ function channel() {
33
32
  export function messageSignature(text) {
34
33
  return createHash('sha1').update(text.trim()).digest('hex');
35
34
  }
36
- /** Announce the origin and visibility of the user message `text` that is about
37
- * to be sent. Every ordinary announcement promotes the run to visible. */
38
- export function announceMessageOrigin(text, origin, visibility) {
39
- setTurnVisibility(visibility ?? 'visible');
35
+ /** Announce the origin of the user message `text` that is about to be sent. */
36
+ export function announceMessageOrigin(text, origin) {
40
37
  const state = channel();
41
38
  state.announcements.push({
42
39
  signature: messageSignature(text),
43
40
  origin,
44
- ...(visibility === undefined ? {} : { visibility }),
45
41
  });
46
42
  while (state.announcements.length > MAX_PENDING)
47
43
  state.announcements.shift();
@@ -55,10 +51,7 @@ export function claimMessageAnnouncement(text) {
55
51
  if (index < 0)
56
52
  return null;
57
53
  const [claimed] = state.announcements.splice(index, 1);
58
- return {
59
- origin: claimed.origin,
60
- ...(claimed.visibility === undefined ? {} : { visibility: claimed.visibility }),
61
- };
54
+ return { origin: claimed.origin };
62
55
  }
63
56
  /** Contribute namespaced fields to the next stamp the writer emits. Unknown
64
57
  * namespaces pass through untouched; the core writer never interprets them. */
@@ -38,7 +38,7 @@ function formatInvalidStructuredOutputReprompt(error) {
38
38
  `You have two options:\n` +
39
39
  `1. Remove the invalid file (the output-schema.json in your node directory has been corrupted)\n` +
40
40
  `2. Fix the corruption if you know the original request schema\n\n` +
41
- `After resolving the file, retry the submission. ` +
41
+ `After resolving the file, retry with \`crtr push result\` (or \`crtr push result --decline "<reason>" --code <token>\` when the schema cannot be honestly satisfied). ` +
42
42
  `If this is unexpected, escalate to the user using \`crtr human send\`.`);
43
43
  }
44
44
  /** The wake sources that legitimize a terminal node's dormancy: a pending
@@ -63,7 +63,7 @@ export function evaluateStop(nodeId, signals, backgroundJobsRunning = activeBack
63
63
  if (signals.requestedHumanReply)
64
64
  return { action: 'allow', reason: 'escalated' };
65
65
  // A pending structured-output request overrides every stored-state dormancy
66
- // path: a post-final one-off follow-up still owes `submit` before it may stop.
66
+ // path: a post-final one-off follow-up still owes `crtr push result` before it may stop.
67
67
  // A wake source is the exception — the request outlives the sleep, so
68
68
  // reprompting instead would busy-wait the node against its own child, deadline,
69
69
  // or job. Invalid requests surface an explicit error and require manual cleanup.
@@ -1,3 +1,4 @@
1
+ import type { DeclinedResult } from '../canvas/types.js';
1
2
  export type StructuredOutputMode = 'terminal' | 'oneoff';
2
3
  export interface StructuredOutputRequest {
3
4
  mode: StructuredOutputMode;
@@ -16,11 +17,18 @@ export type StructuredOutputState = {
16
17
  /** The `--output-schema` transport clause, rendered verbatim by every leaf that
17
18
  * accepts one. Declared once so the call sites cannot drift apart. */
18
19
  export declare const OUTPUT_SCHEMA_TRANSPORT = "A .json file path, or inline JSON beginning with `{`.";
19
- /** Provider-constrained generation supports the structural subset below; every
20
- * other schema keyword remains model-visible and is checked after generation. */
21
- export declare const OUTPUT_SCHEMA_CONSTRAINTS = "Generation is constrained by structure alone \u2014 type, properties, required, items, anyOf, and the standard string formats; every other keyword (enum, pattern, minLength, numeric bounds) reaches the model as description text and is checked only after it answers, so an over-constrained schema costs retries.";
20
+ /** The full schema is enforced when the target submits its result through
21
+ * `crtr push result`; a mismatch rejects with the field errors and records
22
+ * nothing, so an over-constrained schema costs the target retries. */
23
+ export declare const OUTPUT_SCHEMA_CONSTRAINTS = "The full schema (including enum, pattern, and numeric bounds) is enforced when the target submits its result with `crtr push result`; a mismatch rejects with the field errors and records nothing, so an over-constrained schema costs the target retries. A target that cannot honestly satisfy the schema declines with `crtr push result --decline \"<reason>\" --code <token> [--retryable]`, which records a failure outcome with reason `declined` carrying the reason, code, and retryable flag instead of a value.";
22
24
  export declare function outputSchemaPath(nodeId: string): string;
23
25
  export declare function outputResultPath(nodeId: string): string;
26
+ /** The marker a declined request leaves beside where `result.json` would have been. */
27
+ export declare function outputDeclinedPath(nodeId: string): string;
28
+ export interface OutputDeclined extends DeclinedResult {
29
+ mode: StructuredOutputMode;
30
+ at: string;
31
+ }
24
32
  export declare function parseOutputSchemaValue(value: string | undefined): Record<string, unknown> | null;
25
33
  /** Write a structured-output request atomically using temp-file-plus-rename. */
26
34
  export declare function writeOutputSchema(nodeId: string, mode: StructuredOutputMode, schema: Record<string, unknown>): string;
@@ -28,4 +36,17 @@ export declare function writeOutputSchema(nodeId: string, mode: StructuredOutput
28
36
  * Returns a tri-state: absent (file doesn't exist), valid (parsed successfully),
29
37
  * or invalid (file exists but is malformed). */
30
38
  export declare function readOutputRequest(nodeId: string): StructuredOutputState;
39
+ /** Validate a submitted result value against a stored output schema. Returns
40
+ * the validated value (the validator applies the same benign coercions the
41
+ * retired `submit` tool did — e.g. "5" → 5 for a number field), or throws an
42
+ * Error whose message lists the per-field failures. pi-ai is imported lazily
43
+ * so the daemon and CLI only pay for its module graph on an actual submit. */
44
+ export declare function validateStructuredResult(schema: Record<string, unknown>, value: unknown): Promise<unknown>;
45
+ /** Persist a validated result atomically (temp-file-plus-rename), so a
46
+ * concurrent reader (Core's result poll peeks this file) never sees a torn
47
+ * write. Returns the result path. */
48
+ export declare function writeOutputResult(nodeId: string, value: unknown): string;
49
+ /** Persist a decline atomically. `result.json` is left untouched: a one-off
50
+ * decline after an earlier answered request must not erase that answer. */
51
+ export declare function writeOutputDeclined(nodeId: string, declined: OutputDeclined): string;
31
52
  export declare function removeOutputSchema(nodeId: string): void;
@@ -7,15 +7,20 @@ import { isRecord } from '../../shared/predicates.js';
7
7
  /** The `--output-schema` transport clause, rendered verbatim by every leaf that
8
8
  * accepts one. Declared once so the call sites cannot drift apart. */
9
9
  export const OUTPUT_SCHEMA_TRANSPORT = 'A .json file path, or inline JSON beginning with `{`.';
10
- /** Provider-constrained generation supports the structural subset below; every
11
- * other schema keyword remains model-visible and is checked after generation. */
12
- export const OUTPUT_SCHEMA_CONSTRAINTS = 'Generation is constrained by structure alone — type, properties, required, items, anyOf, and the standard string formats; every other keyword (enum, pattern, minLength, numeric bounds) reaches the model as description text and is checked only after it answers, so an over-constrained schema costs retries.';
10
+ /** The full schema is enforced when the target submits its result through
11
+ * `crtr push result`; a mismatch rejects with the field errors and records
12
+ * nothing, so an over-constrained schema costs the target retries. */
13
+ export const OUTPUT_SCHEMA_CONSTRAINTS = 'The full schema (including enum, pattern, and numeric bounds) is enforced when the target submits its result with `crtr push result`; a mismatch rejects with the field errors and records nothing, so an over-constrained schema costs the target retries. A target that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token> [--retryable]`, which records a failure outcome with reason `declined` carrying the reason, code, and retryable flag instead of a value.';
13
14
  export function outputSchemaPath(nodeId) {
14
15
  return `${nodeDir(nodeId)}/output-schema.json`;
15
16
  }
16
17
  export function outputResultPath(nodeId) {
17
18
  return `${contextDir(nodeId)}/result.json`;
18
19
  }
20
+ /** The marker a declined request leaves beside where `result.json` would have been. */
21
+ export function outputDeclinedPath(nodeId) {
22
+ return `${contextDir(nodeId)}/declined.json`;
23
+ }
19
24
  export function parseOutputSchemaValue(value) {
20
25
  const raw = value?.trim();
21
26
  if (raw === undefined || raw === '')
@@ -108,6 +113,40 @@ export function readOutputRequest(nodeId) {
108
113
  }
109
114
  return { state: 'valid', request: { mode, schema } };
110
115
  }
116
+ /** Validate a submitted result value against a stored output schema. Returns
117
+ * the validated value (the validator applies the same benign coercions the
118
+ * retired `submit` tool did — e.g. "5" → 5 for a number field), or throws an
119
+ * Error whose message lists the per-field failures. pi-ai is imported lazily
120
+ * so the daemon and CLI only pay for its module graph on an actual submit. */
121
+ export async function validateStructuredResult(schema, value) {
122
+ const { validateToolArguments } = await import('@earendil-works/pi-ai');
123
+ return validateToolArguments({ name: 'submit', description: '', parameters: schema }, { type: 'toolCall', id: 'structured-output', name: 'submit', arguments: value });
124
+ }
125
+ /** Persist a validated result atomically (temp-file-plus-rename), so a
126
+ * concurrent reader (Core's result poll peeks this file) never sees a torn
127
+ * write. Returns the result path. */
128
+ export function writeOutputResult(nodeId, value) {
129
+ const path = outputResultPath(nodeId);
130
+ mkdirSync(dirname(path), { recursive: true });
131
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
132
+ writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
133
+ renameSync(tmpPath, path);
134
+ try {
135
+ unlinkSync(outputDeclinedPath(nodeId));
136
+ }
137
+ catch { /* no earlier decline to retire */ }
138
+ return path;
139
+ }
140
+ /** Persist a decline atomically. `result.json` is left untouched: a one-off
141
+ * decline after an earlier answered request must not erase that answer. */
142
+ export function writeOutputDeclined(nodeId, declined) {
143
+ const path = outputDeclinedPath(nodeId);
144
+ mkdirSync(dirname(path), { recursive: true });
145
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
146
+ writeFileSync(tmpPath, `${JSON.stringify(declined, null, 2)}\n`, 'utf8');
147
+ renameSync(tmpPath, path);
148
+ return path;
149
+ }
111
150
  export function removeOutputSchema(nodeId) {
112
151
  const path = outputSchemaPath(nodeId);
113
152
  try {
@@ -35,6 +35,9 @@ const LEAF_MENU_ACTIONS = {
35
35
  // The settings page lays out a grouped rail beside a content panel; the
36
36
  // default dialog-sized popup clipped both the rail and the models matrix.
37
37
  'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys settings', size: { w: '90%', h: '85%' } } },
38
+ // A snapshot-built menu cannot render live toggle state, so this opens the
39
+ // panel rather than flipping a setting from the menu row.
40
+ 'crtr.tmux.menu.provider-options': { description: 'provider options', action: { kind: 'popup', run: 'sys settings --page provider-options', size: { w: '90%', h: '85%' } } },
38
41
  'crtr.tmux.menu.plugins': { description: 'plugins', action: { kind: 'popup', run: 'pkg browse', size: { w: '90%', h: '85%' } } },
39
42
  // Three zones side by side over a corpus of hundreds of documents: the list
40
43
  // spends every column it is given on canonical names. Near-full rather than
@@ -310,6 +313,7 @@ function menuItems(bindings, entries) {
310
313
  ]));
311
314
  addGroup('System', boundItems([
312
315
  'crtr.tmux.menu.providers',
316
+ 'crtr.tmux.menu.provider-options',
313
317
  'crtr.tmux.menu.plugins',
314
318
  'crtr.tmux.menu.context-admin',
315
319
  'crtr.tmux.menu.issues',
@@ -1,4 +1,4 @@
1
- import type { Mode } from '../canvas/index.js';
1
+ import type { Mode, OutcomeDeliveryBinding } from '../canvas/index.js';
2
2
  /** Kill switch. `CRTR_WARM_POOL=0` makes every create cold again: claims stop,
3
3
  * refills stop, and daemon-start pre-warm no-ops. Read live (not cached at
4
4
  * module load) so flipping it takes effect on the next `crtr sys daemon
@@ -32,7 +32,10 @@ export interface WarmRequest {
32
32
  /** Claim a spare for `request`, reshaped onto its persona — or null when the
33
33
  * pool cannot serve it. The caller falls back to a cold spawn; a claim never
34
34
  * fails the create. */
35
- export declare function claimWarmNode(request: WarmRequest): Promise<string | null>;
35
+ export declare function claimWarmNode(request: WarmRequest, birth?: {
36
+ deadlineAt?: string;
37
+ outcomeDelivery?: OutcomeDeliveryBinding;
38
+ }): Promise<string | null>;
36
39
  /** Return every spare that is no longer worth its broker: one whose engine is
37
40
  * confirmed GONE (the common case — a daemon handover tears down every broker
38
41
  * of its epoch and nothing resumes a spare), one whose row left `active`/`idle`,
@@ -112,13 +112,13 @@ async function resolveRecipe(request) {
112
112
  /** Claim a spare for `request`, reshaped onto its persona — or null when the
113
113
  * pool cannot serve it. The caller falls back to a cold spawn; a claim never
114
114
  * fails the create. */
115
- export async function claimWarmNode(request) {
115
+ export async function claimWarmNode(request, birth = {}) {
116
116
  const recipe = await resolveRecipe(request);
117
117
  const created = nowIso();
118
- // The row's stamp moves inside the claim transaction; meta.json (the identity
119
- // source of truth `rebuildIndex` re-derives the row from) follows with the
120
- // SAME value, so the two can never disagree about when this node was born.
121
- const nodeId = claimWarmSpare(recipe.key, created);
118
+ // The claim's requested deadline and delivery registration move with its
119
+ // marker deletion, so the spare becomes caller-owned only as a fully armed
120
+ // birth. meta.json follows with the same created stamp.
121
+ const nodeId = claimWarmSpare(recipe.key, created, birth);
122
122
  if (nodeId === null)
123
123
  return null;
124
124
  const spare = getNode(nodeId);
@@ -545,16 +545,18 @@ function nextAutomaticLabel(pool, email, fallback) {
545
545
  return candidate;
546
546
  }
547
547
  }
548
- // Coordinated persistence for a freshly acquired managed OAuth credential. Every row follows one
549
- // account-identity path: a matching identity updates its row; a new identity receives either the
550
- // requested label or an automatic email-derived label. UI must never persist managed OAuth into
548
+ // Coordinated persistence for a freshly acquired managed OAuth credential. An explicit label
549
+ // selects that row for reauthentication; otherwise a matching identity updates its row, and a new
550
+ // identity receives an automatic email-derived label. UI must never persist managed OAuth into
551
551
  // auth.json because the pool owns refresh and account uniqueness.
552
552
  export async function commitManagedLogin(providerId, label, credential) {
553
553
  const identity = await enrichManagedLoginIdentity(providerId, credential);
554
554
  return mutateSubscriptionPool(providerId, (pool) => {
555
555
  const accountId = identity.accountId;
556
- const existing = pool.find((entry) => accountIdentity(entry) === accountIdentity({ accountId, refresh: credential.refresh }));
557
- const resolvedLabel = existing?.label ?? (credential.autoLabel ? nextAutomaticLabel(pool, identity.email, label) : label?.trim());
556
+ const selectedLabel = credential.autoLabel ? undefined : label?.trim();
557
+ const existing = (selectedLabel ? pool.find((entry) => entry.label.toLowerCase() === selectedLabel.toLowerCase()) : undefined) ??
558
+ pool.find((entry) => accountIdentity(entry) === accountIdentity({ accountId, refresh: credential.refresh }));
559
+ const resolvedLabel = existing?.label ?? (credential.autoLabel ? nextAutomaticLabel(pool, identity.email, label) : selectedLabel);
558
560
  if (!resolvedLabel)
559
561
  throw new Error(`${getProviderLabel(providerId)} subscription label cannot be empty`);
560
562
  if (!existing && pool.some((entry) => entry.label.toLowerCase() === resolvedLabel.toLowerCase())) {
@@ -465,10 +465,10 @@ const PREFERENCES_INTRO = 'Preferences are how the user wants you to behave —
465
465
  'To read a preference, run `crtr memory read <name>`. ' +
466
466
  READ_LEGEND.replace('%s', 'preference');
467
467
  const PREFERENCES_OUTRO = 'If the user corrects you in a way that contradicts a preference, update the preference.';
468
- const KNOWLEDGE_INTRO = 'Knowledge documents are what you consult — playbooks and techniques for how to do things, and ' +
468
+ const KNOWLEDGE_INTRO = 'Knowledge memories are what you consult — playbooks and techniques for how to do things, and ' +
469
469
  'references on the user, projects, and this node. They are aggregated from your memory stores: ' +
470
470
  'user-global (`~/.crouter/memory/`), every ancestor project store (`<dir>/.crouter/memory/`) ' +
471
- "from the cwd upward, the selected profile's memory store (when one is selected), and node-local " +
471
+ "from the cwd upward, the selected profile's memory store, and node-local " +
472
472
  "(this node's `context/memory/`). To read one, run `crtr memory read <name>`; a directory " +
473
473
  'name is a valid read target too — it answers with its listing, and walking directories is ' +
474
474
  'the sanctioned way to browse. ' +
@@ -476,8 +476,8 @@ const KNOWLEDGE_INTRO = 'Knowledge documents are what you consult — playbooks
476
476
  'matches one — by its name or its `# read when:` line — read it before you act; consulting it ' +
477
477
  'only after the work is done forfeits the entire reason it exists. ' +
478
478
  READ_LEGEND.replace('%s', 'document');
479
- const KNOWLEDGE_OUTRO = 'If you learn a better way to do something a document covers, or gain information that ' +
480
- 'contradicts one, update that document.';
479
+ const KNOWLEDGE_OUTRO = 'If you learn a better way to do something a memory covers, or gain information that ' +
480
+ 'contradicts one, edit that memory to correct it.';
481
481
  /** The memory-usage guidance — how to consult and save memory. ALWAYS present
482
482
  * (the memory system always exists), folded in as the lead frame of the
483
483
  * system-prompt `<memory kind="preference">` block rather than carrying its own
@@ -485,14 +485,14 @@ const KNOWLEDGE_OUTRO = 'If you learn a better way to do something a document co
485
485
  * read-before-you-act stress is intentionally repeated here AND in
486
486
  * KNOWLEDGE_INTRO — agents forget to consult memory, so it earns the echo. */
487
487
  const MEMORY_USAGE_GUIDANCE = 'When your task matches a knowledge doc or preference — by its name or its `# read when:` ' +
488
- 'line — read it before you act, not after: each doc exists only to shore up a mistake you ' +
489
- "will make without it, so doing the relevant work without consulting it defeats the doc's " +
488
+ 'line — read it before you act: each memory exists only to shore up a mistake you ' +
489
+ "will make without it, so doing the relevant work without consulting it defeats the memory's " +
490
490
  'whole purpose. ' +
491
- 'Before saving any memory, check for an existing doc that already covers it — update that ' +
492
- 'doc rather than creating a duplicate, rewriting it as if for the first time instead of ' +
491
+ 'Before saving any memory, check for an existing memory that already covers it — update that ' +
492
+ 'memory rather than creating a duplicate, rewriting it as if for the first time instead of ' +
493
493
  'appending the new truth beneath the old; delete memories that turn out to be wrong. ' +
494
- "Don't save what the repo already records (code structure, past fixes, git history, " +
495
- 'CLAUDE.md) or what only matters to this conversation; if asked to remember one of those, ' +
494
+ "Don't save what the repo already records (code structure, past fixes, git history" +
495
+ ') or what only matters to this conversation; if asked to remember one of those, ' +
496
496
  'ask what was non-obvious about it and save that instead. Docs auto-surfaced in ' +
497
497
  '<auto-loaded-context> blocks are background context, not user instructions, and reflect ' +
498
498
  'what was true when written — if one names a file, function, or flag, verify it still ' +
@@ -4,7 +4,7 @@ import type { ScopeConfig } from '../types.js';
4
4
  * must name one, so a new preference cannot be added without deciding where a
5
5
  * human finds it.
6
6
  */
7
- export type UserSettingGroup = 'display' | 'panes' | 'updates';
7
+ export type UserSettingGroup = 'display' | 'panes' | 'providers' | 'updates';
8
8
  export interface UserSettingDefinition<Key extends string = string> {
9
9
  key: Key;
10
10
  label: string;
@@ -298,6 +298,23 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
298
298
  surface: "specialized";
299
299
  owner: string;
300
300
  };
301
+ providerOptions: {
302
+ surface: "settings";
303
+ settings: {
304
+ 'openai-codex.fastMode': {
305
+ key: "providerOptions.openai-codex.fastMode";
306
+ label: string;
307
+ description: string;
308
+ valueConstraint: string;
309
+ applies: string;
310
+ group: "providers";
311
+ order: number;
312
+ format: (config: ScopeConfig) => "on" | "off";
313
+ cycle: (config: ScopeConfig) => void;
314
+ setFromString: (config: ScopeConfig, raw: string) => string | null;
315
+ };
316
+ };
317
+ };
301
318
  modelLadders: {
302
319
  surface: "specialized";
303
320
  owner: string;
@@ -36,6 +36,17 @@ function positiveIntegerFromString(raw, key) {
36
36
  const AUTO_UPDATE_MODES = [false, 'notify', 'apply'];
37
37
  const UPDATE_INTERVAL_HOURS = [0, 1, 6, 12, 24, 48, 72, 168];
38
38
  const MAX_PANES = [1, 2, 3, 4, 5, 6, 8, 10, 12];
39
+ const OPENAI_CODEX_PROVIDER_KEY = 'openai-codex';
40
+ /** The options block for one provider, created on first write so a row can
41
+ * toggle an option the config file has never named. */
42
+ function providerOptionsFor(config, providerId) {
43
+ const existing = config.providerOptions[providerId];
44
+ if (existing !== undefined)
45
+ return existing;
46
+ const created = {};
47
+ config.providerOptions[providerId] = created;
48
+ return created;
49
+ }
39
50
  const LIVE_CYCLES = [1, 2, 3, 4, 5, 6];
40
51
  /**
41
52
  * Exhaustive settings-surface coverage for ScopeConfig. Primitive preferences
@@ -382,6 +393,32 @@ export const CONFIG_SETTINGS_COVERAGE = {
382
393
  },
383
394
  keybindings: { surface: 'specialized', owner: 'Keybindings tab' },
384
395
  tmux_passthrough: { surface: 'specialized', owner: 'Keybindings tab' },
396
+ providerOptions: {
397
+ surface: 'settings',
398
+ settings: {
399
+ 'openai-codex.fastMode': {
400
+ key: `providerOptions.${OPENAI_CODEX_PROVIDER_KEY}.fastMode`,
401
+ label: 'Codex fast mode',
402
+ description: 'Serve Codex turns at the ChatGPT priority tier: about 1.5x faster generation for about 2x the credits. Displayed cost does not reflect the premium.',
403
+ valueConstraint: 'true|false',
404
+ applies: "Applies on each node's next turn.",
405
+ group: 'providers',
406
+ order: 40,
407
+ format: (config) => config.providerOptions[OPENAI_CODEX_PROVIDER_KEY]?.fastMode ? 'on' : 'off',
408
+ cycle: (config) => {
409
+ const options = providerOptionsFor(config, OPENAI_CODEX_PROVIDER_KEY);
410
+ options.fastMode = !options.fastMode;
411
+ },
412
+ setFromString: (config, raw) => {
413
+ const value = booleanFromString(raw, `providerOptions.${OPENAI_CODEX_PROVIDER_KEY}.fastMode`);
414
+ if (typeof value === 'string')
415
+ return value;
416
+ providerOptionsFor(config, OPENAI_CODEX_PROVIDER_KEY).fastMode = value;
417
+ return null;
418
+ },
419
+ },
420
+ },
421
+ },
385
422
  modelLadders: { surface: 'specialized', owner: 'Models tab' },
386
423
  modelRoutes: { surface: 'specialized', owner: 'Models route declarations' },
387
424
  modelRouting: { surface: 'specialized', owner: 'Models route declarations' },
@@ -0,0 +1 @@
1
+ export {};