@sapiom/harness 0.1.5 → 0.2.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 (245) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/cli/args.d.ts +16 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +80 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/bin.js +30 -66
  7. package/dist/cli/bin.js.map +1 -1
  8. package/dist/cli/consent.js +1 -1
  9. package/dist/cli/settings.d.ts.map +1 -1
  10. package/dist/cli/settings.js +33 -1
  11. package/dist/cli/settings.js.map +1 -1
  12. package/dist/core/adapters/adapter.d.ts +2 -2
  13. package/dist/core/adapters/claude-code.d.ts +60 -2
  14. package/dist/core/adapters/claude-code.d.ts.map +1 -1
  15. package/dist/core/adapters/claude-code.js +119 -32
  16. package/dist/core/adapters/claude-code.js.map +1 -1
  17. package/dist/core/adapters/codex.d.ts +21 -2
  18. package/dist/core/adapters/codex.d.ts.map +1 -1
  19. package/dist/core/adapters/codex.js +53 -2
  20. package/dist/core/adapters/codex.js.map +1 -1
  21. package/dist/core/adapters/detect.d.ts +9 -0
  22. package/dist/core/adapters/detect.d.ts.map +1 -1
  23. package/dist/core/adapters/detect.js +9 -0
  24. package/dist/core/adapters/detect.js.map +1 -1
  25. package/dist/core/api-key-provider.d.ts +79 -0
  26. package/dist/core/api-key-provider.d.ts.map +1 -0
  27. package/dist/core/api-key-provider.js +91 -0
  28. package/dist/core/api-key-provider.js.map +1 -0
  29. package/dist/core/canvas-body.d.ts +28 -28
  30. package/dist/core/canvas-body.d.ts.map +1 -1
  31. package/dist/core/canvas-body.js +117 -55
  32. package/dist/core/canvas-body.js.map +1 -1
  33. package/dist/core/canvas-derive.d.ts +43 -0
  34. package/dist/core/canvas-derive.d.ts.map +1 -0
  35. package/dist/core/canvas-derive.js +95 -0
  36. package/dist/core/canvas-derive.js.map +1 -0
  37. package/dist/core/canvas-enrichment.d.ts +52 -127
  38. package/dist/core/canvas-enrichment.d.ts.map +1 -1
  39. package/dist/core/canvas-enrichment.js +17 -178
  40. package/dist/core/canvas-enrichment.js.map +1 -1
  41. package/dist/core/canvas-graph.d.ts +40 -1
  42. package/dist/core/canvas-graph.d.ts.map +1 -1
  43. package/dist/core/canvas-graph.js +71 -9
  44. package/dist/core/canvas-graph.js.map +1 -1
  45. package/dist/core/canvas-interconnections.d.ts +26 -8
  46. package/dist/core/canvas-interconnections.d.ts.map +1 -1
  47. package/dist/core/canvas-interconnections.js +132 -24
  48. package/dist/core/canvas-interconnections.js.map +1 -1
  49. package/dist/core/canvas-manifest-check.d.ts.map +1 -1
  50. package/dist/core/canvas-manifest-check.js +15 -2
  51. package/dist/core/canvas-manifest-check.js.map +1 -1
  52. package/dist/core/canvas-render.d.ts +8 -15
  53. package/dist/core/canvas-render.d.ts.map +1 -1
  54. package/dist/core/canvas-render.js +29 -41
  55. package/dist/core/canvas-render.js.map +1 -1
  56. package/dist/core/canvas-run-state.d.ts +50 -5
  57. package/dist/core/canvas-run-state.d.ts.map +1 -1
  58. package/dist/core/canvas-run-state.js +176 -11
  59. package/dist/core/canvas-run-state.js.map +1 -1
  60. package/dist/core/canvas-svg.d.ts +9 -4
  61. package/dist/core/canvas-svg.d.ts.map +1 -1
  62. package/dist/core/canvas-svg.js +84 -13
  63. package/dist/core/canvas-svg.js.map +1 -1
  64. package/dist/core/canvas-template.d.ts +9 -10
  65. package/dist/core/canvas-template.d.ts.map +1 -1
  66. package/dist/core/canvas-template.js +57 -20
  67. package/dist/core/canvas-template.js.map +1 -1
  68. package/dist/core/canvas-watcher.d.ts +22 -1
  69. package/dist/core/canvas-watcher.d.ts.map +1 -1
  70. package/dist/core/canvas-watcher.js +111 -30
  71. package/dist/core/canvas-watcher.js.map +1 -1
  72. package/dist/core/collector/normalizer.d.ts +8 -0
  73. package/dist/core/collector/normalizer.d.ts.map +1 -1
  74. package/dist/core/collector/normalizer.js +8 -0
  75. package/dist/core/collector/normalizer.js.map +1 -1
  76. package/dist/core/collector/store.d.ts +75 -1
  77. package/dist/core/collector/store.d.ts.map +1 -1
  78. package/dist/core/collector/store.js +334 -2
  79. package/dist/core/collector/store.js.map +1 -1
  80. package/dist/core/definition-slug-resolver.d.ts +13 -0
  81. package/dist/core/definition-slug-resolver.d.ts.map +1 -1
  82. package/dist/core/definition-slug-resolver.js +29 -0
  83. package/dist/core/definition-slug-resolver.js.map +1 -1
  84. package/dist/core/errors.d.ts +10 -4
  85. package/dist/core/errors.d.ts.map +1 -1
  86. package/dist/core/errors.js +11 -5
  87. package/dist/core/errors.js.map +1 -1
  88. package/dist/core/example-seed.d.ts +8 -0
  89. package/dist/core/example-seed.d.ts.map +1 -1
  90. package/dist/core/example-seed.js +47 -10
  91. package/dist/core/example-seed.js.map +1 -1
  92. package/dist/core/inject/system-prompt.d.ts +8 -0
  93. package/dist/core/inject/system-prompt.d.ts.map +1 -1
  94. package/dist/core/inject/system-prompt.js +3 -1
  95. package/dist/core/inject/system-prompt.js.map +1 -1
  96. package/dist/core/macros.d.ts.map +1 -1
  97. package/dist/core/macros.js +5 -6
  98. package/dist/core/macros.js.map +1 -1
  99. package/dist/core/paths.d.ts +1 -0
  100. package/dist/core/paths.d.ts.map +1 -1
  101. package/dist/core/paths.js +1 -0
  102. package/dist/core/paths.js.map +1 -1
  103. package/dist/core/record-archive.d.ts +187 -0
  104. package/dist/core/record-archive.d.ts.map +1 -0
  105. package/dist/core/record-archive.js +465 -0
  106. package/dist/core/record-archive.js.map +1 -0
  107. package/dist/core/rehydration.d.ts +72 -0
  108. package/dist/core/rehydration.d.ts.map +1 -0
  109. package/dist/core/rehydration.js +72 -0
  110. package/dist/core/rehydration.js.map +1 -0
  111. package/dist/core/render-local-run.d.ts +82 -0
  112. package/dist/core/render-local-run.d.ts.map +1 -0
  113. package/dist/core/render-local-run.js +105 -0
  114. package/dist/core/render-local-run.js.map +1 -0
  115. package/dist/core/render-log-slice.d.ts +30 -0
  116. package/dist/core/render-log-slice.d.ts.map +1 -0
  117. package/dist/core/render-log-slice.js +49 -0
  118. package/dist/core/render-log-slice.js.map +1 -0
  119. package/dist/core/render-run-state.d.ts.map +1 -1
  120. package/dist/core/render-run-state.js +30 -49
  121. package/dist/core/render-run-state.js.map +1 -1
  122. package/dist/core/resume-brief.d.ts +130 -0
  123. package/dist/core/resume-brief.d.ts.map +1 -0
  124. package/dist/core/resume-brief.js +404 -0
  125. package/dist/core/resume-brief.js.map +1 -0
  126. package/dist/core/rolling-summary.d.ts +130 -0
  127. package/dist/core/rolling-summary.d.ts.map +1 -0
  128. package/dist/core/rolling-summary.js +227 -0
  129. package/dist/core/rolling-summary.js.map +1 -0
  130. package/dist/core/run-local-bootstrap.d.ts +93 -0
  131. package/dist/core/run-local-bootstrap.d.ts.map +1 -0
  132. package/dist/core/run-local-bootstrap.js +155 -0
  133. package/dist/core/run-local-bootstrap.js.map +1 -0
  134. package/dist/core/run-state.d.ts +16 -1
  135. package/dist/core/run-state.d.ts.map +1 -1
  136. package/dist/core/run-state.js +75 -25
  137. package/dist/core/run-state.js.map +1 -1
  138. package/dist/core/session-manager.d.ts +11 -3
  139. package/dist/core/session-manager.d.ts.map +1 -1
  140. package/dist/core/session-manager.js +56 -7
  141. package/dist/core/session-manager.js.map +1 -1
  142. package/dist/core/session-record.d.ts +165 -0
  143. package/dist/core/session-record.d.ts.map +1 -0
  144. package/dist/core/session-record.js +470 -0
  145. package/dist/core/session-record.js.map +1 -0
  146. package/dist/core/spawn-target.d.ts +12 -0
  147. package/dist/core/spawn-target.d.ts.map +1 -0
  148. package/dist/core/spawn-target.js +187 -0
  149. package/dist/core/spawn-target.js.map +1 -0
  150. package/dist/core/stub-feedback.d.ts +52 -0
  151. package/dist/core/stub-feedback.d.ts.map +1 -0
  152. package/dist/core/stub-feedback.js +33 -0
  153. package/dist/core/stub-feedback.js.map +1 -0
  154. package/dist/core/task-manager.d.ts.map +1 -1
  155. package/dist/core/task-manager.js +9 -1
  156. package/dist/core/task-manager.js.map +1 -1
  157. package/dist/core/template-catalog.d.ts +48 -0
  158. package/dist/core/template-catalog.d.ts.map +1 -0
  159. package/dist/core/template-catalog.js +338 -0
  160. package/dist/core/template-catalog.js.map +1 -0
  161. package/dist/core/workflow-registry.d.ts.map +1 -1
  162. package/dist/core/workflow-registry.js +2 -2
  163. package/dist/core/workflow-registry.js.map +1 -1
  164. package/dist/core/workspace-watcher.d.ts.map +1 -1
  165. package/dist/core/workspace-watcher.js +4 -3
  166. package/dist/core/workspace-watcher.js.map +1 -1
  167. package/dist/index.d.ts +12 -0
  168. package/dist/index.d.ts.map +1 -1
  169. package/dist/index.js +21 -0
  170. package/dist/index.js.map +1 -1
  171. package/dist/profiles/default.d.ts.map +1 -1
  172. package/dist/profiles/default.js +9 -14
  173. package/dist/profiles/default.js.map +1 -1
  174. package/dist/server/actions.d.ts +139 -0
  175. package/dist/server/actions.d.ts.map +1 -0
  176. package/dist/server/actions.js +430 -0
  177. package/dist/server/actions.js.map +1 -0
  178. package/dist/server/auth-routes.d.ts +92 -0
  179. package/dist/server/auth-routes.d.ts.map +1 -0
  180. package/dist/server/auth-routes.js +151 -0
  181. package/dist/server/auth-routes.js.map +1 -0
  182. package/dist/server/canvas.d.ts.map +1 -1
  183. package/dist/server/canvas.js +7 -7
  184. package/dist/server/canvas.js.map +1 -1
  185. package/dist/server/fs.d.ts.map +1 -1
  186. package/dist/server/fs.js +17 -1
  187. package/dist/server/fs.js.map +1 -1
  188. package/dist/server/index.d.ts +12 -3
  189. package/dist/server/index.d.ts.map +1 -1
  190. package/dist/server/index.js +0 -0
  191. package/dist/server/index.js.map +1 -1
  192. package/dist/server/ingest.d.ts +11 -0
  193. package/dist/server/ingest.d.ts.map +1 -1
  194. package/dist/server/ingest.js +1 -0
  195. package/dist/server/ingest.js.map +1 -1
  196. package/dist/server/macros.d.ts +4 -6
  197. package/dist/server/macros.d.ts.map +1 -1
  198. package/dist/server/macros.js.map +1 -1
  199. package/dist/server/rest.d.ts +18 -6
  200. package/dist/server/rest.d.ts.map +1 -1
  201. package/dist/server/rest.js +185 -50
  202. package/dist/server/rest.js.map +1 -1
  203. package/dist/server/runs.d.ts +16 -15
  204. package/dist/server/runs.d.ts.map +1 -1
  205. package/dist/server/runs.js +15 -80
  206. package/dist/server/runs.js.map +1 -1
  207. package/dist/server/static.d.ts +15 -1
  208. package/dist/server/static.d.ts.map +1 -1
  209. package/dist/server/static.js +54 -4
  210. package/dist/server/static.js.map +1 -1
  211. package/dist/server/templates.d.ts +24 -0
  212. package/dist/server/templates.d.ts.map +1 -0
  213. package/dist/server/templates.js +54 -0
  214. package/dist/server/templates.js.map +1 -0
  215. package/dist/shared/types.d.ts +558 -66
  216. package/dist/shared/types.d.ts.map +1 -1
  217. package/dist/shared/types.js +19 -11
  218. package/dist/shared/types.js.map +1 -1
  219. package/dist/web/assets/index-DC65cskq.js +446 -0
  220. package/dist/web/assets/index-xtD-3FFa.css +32 -0
  221. package/dist/web/canvas/sess-boot/index.html +305 -0
  222. package/dist/web/index.html +4 -9
  223. package/package.json +8 -4
  224. package/dist/core/canvas-enrich.d.ts +0 -115
  225. package/dist/core/canvas-enrich.d.ts.map +0 -1
  226. package/dist/core/canvas-enrich.js +0 -186
  227. package/dist/core/canvas-enrich.js.map +0 -1
  228. package/dist/core/run-spend.d.ts +0 -55
  229. package/dist/core/run-spend.d.ts.map +0 -1
  230. package/dist/core/run-spend.js +0 -109
  231. package/dist/core/run-spend.js.map +0 -1
  232. package/dist/core/run-transactions.d.ts +0 -60
  233. package/dist/core/run-transactions.d.ts.map +0 -1
  234. package/dist/core/run-transactions.js +0 -129
  235. package/dist/core/run-transactions.js.map +0 -1
  236. package/dist/profiles/canvas-guidelines.d.ts +0 -20
  237. package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
  238. package/dist/profiles/canvas-guidelines.js +0 -48
  239. package/dist/profiles/canvas-guidelines.js.map +0 -1
  240. package/dist/server/skills.d.ts +0 -51
  241. package/dist/server/skills.d.ts.map +0 -1
  242. package/dist/server/skills.js +0 -312
  243. package/dist/server/skills.js.map +0 -1
  244. package/dist/web/assets/index-DM84J83Y.js +0 -234
  245. package/dist/web/assets/index-DemtWW4L.css +0 -32
