@intentic/sandbox-contract 1.159.0 → 1.161.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 (111) hide show
  1. package/dist/agent-catalog.d.ts +1 -0
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +1 -0
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +50 -4
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agent.contract.js +2 -1
  8. package/dist/contracts/agent.contract.js.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +195 -0
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/agents.contract.js +7 -2
  12. package/dist/contracts/agents.contract.js.map +1 -1
  13. package/dist/contracts/claude.contract.d.ts +26 -0
  14. package/dist/contracts/claude.contract.d.ts.map +1 -1
  15. package/dist/contracts/claude.contract.js +2 -1
  16. package/dist/contracts/claude.contract.js.map +1 -1
  17. package/dist/contracts/extensions.contract.d.ts +1 -0
  18. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  19. package/dist/contracts/git.contract.d.ts +13 -0
  20. package/dist/contracts/git.contract.d.ts.map +1 -1
  21. package/dist/contracts/git.contract.js +2 -1
  22. package/dist/contracts/git.contract.js.map +1 -1
  23. package/dist/contracts/grok.contract.d.ts +2 -0
  24. package/dist/contracts/grok.contract.d.ts.map +1 -1
  25. package/dist/contracts/kimi.contract.d.ts +26 -0
  26. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  27. package/dist/contracts/kimi.contract.js +2 -1
  28. package/dist/contracts/kimi.contract.js.map +1 -1
  29. package/dist/contracts/memory.contract.d.ts +34 -0
  30. package/dist/contracts/memory.contract.d.ts.map +1 -0
  31. package/dist/contracts/memory.contract.js +9 -0
  32. package/dist/contracts/memory.contract.js.map +1 -0
  33. package/dist/contracts/panels.contract.d.ts +1 -0
  34. package/dist/contracts/panels.contract.d.ts.map +1 -1
  35. package/dist/contracts/push.contract.d.ts +1 -1
  36. package/dist/contracts/push.contract.js +2 -2
  37. package/dist/contracts/push.contract.js.map +1 -1
  38. package/dist/contracts/sessions.contract.d.ts +2 -0
  39. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  40. package/dist/contracts/settings.contract.d.ts +72 -15
  41. package/dist/contracts/settings.contract.d.ts.map +1 -1
  42. package/dist/contracts/settings.contract.js +3 -2
  43. package/dist/contracts/settings.contract.js.map +1 -1
  44. package/dist/contracts/system.contract.d.ts +15 -2
  45. package/dist/contracts/system.contract.d.ts.map +1 -1
  46. package/dist/contracts/system.contract.js +2 -1
  47. package/dist/contracts/system.contract.js.map +1 -1
  48. package/dist/contracts/translator.contract.d.ts +13 -3
  49. package/dist/contracts/translator.contract.d.ts.map +1 -1
  50. package/dist/contracts/translator.contract.js +1 -1
  51. package/dist/contracts/translator.contract.js.map +1 -1
  52. package/dist/contracts/usage.contract.js +2 -2
  53. package/dist/contracts/usage.contract.js.map +1 -1
  54. package/dist/contracts/workspace.contract.d.ts +38 -5
  55. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  56. package/dist/contracts/workspace.contract.js +2 -1
  57. package/dist/contracts/workspace.contract.js.map +1 -1
  58. package/dist/events.d.ts +124 -2
  59. package/dist/events.d.ts.map +1 -1
  60. package/dist/events.js +21 -3
  61. package/dist/events.js.map +1 -1
  62. package/dist/index.d.ts +491 -30
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +5 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/model-order.d.ts +1 -0
  67. package/dist/model-order.d.ts.map +1 -1
  68. package/dist/model-order.js +1 -0
  69. package/dist/model-order.js.map +1 -1
  70. package/dist/path-refs.d.ts +1 -0
  71. package/dist/path-refs.d.ts.map +1 -1
  72. package/dist/path-refs.js +3 -0
  73. package/dist/path-refs.js.map +1 -1
  74. package/dist/quick-model.d.ts +13 -0
  75. package/dist/quick-model.d.ts.map +1 -0
  76. package/dist/quick-model.js +33 -0
  77. package/dist/quick-model.js.map +1 -0
  78. package/dist/schemas.d.ts +374 -14
  79. package/dist/schemas.d.ts.map +1 -1
  80. package/dist/schemas.js +145 -15
  81. package/dist/schemas.js.map +1 -1
  82. package/dist/title.d.ts +6 -0
  83. package/dist/title.d.ts.map +1 -0
  84. package/dist/title.js +149 -0
  85. package/dist/title.js.map +1 -0
  86. package/package.json +2 -2
  87. package/src/agent-catalog.ts +6 -0
  88. package/src/contracts/agent.contract.ts +14 -1
  89. package/src/contracts/agents.contract.ts +25 -1
  90. package/src/contracts/claude.contract.ts +4 -1
  91. package/src/contracts/git.contract.ts +6 -0
  92. package/src/contracts/kimi.contract.ts +13 -2
  93. package/src/contracts/memory.contract.ts +13 -0
  94. package/src/contracts/push.contract.ts +2 -2
  95. package/src/contracts/settings.contract.ts +16 -4
  96. package/src/contracts/system.contract.ts +4 -0
  97. package/src/contracts/translator.contract.ts +3 -2
  98. package/src/contracts/usage.contract.ts +2 -2
  99. package/src/contracts/workspace.contract.ts +6 -0
  100. package/src/events.ts +85 -6
  101. package/src/index.ts +7 -1
  102. package/src/model-order.test.ts +40 -1
  103. package/src/model-order.ts +15 -0
  104. package/src/path-refs.test.ts +27 -1
  105. package/src/path-refs.ts +16 -0
  106. package/src/quick-model.test.ts +88 -0
  107. package/src/quick-model.ts +103 -0
  108. package/src/schemas.test.ts +30 -1
  109. package/src/schemas.ts +500 -42
  110. package/src/title.test.ts +130 -0
  111. package/src/title.ts +254 -0
package/src/schemas.ts CHANGED
@@ -162,9 +162,18 @@ export type AttachTurn = z.infer<typeof AttachTurnSchema>;
162
162
  // conversationId. Isolated ones own a git worktree (branch agent/<id> in every workspace repo); the fleet
163
163
  // surface shows all of them with live status/activity/cost so the user can drive N agents in parallel.
164
164
 
165
- // idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); landed /
166
- // conflict are outcomes of the land flow; error is a terminal turn failure surfaced on the card.
167
- export const AgentStatusSchema = z.enum(["idle", "running", "awaiting", "landed", "conflict", "error"]);
165
+ // idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); ready /
166
+ // landed / conflict are outcomes of the land flow `ready` is a clean completion whose delta stayed on the
167
+ // agent's branch because auto-land is off (the user lands it deliberately, from the review panel or the card);
168
+ // error is a terminal turn failure surfaced on the card.
169
+ //
170
+ // `interrupted` is the turn that never got to report ANY of those: the daemon died under it (a container
171
+ // rebuild, a crash, an OOM kill), taking the provider process and the whole runtime half of the fleet — status,
172
+ // attention flags, the park a question raised — with it. It exists because the alternative is worse than
173
+ // unlabelled: without it such a turn rehydrates as `idle`, which is the resting status of a turn that finished
174
+ // CLEANLY, so the board files a killed agent under Finished and the question it was holding disappears with the
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"]);
168
177
  export type AgentStatus = z.infer<typeof AgentStatusSchema>;
169
178
  // The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo.
