@intentic/sandbox-contract 1.158.0 → 1.160.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 (122) hide show
  1. package/dist/agent-catalog.d.ts +10 -0
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +14 -1
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +74 -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 +248 -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/automations.contract.d.ts +7 -0
  14. package/dist/contracts/automations.contract.d.ts.map +1 -1
  15. package/dist/contracts/claude.contract.d.ts +26 -0
  16. package/dist/contracts/claude.contract.d.ts.map +1 -1
  17. package/dist/contracts/claude.contract.js +2 -1
  18. package/dist/contracts/claude.contract.js.map +1 -1
  19. package/dist/contracts/extensions.contract.d.ts +1 -0
  20. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  21. package/dist/contracts/git.contract.d.ts +13 -0
  22. package/dist/contracts/git.contract.d.ts.map +1 -1
  23. package/dist/contracts/git.contract.js +2 -1
  24. package/dist/contracts/git.contract.js.map +1 -1
  25. package/dist/contracts/grok.contract.d.ts +2 -0
  26. package/dist/contracts/grok.contract.d.ts.map +1 -1
  27. package/dist/contracts/kimi.contract.d.ts +26 -0
  28. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  29. package/dist/contracts/kimi.contract.js +2 -1
  30. package/dist/contracts/kimi.contract.js.map +1 -1
  31. package/dist/contracts/memory.contract.d.ts +34 -0
  32. package/dist/contracts/memory.contract.d.ts.map +1 -0
  33. package/dist/contracts/memory.contract.js +9 -0
  34. package/dist/contracts/memory.contract.js.map +1 -0
  35. package/dist/contracts/panels.contract.d.ts +1 -0
  36. package/dist/contracts/panels.contract.d.ts.map +1 -1
  37. package/dist/contracts/push.contract.d.ts +1 -1
  38. package/dist/contracts/push.contract.js +2 -2
  39. package/dist/contracts/push.contract.js.map +1 -1
  40. package/dist/contracts/sessions.contract.d.ts +2 -0
  41. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  42. package/dist/contracts/settings.contract.d.ts +32 -0
  43. package/dist/contracts/settings.contract.d.ts.map +1 -1
  44. package/dist/contracts/settings.contract.js +2 -1
  45. package/dist/contracts/settings.contract.js.map +1 -1
  46. package/dist/contracts/system.contract.d.ts +35 -6
  47. package/dist/contracts/system.contract.d.ts.map +1 -1
  48. package/dist/contracts/system.contract.js +2 -1
  49. package/dist/contracts/system.contract.js.map +1 -1
  50. package/dist/contracts/translator.contract.d.ts +13 -3
  51. package/dist/contracts/translator.contract.d.ts.map +1 -1
  52. package/dist/contracts/translator.contract.js +1 -1
  53. package/dist/contracts/translator.contract.js.map +1 -1
  54. package/dist/contracts/workspace.contract.d.ts +43 -5
  55. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  56. package/dist/contracts/workspace.contract.js +3 -1
  57. package/dist/contracts/workspace.contract.js.map +1 -1
  58. package/dist/events.d.ts +176 -2
  59. package/dist/events.d.ts.map +1 -1
  60. package/dist/events.js +16 -5
  61. package/dist/events.js.map +1 -1
  62. package/dist/hostnames.d.ts +2 -0
  63. package/dist/hostnames.d.ts.map +1 -1
  64. package/dist/hostnames.js +27 -1
  65. package/dist/hostnames.js.map +1 -1
  66. package/dist/index.d.ts +566 -19
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +11 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/model-order.d.ts +2 -0
  71. package/dist/model-order.d.ts.map +1 -1
  72. package/dist/model-order.js +3 -0
  73. package/dist/model-order.js.map +1 -1
  74. package/dist/path-refs.d.ts +5 -0
  75. package/dist/path-refs.d.ts.map +1 -0
  76. package/dist/path-refs.js +21 -0
  77. package/dist/path-refs.js.map +1 -0
  78. package/dist/quick-model.d.ts +13 -0
  79. package/dist/quick-model.d.ts.map +1 -0
  80. package/dist/quick-model.js +33 -0
  81. package/dist/quick-model.js.map +1 -0
  82. package/dist/routes.d.ts +8 -0
  83. package/dist/routes.d.ts.map +1 -0
  84. package/dist/routes.js +39 -0
  85. package/dist/routes.js.map +1 -0
  86. package/dist/schemas.d.ts +351 -10
  87. package/dist/schemas.d.ts.map +1 -1
  88. package/dist/schemas.js +143 -13
  89. package/dist/schemas.js.map +1 -1
  90. package/dist/title.d.ts +6 -0
  91. package/dist/title.d.ts.map +1 -0
  92. package/dist/title.js +149 -0
  93. package/dist/title.js.map +1 -0
  94. package/package.json +2 -2
  95. package/src/agent-catalog.ts +51 -1
  96. package/src/contracts/agent.contract.ts +14 -1
  97. package/src/contracts/agents.contract.ts +25 -1
  98. package/src/contracts/claude.contract.ts +4 -1
  99. package/src/contracts/git.contract.ts +6 -0
  100. package/src/contracts/kimi.contract.ts +13 -2
  101. package/src/contracts/memory.contract.ts +13 -0
  102. package/src/contracts/push.contract.ts +2 -2
  103. package/src/contracts/settings.contract.ts +5 -1
  104. package/src/contracts/system.contract.ts +4 -0
  105. package/src/contracts/translator.contract.ts +3 -2
  106. package/src/contracts/workspace.contract.ts +12 -0
  107. package/src/events.ts +89 -11
  108. package/src/hostnames.test.ts +22 -0
  109. package/src/hostnames.ts +40 -1
  110. package/src/index.ts +20 -0
  111. package/src/model-order.test.ts +73 -1
  112. package/src/model-order.ts +31 -0
  113. package/src/path-refs.test.ts +68 -0
  114. package/src/path-refs.ts +59 -0
  115. package/src/quick-model.test.ts +88 -0
  116. package/src/quick-model.ts +103 -0
  117. package/src/routes.test.ts +83 -0
  118. package/src/routes.ts +84 -0
  119. package/src/schemas.test.ts +23 -1
  120. package/src/schemas.ts +461 -38
  121. package/src/title.test.ts +130 -0
  122. package/src/title.ts +254 -0
package/src/schemas.ts CHANGED
@@ -54,6 +54,24 @@ export type EditorContext = z.infer<typeof EditorContextSchema>;
54
54
  // and filesystem paths — the regex is the injection guard. Shared by the turn input and the attach input.