@@ -21,11 +21,27 @@ export declare const HARNESS_PATHS: {
21
21
  readonly settings: "~/.sapiom/harness/settings.json";
22
22
  /** Generated per-session agent config (claude settings/mcp-config files). */
23
23
  readonly generated: "~/.sapiom/harness/generated";
24
- /** The bundled example project, seeded lazily by POST /api/sample-project
25
- * (the welcome panel's "Run the sample project") stable so re-running
26
- * the sample reuses the same copy instead of scattering fresh ones. */
24
+ /**
25
+ * Archived session records one compacted `<harnessSessionId>.json` per
26
+ * conversation (core/record-archive.ts). Deliberately NOT under `generated`:
27
+ * that directory is deleted the moment a session's pty exits, and these have
28
+ * to outlive `events.ndjson`'s 30-day retention, not undercut it.
29
+ */
30
+ readonly records: "~/.sapiom/harness/records";
31
+ /** Where the bundled example project is seeded. Written ONLY by
32
+ * `scripts/seed-example.mjs` (demo prep) since the in-app sample action and
33
+ * its `POST /api/sample-project` route were removed — the running Studio
34
+ * neither seeds nor reads this path, so a directory here is leftover output.
35
+ * Kept stable so re-seeding reuses one copy instead of scattering fresh ones. */
27
36
  readonly sampleProject: "~/.sapiom/harness/sample-project";
28
37
  };
