@sapiom/harness 0.1.6 → 0.2.1

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 (185) hide show
  1. package/CHANGELOG.md +105 -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 +12 -65
  7. package/dist/cli/bin.js.map +1 -1
  8. package/dist/cli/settings.d.ts.map +1 -1
  9. package/dist/cli/settings.js +33 -1
  10. package/dist/cli/settings.js.map +1 -1
  11. package/dist/core/adapters/claude-code.d.ts +60 -2
  12. package/dist/core/adapters/claude-code.d.ts.map +1 -1
  13. package/dist/core/adapters/claude-code.js +119 -32
  14. package/dist/core/adapters/claude-code.js.map +1 -1
  15. package/dist/core/adapters/codex.d.ts +21 -2
  16. package/dist/core/adapters/codex.d.ts.map +1 -1
  17. package/dist/core/adapters/codex.js +53 -2
  18. package/dist/core/adapters/codex.js.map +1 -1
  19. package/dist/core/adapters/detect.d.ts +9 -0
  20. package/dist/core/adapters/detect.d.ts.map +1 -1
  21. package/dist/core/adapters/detect.js +9 -0
  22. package/dist/core/adapters/detect.js.map +1 -1
  23. package/dist/core/canvas-body.d.ts +28 -28
  24. package/dist/core/canvas-body.d.ts.map +1 -1
  25. package/dist/core/canvas-body.js +117 -55
  26. package/dist/core/canvas-body.js.map +1 -1
  27. package/dist/core/canvas-derive.d.ts +43 -0
  28. package/dist/core/canvas-derive.d.ts.map +1 -0
  29. package/dist/core/canvas-derive.js +95 -0
  30. package/dist/core/canvas-derive.js.map +1 -0
  31. package/dist/core/canvas-enrichment.d.ts +52 -127
  32. package/dist/core/canvas-enrichment.d.ts.map +1 -1
  33. package/dist/core/canvas-enrichment.js +17 -178
  34. package/dist/core/canvas-enrichment.js.map +1 -1
  35. package/dist/core/canvas-graph.d.ts +40 -1
  36. package/dist/core/canvas-graph.d.ts.map +1 -1
  37. package/dist/core/canvas-graph.js +71 -9
  38. package/dist/core/canvas-graph.js.map +1 -1
  39. package/dist/core/canvas-interconnections.d.ts +26 -8
  40. package/dist/core/canvas-interconnections.d.ts.map +1 -1
  41. package/dist/core/canvas-interconnections.js +132 -24
  42. package/dist/core/canvas-interconnections.js.map +1 -1
  43. package/dist/core/canvas-manifest-check.d.ts.map +1 -1
  44. package/dist/core/canvas-manifest-check.js +15 -2
  45. package/dist/core/canvas-manifest-check.js.map +1 -1
  46. package/dist/core/canvas-render.d.ts +8 -15
  47. package/dist/core/canvas-render.d.ts.map +1 -1
  48. package/dist/core/canvas-render.js +29 -41
  49. package/dist/core/canvas-render.js.map +1 -1
  50. package/dist/core/canvas-run-state.d.ts +41 -0
  51. package/dist/core/canvas-run-state.d.ts.map +1 -1
  52. package/dist/core/canvas-run-state.js +136 -0
  53. package/dist/core/canvas-run-state.js.map +1 -1
  54. package/dist/core/canvas-svg.d.ts +9 -4
  55. package/dist/core/canvas-svg.d.ts.map +1 -1
  56. package/dist/core/canvas-svg.js +84 -13
  57. package/dist/core/canvas-svg.js.map +1 -1
  58. package/dist/core/canvas-template.d.ts +9 -10
  59. package/dist/core/canvas-template.d.ts.map +1 -1
  60. package/dist/core/canvas-template.js +57 -20
  61. package/dist/core/canvas-template.js.map +1 -1
  62. package/dist/core/canvas-watcher.d.ts +22 -1
  63. package/dist/core/canvas-watcher.d.ts.map +1 -1
  64. package/dist/core/canvas-watcher.js +111 -30
  65. package/dist/core/canvas-watcher.js.map +1 -1
  66. package/dist/core/collector/normalizer.d.ts +8 -0
  67. package/dist/core/collector/normalizer.d.ts.map +1 -1
  68. package/dist/core/collector/normalizer.js +8 -0
  69. package/dist/core/collector/normalizer.js.map +1 -1
  70. package/dist/core/collector/store.d.ts +75 -1
  71. package/dist/core/collector/store.d.ts.map +1 -1
  72. package/dist/core/collector/store.js +334 -2
  73. package/dist/core/collector/store.js.map +1 -1
  74. package/dist/core/errors.d.ts +10 -4
  75. package/dist/core/errors.d.ts.map +1 -1
  76. package/dist/core/errors.js +11 -5
  77. package/dist/core/errors.js.map +1 -1
  78. package/dist/core/example-seed.d.ts +8 -0
  79. package/dist/core/example-seed.d.ts.map +1 -1
  80. package/dist/core/example-seed.js +47 -10
  81. package/dist/core/example-seed.js.map +1 -1
  82. package/dist/core/inject/system-prompt.d.ts +8 -0
  83. package/dist/core/inject/system-prompt.d.ts.map +1 -1
  84. package/dist/core/inject/system-prompt.js +3 -1
  85. package/dist/core/inject/system-prompt.js.map +1 -1
  86. package/dist/core/macros.d.ts.map +1 -1
  87. package/dist/core/macros.js +5 -6
  88. package/dist/core/macros.js.map +1 -1
  89. package/dist/core/paths.d.ts +1 -0
  90. package/dist/core/paths.d.ts.map +1 -1
  91. package/dist/core/paths.js +1 -0
  92. package/dist/core/paths.js.map +1 -1
  93. package/dist/core/record-archive.d.ts +187 -0
  94. package/dist/core/record-archive.d.ts.map +1 -0
  95. package/dist/core/record-archive.js +465 -0
  96. package/dist/core/record-archive.js.map +1 -0
  97. package/dist/core/rehydration.d.ts +72 -0
  98. package/dist/core/rehydration.d.ts.map +1 -0
  99. package/dist/core/rehydration.js +72 -0
  100. package/dist/core/rehydration.js.map +1 -0
  101. package/dist/core/resume-brief.d.ts +130 -0
  102. package/dist/core/resume-brief.d.ts.map +1 -0
  103. package/dist/core/resume-brief.js +404 -0
  104. package/dist/core/resume-brief.js.map +1 -0
  105. package/dist/core/rolling-summary.d.ts +130 -0
  106. package/dist/core/rolling-summary.d.ts.map +1 -0
  107. package/dist/core/rolling-summary.js +227 -0
  108. package/dist/core/rolling-summary.js.map +1 -0
  109. package/dist/core/session-manager.d.ts +11 -3
  110. package/dist/core/session-manager.d.ts.map +1 -1
  111. package/dist/core/session-manager.js +56 -7
  112. package/dist/core/session-manager.js.map +1 -1
  113. package/dist/core/session-record.d.ts +165 -0
  114. package/dist/core/session-record.d.ts.map +1 -0
  115. package/dist/core/session-record.js +470 -0
  116. package/dist/core/session-record.js.map +1 -0
  117. package/dist/core/spawn-target.d.ts +12 -0
  118. package/dist/core/spawn-target.d.ts.map +1 -0
  119. package/dist/core/spawn-target.js +187 -0
  120. package/dist/core/spawn-target.js.map +1 -0
  121. package/dist/core/task-manager.d.ts.map +1 -1
  122. package/dist/core/task-manager.js +9 -1
  123. package/dist/core/task-manager.js.map +1 -1
  124. package/dist/core/template-catalog.d.ts +48 -0
  125. package/dist/core/template-catalog.d.ts.map +1 -0
  126. package/dist/core/template-catalog.js +338 -0
  127. package/dist/core/template-catalog.js.map +1 -0
  128. package/dist/core/workflow-registry.d.ts.map +1 -1
  129. package/dist/core/workflow-registry.js +2 -2
  130. package/dist/core/workflow-registry.js.map +1 -1
  131. package/dist/core/workspace-watcher.d.ts.map +1 -1
  132. package/dist/core/workspace-watcher.js +4 -3
  133. package/dist/core/workspace-watcher.js.map +1 -1
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.js +21 -0
  137. package/dist/index.js.map +1 -1
  138. package/dist/profiles/default.d.ts.map +1 -1
  139. package/dist/profiles/default.js +9 -14
  140. package/dist/profiles/default.js.map +1 -1
  141. package/dist/server/canvas.d.ts.map +1 -1
  142. package/dist/server/canvas.js +7 -7
  143. package/dist/server/canvas.js.map +1 -1
  144. package/dist/server/fs.d.ts.map +1 -1
  145. package/dist/server/fs.js +17 -1
  146. package/dist/server/fs.js.map +1 -1
  147. package/dist/server/index.d.ts +12 -3
  148. package/dist/server/index.d.ts.map +1 -1
  149. package/dist/server/index.js +321 -50
  150. package/dist/server/index.js.map +1 -1
  151. package/dist/server/ingest.d.ts +11 -0
  152. package/dist/server/ingest.d.ts.map +1 -1
  153. package/dist/server/ingest.js +1 -0
  154. package/dist/server/ingest.js.map +1 -1
  155. package/dist/server/macros.d.ts +4 -6
  156. package/dist/server/macros.d.ts.map +1 -1
  157. package/dist/server/macros.js.map +1 -1
  158. package/dist/server/rest.d.ts +18 -6
  159. package/dist/server/rest.d.ts.map +1 -1
  160. package/dist/server/rest.js +185 -48
  161. package/dist/server/rest.js.map +1 -1
  162. package/dist/server/templates.d.ts +24 -0
  163. package/dist/server/templates.d.ts.map +1 -0
  164. package/dist/server/templates.js +54 -0
  165. package/dist/server/templates.js.map +1 -0
  166. package/dist/shared/types.d.ts +477 -29
  167. package/dist/shared/types.d.ts.map +1 -1
  168. package/dist/shared/types.js +19 -11
  169. package/dist/shared/types.js.map +1 -1
  170. package/dist/web/assets/index-SYpugKQ1.css +32 -0
  171. package/dist/web/assets/index-vYYd44hZ.js +446 -0
  172. package/dist/web/index.html +2 -2
  173. package/package.json +5 -4
  174. package/dist/core/canvas-enrich.d.ts +0 -115
  175. package/dist/core/canvas-enrich.d.ts.map +0 -1
  176. package/dist/core/canvas-enrich.js +0 -186
  177. package/dist/core/canvas-enrich.js.map +0 -1
  178. package/dist/profiles/canvas-guidelines.d.ts +0 -20
  179. package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
  180. package/dist/profiles/canvas-guidelines.js +0 -48
  181. package/dist/profiles/canvas-guidelines.js.map +0 -1
  182. package/dist/web/assets/index-DCtyI4VW.css +0 -32
  183. package/dist/web/assets/index-WSw5RvYR.js +0 -445
  184. package/dist/web/welcome-hero-dark.png +0 -0
  185. package/dist/web/welcome-hero-light.png +0 -0