55
55
  const ConversationIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
56
56
 
57
+ // Where a conversation came from when nobody typed it into the browser: an automation wake carrying a message
58
+ // from OUTSIDE the sandbox (a Discord mention, a web-chat visitor, a webhook). Such a wake runs as an ordinary
59
+ // isolated conversation — registry entry, worktree, chat tab, land flow — and this is the only thing that
60
+ // distinguishes it on the surface: the card's provenance line and the reason its first prompt is not the
61
+ // user's. Set daemon-side by the dispatcher that received the message; the browser never sends one.
62
+ export const AgentOriginSchema = z.object({
63
+ // The automation whose configured prompt opened the conversation.
64
+ automationId: z.string(),
65
+ // The listener provider that received the message ("discord", "webchat", …) or "webhook" for an event
66
+ // trigger. An open string for the same reason Trigger.provider is: sources are extension-declared.
67
+ provider: z.string(),
68
+ // The external thread it arrived on — a Discord channel id, a widget conversation id. Absent for webhooks.
69
+ channelId: z.string().optional(),
70
+ // Who sent it, as the source names them.
71
+ author: z.string().optional(),
72
+ });
73
+ export type AgentOrigin = z.infer<typeof AgentOriginSchema>;
74
+
57
75
  // How tool calls are gated — the Claude Agent SDK's PermissionMode, narrowed to the four the composer offers
58
76
  // (the SDK also has 'dontAsk'/'auto', which have no UI here). The user picks one per turn AND the agent can
59
77
  // move itself between them mid-turn, so this is both a turn input and the payload of the `mode` frame.
@@ -89,6 +107,10 @@ export const AgentTurnSchema = z
89
107
  // When true, the turn runs in the conversation's isolated git worktree (created lazily on first use)
90
108
  // instead of the shared /work tree — the parallel-agents mode. Requires conversationId.
91
109
  isolated: z.boolean().optional(),
110
+ // Set ONLY by the daemon's own automation dispatchers: this turn opens a conversation on behalf of an
111
+ // outside message rather than a user. Recorded on the registry entry so the fleet can say where the
112
+ // agent came from. Requires conversationId — there is nothing to record it on otherwise.
113
+ origin: AgentOriginSchema.optional(),
92
114
  // The client-held transcript of a conversation that just switched provider/account: seeds the FIRST
93
115
  // turn of the replacement session. The daemon folds it into the prompt as one role-attributed context
94
116
  // preamble for every runtime. Mutually exclusive with sessionId — a resumed session has its context.
@@ -114,6 +136,9 @@ export const AgentTurnSchema = z
114
136
  })
115
137
  .refine((turn) => turn.isolated !== true || turn.conversationId !== undefined, {
116
138
  message: "isolated requires conversationId",
139
+ })
140
+ .refine((turn) => turn.origin === undefined || turn.conversationId !== undefined, {
141
+ message: "origin requires conversationId",
117
142
  });
118
143
  export type AgentTurn = z.infer<typeof AgentTurnSchema>;
119
144
 
@@ -137,9 +162,18 @@ export type AttachTurn = z.infer<typeof AttachTurnSchema>;
137
162
  // conversationId. Isolated ones own a git worktree (branch agent/<id> in every workspace repo); the fleet
138
163
  // surface shows all of them with live status/activity/cost so the user can drive N agents in parallel.
139
164
 
140
- // idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); landed /
141
- // conflict are outcomes of the land flow; error is a terminal turn failure surfaced on the card.
142
- 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"]);
143
177
  export type AgentStatus = z.infer<typeof AgentStatusSchema>;
144
178
  // The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo.
145
179
  export const AgentActivitySchema = z.object({
@@ -165,10 +199,24 @@ export const AgentSummarySchema = z.object({
165
199
  status: AgentStatusSchema,
166
200
  provider: AgentProviderSchema,
167
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).
168
206
  model: z.string().optional(),
207
+ effort: z.string().optional(),
208
+ thinking: z.boolean().optional(),
169
209
  account: z.string().optional(),
170
210
  // The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
171
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(),
217
+ // Present when the conversation was opened by an outside message rather than by the user (see
218
+ // AgentOriginSchema) — the card's provenance line. Absent ⇒ the user started it.
219
+ origin: AgentOriginSchema.optional(),
172
220
  // The ROOT repo's short base sha — the checkout moment's display identity. Per-repo bases stay
173
221
  // daemon-internal (agents.diff already reports against them).
174
222
  base: z.string().optional(),
@@ -200,7 +248,12 @@ export const AgentSummarySchema = z.object({
200
248
  archivedAt: z.number().optional(),
201
249
  });
202
250
  export type AgentSummary = z.infer<typeof AgentSummarySchema>;
203
- export const AgentsListSchema = z.object({ agents: z.array(AgentSummarySchema) });
251
+ // `rev` is the registry revision this roster was read at — a counter the daemon bumps on every registry change.
252
+ // It is what makes the browser's optimistic writes safe: the fleet is published as full snapshots (last frame
253
+ // wins), so without an ordering stamp a roster READ before a mutation but delivered after it silently puts the
254
+ // mutated agents back. The browser drops any roster older than the newest it has applied, and holds its own
255
+ // pending change until a roster at or past the revision that applied it arrives. See useAgents.ts.
256
+ export const AgentsListSchema = z.object({ agents: z.array(AgentSummarySchema), rev: z.number() });
204
257
  export const AgentIdSchema = z.object({ id: z.string().min(1) });
205
258
  // archive's input: the agents to take off the board. Absent `ids` ⇒ every finished agent that is archivable
206
259
  // right now (the lane header's "Clear"); unarchive always names its ids (a restore, or a bulk archive's undo).
@@ -211,19 +264,92 @@ export const AgentIdsSchema = z.object({ ids: z.array(z.string().min(1)).min(1).
211
264
  // the slower response resurrect what the faster one just filed away — a delta composes where a snapshot races.
212
265
  // Whole summaries rather than ids because the receiving side has to SHOW them (the archive list, and the agent
213
266
  // detail page addressed by id); the ids "Undo" needs come off them for free.
214
- export const AgentsMovedSchema = z.object({ moved: z.array(AgentSummarySchema) });
267
+ // The agents an archive/unarchive actually moved, plus the registry revision that applied the move — the
268
+ // browser holds its optimistic add/remove of exactly these ids until it sees a roster at or past `rev`.
269
+ export const AgentsMovedSchema = z.object({ moved: z.array(AgentSummarySchema), rev: z.number() });
215
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>;
216
297
  // rename's input: the user-chosen display title (bounded like sanitizeTitle's cap).
217
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() });
218
303
  export const AgentFileDiffQuerySchema = z.object({ id: z.string().min(1), repo: z.string().min(1), path: z.string().min(1) });
219
- // land's outcome: per-repo conflicts (dirty-main overlaps or merge conflicts); landed only when every repo
220
- // 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.
221
331
  export const LandResultSchema = z.object({
222
332
  landed: z.boolean(),
223
- 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(),
224
338
  });
225
339
  export type LandResult = z.infer<typeof LandResultSchema>;
226
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
+
227
353
  // ---- routed-provider subscriptions ----
228
354
 
229
355
  // The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
@@ -234,9 +360,19 @@ export type LandResult = z.infer<typeof LandResultSchema>;
234
360
  export const KeyedProviderSchema = z.enum(["codex", "grok", "gemini"]);
235
361
  export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
236
362
 
237
- // Which routed-provider subscriptions are connected in the translator (per provider). Drives the
238
- // "connected / connect subscription" state in Sandbox Agent.
239
- 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
+ });
240
376
  export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
241
377
 
242
378
  // The side-channel body that un-parks a turn waiting on the user. Every interactive card — plan approval,
@@ -293,6 +429,12 @@ export const SteerSchema = z
293
429
  // True cancel for the conversation's in-flight turn — aborts the agent daemon-side, unlike closing the
294
430
  // /agent fetch (which sends no cancel frame).
295
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() });
296
438
 