38
+ /**
39
+ * The file that makes a directory an agent project. Canonical home: three
40
+ * copies of this literal used to live in core/workspace-watcher.ts,
41
+ * core/workflow-registry.ts and (now) server/fs.ts, the first of which carried
42
+ * a "kept in sync with" comment admitting the duplication. Import it.
43
+ */
44
+ export declare const AGENT_PROJECT_MARKER = "sapiom.json";
29
45
  /**
30
46
  * Canvas convention: agents write static HTML here, relative to the session
31
47
  * cwd. The server watches this directory and serves it at
@@ -42,14 +58,6 @@ export declare const CANVAS_INDEX = ".sapiom/canvas/index.html";
42
58
  * pipeline.
43
59
  */
44
60
  export declare const CANVAS_RENDERS_DIR = ".sapiom/canvas/renders";
45
- /**
46
- * Per-workflow enrichment cache lives here (one `<slug>.json` per workflow,
47
- * same slug scheme as CANVAS_RENDERS_DIR), relative to the session cwd:
48
- * `{ graph, enrichment, sourceFingerprint, enrichedAt }`. A stale
49
- * sourceFingerprint keeps the enrichment displayed (with a "stale" chip)
50
- * until a re-run replaces it — see core/canvas-enrich.ts.
51
- */
52
- export declare const CANVAS_CACHE_DIR = ".sapiom/canvas/cache";
53
61
  /**
54
62
  * Composer image attachments (upload / paste / drag-drop) land here, relative
55
63
  * to the session cwd, before their path is relayed into the agent's prompt.
@@ -129,6 +137,16 @@ export interface HarnessSession {
129
137
  * via `PATCH /api/sessions/:id/workflow`; mirrored into
130
138
  * HARNESS_CONTEXT_FILE in the session's cwd so the agent can read it. */
131
139
  boundWorkflowPath: string | null;
140
+ /**
141
+ * The prior session this one was seeded from (portable continue — see
142
+ * core/rehydration.ts), when a brief was ACTUALLY produced and delivered.
143
+ * Null/absent otherwise, including when the client asked to rehydrate from
144
+ * an id our event log holds nothing for: this field is the record of what
145
+ * happened, not of what was requested, so the UI can never present an
146
+ * empty-handed fresh session as a continuation. Absent on sessions
147
+ * persisted by builds from before this existed.
148
+ */
149
+ rehydratedFrom?: string | null;
132
150
  /**
133
151
  * `status === "running"` only means the pty is alive — the agent's TUI
134
152
  * can still be sitting on a blocking prompt (most commonly: "trust this
@@ -144,7 +162,23 @@ export interface HarnessSession {
144
162
  */
145
163
  ready: boolean;
146
164
  }
