@intentic/sandbox-contract 1.165.0 → 1.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/agent-catalog.d.ts +0 -1
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +1 -2
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +2 -2
  6. package/dist/contracts/agents.contract.d.ts +18 -0
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/agents.contract.js.map +1 -1
  9. package/dist/contracts/automations.contract.d.ts +1 -1
  10. package/dist/contracts/ci.contract.d.ts +4 -0
  11. package/dist/contracts/ci.contract.d.ts.map +1 -1
  12. package/dist/contracts/ci.contract.js +2 -1
  13. package/dist/contracts/ci.contract.js.map +1 -1
  14. package/dist/contracts/extensions.contract.d.ts +1 -0
  15. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  16. package/dist/contracts/gate.contract.d.ts +47 -0
  17. package/dist/contracts/gate.contract.d.ts.map +1 -0
  18. package/dist/contracts/gate.contract.js +9 -0
  19. package/dist/contracts/gate.contract.js.map +1 -0
  20. package/dist/contracts/kimi.contract.d.ts +0 -70
  21. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  22. package/dist/contracts/kimi.contract.js +1 -5
  23. package/dist/contracts/kimi.contract.js.map +1 -1
  24. package/dist/contracts/settings.contract.d.ts +37 -3
  25. package/dist/contracts/settings.contract.d.ts.map +1 -1
  26. package/dist/contracts/system.contract.d.ts +68 -8
  27. package/dist/contracts/system.contract.d.ts.map +1 -1
  28. package/dist/contracts/system.contract.js +3 -1
  29. package/dist/contracts/system.contract.js.map +1 -1
  30. package/dist/contracts/translator.contract.d.ts +11 -0
  31. package/dist/contracts/translator.contract.d.ts.map +1 -1
  32. package/dist/contracts/translator.contract.js.map +1 -1
  33. package/dist/contracts/workspace.contract.d.ts +3 -1
  34. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  35. package/dist/events.d.ts +98 -2
  36. package/dist/events.d.ts.map +1 -1
  37. package/dist/events.js +21 -2
  38. package/dist/events.js.map +1 -1
  39. package/dist/index.d.ts +188 -81
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/model-order.d.ts.map +1 -1
  44. package/dist/model-order.js +25 -6
  45. package/dist/model-order.js.map +1 -1
  46. package/dist/schemas.d.ts +179 -21
  47. package/dist/schemas.d.ts.map +1 -1
  48. package/dist/schemas.js +90 -13
  49. package/dist/schemas.js.map +1 -1
  50. package/dist/session-names.d.ts +8 -0
  51. package/dist/session-names.d.ts.map +1 -0
  52. package/dist/session-names.js +17 -0
  53. package/dist/session-names.js.map +1 -0
  54. package/package.json +13 -2
  55. package/src/agent-catalog.ts +6 -11
  56. package/src/contracts/agents.contract.ts +8 -6
  57. package/src/contracts/ci.contract.ts +5 -1
  58. package/src/contracts/gate.contract.ts +19 -0
  59. package/src/contracts/kimi.contract.ts +4 -21
  60. package/src/contracts/system.contract.ts +9 -0
  61. package/src/contracts/translator.contract.ts +4 -6
  62. package/src/events.ts +56 -8
  63. package/src/index.ts +3 -0
  64. package/src/model-order.test.ts +29 -12
  65. package/src/model-order.ts +50 -18
  66. package/src/quick-model.test.ts +3 -3
  67. package/src/schemas.test.ts +20 -0
  68. package/src/schemas.ts +305 -64
  69. package/src/session-names.ts +44 -0
package/src/schemas.ts CHANGED
@@ -50,8 +50,8 @@ export const EditorContextSchema = z.object({
50
50
  });
51
51
  export type EditorContext = z.infer<typeof EditorContextSchema>;
52
52
 
53
- // The client-minted stable conversation identity. Constrained because it lands in branch names (agent/<id>)
54
- // and filesystem paths — the regex is the injection guard. Shared by the turn input and the attach input.
53
+ // The client-minted stable conversation identity. Constrained because isolated conversations also use it in
54
+ // branch names (agent/<id>) and filesystem paths — the regex is the injection guard. Shared by turn + attach.
55
55
  const ConversationIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
56
56
 
57
57
  // Where a conversation came from when nobody typed it into the browser: an automation wake carrying a message
@@ -102,7 +102,7 @@ export const AgentTurnSchema = z
102
102
  account: z.string().optional(),
103
103
  sessionId: z.string().optional(),
104
104
  // The client-minted stable conversation identity (survives provider/account/harness switches, which
105
- // retire sessions). Keys the fleet registry entry, the conversation's worktree, and the turn run.
105
+ // retire sessions). Keys the fleet registry entry and turn run, plus the worktree when isolated.
106
106
  conversationId: ConversationIdSchema.optional(),
107
107
  // When true, the turn runs in the conversation's isolated git worktree (created lazily on first use)
108
108
  // instead of the shared /work tree — the parallel-agents mode. Requires conversationId.
@@ -157,10 +157,10 @@ export const AttachTurnSchema = z.object({
157
157
  });
158
158
  export type AttachTurn = z.infer<typeof AttachTurnSchema>;
159
159
 
160
- // ---- agents: the parallel-conversation fleet ----
161
- // A "fleet agent" is a conversation with a registry entry every isolated conversation, keyed by its
162
- // conversationId. Isolated ones own a git worktree (branch agent/<id> in every workspace repo); the fleet
163
- // surface shows all of them with live status/activity/cost so the user can drive N agents in parallel.
160
+ // ---- agents: the conversation fleet ----
161
+ // A "fleet agent" is any conversation with a registry entry, keyed by its conversationId. Isolated ones own a
162
+ // git worktree (branch agent/<id> in every workspace repo); workspace conversations have no branch. The fleet
163
+ // surface shows both through the same status/activity/cost lifecycle.
164
164
 