297
439
  // ---- claude subscription usage ----
298
440
  // The GATE signal: whether the provider is letting turns through right now, and — when it is refusing — which
@@ -346,6 +488,14 @@ export type AccountUsage = z.infer<typeof AccountUsageSchema>;
346
488
  export const OauthAccountSchema = z.object({
347
489
  id: z.string(),
348
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(),
349
499
  scope: z.string().optional(),
350
500
  connectedAt: z.number(), // epoch ms
351
501
  // Set only when the account's stored credential can no longer be refreshed (revoked/expired refresh token)
@@ -362,6 +512,10 @@ export type OauthAccount = z.infer<typeof OauthAccountSchema>;
362
512
  export const OauthAccountListSchema = z.object({ accounts: z.array(OauthAccountSchema) });
363
513
  // Address one account of a provider (disconnect, and the turn's `account`).
364
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) });
365
519
  // The completing calls carry the user-chosen label (blank ⇒ the daemon derives one from the sign-in identity
366
520
  // or a provider default).
367
521
  export const OauthExchangeSchema = z.object({
@@ -429,10 +583,29 @@ export const ModelsSchema = z.object({ models: z.array(ModelSchema), default: z.
429
583
  // ---- sessions ----
430
584
 
431
585
  export const SessionIdParamSchema = z.object({ id: z.string() });
432
- 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
+ });
433
595
  export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySchema) });
434
596
 
435
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
+
436
609
  // Small user-owned config the /settings routes edit and streamAgent reads — all opt-in booleans the owner
437
610
  // toggles in the UI (so each can be A/B benchmarked):
438
611
  // stableSystemPrompt — keeps the system prompt byte-stable across turns (the delegation note rides the user
@@ -445,6 +618,9 @@ export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySch
445
618
  // guard + fewer output tokens); off ⇒ the native file tools.
446
619
  // terseOutput — appends a concise-response steer to the end of the system prompt (a stable suffix, so it
447
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.
448
624
  // iqSearch — loads the image-baked iq Claude Code plugin (skill + SessionStart nudge) so the agent
449
625
  // prefers the iq CLI over grep/find/Glob; off ⇒ plugin not loaded, native search tools
450
626
  // only. Opt-in (default off); the browser Search box uses iq regardless.
@@ -455,7 +631,7 @@ export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySch
455
631
  // (recorded raw as `heldOut`), so the savings report compares a real cleaned-vs-raw
456
632
  // population instead of an estimate. 0 = no holdout (default).
457
633
  // filterBackend — which cleaner runs the compression: "native" (agent-output-filter, default) or "rtk"
458
- // (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
459
635
  // A/B backend switch, so native and rtk can be benchmarked head-to-head.
460
636
  // The booleans default off, skills defaults [] (no skill loaded), outputCleaners defaults "off" (cleaning off),
461
637
  // outputHoldout 0, filterBackend "native" — a fresh sandbox starts with cleaning and iq off until the owner enables them.