@@ -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
@@ -494,9 +605,17 @@ export type AnalyticsEventType = "session.start" | "prompt.submitted" | "tool.ca
494
605
  */
495
606
  export interface AnalyticsEvent {
496
607
  eventId: string;
497
- /** 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
+ */
498
617
  seq: number;
499
- /** ISO-8601, client clock. Use seq (not ts) for intra-session ordering. */
618
+ /** ISO-8601, client clock. Primary ordering key; `seq` only breaks ties. */
500
619
  ts: string;
501
620
  /** Sapiom user id from auth; null when not logged in. */
502
621
  userId: string | null;
@@ -531,11 +650,158 @@ export interface CollectorBatch {
531
650
  context: CollectorContext;
532
651
  events: AnalyticsEvent[];
533
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
+ }
534
765
  export interface CreateSessionRequest {
535
766
  cwd: string;
536
767
  harness: HarnessKind;
537
768
  /** Profile id; omit for default. */
538
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;
539
805
  }
540
806
  /** Inject text into the session pty (used by macros and the Visualize button). */
541
807
  export interface InjectInputRequest {
@@ -629,6 +895,18 @@ export interface AppState {
629
895
  /** The directory the CLI was launched against — the SPA prefills the
630
896
  * new-session modal with this instead of recentDirs[0]. */
631
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;
632
910
  /** Harness kinds with a working binary on PATH at CLI boot (from doctor()),
633
911
  * in default-preference order — `[0]` is what the auto-created boot
634
912
  * session used. Optional: omitted by callers that construct AppState
@@ -657,14 +935,152 @@ export interface AppState {
657
935
  * then falls back to the SDK's own default host. */
658
936
  agentsBaseUrl?: string;
659
937
  }
660
- /** `POST /api/sample-project` response — the seeded (or reused) example. */
661
- export interface SampleProjectSeedResponse {
662
- /** Directory to open a session in contains the project + its canvas. */
663
- root: string;
664
- /** Absolute path of the scaffolded example project inside `root`. */
665
- projectDir: string;
666
- /** False when an already-seeded copy was reused as-is. */
667
- 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";
668
1084
  }
669
1085
  export interface HarnessSettings {
670
1086
  telemetryOptIn: boolean;
@@ -676,10 +1092,43 @@ export interface HarnessSettings {
676
1092
  * Persisted so the notice never appears again after the first dismiss.
677
1093
  */
678
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;
679
1113
  }
680
1114
  export interface FsDirEntry {
681
1115
  name: string;
682
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;
683
1132
  }
684
1133
  /**
685
1134
  * GET /api/fs/list?path= response — directories only, one level deep.
@@ -728,11 +1177,10 @@ export interface MacroDef {
728
1177
  kind: "open-url";
729
1178
  url: string;
730
1179
  }
731
- /** Force refresh of the bound workflow's canvas: invalidates the
732
- * extraction + enrichment caches, re-renders the deterministic diagram
733
- * instantly, and re-spawns the bounded AI enrichment task (see
734
- * core/canvas-enrich.ts) no pty involved. A cheap no-op when the
735
- * 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. */
736
1184
  | {
737
1185
  kind: "render-canvas";
738
1186
  };