170
179
  export const AgentActivitySchema = z.object({
@@ -190,10 +199,21 @@ export const AgentSummarySchema = z.object({
190
199
  status: AgentStatusSchema,
191
200
  provider: AgentProviderSchema,
192
201
  harness: AgentHarnessSchema,
202
+ // What the agent's last turn ran with — the model, its reasoning effort, and whether extended thinking was
203
+ // on. Recorded per agent because they are facts about THIS conversation: a client opening it seeds its
204
+ // composer from them, rather than from whatever that browser last picked in some other tab. Absent for an
205
+ // agent whose turns predate the record (model has always been kept; the other two are newer).
193
206
  model: z.string().optional(),
207
+ effort: z.string().optional(),
208
+ thinking: z.boolean().optional(),
194
209
  account: z.string().optional(),
195
210
  // The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
196
211
  branch: z.string().optional(),
212
+ // This agent's own answer to "land automatically at turn completion?" — an explicit per-agent override of
213
+ // the sandbox-wide `autoLand` setting. ABSENT ⇒ inherit, which is the common case and the one that keeps
214
+ // the global toggle meaningful: an agent that never expressed an opinion follows the sandbox wherever it
215
+ // is pointed next. Written by `agents.autoLand`; the UI shows the EFFECTIVE value (this ?? the setting).
216
+ autoLand: z.boolean().optional(),
197
217
  // Present when the conversation was opened by an outside message rather than by the user (see
198
218
  // AgentOriginSchema) — the card's provenance line. Absent ⇒ the user started it.
199
219
  origin: AgentOriginSchema.optional(),
@@ -248,17 +268,88 @@ export const AgentIdsSchema = z.object({ ids: z.array(z.string().min(1)).min(1).
248
268
  // browser holds its optimistic add/remove of exactly these ids until it sees a roster at or past `rev`.
249
269
  export const AgentsMovedSchema = z.object({ moved: z.array(AgentSummarySchema), rev: z.number() });
250
270
  export type AgentsMoved = z.infer<typeof AgentsMovedSchema>;
271
+ // What a purge actually deleted. Ids, not summaries: these agents no longer exist anywhere — there is nothing
272
+ // left to show and nothing to put back, so the only thing the caller can do with the answer is drop those rows
273
+ // and count them. No revision either: archived agents are already off the broadcast roster (see `list`), so a
274
+ // purge changes nothing the board's pending-move machinery has to hold a card against.
275
+ export const AgentsRemovedSchema = z.object({ removed: z.array(z.string()) });
276
+ export type AgentsRemoved = z.infer<typeof AgentsRemovedSchema>;
277
+ /* Search the fleet by what the USER wrote — the board's filter (and the popped-out rail's).
278
+ *
279
+ * Deliberately the user's own prompts and nothing else. An agent's replies and its tool output mention
280
+ * nearly every identifier in the workspace, so a transcript-wide match on a fleet this size returns most of
281
+ * the board and the filter stops filtering; the words the user typed are both what they remember and what
282
+ * tells two agents apart. The card TITLE is the first of those prompts (sanitized), so a title match and a
283
+ * prompt match are one rule, not two.
284
+ *
285
+ * Two chars minimum: below that every agent matches and the scan is pure cost.
286
+ */
287
+ export const AgentSearchQuerySchema = z.object({ query: z.string().trim().min(2) });
288
+ // One matching agent, and the evidence for it. `snippet` is the matched prompt windowed around the hit —
289
+ // absent when the match is the title, which the card already shows. A card that matches for a reason the user
290
+ // cannot see is worse than no filter at all.
291
+ export const AgentMatchSchema = z.object({ id: z.string(), snippet: z.string().optional() });
292
+ export type AgentMatch = z.infer<typeof AgentMatchSchema>;
293
+ // `scanned` is how many agents the daemon actually read prompts for, so the board can say when a query saw
294
+ // less than the whole fleet rather than implying it saw all of it.
295
+ export const AgentSearchResultSchema = z.object({ matches: z.array(AgentMatchSchema), scanned: z.number() });
296
+ export type AgentSearchResult = z.infer<typeof AgentSearchResultSchema>;
251
297
  // rename's input: the user-chosen display title (bounded like sanitizeTitle's cap).
252
298
  export const AgentRenameSchema = z.object({ id: z.string().min(1), title: z.string().trim().min(1).max(80) });
299
+ // autoLand's input: this agent's own land-at-completion posture. `null` CLEARS the override back to "inherit
300
+ // the sandbox setting" — the browser sends it whenever the user toggles back to what the global already says,
301
+ // so agents don't accumulate frozen overrides that quietly stop following the global toggle.
302
+ export const AgentAutoLandSchema = z.object({ id: z.string().min(1), autoLand: z.boolean().nullable() });
253
303
  export const AgentFileDiffQuerySchema = z.object({ id: z.string().min(1), repo: z.string().min(1), path: z.string().min(1) });
254
- // land's outcome: per-repo conflicts (dirty-main overlaps or merge conflicts); landed only when every repo
255
- // with changes merged cleanly. Conflicted repos keep their worktree state nothing is lost.
304
+ /* WHY a path would not land. The distinction is the whole difference between an actionable report and a dead
305
+ * end, because the three have nothing in common but their symptom:
306
+ * `workspace` — you have uncommitted edits on that path. Yours is the copy at risk; commit or stash it.
307
+ * `diverged` — the main tree's COMMITTED content moved under the agent since it branched. Nothing of
308
+ * yours is at risk; the agent's delta is simply written against an older file.
309
+ * `binary` — git cannot three-way merge the file at all, so no automatic resolution exists.
310
+ * The old report named only the first, which is the rarest of the three. */
311
+ export const LandConflictReasonSchema = z.enum(["workspace", "diverged", "binary"]);
312
+ export type LandConflictReason = z.infer<typeof LandConflictReasonSchema>;
313
+ export const LandConflictPathSchema = z.object({ path: z.string(), reason: LandConflictReasonSchema });
314
+
315
+ /* land's outcome, per repo of the composition. `paths` is the set that genuinely failed to apply — NOT the
316
+ * whole delta, which is what the first version reported whenever it could not pin the cause down, turning
317
+ * four real conflicts into a wall of fourteen. `clean` counts what would land regardless, so the UI can say
318
+ * how much is being held back by how little, and offer to take it. An empty `paths` with `clean: 0` is the
319
+ * repo-unavailable case: the main checkout is gone, and no path-level account exists. */
320
+ export const LandConflictSchema = z.object({
321
+ repo: z.string(),
322
+ paths: z.array(LandConflictPathSchema),
323
+ clean: z.number(),
324
+ });
325
+ export type LandConflict = z.infer<typeof LandConflictSchema>;
326
+
327
+ // land's outcome; landed only when every repo with changes applied cleanly. Conflicted repos keep their
328
+ // worktree state — nothing is lost, and "Land now" stays available. `resolving` is populated only by a
329
+ // `merge` land: the paths written into the workspace carrying conflict markers, which the user finishes by
330
+ // hand in their own editor exactly as they would any merge.
256
331
  export const LandResultSchema = z.object({
257
332
  landed: z.boolean(),
258
- conflicts: z.array(z.object({ repo: z.string(), paths: z.array(z.string()) })).optional(),
333
+ conflicts: z.array(LandConflictSchema).optional(),
334
+ resolving: z.array(z.object({ repo: z.string(), paths: z.array(z.string()) })).optional(),
335
+ // A `measure` outcome with an outstanding delta: nothing was applied and nothing failed — the work is
336
+ // waiting on the branch for a deliberate Land. `landed: false` alone can't say that (it means refusal).
337
+ held: z.boolean().optional(),
259
338
  });
260
339
  export type LandResult = z.infer<typeof LandResultSchema>;
261
340
 
341
+ /* land's input. `check` is the safe default and the historical behaviour: the delta is applied only if ALL of
342
+ * it applies, so a refusal leaves the workspace byte-identical. `merge` is the escape hatch the conflict
343
+ * report offers — a three-way apply that lands every clean path and leaves the rest with conflict markers to
344
+ * resolve in place. It is opt-in because it WRITES on failure, which is the one thing `check` promises not
345
+ * to do. `measure` is the auto-land-off mode: everything a land does EXCEPT touching the main tree — the
346
+ * provenance commit onto agent/<id>, the cumulative diffstat, and the bookkeeping for work that reached the
347
+ * main line by another road — so a held agent's card stays as current as a landed one's while its delta waits
348
+ * on the branch for a deliberate Land. */
349
+ export const LandModeSchema = z.enum(["check", "merge", "measure"]);
350
+ export type LandMode = z.infer<typeof LandModeSchema>;
351
+ export const AgentLandSchema = z.object({ id: z.string().min(1), mode: LandModeSchema.optional() });
352
+
262
353
  // ---- routed-provider subscriptions ----
263
354
 
264
355
  // The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
@@ -269,9 +360,19 @@ export type LandResult = z.infer<typeof LandResultSchema>;
269
360
  export const KeyedProviderSchema = z.enum(["codex", "grok", "gemini"]);
270
361
  export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
271
362
 
272
- // Which routed-provider subscriptions are connected in the translator (per provider). Drives the
273
- // "connected / connect subscription" state in Sandbox Agent.
274
- export const TranslatorAccountsSchema = z.object({ codex: z.boolean(), grok: z.boolean(), gemini: z.boolean() });
363
+ // One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
364
+ // disconnect addresses and `label` the sign-in identity it reported (the account email, else the file name).
365
+ export const TranslatorAccountSchema = z.object({ name: z.string(), label: z.string() });
366
+ export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
367
+ // Which routed-provider subscriptions are connected in the translator, per provider — a LIST per provider, not
368
+ // a flag: CLIProxyAPI holds any number of auth files per provider side by side and balances requests across
369
+ // them, so connecting a second ChatGPT or Google account is more headroom, and each is disconnectable on its
370
+ // own. Drives the account rows in Sandbox ▸ Agent.
371
+ export const TranslatorAccountsSchema = z.object({
372
+ codex: z.array(TranslatorAccountSchema),
373
+ grok: z.array(TranslatorAccountSchema),
374
+ gemini: z.array(TranslatorAccountSchema),
375
+ });
275
376
  export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
276
377
 
277
378
  // The side-channel body that un-parks a turn waiting on the user. Every interactive card — plan approval,
@@ -328,6 +429,12 @@ export const SteerSchema = z
328
429
  // True cancel for the conversation's in-flight turn — aborts the agent daemon-side, unlike closing the
329
430
  // /agent fetch (which sends no cancel frame).
330
431
  export const StopTurnSchema = z.object({ conversationId: z.string().min(1) });
432
+ // Fire the conversation's remembered usage-limit resume NOW instead of waiting out the reset. `account`
433
+ // points the re-run at a different connected account of the same provider — the "resume on another account"
434
+ // action a spent allowance offers when the sandbox holds more than one; omitted, the turn re-runs on
435
+ // whatever served it (a plain "try again now"). NOT_FOUND when nothing is pending (the failure was already
436
+ // superseded by a fresh turn, or the daemon restarted).
437
+ export const ResumeLimitSchema = z.object({ conversationId: z.string().min(1), account: z.string().min(1).optional() });
331
438
 
332
439
  // ---- claude subscription usage ----
333
440
  // The GATE signal: whether the provider is letting turns through right now, and — when it is refusing — which
@@ -381,6 +488,14 @@ export type AccountUsage = z.infer<typeof AccountUsageSchema>;
381
488
  export const OauthAccountSchema = z.object({
382
489
  id: z.string(),
383
490
  label: z.string(),
491
+ // WHO this account signs in as, in the provider's own words — Anthropic returns the email and the
492
+ // organization alongside the tokens, so a connection can name itself instead of arriving as a second row
493
+ // called "Claude". Kept BESIDE `label` rather than folded into it: the label is the user's to rename, and a
494
+ // renamed account still has to be able to say whose it is. Absent when the provider tells us nothing (a
495
+ // pasted API key carries no identity) — which is exactly when renaming is the only answer, so every
496
+ // sandbox-owned account can be renamed.
497
+ email: z.string().optional(),
498
+ organization: z.string().optional(),
384
499
  scope: z.string().optional(),
385
500
  connectedAt: z.number(), // epoch ms
386
501
  // Set only when the account's stored credential can no longer be refreshed (revoked/expired refresh token)
@@ -397,6 +512,10 @@ export type OauthAccount = z.infer<typeof OauthAccountSchema>;
397
512
  export const OauthAccountListSchema = z.object({ accounts: z.array(OauthAccountSchema) });
398
513
  // Address one account of a provider (disconnect, and the turn's `account`).
399
514
  export const AccountIdSchema = z.object({ id: z.string().min(1) });
515
+ // Rename one account of a provider whose credential the sandbox owns (Claude, Kimi). Blank ⇒ the daemon falls
516
+ // back to the derived name, so clearing a label restores the sign-in identity rather than leaving a nameless
517
+ // row. Grok is absent for the same reason it holds one account: OpenCode owns that credential, not this store.
518
+ export const AccountRenameSchema = z.object({ id: z.string().min(1), label: z.string().max(80) });
400
519
  // The completing calls carry the user-chosen label (blank ⇒ the daemon derives one from the sign-in identity
401
520
  // or a provider default).
402
521
  export const OauthExchangeSchema = z.object({
@@ -464,10 +583,29 @@ export const ModelsSchema = z.object({ models: z.array(ModelSchema), default: z.
464
583
  // ---- sessions ----
465
584
 
466
585
  export const SessionIdParamSchema = z.object({ id: z.string() });
467
- export const SessionSummarySchema = z.object({ id: z.string(), title: z.string(), updatedAt: z.number() });
586
+ export const SessionSummarySchema = z.object({
587
+ id: z.string(),
588
+ title: z.string(),
589
+ updatedAt: z.number(),
590
+ // Why a searched session matched: the line of the USER's own prompt the query hit, windowed around it.
591
+ // Absent on an unfiltered list, and on a match the title already shows — a snippet repeating the row's
592
+ // own heading is noise, not evidence. See AgentMatchSchema for the same field on the fleet's side.
593
+ snippet: z.string().optional(),
594
+ });
468
595
  export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySchema) });
469
596
 
470
597
  // ---- settings: per-sandbox agent settings (.intentic/settings.json) ----
598
+
599
+ // Which prompt the agent is, before this turn composes anything on top. Two built-in bases and an escape
600
+ // hatch: Intentic's own (the default), Claude Code's preset, or the owner's text. Declared out here rather
601
+ // than inline in the settings object because both sides of the wire branch on it — the daemon to build the
602
+ // turn, the browser to decide which base it can show you.
603
+ export const SystemPromptModeSchema = z.enum(["intentic", "claude", "custom"]);
604
+ export type SystemPromptMode = z.infer<typeof SystemPromptModeSchema>;
605
+ // The two bases a user can READ and fork — "custom" is excluded because there is nothing to fetch: it is
606
+ // whatever they have already typed into the settings field.
607
+ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"]) });
608
+
471
609
  // Small user-owned config the /settings routes edit and streamAgent reads — all opt-in booleans the owner
472
610
  // toggles in the UI (so each can be A/B benchmarked):
473
611
  // stableSystemPrompt — keeps the system prompt byte-stable across turns (the delegation note rides the user
@@ -480,6 +618,9 @@ export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySch
480
618
  // guard + fewer output tokens); off ⇒ the native file tools.
481
619
  // terseOutput — appends a concise-response steer to the end of the system prompt (a stable suffix, so it
482
620
  // composes with stableSystemPrompt) to cut the model's OWN output tokens.
621
+ // systemPromptMode — which base the agent's prompt is: "intentic" (default), "claude", or "custom".
622
+ // systemPrompt — the owner's own prompt text, used only by "custom" mode, where it is the ENTIRE system
623
+ // prompt and nothing the daemon would otherwise append rides with it — see its own note.
483
624
  // iqSearch — loads the image-baked iq Claude Code plugin (skill + SessionStart nudge) so the agent
484
625
  // prefers the iq CLI over grep/find/Glob; off ⇒ plugin not loaded, native search tools
485
626
  // only. Opt-in (default off); the browser Search box uses iq regardless.
@@ -490,7 +631,7 @@ export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySch
490
631
  // (recorded raw as `heldOut`), so the savings report compares a real cleaned-vs-raw
491
632
  // population instead of an estimate. 0 = no holdout (default).
492
633
  // filterBackend — which cleaner runs the compression: "native" (agent-output-filter, default) or "rtk"
493
- // (the rtk binary from its installed extension, rewritten at the PreToolUse hook) — an
634
+ // (the image-baked rtk binary, rewritten at the PreToolUse hook) — an
494
635
  // A/B backend switch, so native and rtk can be benchmarked head-to-head.
495
636
  // The booleans default off, skills defaults [] (no skill loaded), outputCleaners defaults "off" (cleaning off),
496
637
  // outputHoldout 0, filterBackend "native" — a fresh sandbox starts with cleaning and iq off until the owner enables them.
@@ -507,33 +648,167 @@ export const SandboxSettingsSchema = z.object({
507
648
  skills: z.array(z.string()).default([]),
508
649
  hashlineEdits: z.boolean().default(false),
509
650
  terseOutput: z.boolean().default(false),
651
+ /* Measurement control for the terse steer, at TURN level — the same trick `outputHoldout` plays over
652
+ * commands, one layer up. A fraction [0,1] of otherwise-eligible turns run WITHOUT the steer and record
653
+ * which arm they ran on (UsageTurn.terse), so the savings report can compare two real populations.
654
+ *
655
+ * It has to be an experiment: unlike a cleaned command, which yields its own raw baseline in the same
656
+ * event, a turn cannot be re-run to see what it would have said unsteered. 0 ⇒ no measurement (every
657
+ * eligible turn is steered), which is the default because the control costs the very tokens it measures. */
658
+ terseHoldout: z.number().min(0).max(1).default(0),
659
+ /* WHICH SYSTEM PROMPT THE AGENT RUNS ON — the base, before anything this turn composes.
660
+ *
661
+ * intentic — Intentic's own prompt, tuned for this harness (intentic-prompt.ts). The default.
662
+ * claude — Claude Code's preset, as shipped in the CLI this sandbox runs. Not a copy stored here, so
663
+ * picking it tracks whatever the installed CLI's prompt is rather than freezing at a snapshot.
664
+ * custom — `systemPrompt` below, and nothing else at all.
665
+ *
666
+ * The first two are peers: both get the harness's own guidance appended (the AskUserQuestion/plan blocks
667
+ * the chat's cards need, the checklist guidance behind the todo panel, the browser-tool guidance), plus the
668
+ * delegation note and the terse steer. `custom` is the one that does not, by the owner's explicit choice —
669
+ * see the field below. */
670
+ systemPromptMode: SystemPromptModeSchema.default("intentic"),
671
+ /* The owner's own prompt, used only when `systemPromptMode` is "custom". Then it is the ENTIRE system
672
+ * prompt: both built-in bases are gone and so is everything the daemon would otherwise append — the widget
673
+ * guidance the chat's cards are driven by, and the terse-output steer (whose toggle goes inert). That is
674
+ * the price of total control, and the UI states it at the moment of the edit rather than letting the
675
+ * widgets go quietly dark. Only the cross-provider delegation note survives, because it has a home outside
676
+ * the system prompt already (the user-message preamble stableSystemPrompt puts it in).
677
+ *
678
+ * Cap is roomy — the bases it stands in for are ~6.8k characters — but finite, because every turn pays it. */
679
+ systemPrompt: z.string().max(20000).default(""),
510
680
  iqSearch: z.boolean().default(false),
511
681
  outputCleaners: z.string().default("off"),
512
682
  outputHoldout: z.number().min(0).max(1).default(0),
513
683
  filterBackend: z.enum(["native", "rtk"]).default("native"),
684
+ /* The model behind the one-click helpers that are not a conversation — today the commit box's autofill.
685
+ * `${provider}:${modelId}`, or EMPTY for Auto, which is the default and the interesting case: Auto is
686
+ * resolved from whatever accounts are connected at the moment it is read (resolveQuickModel), so it can
687
+ * never name a provider this sandbox has no credential for and it improves by itself when one is added.
688
+ * Storing a resolved id here instead would go stale exactly like a pinned model does. */
689
+ quickModel: z.string().default(""),
514
690
  // How long a finished agent stays on the board before it is archived automatically (days; 0 ⇒ never).
515
691
  // Unlike every other flag here this one defaults ON, because the lane it governs is the board's only
516
692
  // terminal state: without a sweep the Finished lane grows for the life of the sandbox, and each card it
517
693
  // holds is a live worktree checkout, not just a row.
518
694
  agentRetentionDays: z.number().min(0).max(365).default(3),
695
+ /* Land a clean turn's delta into the main tree automatically at completion — the Claude Code review model,
696
+ * and the historical behaviour, so it defaults ON (flipping the default would silently change every
697
+ * existing sandbox). OFF holds finished work on the agent's branch instead: the card reads "Ready to
698
+ * land" and the user lands it deliberately, from the review panel or the card. Sandbox-wide because
699
+ * automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
700
+ * browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
701
+ 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),
708
+ /* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
709
+ * socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
710
+ *
711
+ * Defaults ON, unlike autoResumeOnLimit, and the difference is not an inconsistency: a spent allowance is
712
+ * the user's own budget, and resuming into a fresh window spends something they may have been saving. An
713
+ * outage resume spends nothing the dead turn had not already committed, resolves in minutes rather than
714
+ * hours, and — the deciding argument — the turns hurt worst by it are the ones with nobody in the room
715
+ * (automation wakes, Discord, webhooks), which no browser-held preference could ever rescue. It is the same
716
+ * reasoning that leaves the auth resume ungated: this is the provider's failure, not the user's decision. */
717
+ resumeAfterOutage: z.boolean().default(true),
519
718
  });
520
719
  export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
521
720
 
522
- // ---- output-cleaner savings report (rtk-`gain`-style) ----
523
- // Aggregated from historyRoot/logs/filter-stats.jsonl (one row per agent Bash command). `perCleaner` attributes
524
- // which cleaner ids fired across commands; `holdout` is the measured control (commands the holdout bypassed) vs
525
- // the cleaned population — a real saved-% rather than an estimate; `gaps` are high-volume commands that matched
526
- // no cleaner (the next handler to write). Empty/zeroed when no commands have run yet.
527
- export const CleanerSavingsSchema = z.object({
721
+ // One of the two built-in bases, as text: Intentic's own prompt, or Claude Code's preset read out of the CLI
722
+ // this sandbox runs (preset-prompt.ts captures it rather than storing a transcription). What the settings page
723
+ // shows behind "View" and drops into the editor behind "Edit a copy".
724
+ //
725
+ // `version` is the CLI build a captured preset came from, so the UI can say WHICH text the user is looking at:
726
+ // a custom prompt forked from an older build is a snapshot, and the version is the only honest way to tell.
727
+ // Empty for Intentic's prompt, which ships with the app and has no version of its own to report.
728
+ export const BuiltinPromptTextSchema = z.object({ text: z.string(), version: z.string() });
729
+ export type BuiltinPromptText = z.infer<typeof BuiltinPromptTextSchema>;
730
+
731
+ /* ---- savings report: what each token-reduction mechanism actually saved ----
732
+ *
733
+ * TWO FAMILIES, deliberately never one list of bars. They are measured differently, and a chart that ranks
734
+ * them side by side claims a confidence and a denominator that only one of them has:
735
+ *
736
+ * input — shell output the cleaners trimmed before the model ever saw it. Both sides of the comparison come
737
+ * off the SAME command (raw in, emitted out), so the counterfactual is observed rather than
738
+ * estimated: exact, per command, no sample size to argue about.
739
+ * output — the model's own tokens under the terse steer. There is no second run of the same turn to compare
740
+ * against, so the only honest number is an experiment: a turn-level holdout, an n per arm, and a
741
+ * margin. It is absent entirely until both arms are large enough for the delta to mean anything.
742
+ *
743
+ * The two are also in different units of value — a saved tool-output token is saved again on every later
744
+ * request of that conversation, an output token is saved once but costs several times as much — which is the
745
+ * other reason they are separate sections with separate totals rather than one number.
746
+ */
747
+
748
+ // One mechanism's realized saving, biggest first. `savedTokens` is what THIS stage removed from what reached
749
+ // it in pipeline order — sequential attribution, which is why the stages sum exactly to raw − emitted and can
750
+ // be drawn as one stacked bar. It is NOT "what turning this cleaner off would cost you": the cap downstream
751
+ // would have eaten some of the same lines. `commands` is how many commands the stage ran on. Negative for the
752
+ // `footer` stage, which adds the retrieval pointer back — a cost on the same ledger as what it bought.
753
+ export const SavingsStageSchema = z.object({ id: z.string(), commands: z.number(), savedTokens: z.number() });
754
+
755
+ // Whichever cleaner is ACTUALLY compressing output owns these numbers, so they are read from that backend's
756
+ // own ledger: "native" aggregates historyRoot/logs/filter-stats.jsonl (one row per agent Bash command, written
757
+ // by agent-output-filter), "rtk" reads rtk's own gain ledger. Reading one ledger regardless of backend is how
758
+ // this card went stale: under rtk the native filter is switched off, nothing appends, and the last numbers
759
+ // written — a test run's, as it happened — sat on the card looking live.
760
+ export const InputSavingsSchema = z.object({
761
+ // Which backend's ledger these numbers came from — shown on the card, because a number without its source
762
+ // cannot be told apart from a stale one. Defaulted for the same daemon-older-than-browser seam as settings.
763
+ source: z.enum(["native", "rtk"]).default("native"),
764
+ // False ⇒ these totals cover the ledger's whole life, not the range the reader selected: `rtk gain` reports
765
+ // no timestamps, so its numbers cannot be windowed and the screen has to say so rather than let a 7-day
766
+ // filter sit above an all-time figure.
767
+ windowed: z.boolean(),
768
+ // When that ledger last recorded a command (epoch ms), so the card can show its age instead of implying
769
+ // freshness it doesn't have. Absent when the ledger has never been written or its age can't be read.
770
+ updatedAt: z.number().optional(),
528
771
  commands: z.number(),
529
772
  rawTokens: z.number(),
530
773
  emittedTokens: z.number(),
531
774
  savedPct: z.number(),
532
- perCleaner: z.array(z.object({ id: z.string(), commands: z.number() })),
775
+ // Per-stage attribution, biggest first. Native-only: rtk reports totals, not which of its handlers fired.
776
+ perCleaner: z.array(SavingsStageSchema),
777
+ // The measured control — commands the holdout left raw — against the cleaned population. A real saved-%
778
+ // for the pipeline as a whole rather than an estimate, and the only whole-pipeline counterfactual there is.
533
779
  holdout: z.object({ cleaned: z.number(), heldOut: z.number(), measuredSavedPct: z.number().optional() }),
780
+ // High-volume commands that matched no cleaner: where the next handler is worth writing. Native-only.
534
781
  gaps: z.array(z.object({ command: z.string(), tokens: z.number() })),
535
782
  });
536
- export type CleanerSavings = z.infer<typeof CleanerSavingsSchema>;
783
+ export type InputSavings = z.infer<typeof InputSavingsSchema>;
784
+
785
+ // One arm of the turn-level experiment: the turns that ran with the steer, and the turns the holdout ran
786
+ // without it. Mean output tokens PER TURN, because the arms never hold the same number of turns.
787
+ export const SavingsArmSchema = z.object({ turns: z.number(), meanOutputTokens: z.number() });
788
+
789
+ // The terse steer, as measured. Only turns where the steer was ELIGIBLE are counted — a turn under a custom
790
+ // system prompt drops the steer along with everything else the daemon appends, so it belongs to neither arm.
791
+ export const OutputSavingsSchema = z.object({
792
+ on: SavingsArmSchema,
793
+ off: SavingsArmSchema,
794
+ // Turns per arm before a delta is reported at all. Carried on the wire so the screen's "measuring…" state
795
+ // counts toward the daemon's real threshold instead of a number the browser guessed.
796
+ minTurns: z.number(),
797
+ /* The three below are present TOGETHER, and only once both arms clear `minTurns` — a schema that can't
798
+ * express a half-measured experiment is how a 34%-that-becomes-8%-tomorrow never reaches the screen.
799
+ * deltaPct — change in mean output tokens per turn under the steer; negative is a saving.
800
+ * marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
801
+ * savedTokens — what the delta is worth over the turns that actually ran with the steer, in this window. */
802
+ deltaPct: z.number().optional(),
803
+ marginPct: z.number().optional(),
804
+ savedTokens: z.number().optional(),
805
+ });
806
+ export type OutputSavings = z.infer<typeof OutputSavingsSchema>;
807
+
808
+ // `output` is absent when the experiment isn't running at all (terse off, or no holdout set) — a section that
809
+ // isn't there reads as "not measured", which is the truth, while zeros would read as "measured, worth nothing".
810
+ export const SavingsReportSchema = z.object({ input: InputSavingsSchema, output: OutputSavingsSchema.optional() });
811
+ export type SavingsReport = z.infer<typeof SavingsReportSchema>;
537
812
 
538
813
  // ---- intentic CLI ----
539
814
 
@@ -579,6 +854,24 @@ export const GitStatusSchema = z.object({ branch: z.string(), dirty: z.boolean()
579
854
  export const GitFilesSchema = z.object({ files: z.array(z.string()) });
580
855
  export const GitFileSchema = z.object({ path: z.string(), content: z.string() });
581
856
  export const CommitResultSchema = z.object({ committed: z.boolean() });
857
+
858
+ /* AI-drafted commit message. Workspace-wide, not per repo, because the commit box's target IS a set of repos
859
+ * sharing one message — so the draft has to see every one of their diffs to describe what the commit actually
860
+ * records. `repos` and `all` mirror the panel's own commit target exactly: `all` reads the WORKTREE (what
861
+ * "Commit all" would sweep), absent reads the INDEX (what a bare commit records). Getting that wrong would
862
+ * describe changes the commit isn't going to contain. */
863
+ export const CommitMessageDraftSchema = z.object({
864
+ repos: z.array(z.string().min(1)).min(1).max(50),
865
+ all: z.boolean().optional(),
866
+ });
867
+ // The draft plus WHICH model wrote it, so the surface can name it rather than claiming an anonymous "AI" —
868
+ // that name is also the only place the resolved quick model is visible before anyone opens settings.
869
+ export const CommitMessageSchema = z.object({
870
+ message: z.string(),
871
+ provider: z.string(),
872
+ model: z.string(),
873
+ });
874
+ export type CommitMessageDraft = z.infer<typeof CommitMessageSchema>;
582
875
  // One change to a file — an uncommitted working-tree change (status vs HEAD, untracked included), an agent
583
876
  // worktree's delta vs its base, or a file in a commit. `additions`/`deletions` are the numstat line counts,
584
877
  // undefined for a binary file (git reports "-"/"-") or an untracked file (no HEAD blob to diff against).
@@ -658,6 +951,11 @@ export const RepoChangesSchema = z.object({
658
951
  // additions/deletions describe the diff it is listed under, never a conflation of the two.
659
952
  staged: z.array(GitChangeSchema),
660
953
  unstaged: z.array(GitChangeSchema),
954
+ // How many changes were CUT from the two sides above (conflicts are never cut). A cloned monorepo or a
955
+ // mass delete carries six-figure change lists — a payload no panel can render and no browser should hold —
956
+ // so past the daemon's per-repo budget the lists arrive truncated and this carries the dropped count, which
957
+ // the panel adds to its badges and states under the group. Absent ⇒ the lists are complete.
958
+ truncated: z.number().optional(),
661
959
  // Where this repo stands against its remote; `ahead`/`behind` are 0 with no remote or no upstream.
662
960
  remote: GitRemoteStateSchema.optional(),
663
961
  // WHICH AGENT PUT IT THERE: repo-relative path → the agent ids that landed it, newest land first. Keyed by
@@ -665,7 +963,7 @@ export const RepoChangesSchema = z.object({
665
963
  // edited again) and its origin is the same fact for both. Only agents can appear here — a main-tree turn,
666
964
  // a terminal edit and your own typing never pass through land, so they are simply absent (see
667
965
  // agents/origins.ts), which is why the panel badges an agent and says nothing at all for anyone else.
668
- // Ids, not titles: every client already mirrors the fleet registry and can resolve one to the other.
966
+ // Ids, not titles: the identity for every id named here rides the response once, in `originAgents`.
669
967
  origins: z.record(z.string(), z.array(z.string())).optional(),
670
968
  // Why the repo could not be scanned at all, condensed to git's own one-line reason ("fatal: bad object HEAD").
671
969
  // A repo left torn by a canceled or failed upload used to be dropped from the response entirely, so it just
@@ -673,9 +971,31 @@ export const RepoChangesSchema = z.object({
673
971
  error: z.string().optional(),
674
972
  });
675
973
  export type RepoChanges = z.infer<typeof RepoChangesSchema>;
974
+
975
+ // WHO AN ORIGIN ID IS — the display identity of one agent named in `origins`, carried BY THE RESPONSE rather
976
+ // than looked up in the client's fleet roster. The roster is the LIVE board and deliberately drops archived
977
+ // agents (AgentsRegistry.list), while a landing outlives the agent that made it: archiving a finished agent
978
+ // does not commit its lines, so the very common case — land, archive the card, review at leisure — is exactly
979
+ // the one a roster lookup cannot answer, and the panel fell back to "Agent 1a2b3c" with a generic icon for it.
980
+ // The daemon reads attribution and identity from the same registry in the same pass, so it is the one place
981
+ // they cannot disagree. Per response, not per repo: one agent commonly lands into several.
982
+ export const OriginAgentSchema = z.object({
983
+ // Absent for an entry that never got a title (a turn that failed before one was derived).
984
+ title: z.string().optional(),
985
+ provider: AgentProviderSchema,
986
+ });
987
+ export type OriginAgent = z.infer<typeof OriginAgentSchema>;
988
+
676
989
  // The aggregated review set across every repo (root + every discovered repo); a repo appears when it has changes,
677
990
  // when it is out of sync with its remote, or when it failed to scan.
678
- export const GitChangesSchema = z.object({ repos: z.array(RepoChangesSchema) });
991
+ export const GitChangesSchema = z.object({
992
+ repos: z.array(RepoChangesSchema),
993
+ // Keyed by agent id; covers every id any repo's `origins` names, and only those. Absent when nothing in
994
+ // the review is attributable. An id can still be missing from it — the retention sweep can retire an
995
+ // entry whose landed lines are somehow still uncommitted — and the panel keeps its id-shaped fallback for
996
+ // exactly that, rather than dropping the chip and re-attributing the file to the user.
997
+ originAgents: z.record(z.string(), OriginAgentSchema).optional(),
998
+ });
679
999
  export type GitChanges = z.infer<typeof GitChangesSchema>;
680
1000
 
681
1001
  // One file an agent touched, plus whether that change is ALREADY in the main tree. The review lists the
@@ -696,7 +1016,15 @@ export const AgentRepoChangesSchema = z.object({
696
1016
  changes: z.array(AgentChangeSchema),
697
1017
  });
698
1018
  export type AgentRepoChanges = z.infer<typeof AgentRepoChangesSchema>;
699
- export const AgentChangesSchema = z.object({ repos: z.array(AgentRepoChangesSchema) });
1019
+ /* The review, plus WHY the last land refused — because a conflict is discovered by the daemon (a clean turn
1020
+ * auto-lands the moment it finishes) and acted on in the browser, possibly hours later, on a surface the user
1021
+ * reaches by clicking the card's "Resolve conflict". Carrying the report only in the land RESPONSE meant the
1022
+ * one path that opens the review already knowing there is a conflict was the one path that could not show it:
1023
+ * the panel opened with an empty report, no explanation, and no merge affordance — a dead end at the exact
1024
+ * moment the UI had promised something to resolve. It rides the review because that is the surface that
1025
+ * resolves it, and it refreshes with it: every land invalidates this query, so the report is never staler
1026
+ * than the last attempt. */
1027
+ export const AgentChangesSchema = z.object({ repos: z.array(AgentRepoChangesSchema), conflicts: z.array(LandConflictSchema).optional() });
700
1028
  export type AgentChanges = z.infer<typeof AgentChangesSchema>;
701
1029
 
702
1030
  // ---- git history graph (the "Git Graph" view over a repo's real commits) ----
@@ -802,11 +1130,11 @@ export const WorkspaceTreeEntrySchema = z.object({
802
1130
  path: z.string(),
803
1131
  type: z.enum(["file", "dir"]),
804
1132
  size: z.number().optional(),
805
- // Set on a dir whose child list was cut short by the entry cap — some of its items aren't in `children`.
806
- truncated: z.boolean().optional(),
807
1133
  // Ignored-by-tooling (node_modules, .git, .gitignore'd paths, browser profiles): the client grays the row.
808
- // An ignored DIR is listed without `children` — the client lazy-loads it via /workspace/children on expand.
809
1134
  ignored: z.boolean().optional(),
1135
+ // A DIR without `children` was listed but not descended into — because it's ignored, or because the walk's
1136
+ // breadth-first budget stopped above it. Either way the client lazy-loads it via /workspace/children on
1137
+ // expand, so "not loaded yet" and "empty directory" (`children: []`) stay distinguishable.
810
1138
  get children() {
811
1139
  return z.array(WorkspaceTreeEntrySchema).optional();
812
1140
  },
@@ -815,17 +1143,17 @@ export type WorkspaceTreeEntry = z.infer<typeof WorkspaceTreeEntrySchema>;
815
1143
  export const WorkspaceTreeSchema = z.object({
816
1144
  root: z.string(),
817
1145
  tree: z.array(WorkspaceTreeEntrySchema),
818
- // True when the root's own entries were cut by the entry cap (per-dir cuts are flagged on each dir entry).
819
- truncated: z.boolean(),
1146
+ // How many of the ROOT's own entries the budget cut (0 = complete); per-dir cuts are counted on each dir entry.
1147
+ hidden: z.number(),
820
1148
  });
821
1149
  export type WorkspaceTree = z.infer<typeof WorkspaceTreeSchema>;
822
- // Lazy-load one directory's children — for an ignored dir the tree walk didn't descend into. Every returned entry
823
- // is itself `ignored` (it lives under an ignored subtree); child dirs again carry no `children`, so they lazy-load
824
- // on their own expand. `truncated` ⇒ the dir's child list was cut by the entry cap.
1150
+ // Lazy-load one directory's children — for a dir the tree walk listed but didn't descend into. Child dirs again
1151
+ // carry no `children`, so they lazy-load on their own expand. `hidden` = how many entries the cap cut (0 = all
1152
+ // listed).
825
1153
  export const WorkspaceChildrenQuerySchema = z.object({ path: z.string().min(1) });
826
1154
  export const WorkspaceChildrenSchema = z.object({
827
1155
  entries: z.array(WorkspaceTreeEntrySchema),
828
- truncated: z.boolean(),
1156
+ hidden: z.number(),
829
1157
  });
830
1158
  export type WorkspaceChildren = z.infer<typeof WorkspaceChildrenSchema>;
831
1159
  export const WorkspaceFileQuerySchema = z.object({ path: z.string().min(1) });
@@ -906,6 +1234,58 @@ export const WorkspaceSearchResultSchema = z.object({
906
1234
  });
907
1235
  export type WorkspaceSearchResult = z.infer<typeof WorkspaceSearchResultSchema>;
908
1236
 
1237
+ // ---- codebase health: one repository's structure and risk, in numbers ----
1238
+
1239
+ // The repo-level companion to the management panel and the git-history graph: what the same resident engine's
1240
+ // `hotspots` (churn × complexity) and `map` (PageRank over the import graph) verbs rank, as figures a panel can
1241
+ // plot instead of lines a terminal prints.
1242
+ //
1243
+ // Every field is a COUNT that can be recounted in the files themselves — commits, branch points, exported
1244
+ // symbols. Deliberately no composite "maintainability grade": those aren't comparable across projects and can't
1245
+ // be checked, and a repo-health surface that launders counts into a letter is worse than none.
1246
+ // How many hotspot files and key modules a report carries when the caller names no limit. A leaderboard, not an
1247
+ // inventory: past a screenful the ranking stops being the point, and the reader should be reading the files.
1248
+ export const HEALTH_LIMIT = 20;
1249
+ export const WorkspaceHealthQuerySchema = z.object({
1250
+ // "root" (the /work repo) or a nested repo's root-relative dir — the same {repo} ids the git routes take.
1251
+ repo: z.string().min(1),
1252
+ // Churn window (2d, 12h, 1w, 3m). Absent = all of history, which is what a hotspot ranking wants by default.
1253
+ since: z.string().max(16).optional(),
1254
+ limit: z.coerce.number().int().positive().max(200).optional(),
1255
+ });
1256
+ // One file that is BOTH churning and tangled. `score` is the product the ranking sorts by — carried explicitly
1257
+ // so the panel plots the number it ranks by rather than recomputing it.
1258
+ export const WorkspaceHotspotSchema = z.object({
1259
+ path: z.string(),
1260
+ commits: z.number(),
1261
+ adds: z.number(),
1262
+ dels: z.number(),
1263
+ complexity: z.number(),
1264
+ score: z.number(),
1265
+ // Epoch ms of the latest commit touching the file, within the window.
1266
+ latestMs: z.number(),
1267
+ });
1268
+ export type WorkspaceHotspot = z.infer<typeof WorkspaceHotspotSchema>;
1269
+ // A file of the import graph's ranked skeleton — order IS the rank, so no rank number rides along.
1270
+ export const WorkspaceKeyModuleSchema = z.object({ path: z.string(), exports: z.number() });
1271
+ export type WorkspaceKeyModule = z.infer<typeof WorkspaceKeyModuleSchema>;
1272
+ export const WorkspaceHealthSchema = z.object({
1273
+ repo: z.string(),
1274
+ totals: z.object({
1275
+ files: z.number(),
1276
+ symbols: z.number(),
1277
+ // Summed branch points across the scoped files.
1278
+ complexity: z.number(),
1279
+ // How many files qualify as hotspots at all — the lists below are capped, this is not.
1280
+ hotspots: z.number(),
1281
+ }),
1282
+ hotspots: z.array(WorkspaceHotspotSchema),
1283
+ modules: z.array(WorkspaceKeyModuleSchema),
1284
+ // Same index-freshness signal the search route reports: a panel drawn off a half-built index says so.
1285
+ freshness: WorkspaceSearchFreshnessSchema,
1286
+ });
1287
+ export type WorkspaceHealth = z.infer<typeof WorkspaceHealthSchema>;
1288
+
909
1289
  // ---- workspace setup (dependency readiness) ----
910
1290
 
911
1291
  // One project under /work and whether its dependencies are actually installed. A drop omits node_modules/.venv
@@ -981,12 +1361,13 @@ export const TemplatesListSchema = z.object({ templates: z.array(TemplateSummary
981
1361
  export type TemplatesList = z.infer<typeof TemplatesListSchema>;
982
1362
 
983
1363
  // One app instance currently in a monorepo, with its own preview dev server + live status (started/stopped
984
- // from the apps extension). `app` is the user-chosen instance name (the _apps/ dir); `template` is the
985
- // manifest key it was created from (api/web/landing). previewUrl is
1364
+ // from the apps extension). `app` is the user-chosen instance name (the _apps/ dir); `kind` is what sort of
1365
+ // app it is — the manifest key it was scaffolded from (api/web/landing), else the framework detected from its
1366
+ // dependencies (astro/next/…), and absent when it was discovered purely by its `dev` script. previewUrl is
986
1367
  // https://preview-<repo>--<app>-<sandboxId>.<zone> (absent on loopback — no zone or no connect token).
987
1368
  export const RepoAppSchema = z.object({
988
1369
  app: z.string(),
989
- template: z.string(),
1370
+ kind: z.string().optional(),
990
1371
  previewUrl: z.string().optional(),
991
1372
  running: z.boolean(),
992
1373
  healthy: z.boolean(),
@@ -1534,7 +1915,9 @@ export const WorkspaceEventSchema = z.object({
1534
1915
  agentId: z.string(),
1535
1916
  title: z.string().optional(),
1536
1917
  branch: z.string(),
1537
- outcome: z.enum(["landed", "conflict", "idle", "error"]),
1918
+ // `ready` is a clean turn whose delta was HELD on the branch (auto-land off) — for a chore, the moment
1919
+ // before the user's deliberate Land, which is exactly when a pre-land review wants to run.
1920
+ outcome: z.enum(["landed", "conflict", "ready", "idle", "error"]),
1538
1921
  repos: z.array(z.object({ repo: z.string(), from: z.string(), dir: z.string() })),
1539
1922
  });
1540
1923
  export type WorkspaceEvent = z.infer<typeof WorkspaceEventSchema>;
@@ -1681,13 +2064,15 @@ export const PanelSummarySchema = z.object({
1681
2064
  role: z.enum(["intent", "desired-state", "app"]).optional(),
1682
2065
  // Content facts: deploy.config.ts (the intent ledger's day-one marker), desired-state.json (present after
1683
2066
  // the first resolve), .intentic/ui/index.html (a sandboxed directory UI), pnpm-workspace.yaml +
1684
- // turbo.json (a pnpm+turbo monorepo), and vitest evidence (a root vitest.config.ts, or "vitest" in the
1685
- // root manifest / workspace catalog).
2067
+ // turbo.json (a pnpm+turbo monorepo), vitest evidence (a root vitest.config.ts, or "vitest" in the
2068
+ // root manifest / workspace catalog), and docs/user-stories (a directory of stories an agent can test
2069
+ // against the running app — the one fact here that says nothing about the repo's language).
1686
2070
  deployConfig: z.boolean(),
1687
2071
  desiredState: z.boolean(),
1688
2072
  directoryUi: z.boolean(),
1689
2073
  monorepo: z.boolean(),
1690
2074
  vitest: z.boolean(),
2075
+ userStories: z.boolean(),
1691
2076
  });
1692
2077
  export type PanelSummary = z.infer<typeof PanelSummarySchema>;
1693
2078
  export const PanelsListSchema = z.object({ panels: z.array(PanelSummarySchema) });
@@ -1750,11 +2135,19 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
1750
2135
  // declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
1751
2136
  // `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
1752
2137
  // same charset gates a `tmux kill-session -t` shell-out.
2138
+ //
2139
+ // `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
2140
+ // while that pane still lives) are what let a finished session be READ rather than merely listed: the panel's
2141
+ // Recent-terminals popover orders by the one and reports the other ("exit 1 · 1h ago"), and the daemon's
2142
+ // retention sweep ages sessions out by the same clock. 0 is "tmux didn't say" — treated as unknown by both,
2143
+ // never as 1970.
1753
2144
  export const TerminalSessionSchema = z.object({
1754
2145
  name: z.string(),
1755
2146
  label: z.string().optional(),
1756
2147
  kind: z.enum(["shell", "panel", "agent", "job", "process"]),
1757
2148
  running: z.boolean(),
2149
+ activityAt: z.number(),
2150
+ exitCode: z.number().optional(),
1758
2151
  extensionId: z.string().optional(),
1759
2152
  processName: z.string().optional(),
1760
2153
  });
@@ -1833,6 +2226,12 @@ export const InfoSchema = z.object({
1833
2226
  updateAvailable: z.boolean().optional(),
1834
2227
  });
1835
2228
 
2229
+ // A daemon-minted session (system.session): the steady-state browser credential, exchanged for a verified
2230
+ // Google ID token so Google UI is a sign-in moment instead of an hourly renewal. `expiresAt` is epoch ms —
2231
+ // the browser renews ahead of it without parsing the token; `email` is who the daemon verified.
2232
+ export const DaemonSessionSchema = z.object({ token: z.string(), expiresAt: z.number(), email: z.string() });
2233
+ export type DaemonSession = z.infer<typeof DaemonSessionSchema>;
2234
+
1836
2235
  // Intentic-provided host SSH tunnel: minting it needs intentic's PLATFORM Cloudflare account, so the daemon
1837
2236
  // can't do it directly — it relays to the platform authenticated by the connect token (the announce pattern).
1838
2237
  // The panel embeds the returned connector token + hostname in its connect-host one-liner.
@@ -1932,6 +2331,14 @@ export const UsageTurnSchema = z.object({
1932
2331
  cacheCreationTokens: z.number(),
1933
2332
  costUsd: z.number(),
1934
2333
  durationMs: z.number(),
2334
+ /* Which arm of the terse experiment this turn ran on (settings.terseHoldout) — the only record of it, and
2335
+ * the reason the savings report can say what the steer is worth instead of guessing.
2336
+ *
2337
+ * ABSENT means "not part of the experiment", not "off": a turn under a custom system prompt drops the
2338
+ * steer along with everything else the daemon appends, and a turn run with the experiment switched off has
2339
+ * no control to be compared against. Pooling those into the off-arm would compare steered turns against a
2340
+ * population selected by something other than the coin flip, which is not a control at all. */
2341
+ terse: z.boolean().optional(),
1935
2342
  });
1936
2343
  export type UsageTurn = z.infer<typeof UsageTurnSchema>;
1937
2344
 
@@ -1959,12 +2366,14 @@ export const UsageRollupRowSchema = z.object({
1959
2366
  durationMs: z.number(),
1960
2367
  });
1961
2368
  export type UsageRollupRow = z.infer<typeof UsageRollupRowSchema>;
1962
- // Inclusive UTC day bounds (YYYY-MM-DD). Both absent ⇒ the whole ledger.
1963
- export const UsageRollupQuerySchema = z.object({
2369
+ // Inclusive UTC day bounds (YYYY-MM-DD). Both absent ⇒ the whole ledger. Shared by every windowed read of a
2370
+ // daemon ledger (spend, savings): one window shape, so a screen that filters two ledgers at once filters them
2371
+ // with the same calendar.
2372
+ export const DayWindowQuerySchema = z.object({
1964
2373
  from: z.string().optional(),
1965
2374
  to: z.string().optional(),
1966
2375
  });
1967
- export type UsageRollupQuery = z.infer<typeof UsageRollupQuerySchema>;
2376
+ export type DayWindowQuery = z.infer<typeof DayWindowQuerySchema>;
1968
2377
  export const UsageRollupSchema = z.object({ rows: z.array(UsageRollupRowSchema) });
1969
2378
 
1970
2379
  // ---- usage: per-account token/cost totals ----
@@ -2013,6 +2422,45 @@ export const LogReadSchema = z.object({
2013
2422
  });
2014
2423
  export type LogRead = z.infer<typeof LogReadSchema>;
2015
2424
 
2425
+ // ---- memory: the agent's persistent memory notes (.intentic/claude/projects/<project>/memory) ----
2426
+ // The markdown files the agent curates across sessions — MEMORY.md (the index) plus one file per fact. They
2427
+ // live under the workspace's .intentic/claude control plane (symlinked from ~/.claude/projects), which the
2428
+ // generic /workspace file API deliberately refuses (session transcripts and provider state share that tree),
2429
+ // so these purpose-built routes are the only browser surface — scoped to the memory dirs and nothing else.
2430
+
2431
+ export const MemoryFileEntrySchema = z.object({
2432
+ // The project slug the memory belongs to (one dir per agent cwd, e.g. "-history-gits-root").
2433
+ project: z.string(),
2434
+ // Path relative to that project's memory dir, e.g. "MEMORY.md" or "team-conventions.md".
2435
+ name: z.string(),
2436
+ sizeBytes: z.number(),
2437
+ // Epoch ms mtime.
2438
+ modifiedAt: z.number(),
2439
+ });
2440
+ export type MemoryFileEntry = z.infer<typeof MemoryFileEntrySchema>;
2441
+ export const MemoryListSchema = z.object({ files: z.array(MemoryFileEntrySchema) });
2442
+
2443
+ // `project` + `name` ride the query (names may contain slashes, which don't fit a path segment).
2444
+ export const MemoryFileQuerySchema = z.object({
2445
+ project: z.string().min(1),
2446
+ name: z.string().min(1),
2447
+ });
2448
+ export const MemoryFileSchema = z.object({
2449
+ project: z.string(),
2450
+ name: z.string(),
2451
+ content: z.string(),
2452
+ sizeBytes: z.number(),
2453
+ modifiedAt: z.number(),
2454
+ });
2455
+ export type MemoryFile = z.infer<typeof MemoryFileSchema>;
2456
+
2457
+ // Memory notes are small by construction (one fact per file); the cap guards the route, not real usage.
2458
+ export const MemoryWriteSchema = z.object({
2459
+ project: z.string().min(1),
2460
+ name: z.string().min(1),
2461
+ content: z.string().max(1_048_576),
2462
+ });
2463
+
2016
2464
  // A tab's self-report of what it is looking at, keyed by its /events connection's clientId. Full replace,
2017
2465
  // not a merge — an absent field means "cleared", so a tab leaving a file drops the path with the same report.
2018
2466
  export const PresenceReportSchema = z.object({
@@ -2044,7 +2492,10 @@ export type PushSubscription = z.infer<typeof PushSubscriptionSchema>;
2044
2492
 
2045
2493
  // What the service worker renders. `url` is the in-app route the notification opens (the click handler
2046
2494
  // focuses an existing tab there rather than spawning a new one); `tag` collapses repeats — a second
2047
- // "waiting on you" for the same conversation REPLACES the first instead of stacking.
2495
+ // "waiting on you" for the same conversation REPLACES the first instead of stacking. Push payloads are
2496
+ // capped by the push services themselves (~4KB after encryption), which is why nothing here carries a
2497
+ // transcript or a diff — the notification is a pointer back into the workspace, not a delivery mechanism
2498
+ // for content.
2048
2499
  export const PushNotificationSchema = z.object({
2049
2500
  title: z.string().min(1),
2050
2501
  body: z.string(),
@@ -2061,5 +2512,12 @@ export type PushNotification = z.infer<typeof PushNotificationSchema>;
2061
2512
  // granted permission with no server-side row would notify nothing).
2062
2513
  export const PushConfigSchema = z.object({ publicKey: z.string(), subscribed: z.boolean() });
2063
2514
  export const PushEndpointSchema = z.object({ endpoint: z.string().url() });
2064
- // `endpoint` identifies which browser is asking, so `subscribed` can be answered for THIS one.
2515
+ // The optional `endpoint` says WHICH browser is asking; without it `subscribed` could only speak for the
2516
+ // sandbox as a whole, which is never the question the settings toggle needs answered.
2065
2517
  export const PushConfigQuerySchema = z.object({ endpoint: z.string().url().optional() });
2518
+
2519
+ // What a test send actually achieved. `{ ok: true }` would be a lie the one place it matters most: the button
2520
+ // exists to prove a chain the user cannot inspect, so "the daemon accepted the request" is not the answer to
2521
+ // the question being asked. A count separates "your OS swallowed it" from "nothing was sent at all".
2522
+ export const PushTestSchema = z.object({ delivered: z.number().int().nonnegative() });
2523
+ export type PushTest = z.infer<typeof PushTestSchema>;