165
165
  // idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); ready /
166
166
  // landed / conflict are outcomes of the land flow — `ready` is a clean completion whose delta stayed on the
@@ -173,7 +173,31 @@ export type AttachTurn = z.infer<typeof AttachTurnSchema>;
173
173
  // unlabelled: without it such a turn rehydrates as `idle`, which is the resting status of a turn that finished
174
174
  // CLEANLY, so the board files a killed agent under Finished and the question it was holding disappears with the
175
175
  // process that asked it. See agents-store.ts — this is the status a live turn leaves on disk.
176
- export const AgentStatusSchema = z.enum(["idle", "running", "awaiting", "ready", "landed", "conflict", "error", "interrupted"]);
176
+ //
177
+ /* `stopping` and `stopped` are the two halves of a user's Stop, and they exist because a hard-cancel is NOT
178
+ * instant: /agent/stop aborts the provider and then waits for the turn's generator to unwind (worktree and
179
+ * registry cleanup), which is seconds of real time. For that whole window the runtime half still said
180
+ * `running`, so every surface kept its spinner turning on a turn the user had already killed — and then the
181
+ * card jumped to a settled state out of nowhere. `stopping` is what the daemon knows the instant the abort
182
+ * lands, published immediately so the press has a visible result; `stopped` is where the turn comes to rest.
183
+ *
184
+ * `stopped` is deliberately its own value rather than `interrupted` or `error`. Not `error`, which is what a
185
+ * stopped turn used to report (every provider adapter surfaces the abort's unwind as an error frame) — a card
186
+ * accusing the user's own deliberate press of being a failure. Not `interrupted` either: that one means the
187
+ * daemon died under the turn, and a boot pass may re-run it, which is precisely what must never happen to a
188
+ * turn a person chose to end. */
189
+ export const AgentStatusSchema = z.enum([
190
+ "idle",
191
+ "running",
192
+ "awaiting",
193
+ "stopping",
194
+ "stopped",
195
+ "ready",
196
+ "landed",
197
+ "conflict",
198
+ "error",
199
+ "interrupted",
200
+ ]);
177
201
  export type AgentStatus = z.infer<typeof AgentStatusSchema>;
178
202
  // The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo.