@@ -472,24 +648,87 @@ export const SandboxSettingsSchema = z.object({
472
648
  skills: z.array(z.string()).default([]),
473
649
  hashlineEdits: z.boolean().default(false),
474
650
  terseOutput: z.boolean().default(false),
651
+ /* WHICH SYSTEM PROMPT THE AGENT RUNS ON — the base, before anything this turn composes.
652
+ *
653
+ * intentic — Intentic's own prompt, tuned for this harness (intentic-prompt.ts). The default.
654
+ * claude — Claude Code's preset, as shipped in the CLI this sandbox runs. Not a copy stored here, so
655
+ * picking it tracks whatever the installed CLI's prompt is rather than freezing at a snapshot.
656
+ * custom — `systemPrompt` below, and nothing else at all.
657
+ *
658
+ * The first two are peers: both get the harness's own guidance appended (the AskUserQuestion/plan blocks
659
+ * the chat's cards need, the checklist guidance behind the todo panel, the browser-tool guidance), plus the
660
+ * delegation note and the terse steer. `custom` is the one that does not, by the owner's explicit choice —
661
+ * see the field below. */
662
+ systemPromptMode: SystemPromptModeSchema.default("intentic"),
663
+ /* The owner's own prompt, used only when `systemPromptMode` is "custom". Then it is the ENTIRE system
664
+ * prompt: both built-in bases are gone and so is everything the daemon would otherwise append — the widget
665
+ * guidance the chat's cards are driven by, and the terse-output steer (whose toggle goes inert). That is
666
+ * the price of total control, and the UI states it at the moment of the edit rather than letting the
667
+ * widgets go quietly dark. Only the cross-provider delegation note survives, because it has a home outside
668
+ * the system prompt already (the user-message preamble stableSystemPrompt puts it in).
669
+ *
670
+ * Cap is roomy — the bases it stands in for are ~6.8k characters — but finite, because every turn pays it. */
671
+ systemPrompt: z.string().max(20000).default(""),
475
672
  iqSearch: z.boolean().default(false),
476
673
  outputCleaners: z.string().default("off"),
477
674
  outputHoldout: z.number().min(0).max(1).default(0),
478
675
  filterBackend: z.enum(["native", "rtk"]).default("native"),
676
+ /* The model behind the one-click helpers that are not a conversation — today the commit box's autofill.
677
+ * `${provider}:${modelId}`, or EMPTY for Auto, which is the default and the interesting case: Auto is
678
+ * resolved from whatever accounts are connected at the moment it is read (resolveQuickModel), so it can
679
+ * never name a provider this sandbox has no credential for and it improves by itself when one is added.
680
+ * Storing a resolved id here instead would go stale exactly like a pinned model does. */
681
+ quickModel: z.string().default(""),
479
682
  // How long a finished agent stays on the board before it is archived automatically (days; 0 ⇒ never).
480
683
  // Unlike every other flag here this one defaults ON, because the lane it governs is the board's only
481
684
  // terminal state: without a sweep the Finished lane grows for the life of the sandbox, and each card it
482
685
  // holds is a live worktree checkout, not just a row.
483
686
  agentRetentionDays: z.number().min(0).max(365).default(3),
687
+ /* Land a clean turn's delta into the main tree automatically at completion — the Claude Code review model,
688
+ * and the historical behaviour, so it defaults ON (flipping the default would silently change every
689
+ * existing sandbox). OFF holds finished work on the agent's branch instead: the card reads "Ready to
690
+ * land" and the user lands it deliberately, from the review panel or the card. Sandbox-wide because
691
+ * automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
692
+ * browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
693
+ autoLand: z.boolean().default(true),
694
+ // When a turn dies on the Claude subscription's usage limit, re-run it automatically once the limit
695
+ // window resets (a minute after, so a skewed clock can't retry into the same closed window). Off by
696
+ // default: an unattended retry spends the fresh window without the user in the room, so the daemon
697
+ // records every limit-hit either way and the chat OFFERS the toggle at the moment it would have helped —
698
+ // enabling it then still resumes the turn that just bounced.
699
+ autoResumeOnLimit: z.boolean().default(false),
484
700
  });
485
701
  export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
486
702
 
703
+ // One of the two built-in bases, as text: Intentic's own prompt, or Claude Code's preset read out of the CLI
704
+ // this sandbox runs (preset-prompt.ts captures it rather than storing a transcription). What the settings page
705
+ // shows behind "View" and drops into the editor behind "Edit a copy".
706
+ //
707
+ // `version` is the CLI build a captured preset came from, so the UI can say WHICH text the user is looking at:
708
+ // a custom prompt forked from an older build is a snapshot, and the version is the only honest way to tell.
709
+ // Empty for Intentic's prompt, which ships with the app and has no version of its own to report.
710
+ export const BuiltinPromptTextSchema = z.object({ text: z.string(), version: z.string() });
711
+ export type BuiltinPromptText = z.infer<typeof BuiltinPromptTextSchema>;
712
+
487
713
  // ---- output-cleaner savings report (rtk-`gain`-style) ----
488
- // Aggregated from historyRoot/logs/filter-stats.jsonl (one row per agent Bash command). `perCleaner` attributes
489
- // which cleaner ids fired across commands; `holdout` is the measured control (commands the holdout bypassed) vs
490
- // the cleaned population a real saved-% rather than an estimate; `gaps` are high-volume commands that matched
491
- // no cleaner (the next handler to write). Empty/zeroed when no commands have run yet.
714
+ // Whichever cleaner is ACTUALLY compressing output owns the numbers, so the report is read from that backend's
715
+ // own ledger: "native" aggregates historyRoot/logs/filter-stats.jsonl (one row per agent Bash command, written
716
+ // by agent-output-filter), "rtk" reads rtk's own gain ledger. Reading one ledger regardless of backend is how
717
+ // this card went stale: under rtk the native filter is switched off, nothing appends, and the last numbers
718
+ // written — a test run's, as it happened — sat on the card looking live.
719
+ //
720
+ // `perCleaner` attributes which cleaner ids fired across commands; `holdout` is the measured control (commands
721
+ // the holdout bypassed) vs the cleaned population — a real saved-% rather than an estimate; `gaps` are
722
+ // high-volume commands that matched no cleaner (the next handler to write). All three are native-only: rtk
723
+ // reports totals, not per-command attribution, so they arrive empty under that backend. Empty/zeroed when no
724
+ // commands have run yet.
492
725
  export const CleanerSavingsSchema = z.object({
726
+ // Which backend's ledger these numbers came from — shown on the card, because a number without its source
727
+ // cannot be told apart from a stale one. Defaulted for the same daemon-older-than-browser seam as settings.
728
+ source: z.enum(["native", "rtk"]).default("native"),
729
+ // When that ledger last recorded a command (epoch ms), so the card can show its age instead of implying
730
+ // freshness it doesn't have. Absent when the ledger has never been written or its age can't be read.
731
+ updatedAt: z.number().optional(),
493
732
  commands: z.number(),
494
733
  rawTokens: z.number(),
495
734
  emittedTokens: z.number(),
@@ -544,6 +783,24 @@ export const GitStatusSchema = z.object({ branch: z.string(), dirty: z.boolean()
544
783
  export const GitFilesSchema = z.object({ files: z.array(z.string()) });
545
784
  export const GitFileSchema = z.object({ path: z.string(), content: z.string() });
546
785
  export const CommitResultSchema = z.object({ committed: z.boolean() });
786
+
787
+ /* AI-drafted commit message. Workspace-wide, not per repo, because the commit box's target IS a set of repos
788
+ * sharing one message — so the draft has to see every one of their diffs to describe what the commit actually
789
+ * records. `repos` and `all` mirror the panel's own commit target exactly: `all` reads the WORKTREE (what
790
+ * "Commit all" would sweep), absent reads the INDEX (what a bare commit records). Getting that wrong would
791
+ * describe changes the commit isn't going to contain. */
792
+ export const CommitMessageDraftSchema = z.object({
793
+ repos: z.array(z.string().min(1)).min(1).max(50),
794
+ all: z.boolean().optional(),
795
+ });
796
+ // The draft plus WHICH model wrote it, so the surface can name it rather than claiming an anonymous "AI" —
797
+ // that name is also the only place the resolved quick model is visible before anyone opens settings.
798
+ export const CommitMessageSchema = z.object({
799
+ message: z.string(),
800
+ provider: z.string(),
801
+ model: z.string(),
802
+ });
803
+ export type CommitMessageDraft = z.infer<typeof CommitMessageSchema>;
547
804
  // One change to a file — an uncommitted working-tree change (status vs HEAD, untracked included), an agent
548
805
  // worktree's delta vs its base, or a file in a commit. `additions`/`deletions` are the numstat line counts,
549
806
  // undefined for a binary file (git reports "-"/"-") or an untracked file (no HEAD blob to diff against).
@@ -623,6 +880,11 @@ export const RepoChangesSchema = z.object({
623
880
  // additions/deletions describe the diff it is listed under, never a conflation of the two.
624
881
  staged: z.array(GitChangeSchema),
625
882
  unstaged: z.array(GitChangeSchema),
883
+ // How many changes were CUT from the two sides above (conflicts are never cut). A cloned monorepo or a
884
+ // mass delete carries six-figure change lists — a payload no panel can render and no browser should hold —
885
+ // so past the daemon's per-repo budget the lists arrive truncated and this carries the dropped count, which
886
+ // the panel adds to its badges and states under the group. Absent ⇒ the lists are complete.
887
+ truncated: z.number().optional(),
626
888
  // Where this repo stands against its remote; `ahead`/`behind` are 0 with no remote or no upstream.
627
889
  remote: GitRemoteStateSchema.optional(),
628
890
  // WHICH AGENT PUT IT THERE: repo-relative path → the agent ids that landed it, newest land first. Keyed by
@@ -630,7 +892,7 @@ export const RepoChangesSchema = z.object({
630
892
  // edited again) and its origin is the same fact for both. Only agents can appear here — a main-tree turn,
631
893
  // a terminal edit and your own typing never pass through land, so they are simply absent (see
632
894
  // agents/origins.ts), which is why the panel badges an agent and says nothing at all for anyone else.
633
- // Ids, not titles: every client already mirrors the fleet registry and can resolve one to the other.
895
+ // Ids, not titles: the identity for every id named here rides the response once, in `originAgents`.
634
896
  origins: z.record(z.string(), z.array(z.string())).optional(),
635
897
  // Why the repo could not be scanned at all, condensed to git's own one-line reason ("fatal: bad object HEAD").
636
898
  // A repo left torn by a canceled or failed upload used to be dropped from the response entirely, so it just
@@ -638,9 +900,31 @@ export const RepoChangesSchema = z.object({
638
900
  error: z.string().optional(),
639
901
  });
640
902
  export type RepoChanges = z.infer<typeof RepoChangesSchema>;
903
+
904
+ // WHO AN ORIGIN ID IS — the display identity of one agent named in `origins`, carried BY THE RESPONSE rather
905
+ // than looked up in the client's fleet roster. The roster is the LIVE board and deliberately drops archived
906
+ // agents (AgentsRegistry.list), while a landing outlives the agent that made it: archiving a finished agent
907
+ // does not commit its lines, so the very common case — land, archive the card, review at leisure — is exactly
908
+ // the one a roster lookup cannot answer, and the panel fell back to "Agent 1a2b3c" with a generic icon for it.
909
+ // The daemon reads attribution and identity from the same registry in the same pass, so it is the one place
910
+ // they cannot disagree. Per response, not per repo: one agent commonly lands into several.
911
+ export const OriginAgentSchema = z.object({
912
+ // Absent for an entry that never got a title (a turn that failed before one was derived).
913
+ title: z.string().optional(),
914
+ provider: AgentProviderSchema,
915
+ });
916
+ export type OriginAgent = z.infer<typeof OriginAgentSchema>;
917
+
641
918
  // The aggregated review set across every repo (root + every discovered repo); a repo appears when it has changes,
642
919
  // when it is out of sync with its remote, or when it failed to scan.
643
- export const GitChangesSchema = z.object({ repos: z.array(RepoChangesSchema) });
920
+ export const GitChangesSchema = z.object({
921
+ repos: z.array(RepoChangesSchema),
922
+ // Keyed by agent id; covers every id any repo's `origins` names, and only those. Absent when nothing in
923
+ // the review is attributable. An id can still be missing from it — the retention sweep can retire an
924
+ // entry whose landed lines are somehow still uncommitted — and the panel keeps its id-shaped fallback for
925
+ // exactly that, rather than dropping the chip and re-attributing the file to the user.
926
+ originAgents: z.record(z.string(), OriginAgentSchema).optional(),
927
+ });
644
928
  export type GitChanges = z.infer<typeof GitChangesSchema>;
645
929
 
646
930
  // One file an agent touched, plus whether that change is ALREADY in the main tree. The review lists the
@@ -661,7 +945,15 @@ export const AgentRepoChangesSchema = z.object({
661
945
  changes: z.array(AgentChangeSchema),
662
946
  });
663
947
  export type AgentRepoChanges = z.infer<typeof AgentRepoChangesSchema>;
664
- export const AgentChangesSchema = z.object({ repos: z.array(AgentRepoChangesSchema) });
948
+ /* The review, plus WHY the last land refused — because a conflict is discovered by the daemon (a clean turn
949
+ * auto-lands the moment it finishes) and acted on in the browser, possibly hours later, on a surface the user
950
+ * reaches by clicking the card's "Resolve conflict". Carrying the report only in the land RESPONSE meant the
951
+ * one path that opens the review already knowing there is a conflict was the one path that could not show it:
952
+ * the panel opened with an empty report, no explanation, and no merge affordance — a dead end at the exact
953
+ * moment the UI had promised something to resolve. It rides the review because that is the surface that
954
+ * resolves it, and it refreshes with it: every land invalidates this query, so the report is never staler
955
+ * than the last attempt. */
956
+ export const AgentChangesSchema = z.object({ repos: z.array(AgentRepoChangesSchema), conflicts: z.array(LandConflictSchema).optional() });
665
957
  export type AgentChanges = z.infer<typeof AgentChangesSchema>;
666
958
 
667
959
  // ---- git history graph (the "Git Graph" view over a repo's real commits) ----
@@ -767,11 +1059,11 @@ export const WorkspaceTreeEntrySchema = z.object({
767
1059
  path: z.string(),
768
1060
  type: z.enum(["file", "dir"]),
769
1061
  size: z.number().optional(),
770
- // Set on a dir whose child list was cut short by the entry cap — some of its items aren't in `children`.
771
- truncated: z.boolean().optional(),
772
1062
  // Ignored-by-tooling (node_modules, .git, .gitignore'd paths, browser profiles): the client grays the row.
773
- // An ignored DIR is listed without `children` — the client lazy-loads it via /workspace/children on expand.
774
1063
  ignored: z.boolean().optional(),
1064
+ // A DIR without `children` was listed but not descended into — because it's ignored, or because the walk's
1065
+ // breadth-first budget stopped above it. Either way the client lazy-loads it via /workspace/children on
1066
+ // expand, so "not loaded yet" and "empty directory" (`children: []`) stay distinguishable.
775
1067
  get children() {
776
1068
  return z.array(WorkspaceTreeEntrySchema).optional();
777
1069
  },
@@ -780,21 +1072,27 @@ export type WorkspaceTreeEntry = z.infer<typeof WorkspaceTreeEntrySchema>;
780
1072
  export const WorkspaceTreeSchema = z.object({
781
1073
  root: z.string(),
782
1074
  tree: z.array(WorkspaceTreeEntrySchema),
783
- // True when the root's own entries were cut by the entry cap (per-dir cuts are flagged on each dir entry).
784
- truncated: z.boolean(),
1075
+ // How many of the ROOT's own entries the budget cut (0 = complete); per-dir cuts are counted on each dir entry.
1076
+ hidden: z.number(),
785
1077
  });
786
1078
  export type WorkspaceTree = z.infer<typeof WorkspaceTreeSchema>;
787
- // Lazy-load one directory's children — for an ignored dir the tree walk didn't descend into. Every returned entry
788
- // is itself `ignored` (it lives under an ignored subtree); child dirs again carry no `children`, so they lazy-load
789
- // on their own expand. `truncated` ⇒ the dir's child list was cut by the entry cap.
1079
+ // Lazy-load one directory's children — for a dir the tree walk listed but didn't descend into. Child dirs again
1080
+ // carry no `children`, so they lazy-load on their own expand. `hidden` = how many entries the cap cut (0 = all
1081
+ // listed).
790
1082
  export const WorkspaceChildrenQuerySchema = z.object({ path: z.string().min(1) });
791
1083
  export const WorkspaceChildrenSchema = z.object({
792
1084
  entries: z.array(WorkspaceTreeEntrySchema),
793
- truncated: z.boolean(),
1085
+ hidden: z.number(),
794
1086
  });
795
1087
  export type WorkspaceChildren = z.infer<typeof WorkspaceChildrenSchema>;
796
1088
  export const WorkspaceFileQuerySchema = z.object({ path: z.string().min(1) });
797
1089
  export const WorkspaceFileSchema = z.object({ path: z.string(), content: z.string() });
1090
+ // Resolve a file reference an agent (or a compiler, or a terminal) NAMED to the workspace path it means. Prose
1091
+ // paths are routinely partial — a model that has been discussing `_apps/web/src` writes
1092
+ // `pages/workspace/Foo.vue` — so a clickable mention has to be matched as a path SUFFIX against the real tree,
1093
+ // not read as root-relative. `path` is absent when nothing in the workspace ends in that reference.
1094
+ export const WorkspaceResolveQuerySchema = z.object({ path: z.string().min(1).max(512) });
1095
+ export const WorkspaceResolveSchema = z.object({ path: z.string().optional() });
798
1096
  // Direct file management over the /work tree (delete / new folder / rename+move / copy). Byte writes + the
799
1097
  // editor's text save go through the plain POST /workspace/upload route (a body doesn't fit oRPC), not here.
800
1098
  export const WorkspaceDirSchema = z.object({ path: z.string().min(1) });
@@ -865,6 +1163,58 @@ export const WorkspaceSearchResultSchema = z.object({
865
1163
  });
866
1164
  export type WorkspaceSearchResult = z.infer<typeof WorkspaceSearchResultSchema>;
867
1165
 
1166
+ // ---- codebase health: one repository's structure and risk, in numbers ----
1167
+
1168
+ // The repo-level companion to the management panel and the git-history graph: what the same resident engine's
1169
+ // `hotspots` (churn × complexity) and `map` (PageRank over the import graph) verbs rank, as figures a panel can
1170
+ // plot instead of lines a terminal prints.
1171
+ //
1172
+ // Every field is a COUNT that can be recounted in the files themselves — commits, branch points, exported
1173
+ // symbols. Deliberately no composite "maintainability grade": those aren't comparable across projects and can't
1174
+ // be checked, and a repo-health surface that launders counts into a letter is worse than none.
1175
+ // How many hotspot files and key modules a report carries when the caller names no limit. A leaderboard, not an
1176
+ // inventory: past a screenful the ranking stops being the point, and the reader should be reading the files.
1177
+ export const HEALTH_LIMIT = 20;
1178
+ export const WorkspaceHealthQuerySchema = z.object({
1179
+ // "root" (the /work repo) or a nested repo's root-relative dir — the same {repo} ids the git routes take.
1180
+ repo: z.string().min(1),
1181
+ // Churn window (2d, 12h, 1w, 3m). Absent = all of history, which is what a hotspot ranking wants by default.
1182
+ since: z.string().max(16).optional(),
1183
+ limit: z.coerce.number().int().positive().max(200).optional(),
1184
+ });
1185
+ // One file that is BOTH churning and tangled. `score` is the product the ranking sorts by — carried explicitly
1186
+ // so the panel plots the number it ranks by rather than recomputing it.
1187
+ export const WorkspaceHotspotSchema = z.object({
1188
+ path: z.string(),
1189
+ commits: z.number(),
1190
+ adds: z.number(),
1191
+ dels: z.number(),
1192
+ complexity: z.number(),
1193
+ score: z.number(),
1194
+ // Epoch ms of the latest commit touching the file, within the window.
1195
+ latestMs: z.number(),
1196
+ });
1197
+ export type WorkspaceHotspot = z.infer<typeof WorkspaceHotspotSchema>;
1198
+ // A file of the import graph's ranked skeleton — order IS the rank, so no rank number rides along.
1199
+ export const WorkspaceKeyModuleSchema = z.object({ path: z.string(), exports: z.number() });
1200
+ export type WorkspaceKeyModule = z.infer<typeof WorkspaceKeyModuleSchema>;
1201
+ export const WorkspaceHealthSchema = z.object({
1202
+ repo: z.string(),
1203
+ totals: z.object({
1204
+ files: z.number(),
1205
+ symbols: z.number(),
1206
+ // Summed branch points across the scoped files.
1207
+ complexity: z.number(),
1208
+ // How many files qualify as hotspots at all — the lists below are capped, this is not.
1209
+ hotspots: z.number(),
1210
+ }),
1211
+ hotspots: z.array(WorkspaceHotspotSchema),
1212
+ modules: z.array(WorkspaceKeyModuleSchema),
1213
+ // Same index-freshness signal the search route reports: a panel drawn off a half-built index says so.
1214
+ freshness: WorkspaceSearchFreshnessSchema,
1215
+ });
1216
+ export type WorkspaceHealth = z.infer<typeof WorkspaceHealthSchema>;
1217
+
868
1218
  // ---- workspace setup (dependency readiness) ----
869
1219
 
870
1220
  // One project under /work and whether its dependencies are actually installed. A drop omits node_modules/.venv
@@ -940,12 +1290,13 @@ export const TemplatesListSchema = z.object({ templates: z.array(TemplateSummary
940
1290
  export type TemplatesList = z.infer<typeof TemplatesListSchema>;
941
1291
 
942
1292
  // One app instance currently in a monorepo, with its own preview dev server + live status (started/stopped
943
- // from the apps extension). `app` is the user-chosen instance name (the _apps/ dir); `template` is the
944
- // manifest key it was created from (api/web/landing). previewUrl is
1293
+ // from the apps extension). `app` is the user-chosen instance name (the _apps/ dir); `kind` is what sort of
1294
+ // app it is — the manifest key it was scaffolded from (api/web/landing), else the framework detected from its
1295
+ // dependencies (astro/next/…), and absent when it was discovered purely by its `dev` script. previewUrl is
945
1296
  // https://preview-<repo>--<app>-<sandboxId>.<zone> (absent on loopback — no zone or no connect token).
946
1297
  export const RepoAppSchema = z.object({
947
1298
  app: z.string(),
948
- template: z.string(),
1299
+ kind: z.string().optional(),
949
1300
  previewUrl: z.string().optional(),
950
1301
  running: z.boolean(),
951
1302
  healthy: z.boolean(),
@@ -1493,7 +1844,9 @@ export const WorkspaceEventSchema = z.object({
1493
1844
  agentId: z.string(),
1494
1845
  title: z.string().optional(),
1495
1846
  branch: z.string(),
1496
- outcome: z.enum(["landed", "conflict", "idle", "error"]),
1847
+ // `ready` is a clean turn whose delta was HELD on the branch (auto-land off) — for a chore, the moment
1848
+ // before the user's deliberate Land, which is exactly when a pre-land review wants to run.
1849
+ outcome: z.enum(["landed", "conflict", "ready", "idle", "error"]),
1497
1850
  repos: z.array(z.object({ repo: z.string(), from: z.string(), dir: z.string() })),
1498
1851
  });
1499
1852
  export type WorkspaceEvent = z.infer<typeof WorkspaceEventSchema>;
@@ -1546,6 +1899,10 @@ export const AutomationApprovalSchema = z.object({
1546
1899
  automationId: z.string(),
1547
1900
  // The event/listener payload the wake would have carried; absent for schedule triggers.
1548
1901
  payload: z.string().optional(),
1902
+ // The provenance + title the held wake would have opened its conversation with — snapshotted alongside the
1903
+ // payload so an approved external wake surfaces on the fleet exactly as an auto one would have.
1904
+ origin: AgentOriginSchema.optional(),
1905
+ title: z.string().optional(),
1549
1906
  createdAt: z.number(),
1550
1907
  });
1551
1908
  export type AutomationApproval = z.infer<typeof AutomationApprovalSchema>;
@@ -1636,13 +1993,15 @@ export const PanelSummarySchema = z.object({
1636
1993
  role: z.enum(["intent", "desired-state", "app"]).optional(),
1637
1994
  // Content facts: deploy.config.ts (the intent ledger's day-one marker), desired-state.json (present after
1638
1995
  // the first resolve), .intentic/ui/index.html (a sandboxed directory UI), pnpm-workspace.yaml +
1639
- // turbo.json (a pnpm+turbo monorepo), and vitest evidence (a root vitest.config.ts, or "vitest" in the
1640
- // root manifest / workspace catalog).
1996
+ // turbo.json (a pnpm+turbo monorepo), vitest evidence (a root vitest.config.ts, or "vitest" in the
1997
+ // root manifest / workspace catalog), and docs/user-stories (a directory of stories an agent can test
1998
+ // against the running app — the one fact here that says nothing about the repo's language).
1641
1999
  deployConfig: z.boolean(),
1642
2000
  desiredState: z.boolean(),
1643
2001
  directoryUi: z.boolean(),
1644
2002
  monorepo: z.boolean(),
1645
2003
  vitest: z.boolean(),
2004
+ userStories: z.boolean(),
1646
2005
  });
1647
2006
  export type PanelSummary = z.infer<typeof PanelSummarySchema>;
1648
2007
  export const PanelsListSchema = z.object({ panels: z.array(PanelSummarySchema) });
@@ -1696,7 +2055,8 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
1696
2055
  // is the /system/terminal WebSocket, not oRPC): `shell` = a web-* session the user opened (numbered pill),
1697
2056
  // `panel` = a panel-* dev-server session (labeled by its panel key, started via Start; running:false =
1698
2057
  // untracked, e.g. a finished one-shot job's lingering shell), `agent` = an agent-* session the Claude agent's
1699
- // Bash commands run in (live-watchable, AI-marked in the UI), `job` = a job-* session the daemon's terminal
2058
+ // Bash commands run in (live-watchable, AI-marked in the UI; running:false once every window is a finished
2059
+ // command's dead pane, which is what lets the panel sweep it), `job` = a job-* session the daemon's terminal
1700
2060
  // runner executes user-triggered flows in (capability adds, infra check), `process` = a managed background
1701
2061
  // process riding a panel session (an extension's declared processes, dockerd) — surfaced in the panel's
1702
2062
  // background-processes popover with read-only log views, never as a killable tab; running is the actual
@@ -1704,11 +2064,19 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
1704
2064
  // declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
1705
2065
  // `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
1706
2066
  // same charset gates a `tmux kill-session -t` shell-out.
2067
+ //
2068
+ // `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
2069
+ // while that pane still lives) are what let a finished session be READ rather than merely listed: the panel's
2070
+ // Recent-terminals popover orders by the one and reports the other ("exit 1 · 1h ago"), and the daemon's
2071
+ // retention sweep ages sessions out by the same clock. 0 is "tmux didn't say" — treated as unknown by both,
2072
+ // never as 1970.
1707
2073
  export const TerminalSessionSchema = z.object({
1708
2074
  name: z.string(),
1709
2075
  label: z.string().optional(),
1710
2076
  kind: z.enum(["shell", "panel", "agent", "job", "process"]),
1711
2077
  running: z.boolean(),
2078
+ activityAt: z.number(),
2079
+ exitCode: z.number().optional(),
1712
2080
  extensionId: z.string().optional(),
1713
2081
  processName: z.string().optional(),
1714
2082
  });
@@ -1787,6 +2155,12 @@ export const InfoSchema = z.object({
1787
2155
  updateAvailable: z.boolean().optional(),
1788
2156
  });
1789
2157
 
2158
+ // A daemon-minted session (system.session): the steady-state browser credential, exchanged for a verified
2159
+ // Google ID token so Google UI is a sign-in moment instead of an hourly renewal. `expiresAt` is epoch ms —
2160
+ // the browser renews ahead of it without parsing the token; `email` is who the daemon verified.
2161
+ export const DaemonSessionSchema = z.object({ token: z.string(), expiresAt: z.number(), email: z.string() });
2162
+ export type DaemonSession = z.infer<typeof DaemonSessionSchema>;
2163
+
1790
2164
  // Intentic-provided host SSH tunnel: minting it needs intentic's PLATFORM Cloudflare account, so the daemon
1791
2165
  // can't do it directly — it relays to the platform authenticated by the connect token (the announce pattern).
1792
2166
  // The panel embeds the returned connector token + hostname in its connect-host one-liner.
@@ -1967,6 +2341,45 @@ export const LogReadSchema = z.object({
1967
2341
  });
1968
2342
  export type LogRead = z.infer<typeof LogReadSchema>;
1969
2343
 
2344
+ // ---- memory: the agent's persistent memory notes (.intentic/claude/projects/<project>/memory) ----
2345
+ // The markdown files the agent curates across sessions — MEMORY.md (the index) plus one file per fact. They
2346
+ // live under the workspace's .intentic/claude control plane (symlinked from ~/.claude/projects), which the
2347
+ // generic /workspace file API deliberately refuses (session transcripts and provider state share that tree),
2348
+ // so these purpose-built routes are the only browser surface — scoped to the memory dirs and nothing else.
2349
+
2350
+ export const MemoryFileEntrySchema = z.object({
2351
+ // The project slug the memory belongs to (one dir per agent cwd, e.g. "-history-gits-root").
2352
+ project: z.string(),
2353
+ // Path relative to that project's memory dir, e.g. "MEMORY.md" or "team-conventions.md".
2354
+ name: z.string(),
2355
+ sizeBytes: z.number(),
2356
+ // Epoch ms mtime.
2357
+ modifiedAt: z.number(),
2358
+ });
2359
+ export type MemoryFileEntry = z.infer<typeof MemoryFileEntrySchema>;
2360
+ export const MemoryListSchema = z.object({ files: z.array(MemoryFileEntrySchema) });
2361
+
2362
+ // `project` + `name` ride the query (names may contain slashes, which don't fit a path segment).
2363
+ export const MemoryFileQuerySchema = z.object({
2364
+ project: z.string().min(1),
2365
+ name: z.string().min(1),
2366
+ });
2367
+ export const MemoryFileSchema = z.object({
2368
+ project: z.string(),
2369
+ name: z.string(),
2370
+ content: z.string(),
2371
+ sizeBytes: z.number(),
2372
+ modifiedAt: z.number(),
2373
+ });
2374
+ export type MemoryFile = z.infer<typeof MemoryFileSchema>;
2375
+
2376
+ // Memory notes are small by construction (one fact per file); the cap guards the route, not real usage.
2377
+ export const MemoryWriteSchema = z.object({
2378
+ project: z.string().min(1),
2379
+ name: z.string().min(1),
2380
+ content: z.string().max(1_048_576),
2381
+ });
2382
+
1970
2383
  // A tab's self-report of what it is looking at, keyed by its /events connection's clientId. Full replace,
1971
2384
  // not a merge — an absent field means "cleared", so a tab leaving a file drops the path with the same report.
1972
2385
  export const PresenceReportSchema = z.object({
@@ -1998,7 +2411,10 @@ export type PushSubscription = z.infer<typeof PushSubscriptionSchema>;
1998
2411
 
1999
2412
  // What the service worker renders. `url` is the in-app route the notification opens (the click handler
2000
2413
  // focuses an existing tab there rather than spawning a new one); `tag` collapses repeats — a second
2001
- // "waiting on you" for the same conversation REPLACES the first instead of stacking.
2414
+ // "waiting on you" for the same conversation REPLACES the first instead of stacking. Push payloads are
2415
+ // capped by the push services themselves (~4KB after encryption), which is why nothing here carries a
2416
+ // transcript or a diff — the notification is a pointer back into the workspace, not a delivery mechanism
2417
+ // for content.
2002
2418
  export const PushNotificationSchema = z.object({
2003
2419
  title: z.string().min(1),
2004
2420
  body: z.string(),
@@ -2015,5 +2431,12 @@ export type PushNotification = z.infer<typeof PushNotificationSchema>;
2015
2431
  // granted permission with no server-side row would notify nothing).
2016
2432
  export const PushConfigSchema = z.object({ publicKey: z.string(), subscribed: z.boolean() });
2017
2433
  export const PushEndpointSchema = z.object({ endpoint: z.string().url() });
2018
- // `endpoint` identifies which browser is asking, so `subscribed` can be answered for THIS one.
2434
+ // The optional `endpoint` says WHICH browser is asking; without it `subscribed` could only speak for the
2435
+ // sandbox as a whole, which is never the question the settings toggle needs answered.
2019
2436
  export const PushConfigQuerySchema = z.object({ endpoint: z.string().url().optional() });
2437
+
2438
+ // What a test send actually achieved. `{ ok: true }` would be a lie the one place it matters most: the button
2439
+ // exists to prove a chain the user cannot inspect, so "the daemon accepted the request" is not the answer to
2440
+ // the question being asked. A count separates "your OS swallowed it" from "nothing was sent at all".
2441
+ export const PushTestSchema = z.object({ delivered: z.number().int().nonnegative() });
2442
+ export type PushTest = z.infer<typeof PushTestSchema>;