147
- /** A resumable past session discovered from agent transcripts or our registry. */
165
+ /**
166
+ * What resuming a past session would ACTUALLY do — resolved server-side
167
+ * against the agent's own conversation store, never guessed from whether we
168
+ * happen to hold an `agentSessionId`.
169
+ *
170
+ * - `agent-resume`: the agent still holds this conversation, so
171
+ * `HarnessAdapter.resume()` reattaches to it for real.
172
+ * - `rehydrate`: the agent does NOT hold it. Most commonly a session that
173
+ * ended before its first prompt — Claude Code writes no transcript at all
174
+ * for those, and Codex writes no rollout file (see CodexAdapter's
175
+ * `detectBlockingPrompt` for the same rule from the other direction). The
176
+ * conversation is unrecoverable from the agent; the only honest options are
177
+ * a fresh session in the same directory, or (once H3 lands) replaying our
178
+ * own recorded context into one.
179
+ */
180
+ export type SessionResumeMode = "agent-resume" | "rehydrate";
181
+ /** A past session discovered from agent transcripts or our registry. */
148
182
  export interface SessionSummary {
149
183
  /** Back-reference to our session when the registry tracked it (source "registry"). */
150
184
  harnessSessionId?: string;
@@ -154,6 +188,14 @@ export interface SessionSummary {
154
188
  title: string;
155
189
  lastActiveAt: string;
156
190
  source: "registry" | "transcript";
191
+ /**
192
+ * Whether this row can genuinely be handed back to the agent — verified
193
+ * against the agent's own store by `GET /api/sessions/history`, for BOTH
194
+ * row sources. Holding an `agentSessionId` is not evidence of anything:
195
+ * we capture it from the SessionStart hook, which fires long before the
196
+ * agent has a conversation worth resuming.
197
+ */
198
+ resumeMode: SessionResumeMode;
157
199
  /**
158
200
  * Git branch the session was last active on, when the transcript records it.
159
201
  * The strongest within-directory differentiator between otherwise-similar
@@ -167,7 +209,26 @@ export interface SessionSummary {
167
209
  * full read (see the claude-code adapter's full-scan size cap).
168
210
  */
169
211
  messageCount?: number;
212
+ /**
213
+ * Number of human prompts (turns) the harness itself recorded for this
214
+ * session — from the event store's byte-offset index, or from the archived
215
+ * record once the events behind it have been swept. Exact and cheap at any
216
+ * file size — unlike {@link messageCount}, which the claude-code adapter
217
+ * leaves undefined above its full-scan cap. Undefined when the harness has
218
+ * neither events nor an archived record for the session (a transcript the
219
+ * Studio never ran). Prefer this over messageCount when both are present.
220
+ */
221
+ turnCount?: number;
170
222
  }
223
+ /**
224
+ * What an adapter reports having FOUND — everything in a `SessionSummary`
225
+ * except `resumeMode`. Deciding what resuming a row would do is the server's
226
+ * job (`GET /api/sessions/history`), so the adapter contract deliberately
227
+ * can't express it: that's what kept transcript rows hardcoded to
228
+ * un-resumable while registry rows were hardcoded to resumable, each wrong in
229
+ * the opposite direction.
230
+ */
231
+ export type PastSessionRecord = Omit<SessionSummary, "resumeMode">;
171
232
  export interface DoctorCheck {
172
233
  name: string;
173
234
  ok: boolean;
@@ -205,7 +266,31 @@ export interface LaunchOpts {
205
266
  /** Only consulted by `launchTask` — hard cap on agent turns
206
267
  * (`--max-turns`), so a bounded task can't run away. */
207
268
  maxTurns?: number;
269
+ /**
270
+ * Set by the launch-opts builder when this launch's context was seeded from
271
+ * a prior session's recorded events (portable continue — see
272
+ * core/rehydration.ts). Carries the id the brief was built from. Adapters
273
+ * ignore it entirely; `SessionManager.create()` copies it onto
274
+ * {@link HarnessSession.rehydratedFrom} so the UI can say whether the
275
+ * continue really carried context instead of implying it did.
276
+ */
277
+ rehydratedFrom?: string;
208
278
  }
279
+ /**
280
+ * How a harness receives the rehydration brief for a continued session.
281
+ *
282
+ * - `launch-flag`: the adapter puts `LaunchOpts.systemPromptFile`'s contents in
283
+ * front of the agent at spawn time (claude-code's `--append-system-prompt`,
284
+ * codex's `developer_instructions`), so composing the brief into that file is
285
+ * the whole delivery.
286
+ * - `post-ready-injection`: the harness has no such flag, so the brief is sent
287
+ * through the ordinary input path once the session reports `ready` — gated on
288
+ * readiness so it is never written into a TUI sitting on a trust prompt.
289
+ *
290
+ * See `systemPromptDeliveryFor` (core/rehydration.ts) for why the absent case
291
+ * resolves to the fallback rather than the flag.
292
+ */
293
+ export type SystemPromptDelivery = "launch-flag" | "post-ready-injection";
209
294
  /**
210
295
  * One implementation per supported coding agent. Implementations must be
211
296
  * side-effect free until `launch`/`resume` specs are actually spawned.
@@ -218,8 +303,34 @@ export interface HarnessAdapter {
218
303
  resume(agentSessionId: string, opts: LaunchOpts): SpawnSpec;
219
304
  /** How analytics events are sourced for this harness. */
220
305
  eventSource: "hooks" | "transcript-tail";
221
- /** Resumable sessions for a directory (agent-side history). */
222
- listPastSessions(cwd: string): Promise<SessionSummary[]>;
306
+ /**
307
+ * Whether `launch`/`resume` actually put `LaunchOpts.systemPromptFile` in
308
+ * front of the agent. Declared rather than inferred, because the alternative
309
+ * — assuming every adapter honours the field — silently drops a rehydration
310
+ * brief for one that doesn't. Omitted resolves to `post-ready-injection`
311
+ * (see `systemPromptDeliveryFor` in core/rehydration.ts).
312
+ */
313
+ systemPromptDelivery?: SystemPromptDelivery;
314
+ /** Past sessions this agent recorded for a directory (agent-side history).
315
+ * Reports what it found; `resumeMode` is the server's call — see
316
+ * {@link PastSessionRecord}. */
317
+ listPastSessions(cwd: string): Promise<PastSessionRecord[]>;
318
+ /**
319
+ * Does this agent's OWN conversation store still hold `agentSessionId` for
320
+ * `cwd` — i.e. would `resume()` reattach to a real conversation rather than
321
+ * dying on startup?
322
+ *
323
+ * This exists because holding an `agentSessionId` proves nothing: we capture
324
+ * it from the SessionStart hook, which fires before the user has submitted
325
+ * anything, and an agent that never received a prompt writes no transcript
326
+ * at all. Without this check, one in three history rows was a Resume button
327
+ * guaranteed to fail with `No conversation found with session ID: …`.
328
+ *
329
+ * Contract: **never throws**. A missing, unreadable, or empty store is
330
+ * `false` — a resumability probe must not be able to break the history
331
+ * endpoint or `resume()`'s pre-flight.
332
+ */
333
+ canResume(agentSessionId: string, cwd: string): Promise<boolean>;
223
334
  /**
224
335
  * Best-effort scrollback check for this harness's own known blocking
225
336
  * prompts (e.g. "trust this directory?"), for harnesses whose real
@@ -337,38 +448,38 @@ export type BusMessage = {
337
448
  type: "session.activity";
338
449
  harnessSessionId: string;
339
450
  at: string;
340
- };
341
- /** Per-step cost summary from the spend endpoint. */
342
- export interface RunStepSpend {
343
- name: string;
344
- totalUsd: string;
345
- entryCount: number;
346
451
  }
347
- /** Full spend summary for one execution, keyed by executionId. */
348
- export interface RunSpend {
349
- executionId: string;
350
- totalUsd: string;
351
- settleState: string;
352
- byStep: RunStepSpend[];
353
- }
354
- /**
355
- * One billable capability call within a run — the per-call drill-down behind a
356
- * step's cost ("why is this step costly"). Derived from the transactions
357
- * endpoint. Deliberately provider-AGNOSTIC: `capability` is a generic label
358
- * ("LLM", "sandbox", "web search") mapped server-side from the operation, so
359
- * neither the browser nor this file ever carries the upstream provider/model
360
- * name. Token counts are intentionally absent the platform does not record
361
- * per-call tokens for gateway LLM calls (see SAP ticket for the backend work).
362
- */
363
- export interface RunCall {
364
- /** Step this call is attributed to (workflowStepName ?? capability op). */
365
- stepName: string;
366
- /** Provider-agnostic capability label, e.g. "LLM" / "sandbox" / "web search". */
452
+ /**
453
+ * Auth state changed — fired after a successful sign-in (`POST
454
+ * /api/auth/start`) or sign-out (`POST /api/auth/disconnect`). The SPA
455
+ * should refetch `/api/auth/status` (or `/api/state`) on receipt to update
456
+ * its auth UI without a full page reload.
457
+ */
458
+ | {
459
+ type: "auth.changed";
460
+ authenticated: boolean;
461
+ organizationName: string | null;
462
+ };
463
+ /**
464
+ * One capability call a step made during the run. Capability-scoped and
465
+ * provider-agnostic: `capability` is a dotted capability id (e.g.
466
+ * `web.search`, `models.coding.run`) never a provider or model name.
467
+ * `stubUsed` records whether this call was served by a supplied stub instead
468
+ * of a real capability call, which is the single most load-bearing fact when
469
+ * explaining a local (offline) run. Optional fields are ABSENT (not null)
470
+ * when the source does not carry the value — honest absence.
471
+ */
472
+ export interface StepCall {
473
+ /** Dotted capability id (provider-agnostic — never a provider/model name). */
367
474
  capability: string;
368
- /** The capability operation, e.g. "generate" / "create" / "execute". */
369
- op: string;
370
- /** Captured USD for this single call. */
371
- usd: string;
475
+ /** True when a supplied stub served this call rather than the real capability. */
476
+ stubUsed?: boolean;
477
+ /** The arguments the call was made with, when the source carries them. Any
478
+ * JSON shape; ABSENT (not null) otherwise. */
479
+ args?: unknown;
480
+ /** The value the call returned — the served stub value for a local run, or
481
+ * the capability result for a prod run. ABSENT when the source has no result. */
482
+ result?: unknown;
372
483
  }
373
484
  /**
374
485
  * A step's render status, folded from the raw projection step status into the
@@ -378,17 +489,15 @@ export interface RunCall {
378
489
  */
379
490
  export type StepStatus = "pending" | "running" | "passed" | "failed";
380
491
  /** One step as the canvas renders it — status plus the deterministically
381
- * derived cost/latency/error/log slice. Optional fields are ABSENT (not
492
+ * derived latency/error/log slice. Optional fields are ABSENT (not
382
493
  * `undefined`/`0`) when the decoded projection carries no value — honest
383
- * absence, matching the SDK's cost-is-nullable philosophy. */
494
+ * absence. The inspector surfaces logs, latency, and pass/fail only. */
384
495
  export interface StepView {
385
496
  /** Stable id for keyed rendering — the OTel span id, else a step-order key. */
386
497
  id: string;
387
498
  /** Human step label (the projection's stepName). */
388
499
  name: string;
389
500
  status: StepStatus;
390
- /** Captured USD for this step; absent when the read carries no cost. */
391
- costUsd?: number;
392
501
  /** finishedAt − startedAt in ms; absent while running or on bad timestamps. */
393
502
  latencyMs?: number;
394
503
  /** Terminal error message; present only for a failed step that recorded one. */
@@ -396,13 +505,59 @@ export interface StepView {
396
505
  /** Tail-preserving, character-capped executor log text — the debug-macro
397
506
  * context source (trimmed further before injection). Absent when no logs. */
398
507
  logSlice?: string;
508
+ /** The resolved input the step actually ran on, when the source carries it
509
+ * (populated by local stub runs today; a production run projection may
510
+ * expose it in future). Any JSON shape. ABSENT — never `null`/`{}` — when
511
+ * the source has no per-step input, so the inspector shows nothing rather
512
+ * than fabricating a payload. */
513
+ input?: unknown;
514
+ /** The value the step produced, on the same honest-absence terms as
515
+ * {@link StepView.input}: present only when the source captured a real
516
+ * output, absent otherwise (a still-running or output-less step shows no
517
+ * Output block). Any JSON shape. */
518
+ output?: unknown;
519
+ /** The capability calls this step made during the run, in call order. Each
520
+ * entry is capability-scoped and provider-agnostic. Absent (never `[]`)
521
+ * when the source records no call information for this step — honest
522
+ * absence, mirroring `input`/`output`. A local run populates this from
523
+ * the stub client's per-call sink; a prod run leaves it absent when the
524
+ * step projection does not carry dotted-capability call records. */
525
+ calls?: StepCall[];
526
+ }
527
+ /** A supplied stub key that no capability call ever matched in its step — almost
528
+ * always a typo or the wrong path form. Surfaced read-only in the inspector so a
529
+ * no-op mock (a stub that silently served nothing) is visible instead of a
530
+ * mystery. Mirrors agent-core's `UnusedStub` (consumed as-is). */
531
+ export interface UnusedStubView {
532
+ step: string;
533
+ key: string;
399
534
  }
400
535
  /** A whole run as the canvas renders it. `status` is the run lifecycle folded
401
- * to the four states the UI distinguishes; `steps` is order-preserving. */
536
+ * to the four states the UI distinguishes; `steps` is order-preserving.
537
+ *
538
+ * Stub fields are RUN-LEVEL and honest-absence: they are set only by
539
+ * {@link renderLocalRun} for an offline stub run (prod runs from renderRunState
540
+ * never carry them), and only when they carry real signal. A local run is
541
+ * stub-served by construction — every `ctx.sapiom.*` call resolves from a stub —
542
+ * so `stubbed` is the honest per-run truth the inspector marks each executed
543
+ * step with (agent-core records no per-CALL stub attribution, so the chip lives
544
+ * at the granularity the trace actually supports). `unusedStubs`/`stubWarnings`
545
+ * come from the run's terminal NDJSON summary and are ABSENT (not `[]`) when
546
+ * empty, so the read-only notice renders nothing when there is nothing wrong. */
402
547
  export interface RunView {
403
548
  executionId: string;
404
549
  status: "running" | "completed" | "failed" | "cancelled";
405
550
  steps: StepView[];
551
+ /** True when this run was served entirely by stub capabilities (an offline
552
+ * local run). Absent for real (prod / local-backend) runs. Drives the
553
+ * per-step "stubbed" chip. */
554
+ stubbed?: boolean;
555
+ /** Supplied stub keys that matched no capability call this run (likely a typo
556
+ * / wrong path). Absent when none — never an empty array. */
557
+ unusedStubs?: UnusedStubView[];
558
+ /** Human-readable warnings about stub values that matched a key but had the
559
+ * wrong shape (the silent-wrong-data trap). Absent when none. */
560
+ stubWarnings?: string[];
406
561
  }
407
562
  /** One entry in the adapter registry, as the SPA sees it (GET /api/harnesses). */
408
563
  export interface HarnessEntry {
@@ -429,7 +584,7 @@ export interface HarnessEntry {
429
584
  * See the collector README section in packages/harness/src/core/collector/
430
585
  * for the full surface-"ui" contract.
431
586
  */
432
- export type UiEventName = "prompt.submitted" | "session.switched" | "macro.invoked" | "visualize.triggered" | "consent.changed" | "session.created" | "skill.viewed" | "skill.used" | "mcp.install";
587
+ export type UiEventName = "prompt.submitted" | "session.switched" | "macro.invoked" | "visualize.triggered" | "consent.changed" | "session.created" | "mcp.install";
433
588
  export interface UiTrackRequest {
434
589
  /** Dot-canonical event name — one of the UiEventName literals. */
435
590
  event: UiEventName;
@@ -442,7 +597,7 @@ export interface UiTrackRequest {
442
597
  harnessSessionId?: string;
443
598
  }
444
599
  export declare const ANALYTICS_SCHEMA_VERSION = 1;
445
- export type AnalyticsEventType = "session.start" | "prompt.submitted" | "tool.call" | "turn.completed" | "session.end" | "session.switched" | "macro.invoked" | "visualize.triggered" | "consent.changed" | "session.created" | "skill.viewed" | "skill.used" | "mcp.install";
600
+ export type AnalyticsEventType = "session.start" | "prompt.submitted" | "tool.call" | "turn.completed" | "session.end" | "session.switched" | "macro.invoked" | "visualize.triggered" | "consent.changed" | "session.created" | "mcp.install";
446
601
  /**
447
602
  * The normalized event — the shape that (with opt-in) is batched to the
448
603
  * remote collector and always appended to events.ndjson locally.
@@ -450,9 +605,17 @@ export type AnalyticsEventType = "session.start" | "prompt.submitted" | "tool.ca
450
605
  */
451
606
  export interface AnalyticsEvent {
452
607
  eventId: string;
453
- /** Per-harnessSessionId monotonic counter from 1 — ordering + loss detection. */
608
+ /**
609
+ * Per-harnessSessionId counter from 1, for loss detection *within one
610
+ * process epoch* — NOT a session-lifetime ordering key. The counter lives
611
+ * in the server's memory (core/collector/seq.ts), so it restarts at 1 on
612
+ * every harness boot and on every fresh pty for the same session: a real
613
+ * resumed session reads 1, 2 then 1, 1, 1. A reset means "new epoch", never
614
+ * "events lost". Anything that orders events must sort by `(ts, seq)` —
615
+ * see core/session-record.ts.
616
+ */
454
617
  seq: number;
455
- /** ISO-8601, client clock. Use seq (not ts) for intra-session ordering. */
618
+ /** ISO-8601, client clock. Primary ordering key; `seq` only breaks ties. */
456
619
  ts: string;
457
620
  /** Sapiom user id from auth; null when not logged in. */
458
621
  userId: string | null;
@@ -487,11 +650,158 @@ export interface CollectorBatch {
487
650
  context: CollectorContext;
488
651
  events: AnalyticsEvent[];
489
652
  }
653
+ /** One tool invocation inside a turn, from a `tool.call` event. */
654
+ export interface SessionRecordToolCall {
655
+ /** Tool name as the agent reported it; null when the event omitted one. */
656
+ name: string | null;
657
+ /** Stringified tool input (JSON for structured inputs), possibly truncated. */
658
+ input: string | null;
659
+ /** Truncated stringified tool result — see {@link responseTruncated}. */
660
+ responseSummary: string | null;
661
+ /** True when the stored response hit the collector's size cap and the real
662
+ * output was longer. The missing bytes are not recoverable from our log. */
663
+ responseTruncated: boolean;
664
+ /** ISO-8601 timestamp of the `tool.call` event. */
665
+ at: string;
666
+ }
667
+ /** One prompt→response turn. A turn opens on `prompt.submitted` and closes on
668
+ * `turn.completed`; tool calls in between attach to it. */
669
+ export interface SessionRecordTurn {
670
+ /** 1-based ordinal in the record — stable for deep links and test asserts. */
671
+ index: number;
672
+ /**
673
+ * The human prompt that opened the turn. Null for a turn our events imply
674
+ * but never saw a prompt for: tool calls or a completion that arrived with
675
+ * no open turn (a session whose recording started mid-turn, or an agent-
676
+ * initiated turn). Rendered as an explicit "no recorded prompt", never faked.
677
+ */
678
+ prompt: string | null;
679
+ /** ISO-8601 timestamp of the opening `prompt.submitted`; null when absent. */
680
+ promptAt: string | null;
681
+ toolCalls: SessionRecordToolCall[];
682
+ /**
683
+ * The assistant's final message for the turn. This is the Stop hook's LAST
684
+ * assistant message only — narration *between* tool calls is not in our
685
+ * event stream (see core/collector/transcript.ts). Null when the harness
686
+ * never captured one (Codex's rollout has no equivalent field).
687
+ */
688
+ assistantText: string | null;
689
+ /** Model that produced the turn, when the transcript backfill supplied it. */
690
+ model: string | null;
691
+ usage: {
692
+ inputTokens: number | null;
693
+ outputTokens: number | null;
694
+ } | null;
695
+ /** ISO-8601 timestamp of the closing `turn.completed`; null when open. */
696
+ completedAt: string | null;
697
+ /** True when no `turn.completed` ever closed this turn — the session was
698
+ * killed mid-turn, or a new prompt superseded it. */
699
+ incomplete: boolean;
700
+ }
701
+ /**
702
+ * A known, structural gap in a reconstructed record. Codes (not prose) so the
703
+ * UI owns the wording and the set can grow without breaking older clients.
704
+ *
705
+ * - `truncated-tool-output`: at least one tool result hit the collector's size
706
+ * cap; the full output is not in our log.
707
+ * - `assistant-narration-gap`: a turn has tool calls plus a final assistant
708
+ * message, so whatever the agent said *between* those calls is missing.
709
+ * - `missing-assistant-text`: a turn completed with no assistant text at all
710
+ * (Codex, whose rollout carries no equivalent of the Stop hook's field).
711
+ * - `incomplete-final-turn`: the last turn never completed — the session
712
+ * ended mid-turn.
713
+ * - `compacted-archive`: the record was read from its archived copy, whose
714
+ * tool inputs and results are clipped to keep it bounded (see
715
+ * core/record-archive.ts). The conversation is whole; the tool payloads
716
+ * inside it are excerpts.
717
+ * - `dropped-early-turns`: the archived copy kept only the most recent turns —
718
+ * `turns` holds fewer of them than `turnCount` says happened.
719
+ */
720
+ export type SessionRecordLimitation = "truncated-tool-output" | "assistant-narration-gap" | "missing-assistant-text" | "incomplete-final-turn" | "compacted-archive" | "dropped-early-turns";
721
+ /** `GET /api/sessions/:id/record` response. */
722
+ export interface SessionRecord {
723
+ /** The harness session this record was folded from. When several harness
724
+ * sessions share one agent session (a resumed conversation), this is the
725
+ * first of them and `mergedSessionIds` lists them all. */
726
+ harnessSessionId: string;
727
+ /** Every harnessSessionId folded into this record, in first-seen order. */
728
+ mergedSessionIds: string[];
729
+ /** The agent's own session id, when our events carried one. */
730
+ agentSessionId: string | null;
731
+ harness: HarnessKind;
732
+ /** cwd from the `session.start` event; null when it never recorded one. */
733
+ cwd: string | null;
734
+ /** ISO-8601 of the earliest event in the record. */
735
+ startedAt: string | null;
736
+ /** ISO-8601 of the `session.end` event; null for a session that never
737
+ * reported ending (killed, or crashed). */
738
+ endedAt: string | null;
739
+ turns: SessionRecordTurn[];
740
+ /**
741
+ * Human prompts in the CONVERSATION — `turns.filter(t => t.prompt != null)
742
+ * .length` for a record folded from events, and still that same count for an
743
+ * archived record whose oldest turns were dropped to fit its size cap. It
744
+ * describes what happened, not what survived, so a history row's turn count
745
+ * doesn't change when its events get swept; `dropped-early-turns` in
746
+ * {@link limitations} is what says `turns` holds fewer than this.
747
+ */
748
+ turnCount: number;
749
+ /** Events folded into this record (including ones no turn field shows).
750
+ * Like {@link turnCount}, counted before any archive compaction. */
751
+ eventCount: number;
752
+ /** Always true. Present on the wire so no client can mistake a record for a
753
+ * verbatim replay of what the user saw in their terminal. */
754
+ reconstructed: true;
755
+ /**
756
+ * ISO-8601 of when this record was written to the durable archive
757
+ * (`~/.sapiom/harness/records/`), or null when it was folded from the live
758
+ * event log. Non-null therefore means "this is the archived copy": bounded,
759
+ * compacted, and — unlike the events — still here after the analytics sink's
760
+ * retention sweep. The UI says so where the user reads it.
761
+ */
762
+ archivedAt: string | null;
763
+ limitations: SessionRecordLimitation[];
764
+ }
490
765
  export interface CreateSessionRequest {
491
766
  cwd: string;
492
767
  harness: HarnessKind;
493
768
  /** Profile id; omit for default. */
494
769
  profile?: string;
770
+ /**
771
+ * Portable continue: seed this fresh session with a reconstruction of a
772
+ * prior one instead of asking the vendor to reattach. Accepts either a
773
+ * `harnessSessionId` or the agent's own session id — whichever the history
774
+ * row carries — and is what a `resumeMode: "rehydrate"` row posts.
775
+ *
776
+ * Best-effort by contract: an id our event log holds nothing for still
777
+ * creates the session, with `HarnessSession.rehydratedFrom` left null so the
778
+ * caller can tell that no context came across. Refusing instead would block
779
+ * the only thing still possible for that row (a fresh session in the same
780
+ * directory) on a summary that was never going to exist.
781
+ */
782
+ rehydrateFrom?: string;
783
+ }
784
+ /**
785
+ * `POST /api/sessions/adopt` body — takes a transcript-only history row (one
786
+ * the registry never tracked, `SessionSummary.harnessSessionId` absent) into
787
+ * the registry and immediately resumes it, so a row whose transcript really is
788
+ * there reattaches to the agent instead of quietly starting a fresh session.
789
+ *
790
+ * The server re-verifies `resumeMode` itself via `HarnessAdapter.canResume`
791
+ * before registering anything — a client claim is never taken on trust, and a
792
+ * `rehydrate` row 409s with `SESSION_NOT_RESUMEABLE` rather than leaving a
793
+ * phantom record behind.
794
+ */
795
+ export interface AdoptSessionRequest {
796
+ /** The agent's own conversation id, as reported by `GET /sessions/history`. */
797
+ agentSessionId: string;
798
+ harness: HarnessKind;
799
+ cwd: string;
800
+ /** Display title carried over from the history row. */
801
+ title: string;
802
+ /** When the transcript was last touched — becomes the adopted record's
803
+ * `createdAt`/`lastActiveAt` so history keeps sorting it where it was. */
804
+ lastActiveAt: string;
495
805
  }
496
806
  /** Inject text into the session pty (used by macros and the Visualize button). */
497
807
  export interface InjectInputRequest {
@@ -585,6 +895,18 @@ export interface AppState {
585
895
  /** The directory the CLI was launched against — the SPA prefills the
586
896
  * new-session modal with this instead of recentDirs[0]. */
587
897
  launchDir: string;
898
+ /**
899
+ * The HOST's default parent directory for NEW agent projects, before the
900
+ * user's `projectRoot` setting overrides it. The server supplies it because
901
+ * only the server knows which host it is running under: the Electron app
902
+ * passes `<launchDir>/projects` (keeping user code out of the state
903
+ * directory's own listing), while the CLI leaves it as `launchDir` — the
904
+ * developer `cd`'d somewhere on purpose.
905
+ *
906
+ * Optional so existing AppState constructors (tests, mocks) stay valid; the
907
+ * SPA falls back to `launchDir`, which is the CLI behaviour anyway.
908
+ */
909
+ defaultProjectRoot?: string;
588
910
  /** Harness kinds with a working binary on PATH at CLI boot (from doctor()),
589
911
  * in default-preference order — `[0]` is what the auto-created boot
590
912
  * session used. Optional: omitted by callers that construct AppState
@@ -613,14 +935,152 @@ export interface AppState {
613
935
  * then falls back to the SDK's own default host. */
614
936
  agentsBaseUrl?: string;
615
937
  }
616
- /** `POST /api/sample-project` response — the seeded (or reused) example. */
617
- export interface SampleProjectSeedResponse {
618
- /** Directory to open a session in contains the project + its canvas. */
619
- root: string;
620
- /** Absolute path of the scaffolded example project inside `root`. */
621
- projectDir: string;
622
- /** False when an already-seeded copy was reused as-is. */
623
- created: boolean;
938
+ /**
939
+ * The counts a complexity band was derived from, so a surface can explain the
940
+ * band instead of just asserting it. Mirrors core's `TemplateComplexityBasisDto`.
941
+ */
942
+ export interface TemplateComplexityBasis {
943
+ /** Steps declared `kind: 'llm'` — each is a judgment point. */
944
+ llmSteps: number;
945
+ /** Model steps feeding directly into another model step (compounding variance). */
946
+ chainedLlmSteps: number;
947
+ /** Media-generation capabilities (image/video). */
948
+ mediaCapabilities: number;
949
+ capabilityCount: number;
950
+ stepCount: number;
951
+ /** Largest number of outgoing targets on any one step. */
952
+ maxFanOut: number;
953
+ }
954
+ /**
955
+ * How involved a template is, on a 1–5 band derived by core from the template's
956
+ * declared shape. `score` and `label` are redundant on purpose: the score orders
957
+ * and the label reads. A surface renders one of them, never a raw weighted sum —
958
+ * that would invite false precision on what is explicitly a rough estimate.
959
+ *
960
+ * Owned by core's `workflows/template-complexity.ts`. The Studio computes
961
+ * nothing: whether the band is derived (today) or authored (SAP-2086/2087), this
962
+ * type is unchanged and this surface reads whatever core serves.
963
+ */
964
+ export interface TemplateComplexity {
965
+ /** 1–5, monotonic in `label`. */
966
+ score: number;
967
+ /** `Minimal` | `Simple` | `Moderate` | `Involved` | `Advanced`. A loose string
968
+ * for the same reason `category` is: the band vocabulary is owned upstream. */
969
+ label: string;
970
+ basis: TemplateComplexityBasis;
971
+ }
972
+ /**
973
+ * A gallery card, relayed verbatim from core's `GET /v1/workflows/templates`
974
+ * (`TemplateSummaryDto`). The Studio does NOT own this taxonomy — the registry
975
+ * in the public sapiom-js repo does, and core derives the complexity band from
976
+ * each template's declared shape. Fields are carried through untouched so the
977
+ * Studio's list and the dashboard's Template library can never disagree.
978
+ */
979
+ export interface TemplateSummary {
980
+ id: string;
981
+ name: string;
982
+ description: string;
983
+ tags: string[];
984
+ /** Registry outcome-axis id (`starter`, `revenue-marketing`, …), or null for a
985
+ * template predating the field. A loose string on purpose: the taxonomy is
986
+ * owned upstream, so an unrecognised id buckets as uncategorised rather than
987
+ * dropping the card. */
988
+ category: string | null;
989
+ /** What starts a run (`on-demand` | `scheduled` | `on-webhook` | `on-event`),
990
+ * or null when undeclared. */
991
+ cadence: string | null;
992
+ stepCount: number;
993
+ /** Dotted catalog capability ids (e.g. `web.search`). */
994
+ capabilities: string[];
995
+ /**
996
+ * How involved the template is. Replaced an estimated per-run cost that core
997
+ * could only compute for 5 of 26 templates; a band is defined for every one.
998
+ *
999
+ * NULLABLE HERE THOUGH CORE TYPES IT REQUIRED, and that is not belt-and-braces.
1000
+ * The Studio is a published npm package: an old copy can point at any backend,
1001
+ * and a fresh copy can point at a backend that predates the field (a local
1002
+ * stack, a self-hosted one, prod before the promotion). Typing it required
1003
+ * would put an unguarded dereference in the row renderer and take out the whole
1004
+ * dialog. One row degrading to an em dash is the right failure.
1005
+ *
1006
+ * Note the em dash means something new: it used to be "no cost estimate
1007
+ * exists", the majority case; it now means "this response predates the band" —
1008
+ * a different claim wearing the same glyph, and one nobody should ever see
1009
+ * against a current backend.
1010
+ */
1011
+ complexity: TemplateComplexity | null;
1012
+ }
1013
+ /**
1014
+ * One step of a template's declared graph (core's `DefinitionStepDto` subset the
1015
+ * detail pane renders).
1016
+ *
1017
+ * `kind`/`sublabel` come from `classifyStepKind` — the SAME precedence the canvas
1018
+ * applies to a real definition — so the preview and the post-clone canvas agree.
1019
+ * There is deliberately no `terminal` boolean: the four transition kinds
1020
+ * (`continue`/`pause`/`terminate`/`fail`) do not collapse into one, and pretending
1021
+ * they do renders a fail-only sink as a green success exit.
1022
+ */
1023
+ export interface TemplateStepView {
1024
+ name: string;
1025
+ description: string | null;
1026
+ capabilities: string[];
1027
+ /** Canvas node kind: `entry` | `step` | `pause` | `terminal-success` | `terminal-warn`. */
1028
+ kind: string;
1029
+ /** One-line role, e.g. `entry`, `step · can also fail`, `terminal · success`. */
1030
+ sublabel: string;
1031
+ }
1032
+ /** One edge of a template's declared graph (core's `DefinitionTransitionDto`). */
1033
+ export interface TemplateTransitionView {
1034
+ from: string;
1035
+ to: string;
1036
+ /** The signal a `pause` edge waits for; null for a plain `continue`. */
1037
+ label: string | null;
1038
+ /** `cross` for a pause edge (as the canvas draws it), else continue flow. */
1039
+ kind: "continue" | "pause";
1040
+ }
1041
+ /**
1042
+ * `GET /api/templates/:id` — the summary plus the rich manifest the detail pane
1043
+ * renders. Core expands the registry's hand-authored step list into the same
1044
+ * graph shapes a real definition uses, so the preview and the post-clone canvas
1045
+ * are the same vocabulary.
1046
+ */
1047
+ export interface TemplateDetailView extends TemplateSummary {
1048
+ /** Prose from the co-located `template.json`; null when the manifest omits it. */
1049
+ whatItDoes: string | null;
1050
+ /** Path inside the sapiom-js repo the fork is seeded from. */
1051
+ sourcePath: string | null;
1052
+ steps: TemplateStepView[];
1053
+ transitions: TemplateTransitionView[];
1054
+ author: {
1055
+ name: string;
1056
+ url: string | null;
1057
+ } | null;
1058
+ useCases: string[];
1059
+ /** Markdown. */
1060
+ notes: string | null;
1061
+ examples: Array<{
1062
+ title: string | null;
1063
+ input: unknown;
1064
+ output: unknown;
1065
+ }>;
1066
+ /** Credentials the template needs supplied before a deployed run works. */
1067
+ requiredSecrets: Array<{
1068
+ key: string;
1069
+ label: string;
1070
+ description: string | null;
1071
+ }>;
1072
+ }
1073
+ /**
1074
+ * `GET /api/templates` response. `source` tells the SPA whether it is looking at
1075
+ * the live catalog or the offline fallback, so the dialog can say so instead of
1076
+ * silently presenting two entries as the whole gallery — the failure mode this
1077
+ * endpoint exists to fix.
1078
+ */
1079
+ export interface TemplateListResponse {
1080
+ templates: TemplateSummary[];
1081
+ source: "live" | "fallback";
1082
+ /** Why the live fetch was not used, when `source` is `fallback`. */
1083
+ reason?: "signed-out" | "unreachable";
624
1084
  }
625
1085
  export interface HarnessSettings {
626
1086
  telemetryOptIn: boolean;
@@ -632,10 +1092,43 @@ export interface HarnessSettings {
632
1092
  * Persisted so the notice never appears again after the first dismiss.
633
1093
  */
634
1094
  telemetryNoticeDismissed?: boolean;
1095
+ /**
1096
+ * Where NEW agent projects are created (the add-workspace template and idea
1097
+ * doors). Absent until the user changes it, in which case the host default
1098
+ * (`AppState.defaultProjectRoot`) applies.
1099
+ *
1100
+ * Deliberately the same value the door itself edits: changing the root while
1101
+ * creating a project saves it as the default, so there is one place to set it
1102
+ * rather than a door value that silently diverges from a settings value.
1103
+ */
1104
+ projectRoot?: string;
1105
+ /**
1106
+ * Opt-in: periodically fold a live session's record into a ≤500-word rolling
1107
+ * summary (see core/rolling-summary.ts), which a later portable continue
1108
+ * reads to explain what the session was *for* rather than only what it last
1109
+ * did. Off by default because it spends tokens on a background LLM call the
1110
+ * user never asked for. With it off, briefs degrade to last-N-turns.
1111
+ */
1112
+ rollingSummary?: boolean;
635
1113
  }
636
1114
  export interface FsDirEntry {
637
1115
  name: string;
638
1116
  path: string;
1117
+ /**
1118
+ * Whether this directory directly contains AGENT_PROJECT_MARKER.
1119
+ *
1120
+ * Load-bearing, not decorative: without it a picker cannot tell an agent
1121
+ * project from any other folder, so it has to offer every escape hatch
1122
+ * (register / scaffold / template / bulk-scan / install-MCP) at all times —
1123
+ * which is exactly what made the old add-workspace dialog unusable. With it,
1124
+ * those become outcomes of what we found rather than permanent options.
1125
+ *
1126
+ * Only ONE level deep, matching this endpoint's contract. A `false` here does
1127
+ * not mean the subtree is empty of projects — a container folder whose
1128
+ * children are projects reports `false` (the rail's recursive scan is the
1129
+ * thing that answers "anything under here?").
1130
+ */
1131
+ hasAgentProject: boolean;
639
1132
  }
640
1133
  /**
641
1134
  * GET /api/fs/list?path= response — directories only, one level deep.
@@ -684,11 +1177,10 @@ export interface MacroDef {
684
1177
  kind: "open-url";
685
1178
  url: string;
686
1179
  }
687
- /** Force refresh of the bound workflow's canvas: invalidates the
688
- * extraction + enrichment caches, re-renders the deterministic diagram
689
- * instantly, and re-spawns the bounded AI enrichment task (see
690
- * core/canvas-enrich.ts) no pty involved. A cheap no-op when the
691
- * session is unbound. */
1180
+ /** Refresh of the bound workflow's canvas: invalidates the extraction
1181
+ * cache and re-renders the fully deterministic diagram (structure +
1182
+ * derived annotations, no LLM, no user token) no pty involved. A cheap
1183
+ * no-op when the session is unbound. */
692
1184
  | {
693
1185
  kind: "render-canvas";
694
1186
  };