179
203
  export const AgentActivitySchema = z.object({
@@ -355,9 +379,8 @@ export const AgentLandSchema = z.object({ id: z.string().min(1), mode: LandModeS
355
379
  // The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
356
380
  // which holds their SUBSCRIPTION OAuth and re-serves it behind an Anthropic endpoint. The `claude` provider is
357
381
  // absent — native Anthropic OAuth serves it directly, without the translator. Codex and Grok also have a native
358
- // runtime and so carry the harness axis; `gemini` is routed-only (Google publishes no Anthropic-protocol
359
- // endpoint and this sandbox bakes no Gemini runtime), so a Gemini turn is always a Claude Code turn.
360
- export const KeyedProviderSchema = z.enum(["codex", "grok", "gemini"]);
382
+ // runtime and so carry the harness axis; Kimi and Gemini are routed-only, so their turns always use Claude Code.
383
+ export const KeyedProviderSchema = z.enum(["codex", "grok", "kimi", "gemini"]);
361
384
  export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
362
385
 
363
386
  // One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
@@ -371,6 +394,7 @@ export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
371
394
  export const TranslatorAccountsSchema = z.object({
372
395
  codex: z.array(TranslatorAccountSchema),
373
396
  grok: z.array(TranslatorAccountSchema),
397
+ kimi: z.array(TranslatorAccountSchema),
374
398
  gemini: z.array(TranslatorAccountSchema),
375
399
  });
376
400
  export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
@@ -525,11 +549,6 @@ export const OauthExchangeSchema = z.object({
525
549
  label: z.string().optional(),
526
550
  });
527
551
  export const AuthorizeChallengeSchema = z.object({ authorizeUrl: z.string(), verifier: z.string(), state: z.string() });
528
- // Kimi (Moonshot) authenticates with an API key, not OAuth: the user pastes a key from their Moonshot account
529
- // and the sandbox stores it as an account (one key per account, several accounts side by side). `label` is the
530
- // user's display name (blank ⇒ the daemon derives a default). The key never rides back out — connection status
531
- // is existence in `/kimi/accounts`.
532
- export const KimiConnectSchema = z.object({ apiKey: z.string().min(1), label: z.string().optional() });
533
552
  // xAI Grok (via OpenCode) uses subscription OAuth via the headless device-code method. `start` returns the
534
553
  // `url` the user opens (xAI's verification_uri_complete, which pre-fills the code) and `code` — the same
535
554
  // one-time code, surfaced so the card matches x.ai exactly. There is no paste-back: OpenCode polls to
@@ -539,13 +558,15 @@ export const KimiConnectSchema = z.object({ apiKey: z.string().min(1), label: z.
539
558
  // A device-code login start: the verification URL + the one-time code the user enters there. The native Grok
540
559
  // flow (via OpenCode) — see TranslatorStartSchema for the routed-provider connect, which adds `state`.
541
560
  export const DeviceStartSchema = z.object({ url: z.string(), code: z.string() });
542
- // A routed-provider subscription login start (codex/grok/gemini via CLIProxyAPI). Codex and Grok mint a
543
- // one-time device `code` the user enters at the provider's site, and CLIProxyAPI polls to completion on its
544
- // own the card only waits. Google publishes no device flow: the user approves in a browser and is redirected
545
- // to a loopback URL this sandbox never receives, so `code` is empty and the card asks them to paste that URL
546
- // back (see TranslatorCompleteSchema). Which half a provider uses is READ from the response rather than
547
- // hardcoded per provider, so the card needs no provider table.
548
- export const TranslatorStartSchema = z.object({ url: z.string(), code: z.string(), state: z.string() });
561
+ // A routed-provider subscription login start (codex/grok/kimi/gemini via CLIProxyAPI). Device flows poll to
562
+ // completion after the user approves upstream; redirect flows need the browser's landing URL pasted back. The
563
+ // explicit flow discriminator matters even when a provider's verification URL already embeds its optional code.
564
+ export const TranslatorStartSchema = z.object({
565
+ url: z.string(),
566
+ code: z.string(),
567
+ state: z.string(),
568
+ flow: z.enum(["device", "redirect"]),
569
+ });
549
570
  // The paste-back half of a redirect login: the URL the provider sent the browser to, carrying the grant as
550
571
  // ?code=&state=. `state` ties it to the handshake that issued it — the translator rejects a mismatch.
551
572
  export const TranslatorCompleteSchema = z.object({
@@ -555,7 +576,7 @@ export const TranslatorCompleteSchema = z.object({
555
576
  });
556
577
  // A provider's model catalog, resolved daemon-side from live discovery with a persisted last-known-good list and
557
578
  // a seed floor (Grok via opencode.ts xaiModels, Codex via codex-models.ts, Claude via the Agent SDK's
558
- // supportedModels) — never empty, so the picker is never blank. `label` is the humanized display name; `default`
579
+ // supportedModels) — never empty, so the picker is never blank. `label` is the provider's display name; `default`
559
580
  // is the model a fresh chat on that provider seeds (always present). Shared by /grok/models, /codex/models,
560
581
  // /claude/models. `efforts` is the reasoning-effort tiers the model accepts (Claude reports them per model);
561
582
  // empty ⇒ the client's default tiers.
@@ -563,8 +584,8 @@ export const TranslatorCompleteSchema = z.object({
563
584
  // EVERY field here is provider-reported — nothing about a model is curated in this repo, so a new release or a
564
585
  // renamed family flows to the UI with no code change. Providers differ in how much they publish: the Claude
565
586
  // Agent SDK reports a display name, a capability description, effort tiers, and capability flags, while the
566
- // OpenAI-compatible /v1/models endpoints (codex/grok/kimi) report ids only — those rows render label-only, and
567
- // that absence is the honest answer rather than something to paper over with a hand-written table.
587
+ // Some OpenAI-compatible /v1/models endpoints report ids only — those rows render label-only, and that absence
588
+ // is the honest answer rather than something to paper over with a hand-written table.
568
589
  //
569
590
  // ORDER IS MEANINGFUL: `models` arrives in the provider's own preference order, which is what the picker sorts
570
591
  // by, and `default` is the provider's own default. Neither is re-ranked locally.
@@ -624,6 +645,11 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
624
645
  // iqSearch — loads the image-baked iq Claude Code plugin (skill + SessionStart nudge) so the agent
625
646
  // prefers the iq CLI over grep/find/Glob; off ⇒ plugin not loaded, native search tools
626
647
  // only. Opt-in (default off); the browser Search box uses iq regardless.
648
+ // iqContext — retrieves for the user's message BEFORE the turn starts and prepends the ranked answer
649
+ // to it, so the model opens with the anchors instead of paying a search round-trip to
650
+ // find them. Independent of iqSearch: that one teaches the agent to search, this one
651
+ // answers ahead of it.
652
+ // iqContextHoldout — measurement control for iqContext, same shape as terseHoldout (UsageTurn.iqContext).
627
653
  // outputCleaners — the Bash output-cleaner spec (agent-output-filter): "off" = filter disabled (default),
628
654
  // "" = all cleaners on, else an iq-style allow-list / default-minus
629
655
  // spec ("git,pnpm" = only those; "-cap" = all except). Threaded to the filter via env.
@@ -643,6 +669,12 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
643
669
  // surface fail to parse the moment a toggle is added — which reaches the user as a page of switches that are
644
670
  // silently dead, not as an error. It also means an older on-disk manifest keeps the owner's other picks rather
645
671
  // than being discarded whole.
672
+ // Build-wide kill switch for usage-limit auto-resume. Keep the setting and implementation in place while the
673
+ // feature is disabled, but clamp every daemon response and settings write to OFF so a persisted `true` from an
674
+ // older build cannot keep spending a newly reset allowance. The web also reads this constant to render the
675
+ // control unavailable and to distrust a stale daemon that still reports a scheduled resume.
676
+ export const USAGE_LIMIT_AUTO_RESUME_ENABLED: boolean = false;
677
+
646
678
  export const SandboxSettingsSchema = z.object({
647
679
  stableSystemPrompt: z.boolean().default(false),
648
680
  skills: z.array(z.string()).default([]),
@@ -678,6 +710,21 @@ export const SandboxSettingsSchema = z.object({
678
710
  * Cap is roomy — the bases it stands in for are ~6.8k characters — but finite, because every turn pays it. */
679
711
  systemPrompt: z.string().max(20000).default(""),
680
712
  iqSearch: z.boolean().default(false),
713
+ /* RETRIEVE BEFORE THE TURN, don't wait to be asked. The daemon runs the user's message through the resident
714
+ * iq engine and prepends the ranked answer to it, so a turn that would have opened with two or three search
715
+ * calls opens with the anchors already in hand. Independent of `iqSearch`, which only teaches the agent to
716
+ * reach for the CLI once it decides to search — this one answers ahead of that decision, and the two
717
+ * compose: the injected capsule names the anchors, the CLI is there for what it missed.
718
+ *
719
+ * It rides the USER message (turn-context.ts), never the system prompt, for the same reason the setup
720
+ * notice does: it changes every turn, and the system prefix is kept byte-stable for the prompt cache.
721
+ * Off by default — it spends input tokens on every eligible turn, and whether that trade pays is exactly
722
+ * what the holdout below is for. */
723
+ iqContext: z.boolean().default(false),
724
+ // Measurement control for the pre-injection, identical in shape to `terseHoldout`: a fraction [0,1] of
725
+ // otherwise-eligible turns run WITHOUT the retrieved context and stamp their arm onto the ledger
726
+ // (UsageTurn.iqContext), so the report compares two real populations of turns instead of asserting a saving.
727
+ iqContextHoldout: z.number().min(0).max(1).default(0),
681
728
  outputCleaners: z.string().default("off"),
682
729
  outputHoldout: z.number().min(0).max(1).default(0),
683
730
  filterBackend: z.enum(["native", "rtk"]).default("native"),
@@ -699,12 +746,13 @@ export const SandboxSettingsSchema = z.object({
699
746
  * automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
700
747
  * browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
701
748
  autoLand: z.boolean().default(true),
702
- // When a turn dies on the Claude subscription's usage limit, re-run it automatically once the limit
703
- // window resets (a minute after, so a skewed clock can't retry into the same closed window). Off by
704
- // default: an unattended retry spends the fresh window without the user in the room, so the daemon
705
- // records every limit-hit either way and the chat OFFERS the toggle at the moment it would have helped —
706
- // enabling it then still resumes the turn that just bounced.
707
- autoResumeOnLimit: z.boolean().default(false),
749
+ // Latent opt-in for re-running a turn after the Claude subscription's usage window resets. It defaults off
750
+ // because an unattended retry spends the fresh allowance without the user in the room, and the build-wide
751
+ // gate above currently clamps even an older saved opt-in off while the feature is unavailable.
752
+ autoResumeOnLimit: z
753
+ .boolean()
754
+ .default(false)
755
+ .overwrite((enabled) => USAGE_LIMIT_AUTO_RESUME_ENABLED && enabled),
708
756
  /* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
709
757
  * socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
710
758
  *
@@ -726,6 +774,34 @@ export const SandboxSettingsSchema = z.object({
726
774
  * OFF still records the interruption: the fleet card reads `interrupted` (see AgentStatusSchema) and an
727
775
  * automation's row shows an `interrupted` run — nothing is re-run, but nothing is silently lost either. */
728
776
  autoResumeOnRestart: z.boolean().default(true),
777
+ /* THE LANDING GATE — the check command run over the COMPOSITE of landed work after a landing debounce.
778
+ * Empty ⇒ no gate at all, which is the default: only the owner knows what verifies this workspace, and a
779
+ * guessed command that fails on a fresh clone would read as the gate finding a bug on its first run.
780
+ *
781
+ * Configuring it is the opt-in, which is why there is no separate enable flag to disagree with it. The
782
+ * command runs in the workspace root through `sh -c`, exactly as a terminal would run it (see gate/gate.ts
783
+ * for why this is NOT an automation guard: a suite outlives GUARD_TIMEOUT_MS, and a timed-out guard reads
784
+ * as "skipped" — a silent green over a suite that never finished). */
785
+ gateCommand: z.string().max(500).default(""),
786
+ /* How long after a land the gate waits before running. A landing burst is the case this exists for: five
787
+ * agents finishing within a minute of each other are five lands, and a gate that ran per land would spend
788
+ * five suites to answer about four trees nobody will ever push. Every land re-arms the timer, so the run
789
+ * happens once, on the tree the user is about to review.
790
+ *
791
+ * It counts from the last LAND and nothing else — explicitly not "until the fleet is idle". Agents here run
792
+ * for hours, so a fleet of twenty with one long runner would never present a quiet moment, and a gate that
793
+ * waited for one would only ever fire when clicked (gate/gate.ts). */
794
+ gateQuietMs: z.number().min(0).max(600_000).default(20_000),
795
+ // Ceiling on one gate run, after which the child is killed and the verdict is `failed` with `timedOut`.
796
+ // Never a pass: a suite that did not finish has not said anything about the tree, and the one thing this
797
+ // gate exists to prevent is a green light nobody earned.
798
+ gateTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
799
+ /* Wake a fixer automatically when the gate goes red, instead of only lighting the badge. ON with a
800
+ * configured command, unlike the other unattended-spend toggles (autoResumeOnLimit), and the difference is
801
+ * that the spend here is the POINT: a red gate whose fix waits for the user to notice has moved the CI
802
+ * round-trip into the workspace without removing it from the user's day. One attempt per verdict, so a
803
+ * command that fails for a reason no agent can fix costs one turn, not a loop (gate/gate.ts). */
804
+ gateAutoFix: z.boolean().default(true),
729
805
  });
730
806
  export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
731
807
 
@@ -793,13 +869,21 @@ export const InputSavingsSchema = z.object({
793
869
  });
794
870
  export type InputSavings = z.infer<typeof InputSavingsSchema>;
795
871
 
796
- // One arm of the turn-level experiment: the turns that ran with the steer, and the turns the holdout ran
797
- // without it. Mean output tokens PER TURN, because the arms never hold the same number of turns.
798
- export const SavingsArmSchema = z.object({ turns: z.number(), meanOutputTokens: z.number() });
872
+ // One arm of a turn-level experiment: the turns that ran with the mechanism, and the turns the holdout ran
873
+ // without it. A mean PER TURN, because the arms never hold the same number of turns.
874
+ export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() });
799
875
 
800
- // The terse steer, as measured. Only turns where the steer was ELIGIBLE are counted a turn under a custom
801
- // system prompt drops the steer along with everything else the daemon appends, so it belongs to neither arm.
802
- export const OutputSavingsSchema = z.object({
876
+ /* A turn-level A/B the one shape both of this sandbox's turn experiments report in, because they differ in
877
+ * nothing but which flag flips and what the turns are judged on. Only turns the mechanism was ELIGIBLE for are
878
+ * counted: a turn under a custom system prompt drops the terse steer along with everything else the daemon
879
+ * appends, so it belongs to neither arm.
880
+ *
881
+ * `metric` says what `mean` counts and what `deltaPct` is a delta in. The terse steer is judged on the model's
882
+ * OWN output tokens, which is the thing it steers. Pre-injection is judged on COST, because it spends input
883
+ * tokens deliberately to buy back search turns — scored on output tokens it would look like a pure expense,
884
+ * and scored on input tokens like a pure loss; the trade only nets out in money. */
885
+ export const TurnExperimentSchema = z.object({
886
+ metric: z.enum(["outputTokens", "costUsd"]),
803
887
  on: SavingsArmSchema,
804
888
  off: SavingsArmSchema,
805
889
  // Turns per arm before a delta is reported at all. Carried on the wire so the screen's "measuring…" state
@@ -807,18 +891,24 @@ export const OutputSavingsSchema = z.object({
807
891
  minTurns: z.number(),
808
892
  /* The three below are present TOGETHER, and only once both arms clear `minTurns` — a schema that can't
809
893
  * express a half-measured experiment is how a 34%-that-becomes-8%-tomorrow never reaches the screen.
810
- * deltaPct — change in mean output tokens per turn under the steer; negative is a saving.
811
- * marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
812
- * savedTokens — what the delta is worth over the turns that actually ran with the steer, in this window. */
894
+ * deltaPct — change in the metric's mean per turn under the mechanism; negative is a saving.
895
+ * marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
896
+ * saved — what the delta is worth over the turns that actually ran with it, in this window, in the
897
+ * metric's own unit (tokens, or dollars). */
813
898
  deltaPct: z.number().optional(),
814
899
  marginPct: z.number().optional(),
815
- savedTokens: z.number().optional(),
900
+ saved: z.number().optional(),
816
901
  });
817
- export type OutputSavings = z.infer<typeof OutputSavingsSchema>;
902
+ export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
818
903
 
819
- // `output` is absent when the experiment isn't running at all (terse off, or no holdout set) — a section that
820
- // isn't there reads as "not measured", which is the truth, while zeros would read as "measured, worth nothing".
821
- export const SavingsReportSchema = z.object({ input: InputSavingsSchema, output: OutputSavingsSchema.optional() });
904
+ // `output`/`context` are absent when that experiment isn't running at all (its flag off, or no holdout set) — a
905
+ // section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
906
+ // worth nothing".
907
+ export const SavingsReportSchema = z.object({
908
+ input: InputSavingsSchema,
909
+ output: TurnExperimentSchema.optional(),
910
+ context: TurnExperimentSchema.optional(),
911
+ });
822
912
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
823
913
 
824
914
  // ---- intentic CLI ----
@@ -971,9 +1061,9 @@ export const RepoChangesSchema = z.object({
971
1061
  remote: GitRemoteStateSchema.optional(),
972
1062
  // WHICH AGENT PUT IT THERE: repo-relative path → the agent ids that landed it, newest land first. Keyed by
973
1063
  // PATH rather than carried on each GitChange because a path can be listed on two sides at once (staged and
974
- // edited again) and its origin is the same fact for both. Only agents can appear here — a main-tree turn,
975
- // a terminal edit and your own typing never pass through land, so they are simply absent (see
976
- // agents/origins.ts), which is why the panel badges an agent and says nothing at all for anyone else.
1064
+ // edited again) and its origin is the same fact for both. Only branch-backed agents whose work passed
1065
+ // through land can appear here; workspace conversations, terminal edits and the user's typing are absent
1066
+ // (see agents/origins.ts), so the panel badges an attributable agent and says nothing for anyone else.
977
1067
  // Ids, not titles: the identity for every id named here rides the response once, in `originAgents`.
978
1068
  origins: z.record(z.string(), z.array(z.string())).optional(),
979
1069
  // Why the repo could not be scanned at all, condensed to git's own one-line reason ("fatal: bad object HEAD").
@@ -1215,8 +1305,9 @@ export type WorkspaceClassification = z.infer<typeof WorkspaceClassificationSche
1215
1305
  // within `text` so clients highlight without re-finding the needle.
1216
1306
  export const WorkspaceSearchQuerySchema = z.object({
1217
1307
  query: z.string().min(2).max(512),
1218
- // Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface.
1219
- mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast", "ask"]).optional(),
1308
+ // Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface. Natural language
1309
+ // has no verb of its own: `q` classifies the query and answers it semantically when the words call for it.
1310
+ mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast"]).optional(),
1220
1311
  includeIgnored: z.stringbool().optional(),
1221
1312
  limit: z.coerce.number().int().positive().optional(),
1222
1313
  after: z.string().optional(),
@@ -1244,6 +1335,9 @@ export const WorkspaceSearchFreshnessSchema = z.object({
1244
1335
  state: z.enum(["fresh", "building", "stale"]),
1245
1336
  ageMs: z.number().optional(),
1246
1337
  progress: z.number().optional(),
1338
+ // How many files the index has not caught up with, when it is stale. A count is reportable; "stale" alone
1339
+ // reads as a warning about the answer, which it almost never is.
1340
+ behind: z.number().optional(),
1247
1341
  });
1248
1342
  export type WorkspaceSearchFreshness = z.infer<typeof WorkspaceSearchFreshnessSchema>;
1249
1343
  export const WorkspaceSearchResultSchema = z.object({
@@ -1255,8 +1349,12 @@ export const WorkspaceSearchResultSchema = z.object({
1255
1349
  truncated: z.boolean(),
1256
1350
  cursor: z.string().optional(),
1257
1351
  hint: z.string().optional(),
1258
- // Code-graph neighbors of the top hits (definition anchors + ready-made follow-up commands).
1352
+ // Code-graph neighbors of the top hits (definition anchors + the strongest caller of each).
1259
1353
  related: z.array(z.string()).optional(),
1354
+ // Ranked `path:line` anchors that placed but were NOT shown, best first — the answer often sits at rank 5–13,
1355
+ // behind groups the budget spent itself on. The text surface has always printed this map; a JSON caller could
1356
+ // not see it, so it had to page through `cursor` to learn what the terminal was told up front.
1357
+ candidates: z.array(z.string()).optional(),
1260
1358
  // Run provenance for benchmarking: retrieval stages DISABLED this invocation (absent = full pipeline).
1261
1359
  features: z.array(z.string()).optional(),
1262
1360
  });
@@ -2016,9 +2114,9 @@ export const AutomationRunSchema = z.object({
2016
2114
  // as "it never fired" — the one reading a 3 a.m. automation must not be given.
2017
2115
  outcome: z.enum(["completed", "skipped", "error", "interrupted"]),
2018
2116
  detail: z.string().optional(),
2019
- // The runtime session the wake ran in, so the row can open the transcript. Absent for a run that never
2020
- // reached a provider (skipped by its guard) or whose provider minted no session before it died.
2021
- sessionId: z.string().optional(),
2117
+ // The stable conversation opened by the wake, so the row can open the provider-neutral agent transcript.
2118
+ // Absent only for a run skipped before a conversation was needed.
2119
+ conversationId: z.string().optional(),
2022
2120
  });
2023
2121
  export type AutomationRun = z.infer<typeof AutomationRunSchema>;
2024
2122
 
@@ -2118,9 +2216,17 @@ export const CiRunsResponseSchema = z.object({
2118
2216
  repos: z.array(CiRepoSchema),
2119
2217
  // Newest first, across all mapped repos.
2120
2218
  runs: z.array(PipelineRunSchema),
2219
+ // When the owner last opened the pipelines view. Rides the runs response so the rail can decide what is
2220
+ // NEW without a second call — a breakage older than this has already been seen and must not badge again.
2221
+ // Absent ⇒ never opened, so everything counts as unseen.
2222
+ seenAt: z.number().optional(),
2121
2223
  });
2122
2224
  export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
2123
2225
 
2226
+ // Stamping the view as read hands back the timestamp it wrote, so the client updates without a refetch.
2227
+ export const CiSeenResponseSchema = z.object({ seenAt: z.number() });
2228
+ export type CiSeenResponse = z.infer<typeof CiSeenResponseSchema>;
2229
+
2124
2230
  // rerun/cancel/fix address a run by repo + vendor id; the daemon re-resolves repo → project + token per call,
2125
2231
  // so a stale card can't act on a project the workspace no longer maps to.
2126
2232
  export const CiRunParamSchema = z.object({ repo: z.string(), runId: z.number() });
@@ -2130,6 +2236,96 @@ export type CiRunParam = z.infer<typeof CiRunParamSchema>;
2130
2236
  export const CiFixResponseSchema = z.object({ conversationId: z.string() });
2131
2237
  export type CiFixResponse = z.infer<typeof CiFixResponseSchema>;
2132
2238
 
2239
+ /* ---- the landing gate: the workspace's own verdict on the composite of landed work ----
2240
+ *
2241
+ * WHERE THIS SITS, and why it is not one of the four other places it could:
2242
+ *
2243
+ * A fleet of 5-20 agents lands work into the main tree as UNCOMMITTED changes (agents/land.ts), the user
2244
+ * reviews and commits it by parts, pushes, and CI answers minutes later. This gate front-runs that answer by
2245
+ * asking the same question of the same artifact, before the push.
2246
+ *
2247
+ * NOT inside an agent's turn. An isolated worktree's `node_modules` reads as the MAIN checkout's, so a
2248
+ * monorepo's workspace links resolve cross-package imports to /work's sources rather than the worktree's edited
2249
+ * ones (agents/worktrees.ts). A suite run in a worktree therefore tests the agent's edits against everyone
2250
+ * else's UNEDITED siblings: it invents failures that don't exist and passes changes that break on the
2251
+ * composite, and two agents editing one contract each go green alone and red together. The composite is the
2252
+ * only honest artifact, and it exists in exactly one place — the main working tree.
2253
+ *
2254
+ * NOT at commit. The user commits BY PARTS, and a suite reads the worktree, not the index — so a verdict taken
2255
+ * at a partial commit describes a tree that never gets pushed as such. Commit is where a verdict is DISPLAYED
2256
+ * (ReviewPanel's badge), computed earlier.
2257
+ *
2258
+ * NOT at push. By then HEAD has moved, per-path attribution has expired (agents/origins.ts), and the agents may
2259
+ * be archived with their worktrees reclaimed — so the fix starts cold, in the same position `/ci/fix` is in.
2260
+ * That saves the CI round-trip and none of the context switch.
2261
+ *
2262
+ * So: after the land, before the staging — the one window where the artifact is what CI will see, attribution
2263
+ * is still live, and nobody is waiting on it. */
2264
+
2265
+ /* What the gate has to say about the tree right now.
2266
+ *
2267
+ * idle — no command configured, or nothing has run yet.
2268
+ * armed — work landed; the quiet period is counting down (see gateQuietMs).
2269
+ * running — the check is live. `output` grows as it streams.
2270
+ * passed — exited 0 over `fingerprint`.
2271
+ * failed — exited non-zero, or was killed by gateTimeoutMs (`timedOut`). The state a fix answers.
2272
+ * error — the gate itself could not run: the command was not spawnable. NOT a fix-able failure, because
2273
+ * there is nothing wrong with the code — the gate is misconfigured, and saying "tests failed"
2274
+ * would send an agent hunting a bug that isn't there.
2275
+ * cancelled — the user stopped the run, or the tree moved under it.
2276
+ */
2277
+ export const GateStatusSchema = z.enum(["idle", "armed", "running", "passed", "failed", "error", "cancelled"]);
2278
+ export type GateStatus = z.infer<typeof GateStatusSchema>;
2279
+
2280
+ // An agent whose landed work the failure implicates. `paths` are its attributed files that the check's own
2281
+ // output NAMED — empty when the output named none of them, which is the honest shape for a failure that could
2282
+ // not be pinpointed (an integration break between two deltas, a suite that prints no paths at all): the agent
2283
+ // is listed because its work is in the tree under test, not because anything accused it.
2284
+ export const GateAgentSchema = z.object({
2285
+ agentId: z.string(),
2286
+ title: z.string().optional(),
2287
+ provider: AgentProviderSchema.optional(),
2288
+ paths: z.array(z.string()),
2289
+ });
2290
+ export type GateAgent = z.infer<typeof GateAgentSchema>;
2291
+
2292
+ // The fix turn one red verdict got. It is a workspace conversation because the composite it must reproduce
2293
+ // lives as uncommitted content in the main working tree; it still has the same stable registry identity as any
2294
+ // other conversation.
2295
+ export const GateFixSchema = z.object({
2296
+ startedAt: z.number(),
2297
+ conversationId: z.string(),
2298
+ // `running` while the turn streams; `done` when it ended cleanly, whatever the re-check then said;
2299
+ // `error` when the turn itself failed (a provider outage, no credential), which is worth distinguishing
2300
+ // because it is the one case where re-running the fix could still help.
2301
+ outcome: z.enum(["running", "done", "error"]),
2302
+ detail: z.string().optional(),
2303
+ });
2304
+ export type GateFix = z.infer<typeof GateFixSchema>;
2305
+
2306
+ export const GateVerdictSchema = z.object({
2307
+ status: GateStatusSchema,
2308
+ // The command this verdict ran, echoed rather than read back from settings: a verdict read after the
2309
+ // setting changed still has to say what produced it.
2310
+ command: z.string(),
2311
+ startedAt: z.number().optional(),
2312
+ finishedAt: z.number().optional(),
2313
+ exitCode: z.number().optional(),
2314
+ timedOut: z.boolean().optional(),
2315
+ // The check's own output, tail-capped (GATE_OUTPUT_BYTES). The tail, not the head: a suite's verdict and
2316
+ // its failure summary are at the end, and a head-capped buffer of a chatty build is all progress lines.
2317
+ output: z.string(),
2318
+ /* WHICH TREE this verdict is about — HEAD plus the shape of every repo's uncommitted content. Recomputed
2319
+ * on read: when it no longer matches, the verdict is `stale` and the badge says so instead of asserting a
2320
+ * green light over a tree that has since moved. This is what keeps a passed verdict from outliving its
2321
+ * subject when the user edits, discards, or commits half of it. */
2322
+ fingerprint: z.string(),
2323
+ stale: z.boolean(),
2324
+ implicated: z.array(GateAgentSchema),
2325
+ fix: GateFixSchema.optional(),
2326
+ });
2327
+ export type GateVerdict = z.infer<typeof GateVerdictSchema>;
2328
+
2133
2329
  // ---- drafts: agent-proposed posts awaiting owner approval (.intentic/drafts/<id>.json) ----
2134
2330
  // One JSON file per draft. The AGENT creates drafts with its normal file tools — it can't call daemon routes,
2135
2331
  // the same split as the environment proposal — while the daemon edits/deletes them on the owner's behalf, so
@@ -2271,11 +2467,9 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
2271
2467
  // process (a lingering shell after a crash reads false). A process row that maps to an installed extension's
2272
2468
  // declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
2273
2469
  // `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
2274
- // same charset gates a `tmux kill-session -t` shell-out. `browser` = a `browser-<sdk session>` Chromium the
2275
- // agent is driving through its @playwright/mcp tools (browser/browser-sessions.ts) watchable live over the
2276
- // /system/browser-view WebSocket, `running` while that Chromium is connected, and hidden from the strip by
2277
- // the same rule as `agent`: it is a record of work, not a place. Its `label` is the page's own title and
2278
- // `url` the page it is on, which is the one thing a browser pill has to say that a terminal pill does not.
2470
+ // same charset gates a `tmux kill-session -t` shell-out. The agent's BROWSER is deliberately NOT one of these
2471
+ // kinds: a Chromium with its own tab strip is a surface in its own right, not a pane in the terminal panel, so
2472
+ // it lists from /system/browsers with the pages it has open (BrowserSessionSchema below).
2279
2473
  //
2280
2474
  // `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
2281
2475
  // while that pane still lives) are what let a finished session be READ rather than merely listed: the panel's
@@ -2285,19 +2479,56 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
2285
2479
  export const TerminalSessionSchema = z.object({
2286
2480
  name: z.string(),
2287
2481
  label: z.string().optional(),
2288
- kind: z.enum(["shell", "panel", "agent", "job", "process", "browser"]),
2482
+ kind: z.enum(["shell", "panel", "agent", "job", "process"]),
2289
2483
  running: z.boolean(),
2290
2484
  activityAt: z.number(),
2291
2485
  exitCode: z.number().optional(),
2292
2486
  extensionId: z.string().optional(),
2293
2487
  processName: z.string().optional(),
2294
- // Browser sessions only: the page the agent is on right now.
2295
- url: z.string().optional(),
2296
2488
  });
2297
2489
  export const TerminalsListSchema = z.object({ sessions: z.array(TerminalSessionSchema) });
2298
2490
  export type TerminalsList = z.infer<typeof TerminalsListSchema>;
2299
2491
  export const TerminalNameParamSchema = z.object({ name: z.string() });
2300
2492
 
2493
+ /* ---- browsers: the Chromium the agent drives through its @playwright/mcp tools ----
2494
+ *
2495
+ * A `browser-<sdk session>` Chromium (browser/browser-sessions.ts), watchable live over the
2496
+ * /system/browser-view WebSocket. It lists apart from the terminals because it is shaped differently in the one
2497
+ * way that decides a UI: a terminal is ONE stream of bytes, while a browser holds SEVERAL pages at once and the
2498
+ * question "what is the agent looking at?" only has an answer if the wire carries all of them. So `pages` is the
2499
+ * point of this schema — the view renders them as a tab strip and binds the screencast to whichever the user
2500
+ * picks, and `active` is the one the agent itself last touched (what the view follows until the user says
2501
+ * otherwise).
2502
+ *
2503
+ * `id` is opaque and minted per session, and it is what makes a tab survive a relist: it is stable for the life
2504
+ * of the page, unlike its url (the agent navigates away) or its position (a closed tab renumbers the rest). */
2505
+ export const BrowserPageSchema = z.object({
2506
+ id: z.string(),
2507
+ // The page's own title. Absent mid-navigation, which is exactly when a tab still needs to render.
2508
+ title: z.string().optional(),
2509
+ url: z.string(),
2510
+ // The page the agent last drove. Exactly one page per running session has it.
2511
+ active: z.boolean(),
2512
+ });
2513
+ export const BrowserSessionSchema = z.object({
2514
+ name: z.string(),
2515
+ // The pill's text: the active page's title, else its host, else which browser this is.
2516
+ label: z.string(),
2517
+ // Which MCP server drives it: `web` (the credential-free browser) or a logged-in capability's id — the
2518
+ // difference between a throwaway page and one signed in as the user, which is worth saying out loud.
2519
+ server: z.string(),
2520
+ // False once that Chromium is gone (the turn ended, the agent closed it, it crashed). A finished session
2521
+ // still lists for a while, with the pages it had — the record of where the agent went.
2522
+ running: z.boolean(),
2523
+ activityAt: z.number(),
2524
+ pages: z.array(BrowserPageSchema),
2525
+ });
2526
+ export type BrowserPage = z.infer<typeof BrowserPageSchema>;
2527
+ export type BrowserSession = z.infer<typeof BrowserSessionSchema>;
2528
+ export const BrowsersListSchema = z.object({ sessions: z.array(BrowserSessionSchema) });
2529
+ export type BrowsersList = z.infer<typeof BrowsersListSchema>;
2530
+ export const BrowserNameParamSchema = z.object({ name: z.string() });
2531
+
2301
2532
  // ---- environment: the overlay Dockerfile extending the sandbox image ----
2302
2533
  // The approved file is DAEMON-COMPOSED: pinned FROM + capability fragments + the owner-approved custom section.
2303
2534
  // The agent writes the proposal file (.intentic/environment.Dockerfile — custom-section content only, no FROM)
@@ -2463,7 +2694,8 @@ export const UsageTurnSchema = z.object({
2463
2694
  // when the provider's own subscription default served it without the daemon naming one.
2464
2695
  model: z.string().optional(),
2465
2696
  harness: z.string(),
2466
- // The conversation this turn belonged to, so spend can join to a fleet agent. Absent on a main-tree turn.
2697
+ // The conversation this turn belonged to, so spend can join to a fleet agent. Absent only for an internal
2698
+ // one-shot turn that has no conversation identity.
2467
2699
  conversationId: z.string().optional(),
2468
2700
  // The provider's own turn count for the request (a Claude "turn" can be several under the hood), so turns
2469
2701
  // and cost stay comparable across providers. 1 when the provider reported none.
@@ -2482,6 +2714,15 @@ export const UsageTurnSchema = z.object({
2482
2714
  * no control to be compared against. Pooling those into the off-arm would compare steered turns against a
2483
2715
  * population selected by something other than the coin flip, which is not a control at all. */
2484
2716
  terse: z.boolean().optional(),
2717
+ /* Which arm of the pre-injection experiment this turn ran on (settings.iqContextHoldout), on the same terms
2718
+ * as `terse` above: absent ⇒ outside the experiment.
2719
+ *
2720
+ * TRUE means the turn was ASSIGNED the retrieved context, not that a note was necessarily prepended — a
2721
+ * treatment turn whose retrieval came back empty or unconfident injects nothing. That is deliberate: the
2722
+ * arms have to be the coin flip's populations, and re-labelling a turn by what retrieval happened to find
2723
+ * would sort turns by how searchable their question was, which is a property of the question. The control
2724
+ * arm contains the same unsearchable questions in the same proportion, so they cancel. */
2725
+ iqContext: z.boolean().optional(),
2485
2726
  });
2486
2727
  export type UsageTurn = z.infer<typeof UsageTurnSchema>;
2487
2728