@roubo/shared 0.1.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.
package/types.ts ADDED
@@ -0,0 +1,1975 @@
1
+ // ── roubo.yaml configuration types (derived from Zod schema in config-schema.ts) ──
2
+
3
+ import type {
4
+ RouboConfig,
5
+ ComponentConfig,
6
+ LoginConfig,
7
+ ToolConfig,
8
+ JigSettings,
9
+ } from "./config-schema.js";
10
+
11
+ export type {
12
+ RouboConfig,
13
+ ProjectConfig,
14
+ LayoutConfig,
15
+ ComponentType,
16
+ ComponentConfig,
17
+ ComponentBinding,
18
+ DockerComponentConfig,
19
+ MigrationConfig,
20
+ ConnectionConfig,
21
+ PortConfig,
22
+ LoginStep,
23
+ LoginConfig,
24
+ ToolConfig,
25
+ InspectionConfig,
26
+ BenchesConfig,
27
+ JigsConfig,
28
+ UserConfig,
29
+ IntegrationConfig,
30
+ IntegrationAdvanced,
31
+ IntegrationOverride,
32
+ CapturedUserId,
33
+ ConfigFieldError,
34
+ JigSettings,
35
+ SourceEntry,
36
+ } from "./config-schema.js";
37
+
38
+ export {
39
+ RouboConfigSchema,
40
+ ProjectConfigSchema,
41
+ LayoutConfigSchema,
42
+ ComponentConfigSchema,
43
+ ComponentBindingSchema,
44
+ PortConfigSchema,
45
+ ToolConfigSchema,
46
+ InspectionConfigSchema,
47
+ BenchesConfigSchema,
48
+ UserConfigSchema,
49
+ IntegrationConfigSchema,
50
+ IntegrationAdvancedSchema,
51
+ IntegrationOverrideSchema,
52
+ CapturedUserIdSchema,
53
+ SourceEntrySchema,
54
+ zodIssuesToValidationErrors,
55
+ zodIssuesToFieldMap,
56
+ } from "./config-schema.js";
57
+
58
+ export { deepMergeIntegration } from "./deep-merge.js";
59
+
60
+ // ── roubo-plugin.yaml manifest types (derived from Zod schema in plugin-manifest-schema.ts) ──
61
+
62
+ export {
63
+ PluginManifestSchema,
64
+ CredentialSlotSchema,
65
+ NetworkPermissionsSchema,
66
+ CredentialsPermissionsSchema,
67
+ FilesystemPermissionsSchema,
68
+ ProcessesPermissionSchema,
69
+ PluginPermissionsSchema,
70
+ PluginCapabilitiesSchema,
71
+ PluginDefaultIntegrationConfigSchema,
72
+ PluginIconSchema,
73
+ } from "./plugin-manifest-schema.js";
74
+
75
+ export type {
76
+ PluginManifest,
77
+ CredentialSlot,
78
+ NetworkPermissions,
79
+ CredentialsPermissions,
80
+ FilesystemPermissions,
81
+ ProcessesPermission,
82
+ PluginPermissions,
83
+ PluginCapabilities,
84
+ PluginDefaultIntegrationConfig,
85
+ PluginIcon,
86
+ } from "./plugin-manifest-schema.js";
87
+
88
+ export { parseManifest } from "./plugin-manifest.js";
89
+ export type { ParseManifestResult } from "./plugin-manifest.js";
90
+
91
+ export {
92
+ PLUGIN_ENABLE_STATE_SCHEMA_VERSION,
93
+ BUNDLED_PLUGIN_IDS,
94
+ PluginEnableStateSchema,
95
+ PluginEnableStateValueSchema,
96
+ } from "./plugin-enable-state-schema.js";
97
+ export type {
98
+ PluginEnableState,
99
+ PluginEnableStateValue,
100
+ BundledPluginId,
101
+ } from "./plugin-enable-state-schema.js";
102
+
103
+ export {
104
+ PLUGIN_CONSENT_STATE_SCHEMA_VERSION,
105
+ PERMISSION_CATEGORIES,
106
+ ConsentRecordSchema,
107
+ PluginConsentStateSchema,
108
+ declaredCategories,
109
+ isFullyAcknowledged,
110
+ } from "./plugin-consent-schema.js";
111
+ export type {
112
+ PermissionCategory,
113
+ ConsentRecord,
114
+ PluginConsentState,
115
+ } from "./plugin-consent-schema.js";
116
+
117
+ export type {
118
+ SourceCandidateIcon,
119
+ SourceCandidateItem,
120
+ SourceCandidateCategory,
121
+ SourceCandidatesShape,
122
+ SourceCandidatesResponse,
123
+ SearchableSourceCategory,
124
+ SourceCategoryOption,
125
+ GetSourceOptionsParams,
126
+ SourceOptionsResult,
127
+ SourceSelection,
128
+ SourceSelectionEntry,
129
+ } from "./integration-types.js";
130
+
131
+ export type {
132
+ PluginStatus,
133
+ PluginSource,
134
+ RestartEvent,
135
+ PluginError,
136
+ LogLine,
137
+ IsolationNotice,
138
+ PluginRecord,
139
+ } from "./plugin-runtime-types.js";
140
+
141
+ import type { CapturedUserId, IntegrationConfig } from "./config-schema.js";
142
+ import type {
143
+ PluginPermissions,
144
+ PluginDefaultIntegrationConfig,
145
+ } from "./plugin-manifest-schema.js";
146
+ import type { IsolationNotice, PluginStatus } from "./plugin-runtime-types.js";
147
+ import type { PluginManifest } from "./plugin-manifest-schema.js";
148
+
149
+ /**
150
+ * Result of the first stage of the plugin install flow
151
+ * (`POST /api/plugins/install`). The host has cloned (Git URL flow) or copied
152
+ * (local directory flow) the candidate plugin into a staging directory under
153
+ * `~/.roubo/plugins/.staging/<stagingToken>/`, parsed and validated its
154
+ * `roubo-plugin.yaml`, and is now waiting for the user to either accept the
155
+ * declared permissions (`POST /install/:token/confirm`) or cancel
156
+ * (`POST /install/:token/cancel`, which removes the staging directory).
157
+ */
158
+ export interface InstallPreview {
159
+ stagingToken: string;
160
+ manifest: PluginManifest;
161
+ source: InstallSource;
162
+ }
163
+
164
+ export type InstallSource =
165
+ | { type: "git"; url: string; directory?: string }
166
+ | { type: "local"; path: string };
167
+
168
+ /**
169
+ * Stable error codes emitted by the install pipeline. Routes map these to
170
+ * HTTP status codes; the client surfaces the human-readable message in an
171
+ * inline red banner.
172
+ */
173
+ export type InstallErrorCode =
174
+ | "invalid-input"
175
+ | "clone-failed"
176
+ | "missing-manifest"
177
+ | "invalid-manifest"
178
+ | "incompatible-host"
179
+ | "duplicate-id"
180
+ | "unknown-token"
181
+ | "update-target-missing"
182
+ // The staged package's content digest did not match the expected digest from
183
+ // the signed catalog entry (CP-FR-021): a tampered or substituted package.
184
+ | "integrity-failed"
185
+ // The catalog entry has been revoked / taken down (CP-FR-021): it cannot be
186
+ // installed or updated.
187
+ | "revoked"
188
+ // The static catalog failed signature verification (CP-FR-021): a tampered,
189
+ // missing, or unsigned catalog. The marketplace fails closed (zero listings).
190
+ | "catalog-unverified"
191
+ | "internal";
192
+
193
+ export interface InstallErrorBody {
194
+ error: string;
195
+ code: InstallErrorCode;
196
+ }
197
+
198
+ // --- Marketplace catalog (CP-FR-020 / CP-NFR-007 / CP-US-010, issue #621) ----
199
+ //
200
+ // The marketplace serves a first-party-curated catalog of plugins (both
201
+ // `component` and `integration` kinds). The catalog is a static, checked-in
202
+ // manifest read server-side; each entry is cross-referenced against the
203
+ // installed plugin set to annotate its install / update state.
204
+ //
205
+ // Channel integrity (CP-FR-021, issue #622): the catalog is wrapped in a signed
206
+ // envelope (a detached ed25519 signature over the canonical payload bytes,
207
+ // verified against a bundled first-party public key) and every entry carries an
208
+ // expected content `integrity` digest plus an optional `revoked` flag. The
209
+ // `verified` flag is a display-only first-party curation marker, distinct from
210
+ // the cryptographic signature: a card shows "Verified" when `verified` is true
211
+ // AND the catalog signature validated. There is no third-party submission path.
212
+
213
+ /**
214
+ * Plugin kinds surfaced by the marketplace. Mirrors the host's plugin-manifest
215
+ * `kind` discriminator; restated here so `shared` consumers don't reach into
216
+ * the manifest schema for the marketplace UI.
217
+ */
218
+ export type MarketplaceKind = "component" | "integration";
219
+
220
+ /**
221
+ * One curated catalog entry as authored in the static manifest. The `source`
222
+ * is the git URL the install/update flow clones from; its optional `directory`
223
+ * names the subdirectory of the cloned repository that holds the plugin package
224
+ * (the monorepo-subdir source model, issue #750), so a component published
225
+ * inside a monorepo (e.g. `plugins/process`) stages and installs just that
226
+ * subdirectory rather than the whole repo. `verified` is the display-only
227
+ * first-party curation flag.
228
+ *
229
+ * `integrity` is the expected content digest of the staged package
230
+ * (`sha256-<hex>`); after staging and before commit, the installer recomputes
231
+ * the staged package digest and rejects a mismatch (CP-FR-021). `provenance` is
232
+ * the registry path shown in the detail drawer. `revoked` marks a withdrawn /
233
+ * taken-down entry: it is filtered out of `listCatalog` and rejected by
234
+ * install / update.
235
+ */
236
+ export interface MarketplaceCatalogEntry {
237
+ id: string;
238
+ name: string;
239
+ kind: MarketplaceKind;
240
+ version: string;
241
+ summary: string;
242
+ source: { type: "git"; url: string; directory?: string };
243
+ provenance: string;
244
+ integrity: string;
245
+ revoked?: boolean;
246
+ verified: boolean;
247
+ }
248
+
249
+ /**
250
+ * The signed catalog envelope as authored in the static manifest. `signature`
251
+ * is a base64-encoded detached ed25519 signature over the canonical bytes of
252
+ * `payload` (see the server's marketplace-integrity service). The server
253
+ * verifies the signature at load and fails closed (zero listings) on any
254
+ * mismatch.
255
+ */
256
+ export interface SignedMarketplaceCatalog {
257
+ payload: { entries: MarketplaceCatalogEntry[] };
258
+ signature: string;
259
+ }
260
+
261
+ /**
262
+ * A catalog entry annotated with the consumer's local install state. Returned
263
+ * by `GET /api/marketplace/plugins`. `installed` reflects whether a plugin with
264
+ * this id is present in `listInstalled()`; `installedVersion` is its on-disk
265
+ * manifest version (null when the manifest is unreadable); `updateAvailable` is
266
+ * true when the catalog version is strictly newer than the installed version.
267
+ */
268
+ export interface MarketplaceListing extends MarketplaceCatalogEntry {
269
+ installed: boolean;
270
+ installedVersion: string | null;
271
+ updateAvailable: boolean;
272
+ }
273
+
274
+ /** Response shape for `GET /api/marketplace/plugins`. */
275
+ export interface MarketplaceCatalogResponse {
276
+ curated: true;
277
+ listings: MarketplaceListing[];
278
+ }
279
+
280
+ /**
281
+ * Error body returned by `GET /api/marketplace/plugins` when the static catalog
282
+ * fails signature verification (CP-FR-021). The server fails closed: it returns
283
+ * this typed error (HTTP 502) with zero listings rather than a silent empty
284
+ * success, so the client can render an unverified-catalog error and render no
285
+ * plugin cards (CP-TC-118). Distinct from a transport / registry-unavailable
286
+ * failure, which surfaces as a generic load error (CP-TC-106).
287
+ */
288
+ export interface MarketplaceCatalogErrorBody {
289
+ error: string;
290
+ code: "catalog-unverified";
291
+ }
292
+
293
+ /**
294
+ * Tells the Issue source tile which caption to render under the configured
295
+ * variant. Derived server-side from the committed roubo.yaml integration block
296
+ * and the per-user override.
297
+ */
298
+ export type IntegrationCaptionKey = "yaml-only" | "override-only" | "yaml-and-override" | "none";
299
+
300
+ /**
301
+ * Snapshot of a project's effective integration state, returned by
302
+ * `GET /api/projects/:projectId/integration`. The tile consumes this directly
303
+ * and routes to one of three variants:
304
+ * - `plugin == null` → unconfigured
305
+ * - `plugin != null && !plugin.installed` → missing-plugin
306
+ * - otherwise → configured
307
+ */
308
+ export interface ProjectIntegrationState {
309
+ effective: IntegrationConfig;
310
+ committed: IntegrationConfig | null;
311
+ override: IntegrationConfig | null;
312
+ plugin: {
313
+ id: string;
314
+ installed: boolean;
315
+ status: PluginStatus | null;
316
+ manifest: {
317
+ name: string;
318
+ /**
319
+ * JSON-Schema-derived shape describing the per-project config form.
320
+ * Opaque to roubo; rendered by the client's ConfigSchemaForm.
321
+ */
322
+ configSchema?: Record<string, unknown>;
323
+ /**
324
+ * Full declared permissions for the plugin. The Configure dialog reads
325
+ * `credentials.slots[*].description` to label password fields.
326
+ */
327
+ permissions?: PluginPermissions;
328
+ /**
329
+ * Plugin-global default integration config (FR-064). The Configure dialog
330
+ * reads `excludedStatusCategories` to seed and gate the status-category
331
+ * exclusion control without a second fetch.
332
+ */
333
+ defaultIntegrationConfig?: PluginDefaultIntegrationConfig;
334
+ } | null;
335
+ } | null;
336
+ captionKey: IntegrationCaptionKey;
337
+ /**
338
+ * Set when the committed roubo.yaml resolves to a different integration than
339
+ * the effective (override-resolved) one, along either axis that changes which
340
+ * host a teammate would reach: the `plugin` id, or (for multi-instance
341
+ * plugins like `ghe`) the `instance`. The integration works locally because
342
+ * the per-user override wins, but a teammate cloning the repo would resolve
343
+ * the committed values instead, against a host they likely cannot reach. The
344
+ * tile surfaces this and offers to promote the effective integration into the
345
+ * committed config. `null` when committed and effective agree, or the
346
+ * committed config names no plugin.
347
+ */
348
+ integrationMismatch?: {
349
+ committedPlugin: string;
350
+ effectivePlugin: string;
351
+ committedInstance: string | null;
352
+ effectiveInstance: string | null;
353
+ } | null;
354
+ }
355
+
356
+ /**
357
+ * Result of GET /api/plugins/:pluginId/integration: the global-defaults
358
+ * read used by the Plugins settings page's Configure dialog. Mirrors the
359
+ * `plugin` and `effective` shape of `ProjectIntegrationState` so the
360
+ * existing dialog can seed itself the same way, but omits the
361
+ * committed/override/captionKey fields that only make sense in a project
362
+ * scope.
363
+ */
364
+ export interface GlobalPluginIntegrationState {
365
+ effective: IntegrationConfig;
366
+ plugin: NonNullable<ProjectIntegrationState["plugin"]>;
367
+ }
368
+
369
+ /**
370
+ * Classifier for plugin-reported test-connection failures. The host
371
+ * translates raw plugin error strings into one of these kinds so the
372
+ * Configure dialog can render the right result-strip variant and the
373
+ * inline "Enable self-signed TLS" recovery affordance (TC-060/061/062).
374
+ */
375
+ export type IntegrationTestErrorKind = "auth" | "network" | "tls" | "other";
376
+
377
+ export interface IntegrationTestErrorPayload {
378
+ kind: IntegrationTestErrorKind;
379
+ message: string;
380
+ }
381
+
382
+ /**
383
+ * Stable identifier for a row in the Test Connection result strip. `"issues"`
384
+ * is always present on a successful test; the three alert categories appear
385
+ * only when the project has at least one source with that category enabled.
386
+ */
387
+ export type IntegrationCategoryId = "issues" | "code-scanning" | "secret-scanning" | "dependabot";
388
+
389
+ /**
390
+ * Per-row status in the Test Connection result strip (FR-047, WU-041, WU-034).
391
+ *
392
+ * - `ok`: probe succeeded
393
+ * - `scope-missing`: token lacks the required scope (401/403)
394
+ * - `not-enabled`: feature is not enabled for the probed repo (404/410/451)
395
+ * - `timed-out`: probe exceeded the per-probe cap (rendered amber; does not
396
+ * fail the overall test)
397
+ * - `error`: probe returned an unexpected status or threw a non-timeout error
398
+ */
399
+ export type IntegrationCategoryStatus =
400
+ | "ok"
401
+ | "scope-missing"
402
+ | "not-enabled"
403
+ | "timed-out"
404
+ | "error";
405
+
406
+ /**
407
+ * One row in the Test Connection result strip. `label` is the human-facing
408
+ * string the strip renders (sourced from `INTEGRATION_CATEGORY_LABELS` below).
409
+ * `detail` is rendered verbatim
410
+ * underneath the row when present (e.g. "Timed out", "Token missing
411
+ * `security_events` scope"). `httpStatus` is included for diagnostics and
412
+ * tests; the UI does not render it directly.
413
+ */
414
+ export interface IntegrationCategoryReport {
415
+ category: IntegrationCategoryId;
416
+ label: string;
417
+ status: IntegrationCategoryStatus;
418
+ detail?: string;
419
+ httpStatus?: number;
420
+ }
421
+
422
+ /**
423
+ * Response shape for `POST /api/projects/:projectId/integration/test` and
424
+ * `POST /api/plugins/:pluginId/integration/test`. On `ok: true`, `identity`
425
+ * carries the value returned by `plugin.getCurrentUser`, which the dialog
426
+ * stashes and submits as `capturedUserId` when the user saves. `categories`
427
+ * drives the per-row Test Connection result strip (WU-041); the host always
428
+ * emits at least an Issues row on success, and adds alert-category rows for
429
+ * each category enabled by at least one saved source. Omitted on the failure
430
+ * variant.
431
+ */
432
+ export type IntegrationTestResult =
433
+ | { ok: true; identity: CapturedUserId; categories?: IntegrationCategoryReport[] }
434
+ | { ok: false; error: IntegrationTestErrorPayload };
435
+
436
+ /**
437
+ * Human-facing labels for each Test Connection result-strip row. Kept in
438
+ * `shared/` so client, server, and plugin code all read from one place.
439
+ */
440
+ export const INTEGRATION_CATEGORY_LABELS: Record<IntegrationCategoryId, string> = {
441
+ issues: "Issues",
442
+ "code-scanning": "Code Scanning alerts",
443
+ "secret-scanning": "Secret Scanning alerts",
444
+ dependabot: "Dependabot alerts",
445
+ };
446
+
447
+ /**
448
+ * Discrete states a plugin's connection can be in. Drives the
449
+ * `ConnectionStatusPill` taxonomy (mockups §21) and will back the
450
+ * `getConnectionStatus()` plugin RPC (FR-055) when that lands in a later WU.
451
+ *
452
+ * - `connected`: healthy, last check succeeded
453
+ * - `disconnected`: plugin is enabled but no credentials are configured
454
+ * - `auth-problem`: token expired / 401 / re-auth needed
455
+ * - `errored`: rate-limited, unreachable, crashed, or never-checked
456
+ * - `disabled`: plugin not enabled; never reflects connectivity
457
+ */
458
+ export type ConnectionState =
459
+ | "connected"
460
+ | "disconnected"
461
+ | "auth-problem"
462
+ | "errored"
463
+ | "disabled";
464
+
465
+ /**
466
+ * Cached connection-status snapshot for a plugin. `detail` is surfaced in the
467
+ * pill's tooltip on `auth-problem` and `errored`. `checkedAt` is an ISO
468
+ * timestamp the pill renders as an "as of HH:MM" suffix; omit for the
469
+ * `disabled` variant (which never carries a timestamp).
470
+ */
471
+ export interface ConnectionStatus {
472
+ state: ConnectionState;
473
+ detail?: string;
474
+ checkedAt?: string;
475
+ // Present on `connected` when the plugin can cheaply resolve the
476
+ // authenticated account (e.g. from the same `GET /user` probe). Drives the
477
+ // "Connected as <login>" label in the Configure dialog; omitted otherwise.
478
+ account?: { login: string };
479
+ }
480
+
481
+ /**
482
+ * Body shape for `PUT /api/projects/:projectId/integration/config`. Every key
483
+ * is optional; provided keys replace their counterpart in the existing
484
+ * override (per FR-023's "arrays REPLACE" rule, which extends to objects
485
+ * here since the override is shallow per top-level key).
486
+ */
487
+ export interface IntegrationConfigUpdate {
488
+ instance?: string;
489
+ sources?: Record<string, Array<string | number>>;
490
+ advanced?: Record<string, unknown>;
491
+ capturedUserId?: CapturedUserId;
492
+ /**
493
+ * Root-level status-category exclusion (FR-010), editable from the Configure
494
+ * dialog. Shallow-replaces the committed value in the per-project override;
495
+ * an empty array means "exclude nothing", distinct from omitting the key
496
+ * (which leaves the existing override untouched).
497
+ */
498
+ excludedStatusCategories?: string[];
499
+ }
500
+
501
+ /**
502
+ * Response of `GET /integration/status-categories` (issue #453). `supported` is
503
+ * true only when the active plugin's discovery RPC returned; on any failure
504
+ * (no active plugin, discovery unimplemented, network / auth error) the host
505
+ * returns `{ supported: false, categories: [] }` so the Configure dialog falls
506
+ * back to its canonical status-category set.
507
+ */
508
+ export interface StatusCategoriesResponse {
509
+ supported: boolean;
510
+ categories: string[];
511
+ }
512
+
513
+ /**
514
+ * Project-scoped fields owned by the active integration plugin (FR-070).
515
+ * Returned by `GET /api/projects/:projectId/integration/fields`. Stored
516
+ * canonically in roubo.yaml; the plugin's Configure modal is the edit
517
+ * surface and the legacy config/raw PUT shims forward into the setter.
518
+ */
519
+ export interface IntegrationFields {
520
+ repo?: string;
521
+ githubProject?: number;
522
+ submodules?: Record<string, string>;
523
+ /** Echoed so the client can hide meta-repo-only controls without a second fetch. */
524
+ layoutType?: "meta-repo" | "monorepo" | "single-repo";
525
+ }
526
+
527
+ /**
528
+ * Body shape for `PUT /api/projects/:projectId/integration/fields`. Each key
529
+ * is optional; provided keys overwrite their counterpart, `undefined` keys
530
+ * are left alone, and an explicit `null` clears the field in roubo.yaml.
531
+ */
532
+ export interface IntegrationFieldsUpdate {
533
+ repo?: string | null;
534
+ githubProject?: number | null;
535
+ submodules?: Record<string, string> | null;
536
+ }
537
+
538
+ /**
539
+ * Serializable subset of `PluginRecord` returned by `GET /api/plugins`. Drives
540
+ * the radio list inside the Switch integration dialog.
541
+ */
542
+ export interface InstalledPluginSummary {
543
+ id: string;
544
+ name: string;
545
+ status: PluginStatus;
546
+ lastError?: string;
547
+ isolationNotices?: IsolationNotice[];
548
+ }
549
+
550
+ export const DONE_STATUSES = new Set(["done", "closed", "archived", "cancelled"]);
551
+
552
+ // ── Project registry types ──
553
+
554
+ /**
555
+ * Per-project settings. Keep this shape extensible: new per-project features
556
+ * (e.g. per-project jig defaults, per-project Claude Code overrides)
557
+ * should be added as new top-level keys alongside `worktreeSource`, not
558
+ * nested inside it. A missing `settings` key in persisted state is
559
+ * interpreted as all defaults. See `project-registry.ts` for the
560
+ * "missing = on" defaulting rule (R4).
561
+ */
562
+ export interface ProjectSettings {
563
+ worktreeSource: {
564
+ branchFromDefault: boolean;
565
+ pullLatest: boolean;
566
+ };
567
+ }
568
+
569
+ /**
570
+ * Default `ProjectSettings` used when persisted state has no `settings` key.
571
+ * NOTE: both toggles default to `true`, which is the OPPOSITE of the usual
572
+ * "missing field = false" convention. See R4 in
573
+ * `specs/prd-worktree-source-settings.md`.
574
+ */
575
+ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
576
+ worktreeSource: {
577
+ branchFromDefault: true,
578
+ pullLatest: true,
579
+ },
580
+ };
581
+
582
+ /**
583
+ * GET /:projectId/settings response: persisted ProjectSettings plus the
584
+ * server-computed default branch (or the R1 error if detection fails).
585
+ * These two extras are read-only: never persisted, never accepted by PUT.
586
+ */
587
+ export interface ProjectSettingsResponse extends ProjectSettings {
588
+ defaultBranch?: string;
589
+ defaultBranchError?: string;
590
+ }
591
+
592
+ export interface RegisteredProject {
593
+ id: string;
594
+ repoPath: string;
595
+ config?: RouboConfig;
596
+ configValid: boolean;
597
+ configError?: string;
598
+ settings: ProjectSettings;
599
+ }
600
+
601
+ // ── Bench types ──
602
+
603
+ export type BenchStatus = "idle" | "preparing" | "active" | "error" | "clearing";
604
+ export type ComponentStatusValue =
605
+ | "stopped"
606
+ | "starting"
607
+ | "running"
608
+ | "error"
609
+ | "stopping"
610
+ | "completed";
611
+
612
+ export type ProvisioningStepStatus = "pending" | "running" | "done" | "error" | "cancelled";
613
+
614
+ export interface ProvisioningStep {
615
+ id: string;
616
+ label: string;
617
+ status: ProvisioningStepStatus;
618
+ error?: string;
619
+ phases?: ComponentPhase[];
620
+ }
621
+
622
+ export type ComponentPhaseStatus = "pending" | "running" | "done" | "error";
623
+
624
+ export interface ComponentPhase {
625
+ label: string;
626
+ status: ComponentPhaseStatus;
627
+ }
628
+
629
+ /**
630
+ * A component's observable lifecycle state, pushed by the host (never polled,
631
+ * NFR-002). Most components rest in `running` or `stopped`; `completed` is the
632
+ * one-shot terminal state (FR-014 / FR-022): a run-to-completion descriptor that
633
+ * exits 0 is neither `stopped` (idle, never started) nor `error` (failed), so it
634
+ * lands in its own distinct terminal state. A non-zero exit or a `timeoutMs`
635
+ * breach drives `error` instead.
636
+ */
637
+ export interface ComponentStatus {
638
+ name: string;
639
+ status: ComponentStatusValue;
640
+ pid?: number;
641
+ containerId?: string;
642
+ error?: string;
643
+ statusDetail?: string;
644
+ statusDetailStartedAt?: string;
645
+ startedAt?: string;
646
+ phases?: ComponentPhase[];
647
+ /**
648
+ * True once the component's `setup` command has run successfully on this
649
+ * bench, or trivially true if the component config defines no setup. Lets a
650
+ * subsequent Start skip re-running setup after a Stop → Start cycle.
651
+ */
652
+ setupComplete: boolean;
653
+ }
654
+
655
+ /**
656
+ * A single log line a component plugin pushes to the host via
657
+ * `host.component.reportLog`. Push-based so the host never polls the plugin for
658
+ * logs (NFR-002).
659
+ */
660
+ export interface ComponentLogLine {
661
+ source: "stdout" | "stderr";
662
+ text: string;
663
+ ts: string;
664
+ }
665
+
666
+ /**
667
+ * The permission categories the broker enforces (F2.1, #618). Every broker
668
+ * method maps to one of these; a call whose category the plugin did not declare
669
+ * is denied with a permission-denied error before reaching the host delegate.
670
+ */
671
+ export type BrokerPermissionCategory = "process" | "docker" | "ports";
672
+
673
+ /**
674
+ * Structured payload carried by a broker permission-denied error
675
+ * (PERMISSION_DENIED_CODE, -32001). Mirrors FilesystemPermissionDeniedData /
676
+ * ProcessesPermissionDeniedData so every host surface reports denials in the
677
+ * same shape. `method` is the broker method that was denied; `reason` is fixed
678
+ * (the plugin did not declare the category the method needs).
679
+ */
680
+ export interface BrokerPermissionDeniedData {
681
+ code: "permission-denied";
682
+ category: BrokerPermissionCategory;
683
+ method: string;
684
+ reason: "category-not-declared";
685
+ }
686
+
687
+ /**
688
+ * One record of a privileged HostComponentBroker call (FR-019, v2 audit). The
689
+ * AuditLog appends one entry per gated method invocation (host.process.*,
690
+ * host.docker.*, host.ports.get): `outcome` is "allowed" when the plugin held
691
+ * the permission category, "denied" when it did not. `params` captures the raw
692
+ * incoming arguments (recorded before per-param validation, so a denied or
693
+ * early-rejected call still has its params logged). The ungated
694
+ * host.component.report* and host.capability.query methods are not privileged
695
+ * and produce no entry.
696
+ */
697
+ export interface AuditEntry {
698
+ /** ISO-8601 timestamp of when the call was recorded. */
699
+ ts: string;
700
+ /** The plugin that made the call. */
701
+ pluginId: string;
702
+ /** The bench the call was scoped to. */
703
+ benchId: number;
704
+ /** The broker method name (e.g. "host.process.start"). */
705
+ method: string;
706
+ /** The raw incoming params, captured before validation. */
707
+ params: unknown;
708
+ /** Whether the plugin held the required permission category. */
709
+ outcome: "allowed" | "denied";
710
+ /**
711
+ * Where the entry was attributed (F2.3, #620). Omitted (or "broker") for the
712
+ * always-on broker choke-point; "sandbox" for an OS-layer denial the
713
+ * PluginIsolationSandbox could attribute to the plugin (e.g. an undeclared
714
+ * outbound connection blocked at the container/VM boundary). The broker and
715
+ * the OS tier share one audit shape so a query returns both in one stream.
716
+ */
717
+ source?: "broker" | "sandbox";
718
+ }
719
+
720
+ /**
721
+ * One record of a privileged gate-lifecycle plugin call (FR-007, NFR-001). The
722
+ * broker `AuditEntry` above is bench-scoped (it carries a `benchId` and a
723
+ * `host.*` broker `method`), but a gate close is project- and gate-scoped: it
724
+ * has no bench, and it routes through the integration plugin's
725
+ * `applyTransition` RPC rather than the HostComponentBroker. Rather than overload
726
+ * the bench-scoped shape, the GateLifecycleCoordinator records this dedicated
727
+ * entry: which gate's tracker issue was transitioned, via which plugin and
728
+ * transition, and whether the privileged call was applied or skipped (the
729
+ * already-done idempotent no-op).
730
+ */
731
+ export interface GateAuditEntry {
732
+ /** ISO-8601 timestamp of when the call was recorded. */
733
+ ts: string;
734
+ /** The project the gate belongs to. */
735
+ projectId: string;
736
+ /** The integration plugin the transition was routed through. */
737
+ pluginId: string;
738
+ /** The verify unit (gate) whose tracker issue was acted on. */
739
+ gateId: string;
740
+ /** The gate's tracker issue ref (issue number / key) that was transitioned. */
741
+ trackerRef: string;
742
+ /** The plugin transition name applied (e.g. "close"); omitted when skipped. */
743
+ transitionName?: string;
744
+ /**
745
+ * "closed" when a done-bound transition was applied; "already-done" when the
746
+ * issue was already in a done state and the close was an idempotent no-op.
747
+ */
748
+ outcome: "closed" | "already-done";
749
+ }
750
+
751
+ /**
752
+ * One record of a privileged tracker-action plugin call routed through the
753
+ * TrackerActionGateway (verify-gate FR-011, NFR-001, NFR-005; #705). These ops
754
+ * (create-issue, add-blocking-link, close-gate) are project-scoped, not
755
+ * bench-scoped, so they do not fit the bench-scoped broker `AuditEntry` (which
756
+ * carries a `benchId` and a `host.*` method); and unlike the gate-close-only
757
+ * `GateAuditEntry` they cover create / link too and must record the
758
+ * capability-refused attempt. The gateway records one entry per attempt: an
759
+ * "applied" outcome for a performed op (including a close-on-pass whose
760
+ * underlying issue was already done, since `onGatePassed` returns void and the
761
+ * gateway cannot observe that idempotent no-op; the already-done nuance is
762
+ * captured at gate granularity by `GateAuditEntry`), a "skipped" outcome when
763
+ * there is nothing to do (a close-gate for a gate with no filed tracker issue),
764
+ * and a "refused" outcome when a missing capability or absent consent blocked
765
+ * the call before it reached the plugin. No tracker tokens or secrets are ever
766
+ * placed on this entry (NFR-001).
767
+ */
768
+ export interface TrackerActionAuditEntry {
769
+ /** ISO-8601 timestamp of when the call was recorded. */
770
+ ts: string;
771
+ /** The project the action was scoped to. */
772
+ projectId: string;
773
+ /** The integration plugin the action was routed through. */
774
+ pluginId: string;
775
+ /** The privileged op attempted. */
776
+ action: "createIssue" | "addBlockedBy" | "closeGate";
777
+ /**
778
+ * Whether the privileged op was performed ("applied"; for close-gate this also
779
+ * covers the case where `onGatePassed` found the issue already done, an
780
+ * idempotent no-op the void-returning call cannot distinguish here), there was
781
+ * nothing to do ("skipped", e.g. a close-gate for a gate with no filed tracker
782
+ * issue), or it was blocked before the plugin call by a missing capability or
783
+ * absent consent ("refused").
784
+ */
785
+ outcome: "applied" | "skipped" | "refused";
786
+ /**
787
+ * Present on a "refused" outcome: the legible reason the op was blocked (e.g.
788
+ * "capability supportsCreateIssue not declared" or "plugin not consented").
789
+ * Never carries a token or secret.
790
+ */
791
+ reason?: string;
792
+ /**
793
+ * Non-secret op identifiers for traceability (issue refs, gate id). The
794
+ * gateway only ever populates this with public refs, never credentials.
795
+ */
796
+ refs?: Record<string, string>;
797
+ }
798
+
799
+ /**
800
+ * Whether the fix-issue filer completed both of its steps. `complete` means the
801
+ * fix issue was created AND registered as a blocker on the gate. `link_pending`
802
+ * means the issue was created but the block-link step failed afterwards, so the
803
+ * partial state is surfaced for a link-only retry (verify-gate FR-009, FR-010,
804
+ * NFR-003; #706). The gate is never falsely passable in either state: the failed
805
+ * gating case keeps it non-passable regardless of the link's outcome.
806
+ */
807
+ export type FixIssueLinkStatus = "complete" | "link_pending";
808
+
809
+ /**
810
+ * The per-request outcome of filing a fix issue for a failed gating case and
811
+ * wiring it to block the gate (verify-gate FR-009, FR-010, NFR-003; #706).
812
+ *
813
+ * The filer is create-then-link: it creates the tracker issue, then registers it
814
+ * as a blocker on the gate. When the link step fails after the issue is created,
815
+ * the record returns `linkStatus: 'link_pending'` carrying the created
816
+ * `fixIssueRef`, so a link-only retry (driven by `existingFixRef` on the request)
817
+ * covers only the outstanding link step rather than creating a duplicate issue.
818
+ * This record is per-request and never persisted: the durable blocking state
819
+ * lives in the tracker (`tracker.blocked_by_refs` is its derived projection), and
820
+ * the gate's passability is decided by the pure evaluator over the recorded case
821
+ * results, not by this record.
822
+ */
823
+ export interface FixIssueRecord {
824
+ /** The created fix issue's external tracker ref (e.g. "owner/repo#452"). */
825
+ fixIssueRef: string;
826
+ /** The gate's tracker ref the fix issue was wired to block (e.g. "owner/repo#451"). */
827
+ gateRef: string;
828
+ /** The failed gating case id the fix issue was filed for (e.g. "TC-024"). */
829
+ failedCaseId: string;
830
+ /** Whether both steps completed, or the link step is still pending a retry. */
831
+ linkStatus: FixIssueLinkStatus;
832
+ /** ISO-8601 timestamp of when the record was produced. */
833
+ createdAt: string;
834
+ }
835
+
836
+ /**
837
+ * Request body for `POST /api/projects/:projectId/gates/:gateId/fix-issues`
838
+ * (verify-gate FR-009, FR-010, NFR-001, NFR-003; #706). `notes` is required and
839
+ * must be non-empty (empty notes are rejected with a 422). `evidence` is an
840
+ * optional in-workspace relative path for a notes artifact, confined by the
841
+ * `resolveWithin` safe-path barrier (a path-escaping value is rejected). The
842
+ * optional `existingFixRef` drives the link-only retry: when set, the filer skips
843
+ * the create step and runs only the block-link step against that already-created
844
+ * ref (NFR-003).
845
+ */
846
+ export interface FileFixIssueRequest {
847
+ /** The failed gating case the fix issue is filed for. */
848
+ failedCaseId: string;
849
+ /** The verifier's failure notes. Required and non-empty. */
850
+ notes: string;
851
+ /** Optional in-workspace relative path for a notes artifact (safe-path confined). */
852
+ evidence?: string;
853
+ /** Optional already-created fix issue ref, to run only the link step (NFR-003). */
854
+ existingFixRef?: string;
855
+ }
856
+
857
+ /**
858
+ * The OS-isolation tiers the PluginIsolationSandbox can place a component plugin
859
+ * process inside (F2.3, #620; backend chosen by SPK-2 / spike #599). Ordered
860
+ * highest-isolation-first: `vz-vm` (Virtualization.framework per-plugin VM) is
861
+ * the strongest, then `apple-container` (the macOS 15+ Apple container
862
+ * framework), then `docker` (container-per-plugin where a Docker engine is
863
+ * already present), degrading to the `broker-only` floor where no isolation
864
+ * runtime is available. The floor carries no isolation-attributable overhead and
865
+ * is always selectable, so enforcement never depends on Docker (FR-018).
866
+ */
867
+ export type IsolationTier = "vz-vm" | "apple-container" | "docker" | "broker-only";
868
+
869
+ /**
870
+ * Which OS-isolation runtimes the host can actually drive (F2.3, #620). Probed
871
+ * at runtime via the NFR-005 host-capability gate, never assumed: a host without
872
+ * any runtime degrades to the `broker-only` floor. Each flag is true only when
873
+ * the runtime is present AND usable (e.g. the Docker daemon is reachable, not
874
+ * merely installed).
875
+ */
876
+ export interface IsolationCapabilities {
877
+ /** Virtualization.framework is present and a per-plugin VM can be driven. */
878
+ vzVm: boolean;
879
+ /** The Apple `container` framework (macOS 15+, Apple silicon) is present. */
880
+ appleContainer: boolean;
881
+ /** A Docker engine is installed and the daemon is reachable. */
882
+ docker: boolean;
883
+ }
884
+
885
+ /**
886
+ * The egress policy the sandbox applies to a plugin process, derived from the
887
+ * manifest's `permissions.network` declaration (F2.3, #620). When the plugin
888
+ * declares no network hosts, the sandbox denies all egress (`mode: "deny-all"`)
889
+ * so an undeclared outbound connection is blocked at the OS layer: there is no
890
+ * `host.network.*` broker method, so undeclared egress can only be stopped
891
+ * below the broker. When hosts are declared, the policy carries that allowlist
892
+ * for the runtime to apply.
893
+ */
894
+ export interface SandboxEgressPolicy {
895
+ mode: "deny-all" | "allow-listed";
896
+ /** Declared network hosts (empty when `mode` is "deny-all"). */
897
+ allowedHosts: string[];
898
+ }
899
+
900
+ /**
901
+ * The concrete spawn the host should perform to run a plugin under a non-floor
902
+ * isolation tier (F2.3, #620). `command` + `args` replace the direct
903
+ * `spawn(process.execPath, [entry])`. For the `docker` tier the shape depends
904
+ * on the egress policy:
905
+ *
906
+ * - deny-all (no declared network hosts):
907
+ * `docker run --rm -i --network none -v <pluginDir>:/roubo-plugin:ro
908
+ * -w /roubo-plugin node:24-slim node /roubo-plugin/<entryRel>`
909
+ *
910
+ * - allow-listed (declared hosts present):
911
+ * `docker run --rm -i --cap-add NET_ADMIN
912
+ * -e ROUBO_ALLOWED_HOSTS=<comma-separated hosts>
913
+ * -v <pluginDir>:/roubo-plugin:ro -w /roubo-plugin
914
+ * roubo-plugin-egress:node24 sh -c '<iptables-setup>; exec node /roubo-plugin/<entryRel>'`
915
+ *
916
+ * `env` is merged over the base spawn env. `egress` is the derived network
917
+ * policy. The `broker-only` floor produces no SandboxedSpawn; the host spawns
918
+ * the plugin directly.
919
+ */
920
+ export interface SandboxedSpawn {
921
+ command: string;
922
+ args: string[];
923
+ env: Record<string, string>;
924
+ egress: SandboxEgressPolicy;
925
+ }
926
+
927
+ /**
928
+ * Per-bench data the HostComponentBroker needs to service a component plugin's
929
+ * privileged calls. Injected at broker construction so handlers never reach into
930
+ * globals: ports are pre-resolved host-side, status and log reporting are
931
+ * push-based sinks, and the enforced permission check is supplied by the caller.
932
+ */
933
+ export interface BrokerContext {
934
+ /** The plugin this broker serves; stamped onto every audit entry. */
935
+ pluginId: string;
936
+ /** The bench this broker is scoped to; stamped onto every audit entry. */
937
+ benchId: number;
938
+ /** Host-allocated ports for this bench, keyed by component name. */
939
+ ports: Record<string, number>;
940
+ /** Push sink invoked by `host.component.reportStatus`. */
941
+ reportStatus: (status: ComponentStatus) => void;
942
+ /**
943
+ * Push sink invoked by `host.component.reportLog`. The `componentName` is the
944
+ * one the call named in its params, so a bench with two plugin-bound
945
+ * components routes each component's output to its own log instead of
946
+ * overwriting whichever provisioned last (#685).
947
+ */
948
+ reportLog: (componentName: string, line: ComponentLogLine) => void;
949
+ /**
950
+ * Permission check. Returns false when the plugin did not declare a category.
951
+ * The broker denies any call whose category returns false with a
952
+ * permission-denied error, before delegating to the host (F2.1, #618).
953
+ */
954
+ hasPermission: (category: BrokerPermissionCategory) => boolean;
955
+ /**
956
+ * Audit sink invoked for every privileged broker call (FR-019). Records the
957
+ * call's outcome (allowed or denied) into the AuditLog.
958
+ */
959
+ recordAudit: (entry: AuditEntry) => void;
960
+ /** Records an externally-assigned container against a component. */
961
+ assignContainer?: (componentName: string, containerId: string) => void;
962
+ }
963
+
964
+ /**
965
+ * Result of `host.capability.query`. For a known method `available` is true and
966
+ * `introducedIn` carries the broker API version that first shipped it; for an
967
+ * unknown or future method `available` is false and `introducedIn` is omitted.
968
+ * The query never produces a host-side error (FR-017).
969
+ */
970
+ export interface CapabilityQueryResult {
971
+ available: boolean;
972
+ introducedIn?: string;
973
+ }
974
+
975
+ export interface Bench {
976
+ id: number;
977
+ projectId: string;
978
+ branch: string;
979
+ workspacePath: string;
980
+ status: BenchStatus;
981
+ ports: Record<string, number>;
982
+ components: Record<string, ComponentStatus>;
983
+ createdAt: string;
984
+ error?: string;
985
+ provisioningSteps: ProvisioningStep[];
986
+ teardownSteps: ProvisioningStep[];
987
+ assignedContainers?: Record<string, AssignedContainer>;
988
+ assignedIssue?: AssignedIssue;
989
+ notifications: BenchNotification[];
990
+ /**
991
+ * The branch the worktree was cut from (e.g. "main"). Captured at
992
+ * provisioning time. Absent on benches created before this field existed.
993
+ */
994
+ baseBranch?: string;
995
+ /**
996
+ * The 7-character short SHA of HEAD in the new worktree immediately after
997
+ * `git worktree add`. Absent on legacy benches or if rev-parse failed.
998
+ */
999
+ baseCommit?: string;
1000
+ /**
1001
+ * The ID of the jig that was auto-injected when this bench was created
1002
+ * via an issue assignment. Absent on benches created without issue assignment
1003
+ * or when auto-injection was disabled.
1004
+ */
1005
+ injectedJigId?: string;
1006
+ /**
1007
+ * Where the injected jig came from in the resolution hierarchy.
1008
+ * Always `undefined` when `injectedJigId` is `undefined`.
1009
+ */
1010
+ injectedJigSource?: JigDefaultSource;
1011
+ /**
1012
+ * Bench variant discriminator. Absent (`undefined`) means a normal bench;
1013
+ * `'testbench'` marks a TestBench variant, which surfaces the TestBench tab
1014
+ * and binds a focused spec. Normal benches never carry this field, so they
1015
+ * are unaffected by TestBench behaviour.
1016
+ */
1017
+ variant?: "testbench";
1018
+ /**
1019
+ * Absolute path to the spec the TestBench is currently focused on. Mutable
1020
+ * and re-pointable: a TestBench can be retargeted at a different spec over
1021
+ * its lifetime. Only meaningful when `variant === 'testbench'`. Re-validated
1022
+ * with resolveWithin when loaded (enforcement lives in the testbench store).
1023
+ */
1024
+ focusedSpecPath?: string;
1025
+ }
1026
+
1027
+ // ── Git dirty-state types ──
1028
+
1029
+ export type DirtyReasonKind =
1030
+ | "dirty-worktree"
1031
+ | "stash"
1032
+ | "unpushed-commits"
1033
+ | "no-upstream"
1034
+ | "local-only-after-merge";
1035
+
1036
+ /**
1037
+ * A single reason a bench is not safe to tear down.
1038
+ * `location` is `'workspace'` for the main worktree, or the submodule's
1039
+ * `$displaypath` (relative to the superproject root) for a submodule.
1040
+ * `detail` is a short human-readable qualifier, e.g. "3 modified, 1 untracked",
1041
+ * "1 stash", "2 commits ahead".
1042
+ */
1043
+ export interface DirtyReason {
1044
+ kind: DirtyReasonKind;
1045
+ location: string;
1046
+ detail: string;
1047
+ }
1048
+
1049
+ export interface DirtyState {
1050
+ clean: boolean;
1051
+ reasons: DirtyReason[];
1052
+ }
1053
+
1054
+ // ── Notification types ──
1055
+
1056
+ export type NotificationType =
1057
+ | "claude-exited"
1058
+ | "claude-waiting"
1059
+ | "terminal-waiting"
1060
+ | "bench-ready"
1061
+ | "bench-error"
1062
+ | "inspection-complete"
1063
+ | "component-error";
1064
+
1065
+ export type NotificationPriority = "info" | "action-needed";
1066
+
1067
+ export interface BenchNotification {
1068
+ id: string;
1069
+ type: NotificationType;
1070
+ priority: NotificationPriority;
1071
+ sourceSessionId?: string;
1072
+ metadata?: Record<string, unknown>;
1073
+ createdAt: string;
1074
+ }
1075
+
1076
+ // ── Resolved tool types ──
1077
+
1078
+ export interface ResolvedTool {
1079
+ name: string;
1080
+ icon: string;
1081
+ type: "browser" | "shell";
1082
+ url?: string;
1083
+ command?: string;
1084
+ requires?: string;
1085
+ login?: LoginConfig;
1086
+ enabled: boolean;
1087
+ requiresUserPicker: boolean;
1088
+ }
1089
+
1090
+ export interface ExecuteToolRequest {
1091
+ userName?: string;
1092
+ }
1093
+
1094
+ export interface ToolResult {
1095
+ success: boolean;
1096
+ error?: string;
1097
+ login?: LoginConfig;
1098
+ }
1099
+
1100
+ // ── Container assignment types ──
1101
+
1102
+ export interface AssignedContainer {
1103
+ containerId: string;
1104
+ containerName: string;
1105
+ port: number;
1106
+ }
1107
+
1108
+ export interface AssignContainerRequest {
1109
+ containerId: string;
1110
+ component: string;
1111
+ }
1112
+
1113
+ // ── API request/response types ──
1114
+
1115
+ export interface RegisterProjectRequest {
1116
+ repoPath: string;
1117
+ }
1118
+
1119
+ export interface CreateBenchRequest {
1120
+ branch?: string;
1121
+ /**
1122
+ * The issue's externalId (e.g. `owner/repo#123`, `owner/repo#code-scanning-117`,
1123
+ * or a Jira key like `PROJ-45`). When set, the server fetches the issue via the
1124
+ * active plugin's `getIssue` and creates a bench assigned to it.
1125
+ */
1126
+ externalId?: string;
1127
+ branchConflictResolution?: "resume" | "new";
1128
+ /**
1129
+ * TestBench variant discriminator. When `'testbench'`, the create path ignores
1130
+ * issue/branch coupling and instead binds the bench to `focusedSpecPath`.
1131
+ */
1132
+ variant?: "testbench";
1133
+ /**
1134
+ * Absolute (or repo-relative) path to the focused spec's `test-cases.json`.
1135
+ * Required when `variant === 'testbench'`; validated for containment against
1136
+ * the project repo server-side.
1137
+ */
1138
+ focusedSpecPath?: string;
1139
+ }
1140
+
1141
+ export interface ApiError {
1142
+ error: string;
1143
+ details?: string;
1144
+ }
1145
+
1146
+ // ── Persisted state types ──
1147
+
1148
+ export interface PersistedProjectEntry {
1149
+ id: string;
1150
+ repoPath: string;
1151
+ settings?: ProjectSettings;
1152
+ }
1153
+
1154
+ export interface PersistedBench {
1155
+ id: number;
1156
+ projectId: string;
1157
+ branch: string;
1158
+ workspacePath: string;
1159
+ ports: Record<string, number>;
1160
+ createdAt: string;
1161
+ assignedContainers?: Record<string, AssignedContainer>;
1162
+ assignedIssue?: AssignedIssue;
1163
+ notifications?: BenchNotification[];
1164
+ /** Persisted mirror of Bench.baseBranch. */
1165
+ baseBranch?: string;
1166
+ /** Persisted mirror of Bench.baseCommit. */
1167
+ baseCommit?: string;
1168
+ /** Persisted mirror of Bench.injectedJigId. */
1169
+ injectedJigId?: string;
1170
+ /** Persisted mirror of Bench.injectedJigSource. */
1171
+ injectedJigSource?: JigDefaultSource;
1172
+ /**
1173
+ * Persisted mirror of Bench.variant. Absent means a normal bench;
1174
+ * `'testbench'` marks a TestBench variant.
1175
+ */
1176
+ variant?: "testbench";
1177
+ /**
1178
+ * Persisted mirror of Bench.focusedSpecPath: the absolute path to the spec
1179
+ * the TestBench is focused on. Mutable and re-pointable across reboots. Only
1180
+ * meaningful when `variant === 'testbench'`.
1181
+ */
1182
+ focusedSpecPath?: string;
1183
+ /**
1184
+ * Persisted mirror of `bench.components[name].setupComplete`, keyed by
1185
+ * component name. Components themselves are runtime-only; only this flag
1186
+ * survives reboots so a future Start can skip re-running setup.
1187
+ * Absent on benches written before this field existed; load-time migration
1188
+ * coerces missing entries to `true` (those benches were created under the
1189
+ * old full-provisioning flow, so setup already ran).
1190
+ */
1191
+ componentSetupState?: Record<string, boolean>;
1192
+ }
1193
+
1194
+ /**
1195
+ * One ResourceOwnershipLedger entry: the processes and compose projects the
1196
+ * host started on a single plugin's behalf, scoped to a single bench. The host
1197
+ * owns every handle, so the ledger is how the startup orphan sweep (issue #613)
1198
+ * can reap resources that escaped a plugin crash or a host restart (FR-015).
1199
+ *
1200
+ * Stored as a flat array of entries (not a nested `Record<pluginId, ...>`) so a
1201
+ * plugin-supplied `pluginId` never becomes an object key. That keeps the
1202
+ * persisted shape off the CodeQL prototype-pollution surface that indexing by a
1203
+ * user-controlled name would otherwise create.
1204
+ */
1205
+ export interface ResourceOwnershipEntry {
1206
+ /** The plugin that owns these resources. May be plugin-supplied; never used as an object key. */
1207
+ pluginId: string;
1208
+ /** The bench the resources belong to. */
1209
+ benchId: number;
1210
+ /** Opaque process-manager ids the host spawned for this (plugin, bench). */
1211
+ processIds: string[];
1212
+ /** Compose project names (the `roubo-<projectId>-bench-<N>` convention) the host brought up. */
1213
+ composeProjects: string[];
1214
+ }
1215
+
1216
+ export interface PersistedState {
1217
+ benches: PersistedBench[];
1218
+ /**
1219
+ * ResourceOwnershipLedger (FR-015): per-plugin, per-bench record of host-owned
1220
+ * processes and compose projects. Optional and additive, so a state.json
1221
+ * written before this field existed loads unchanged (no schema migration).
1222
+ */
1223
+ resourceOwnership?: ResourceOwnershipEntry[];
1224
+ /**
1225
+ * Single commit point for the pre-plugin → plugin migration (WU-024 / issue #42).
1226
+ * Absent on pre-migration installs; bumped to 1 only after every migration
1227
+ * side-effect has succeeded. Used as the idempotency gate.
1228
+ */
1229
+ schemaVersion?: number;
1230
+ /** Set alongside `schemaVersion` so the one-time banner can pick its variant. */
1231
+ migration?: MigrationRecord;
1232
+ /**
1233
+ * One-time notice markers, keyed by a stable marker id, recording an ISO 8601
1234
+ * timestamp when each notice became applicable. Distinct from the WU-024
1235
+ * single `migration` record above so independent one-time notices never
1236
+ * overwrite one another. The client renders a marker's banner once and uses
1237
+ * its timestamp as the localStorage dismissal key, so it never reappears
1238
+ * after dismiss. A fresh install seeds every known marker as already-satisfied
1239
+ * (timestamp `"seeded"`) so a banner explaining a changed default never shows
1240
+ * to a user who never saw the old default. See `onlyToDoNoticeMarker` (issue #558).
1241
+ */
1242
+ notices?: Record<string, string>;
1243
+ }
1244
+
1245
+ /**
1246
+ * Marker id for the only-to-do default-change notice (FR-018, issue #558). The
1247
+ * banner explaining that the cut list now excludes In Progress by default shows
1248
+ * once on the first boot of an existing install after upgrade, then never again.
1249
+ */
1250
+ export const ONLY_TO_DO_NOTICE_MARKER = "only-to-do-default-v1";
1251
+
1252
+ export interface MigrationRecord {
1253
+ status: "success" | "rolled-back";
1254
+ /** ISO 8601. The banner uses this as the dismissal-marker key. */
1255
+ at: string;
1256
+ reason?: string;
1257
+ migratedProjectIds: string[];
1258
+ }
1259
+
1260
+ export interface PersistedProjects {
1261
+ projects: PersistedProjectEntry[];
1262
+ }
1263
+
1264
+ export interface ProjectPermissions {
1265
+ allow: string[];
1266
+ deny: string[];
1267
+ // optional for legacy state files written before ask support was added
1268
+ ask?: string[];
1269
+ }
1270
+
1271
+ // ── Filesystem browsing types ──
1272
+
1273
+ export interface DirectoryEntry {
1274
+ name: string;
1275
+ path: string;
1276
+ hasGit: boolean;
1277
+ }
1278
+
1279
+ export interface BrowseDirectoryResponse {
1280
+ path: string;
1281
+ entries: DirectoryEntry[];
1282
+ }
1283
+
1284
+ // ── Config creator types ──
1285
+
1286
+ export interface SuggestedComponent {
1287
+ key: string;
1288
+ config: ComponentConfig;
1289
+ source: string;
1290
+ }
1291
+
1292
+ export interface SuggestedTool {
1293
+ config: ToolConfig;
1294
+ source: string;
1295
+ }
1296
+
1297
+ export interface RepoScanResult {
1298
+ detected: {
1299
+ hasGit: boolean;
1300
+ submodules: Record<string, string>;
1301
+ structureType: "meta-repo" | "monorepo" | "single-repo";
1302
+ dockerComposeFiles: string[];
1303
+ dockerComposeServiceNames: Record<string, string[]>;
1304
+ dockerComposePortVars: Record<string, Record<string, string | null>>; // composeFile → serviceName → port env var name (or null if hardcoded)
1305
+ dockerComposeVars: Record<string, Record<string, Record<string, string | null>>>; // composeFile → serviceName → varName → default value (or null)
1306
+ dotnetProjects: string[];
1307
+ solutionFiles: string[];
1308
+ viteProjects: string[];
1309
+ envFiles: string[];
1310
+ suggestedName: string;
1311
+ suggestedRepo: string | null;
1312
+ suggestedComponents: SuggestedComponent[];
1313
+ suggestedTools: SuggestedTool[];
1314
+ };
1315
+ existingConfig: { path: string; config: RouboConfig } | null;
1316
+ }
1317
+
1318
+ export interface ConfigValidationResult {
1319
+ valid: boolean;
1320
+ errors: Array<{ path: string; message: string }>;
1321
+ portConflicts: Array<{
1322
+ port: string;
1323
+ base: number;
1324
+ conflictsWith: {
1325
+ projectId: string;
1326
+ projectName: string;
1327
+ port: string;
1328
+ range: [number, number];
1329
+ };
1330
+ }>;
1331
+ }
1332
+
1333
+ export interface SaveConfigRequest {
1334
+ repoPath: string;
1335
+ config: RouboConfig;
1336
+ }
1337
+
1338
+ export interface SaveConfigResponse {
1339
+ path: string;
1340
+ config: RouboConfig;
1341
+ }
1342
+
1343
+ export interface ValidateConfigRequest {
1344
+ config: RouboConfig;
1345
+ currentProjectId?: string;
1346
+ }
1347
+
1348
+ export interface ScanRepoRequest {
1349
+ repoPath: string;
1350
+ }
1351
+
1352
+ export interface CheckConfigRequest {
1353
+ repoPath: string;
1354
+ }
1355
+
1356
+ export interface CheckConfigPreview {
1357
+ name: string;
1358
+ displayName: string;
1359
+ ports: { name: string; base: number }[];
1360
+ benchCap: number;
1361
+ }
1362
+
1363
+ export interface CheckConfigResult {
1364
+ hasConfig: boolean;
1365
+ configValid: boolean;
1366
+ projectName?: string;
1367
+ displayName?: string;
1368
+ error?: string;
1369
+ alreadyRegistered: boolean;
1370
+ project?: RegisteredProject;
1371
+ preview?: CheckConfigPreview;
1372
+ }
1373
+
1374
+ // ── Terminal types ──
1375
+
1376
+ export type ClaudeCodeMode = "auto" | "plan" | "plan-auto";
1377
+
1378
+ export function deriveClaudeCodeMode(settings?: ClaudeCodeSettings): ClaudeCodeMode | undefined {
1379
+ if (!settings) return undefined;
1380
+ const { enableAutoMode, startInPlanMode } = settings;
1381
+ if (enableAutoMode && startInPlanMode) return "plan-auto";
1382
+ if (enableAutoMode) return "auto";
1383
+ if (startInPlanMode) return "plan";
1384
+ return undefined;
1385
+ }
1386
+
1387
+ export interface TerminalSession {
1388
+ id: string;
1389
+ benchKey: string;
1390
+ label: string;
1391
+ createdAt: string;
1392
+ command?: string;
1393
+ status: "live" | "ended";
1394
+ exitCode?: number;
1395
+ claudeCodeMode?: ClaudeCodeMode;
1396
+ }
1397
+
1398
+ export interface PersistedTerminalSession {
1399
+ session: TerminalSession;
1400
+ buffer: string[];
1401
+ persistedAt: string;
1402
+ }
1403
+
1404
+ export interface TerminalCreateRequest {
1405
+ command?: string;
1406
+ jigId?: string;
1407
+ }
1408
+
1409
+ export interface TerminalCreateResponse {
1410
+ sessionId: string;
1411
+ label: string;
1412
+ wsUrl: string;
1413
+ jigInjected?: boolean;
1414
+ /** Set when autoExecute=false: jig is scheduled to be written to PTY, not yet sent */
1415
+ jigScheduled?: boolean;
1416
+ /** Set when the resolved jig content exceeds MAX_CLI_PROMPT_LENGTH and was truncated */
1417
+ sizeWarning?: boolean;
1418
+ }
1419
+
1420
+ // ── Inspection run types ──
1421
+
1422
+ export type InspectionRunStatus = "running" | "passed" | "failed" | "error" | "aborted";
1423
+
1424
+ export interface InspectionRun {
1425
+ id: string;
1426
+ projectId: string;
1427
+ benchId: number;
1428
+ status: InspectionRunStatus;
1429
+ filter?: string;
1430
+ output: string[];
1431
+ exitCode: number | null;
1432
+ startedAt: string;
1433
+ completedAt?: string;
1434
+ }
1435
+
1436
+ export interface StartInspectionRequest {
1437
+ filter?: string;
1438
+ }
1439
+
1440
+ // ── GitHub issue types ──
1441
+ // Legacy: not on the issue-retrieval request path after WU-016. Still used by
1442
+ // internal bench-assignment flows (server/services/{issue-assignment,auto-clear}.ts)
1443
+ // pending a follow-up WU that migrates bench state to externalId.
1444
+
1445
+ export interface GitHubIssue {
1446
+ number: number;
1447
+ title: string;
1448
+ body: string | null;
1449
+ state: string;
1450
+ labels: string[];
1451
+ assignee?: string;
1452
+ milestone?: string;
1453
+ type?: string;
1454
+ createdAt: string;
1455
+ updatedAt: string;
1456
+ commentsCount: number;
1457
+ htmlUrl: string;
1458
+ blockedBy?: Array<{ number: number; title: string }>;
1459
+ blockingCount?: number;
1460
+ }
1461
+
1462
+ export interface GitHubIssueComment {
1463
+ id: number;
1464
+ body: string;
1465
+ user: string;
1466
+ createdAt: string;
1467
+ }
1468
+
1469
+ /**
1470
+ * Plugin-produced normalized issue contract. Every integration plugin
1471
+ * (github-com, github-enterprise, jira, third-party) returns issues in
1472
+ * this shape; every Roubo consumer reads this shape.
1473
+ *
1474
+ * Intentionally excludes sprint, fixVersion, custom fields, attachments,
1475
+ * comments, and hierarchical links (parent/children/epic). See FR-021.
1476
+ */
1477
+ export interface NormalizedIssue {
1478
+ integrationId: string;
1479
+ externalId: string;
1480
+ externalUrl: string;
1481
+ title: string;
1482
+ body: string | null;
1483
+ currentState: string;
1484
+ allowedTransitions: string[];
1485
+ assignees: Array<{ externalId: string; displayName: string }>;
1486
+ labels: string[];
1487
+ issueType: string | null;
1488
+ blocks: string[];
1489
+ blockedBy: string[];
1490
+ updatedAt: string;
1491
+ raw: unknown;
1492
+ // Keys match facet ids returned by `filterFacets` (host-API 1.1.0+). Plugins
1493
+ // built against 1.0.0 omit this; core treats absence as an empty map.
1494
+ facetValues?: Record<string, string | string[]>;
1495
+ }
1496
+
1497
+ /**
1498
+ * A normalized comment on a NormalizedIssue, returned by the active
1499
+ * integration plugin's `getComments` JSON-RPC method (WU-016).
1500
+ */
1501
+ export interface NormalizedComment {
1502
+ externalId: string;
1503
+ author: { externalId: string; displayName: string };
1504
+ body: string;
1505
+ createdAt: string;
1506
+ updatedAt: string;
1507
+ }
1508
+
1509
+ /**
1510
+ * One entry of the source list the host passes into source-bound contract
1511
+ * methods. Mirrors `ConfiguredSource` in `@roubo/plugin-sdk`.
1512
+ */
1513
+ export interface ConfiguredSource {
1514
+ kind: string;
1515
+ externalId: string;
1516
+ /**
1517
+ * Jira self-hosted only: the project key this source is scoped to under the
1518
+ * project-first selection model. Other plugins ignore.
1519
+ */
1520
+ project?: string;
1521
+ /**
1522
+ * Jira self-hosted only: for a `board` source, resolve to the active sprint
1523
+ * (default) or the whole board's backing filter.
1524
+ */
1525
+ boardMode?: "active-sprint" | "whole-board";
1526
+ /**
1527
+ * Jira self-hosted only: for the synthetic `mine` source, scope to the
1528
+ * in-scope projects or match anywhere.
1529
+ */
1530
+ mineScope?: "in-project" | "anywhere";
1531
+ /**
1532
+ * GitHub family only: per-source toggles for Code Scanning, Secret Scanning,
1533
+ * and Dependabot alerts. Default false on each. Other plugins ignore.
1534
+ */
1535
+ includeCodeQLAlerts?: boolean;
1536
+ includeSecretScanningAlerts?: boolean;
1537
+ includeDependabotAlerts?: boolean;
1538
+ }
1539
+
1540
+ /**
1541
+ * Discriminator for `ListIssuesWarning.code`. Mirrors
1542
+ * `ListIssuesWarningCode` in `@roubo/plugin-sdk`. The cut-list source picker
1543
+ * uses this to pick chip variants for the GitHub family's PAT/OAuth
1544
+ * remediation affordances.
1545
+ */
1546
+ export type ListIssuesWarningCode =
1547
+ | "missing-scope"
1548
+ | "scope-unverifiable"
1549
+ | "feature-disabled"
1550
+ | "insufficient-permission"
1551
+ | "not-found"
1552
+ | "rate-limited"
1553
+ | "unknown";
1554
+
1555
+ /**
1556
+ * Non-fatal warning about a single source / category for a `listIssues` call.
1557
+ * Mirrors `ListIssuesWarning` in `@roubo/plugin-sdk`. A given
1558
+ * `(sourceExternalId, category)` warning clears on the next successful
1559
+ * page-1 pull that omits it.
1560
+ *
1561
+ * `code` is an optional discriminator the client uses to pick a chip variant.
1562
+ */
1563
+ export interface ListIssuesWarning {
1564
+ category: "code-scanning" | "secret-scanning" | "dependabot" | string;
1565
+ sourceExternalId: string;
1566
+ cause: string;
1567
+ code?: ListIssuesWarningCode;
1568
+ detail?: { status?: number; code?: string; missingScope?: string };
1569
+ }
1570
+
1571
+ /**
1572
+ * Descriptor returned by the active integration plugin's `filterFacets` RPC
1573
+ * (host-API 1.1.0+). The cut-list filter row renders one section per facet;
1574
+ * `enum-async` sections load their options lazily via `getFacetOptions`.
1575
+ * Mirrors `FilterFacet` in `@roubo/plugin-sdk` so the web client can consume
1576
+ * the server's `/integration/filter-facets` response without depending on
1577
+ * the plugin SDK.
1578
+ */
1579
+ export interface FilterFacet {
1580
+ id: string;
1581
+ label: string;
1582
+ type: "enum" | "enum-async" | "multi-enum";
1583
+ options?: FilterFacetOption[];
1584
+ }
1585
+
1586
+ /**
1587
+ * One option for a `FilterFacet`. Used both inline (eager `enum`/`multi-enum`)
1588
+ * and as the response shape of `getFacetOptions` (lazy `enum-async`). Mirrors
1589
+ * `FilterFacetOption` in `@roubo/plugin-sdk`.
1590
+ */
1591
+ export interface FilterFacetOption {
1592
+ value: string;
1593
+ label: string;
1594
+ }
1595
+
1596
+ /**
1597
+ * Descriptor returned by the active integration plugin's `getSortFields` RPC
1598
+ * (host-API 1.2.0+, CLI-FR-009). The cut-list sort picker renders one option
1599
+ * per field; `defaultDir` is the direction first applied when the user selects
1600
+ * the field. Mirrors `SortField` in `@roubo/plugin-sdk` so the web client can
1601
+ * consume the server's `/issues/sort-fields` response without depending on the
1602
+ * plugin SDK. An empty array (or `MethodNotFound` from an older plugin) means
1603
+ * the host renders no picker (CLI-FR-011).
1604
+ */
1605
+ export interface SortField {
1606
+ id: string;
1607
+ label: string;
1608
+ defaultDir: "asc" | "desc";
1609
+ }
1610
+
1611
+ /** Parameters for the plugin's paginated `listIssues` JSON-RPC call (FR-022). */
1612
+ export interface ListIssuesParams {
1613
+ sources: ConfiguredSource[];
1614
+ cursor: string | null;
1615
+ pageSize: number;
1616
+ filters?: { labels?: string[]; search?: string };
1617
+ /**
1618
+ * Plugin-declared sort selection (CLI-FR-009/CLI-FR-010). `sortBy` is a
1619
+ * field id the plugin returned from `getSortFields`; `sortDir` is the
1620
+ * direction. Applied source-side by the plugin so the order is stable across
1621
+ * pages. Absent means the plugin's natural order (key-ascending fallback,
1622
+ * CLI-FR-010); a plugin that ignores these fields yields its natural order.
1623
+ */
1624
+ sortBy?: string;
1625
+ sortDir?: "asc" | "desc";
1626
+ /**
1627
+ * Status exclusion resolved by the host from the three-layer merge (FR-009,
1628
+ * FR-010). Applied in the query so excluded issues never occupy a result
1629
+ * page. `excludedStatusCategories` is the category-first default (e.g.
1630
+ * `["Done"]`); `excludedStatuses` is the status-name list used by a plugin
1631
+ * as the fallback when the instance does not support `statusCategory` in its
1632
+ * query language. A plugin that does not do server-side exclusion ignores both.
1633
+ */
1634
+ excludedStatusCategories?: string[];
1635
+ excludedStatuses?: string[];
1636
+ }
1637
+
1638
+ /**
1639
+ * Server response to `GET /api/projects/:projectId/issues`.
1640
+ * `stalled` is set by the host when the plugin paginator misbehaves
1641
+ * (TC-071): in that case `nextCursor` is forced to `null` so the
1642
+ * client stops fetching, and the UI surfaces a note.
1643
+ *
1644
+ * FR-014: when the active plugin is `errored` or `disabled` and a prior
1645
+ * first-page response was cached, the host serves that snapshot with
1646
+ * `stale: true` and `snapshotCapturedAt` set to the ISO timestamp of the
1647
+ * captured response. The matching cut-list banner is tracked in #263.
1648
+ */
1649
+ export interface PaginatedIssues {
1650
+ items: NormalizedIssue[];
1651
+ nextCursor: string | null;
1652
+ stalled?: boolean;
1653
+ /** Per-source per-category non-fatal warnings from the underlying plugin call. */
1654
+ warnings?: ListIssuesWarning[];
1655
+ /** True when this response is a cached snapshot served because the plugin is unavailable. */
1656
+ stale?: boolean;
1657
+ /**
1658
+ * ISO timestamp of the cached response. Present when this body carries a
1659
+ * persisted snapshot: set on the FR-014 errored/disabled stale serve and on
1660
+ * the stale-while-revalidate warm serve (`cacheStatus: 'revalidating'`).
1661
+ */
1662
+ snapshotCapturedAt?: string;
1663
+ /**
1664
+ * Where this first-page response came from, the stale-while-revalidate
1665
+ * cache-state signal the client maps onto the warm / revalidating / stale
1666
+ * indicator (CLI-FR-002):
1667
+ * - `'revalidating'`: served instantly from the persisted disk snapshot while
1668
+ * a background revalidation fetches fresh data (the warm path).
1669
+ * - `'miss'`: no usable snapshot, the live response was fetched (and, for a
1670
+ * first page, persisted).
1671
+ * - `'hit'`: served from the snapshot without triggering a background
1672
+ * revalidation.
1673
+ * Additive and first-page-only; absent on paginated (cursor > 0) responses.
1674
+ */
1675
+ cacheStatus?: "hit" | "miss" | "revalidating";
1676
+ /**
1677
+ * Count of issues the active plugin dropped in-query (e.g. status-category
1678
+ * exclusion, FR-009/FR-010). Passed through from the plugin's
1679
+ * `ListIssuesResult`; omitted when the plugin can't cheaply report it.
1680
+ */
1681
+ excludedCount?: number;
1682
+ }
1683
+
1684
+ /** Server response to `GET /api/projects/:projectId/issue-types` (WU-016). */
1685
+ export type ProjectIssueTypesV2Response =
1686
+ | { configured: true; types: string[] }
1687
+ | {
1688
+ configured: false;
1689
+ reason: ProjectIssueTypesUnavailableReason;
1690
+ types: string[];
1691
+ };
1692
+
1693
+ export interface AssignedIssue {
1694
+ // Legacy GitHub issue number. Present for github-com issues and security
1695
+ // alerts (where it holds the alert number); load-time migration derives
1696
+ // externalId from this for pre-plugin benches. Absent for integrations whose
1697
+ // issues have no numeric form (e.g. Jira keys like PLNRPTGOOG-3782), which
1698
+ // identify by externalId/integrationId instead. Consumers that need a GitHub
1699
+ // issue number must guard on its presence.
1700
+ number?: number;
1701
+ integrationId: string;
1702
+ externalId: string;
1703
+ title: string;
1704
+ // Plugin-provided externalIds of the issues blocking this one (e.g.
1705
+ // `owner/repo#123` for GitHub, `PROJ-45` for Jira). Populated on bench-detail
1706
+ // fetch when enforceIssueDependencies is on. Empty/absent means unblocked.
1707
+ blockedBy?: string[];
1708
+ /**
1709
+ * PRs seeded at assignment time from CrossReferencedEvent timeline items
1710
+ * (e.g. `Closes #123` in PR bodies). Does not include PRs linked via
1711
+ * GitHub's UI sidebar (DevelopmentEvent/ConnectedEvent).
1712
+ */
1713
+ // Optional for backwards compat: state.json persisted before this field was added will lack it.
1714
+ linkedPullRequests?: Array<{
1715
+ repoFullName: string;
1716
+ number: number;
1717
+ }>;
1718
+ /**
1719
+ * Frozen snapshot of the issue's type at assignment time (e.g. "bug",
1720
+ * "security-dependabot"). Drives blueprint-by-issue-type counting in the
1721
+ * source-picker Configure dialog and survives the user toggling the alert
1722
+ * category off afterwards. Never re-validated against current listIssueTypes.
1723
+ * Optional: benches assigned before this field was added persist without it.
1724
+ */
1725
+ issueType?: string | null;
1726
+ /**
1727
+ * Plugin-scoped opaque payload (NFR-004). Allowed on the active bench's
1728
+ * assignedIssue so a plugin can re-hydrate context across Roubo restarts.
1729
+ * Removed from state.json when the bench is cleared (removeBench filters
1730
+ * the bench record out entirely). Plugins MUST NOT include PII here
1731
+ * unless functionally required.
1732
+ */
1733
+ raw?: unknown;
1734
+ }
1735
+
1736
+ export interface AssignIssueRequest {
1737
+ /**
1738
+ * The issue's externalId (e.g. `owner/repo#123`, `owner/repo#code-scanning-117`,
1739
+ * or a Jira key like `PROJ-45`). The issue is resolved via the active plugin.
1740
+ */
1741
+ externalId: string;
1742
+ }
1743
+
1744
+ export interface AssignIssueResponse {
1745
+ bench: Bench;
1746
+ terminalSessionId: string | undefined;
1747
+ }
1748
+
1749
+ // ── GitHub project types ──
1750
+
1751
+ export interface GitHubProject {
1752
+ number: number;
1753
+ title: string;
1754
+ }
1755
+
1756
+ // ── GitHub project item types ──
1757
+
1758
+ export interface GitHubProjectItem {
1759
+ issue: GitHubIssue;
1760
+ status?: string | null;
1761
+ }
1762
+
1763
+ // ── GitHub issue type types ──
1764
+
1765
+ export interface GitHubIssueType {
1766
+ id: string;
1767
+ name: string;
1768
+ description?: string;
1769
+ color?: string;
1770
+ }
1771
+
1772
+ export type ProjectIssueTypesUnavailableReason = "none-defined" | "not-connected";
1773
+
1774
+ export type ProjectIssueTypesResponse =
1775
+ | { configured: true; types: GitHubIssueType[] }
1776
+ | {
1777
+ configured: false;
1778
+ reason: ProjectIssueTypesUnavailableReason;
1779
+ types: GitHubIssueType[];
1780
+ };
1781
+
1782
+ // ── Jig management types ──
1783
+
1784
+ export type JigSource = "app" | "project";
1785
+
1786
+ export interface JigMeta {
1787
+ id: string;
1788
+ name: string;
1789
+ description: string;
1790
+ icon: string;
1791
+ source: JigSource;
1792
+ createdAt?: string; // ISO-8601; absent for the embedded global default
1793
+ updatedAt?: string; // ISO-8601
1794
+ approxTokens?: number; // chars/4 estimate, lets UIs render a context-usage signal
1795
+ }
1796
+
1797
+ export interface JigDetail extends JigMeta {
1798
+ content: string;
1799
+ sizeBytes: number;
1800
+ sizeWarning?: boolean;
1801
+ approxTokens: number; // always present on detail responses
1802
+ }
1803
+
1804
+ export const GLOBAL_DEFAULT_JIG_ID = "__global_default__";
1805
+
1806
+ /** Default Claude context window size in tokens. Used for jig context-usage estimates. */
1807
+ export const DEFAULT_CONTEXT_WINDOW = 200_000;
1808
+
1809
+ /** Prefix for component provisioning step IDs; must stay in sync between server and client. */
1810
+ export const COMPONENT_STEP_PREFIX = "component:";
1811
+
1812
+ /**
1813
+ * Delay (ms) between creating a Claude terminal session and writing to it.
1814
+ * Claude Code needs time to start and begin accepting stdin; writing too early
1815
+ * causes the jig to be silently dropped. If Claude starts slowly (e.g. slow
1816
+ * machine, heavy load), this delay may still not be enough; the injection will
1817
+ * fail without any error signal.
1818
+ */
1819
+ export const CLAUDE_STARTUP_DELAY_MS = 1500;
1820
+
1821
+ export const DEFAULT_JIG_SETTINGS: JigSettings = {
1822
+ autoInject: true,
1823
+ autoExecute: true,
1824
+ };
1825
+
1826
+ export interface InjectJigRequest {
1827
+ jigId: string;
1828
+ sessionId?: string;
1829
+ }
1830
+
1831
+ export interface InjectJigResponse {
1832
+ success: boolean;
1833
+ resolvedLength: number;
1834
+ }
1835
+
1836
+ export interface JigCreateRequest {
1837
+ name: string; // 1–100 chars after trim
1838
+ description: string; // 1–300 chars after trim
1839
+ icon?: string; // optional; defaults to 'file-text'
1840
+ content: string; // non-empty; max 200 KB utf-8
1841
+ }
1842
+
1843
+ export interface JigUpdateRequest {
1844
+ name?: string;
1845
+ description?: string;
1846
+ icon?: string;
1847
+ content?: string;
1848
+ }
1849
+
1850
+ export type JigReference =
1851
+ | { type: "app-default" }
1852
+ | { type: "project-default"; projectId: string; projectName: string }
1853
+ | {
1854
+ type: "issue-type-mapping";
1855
+ projectId: string;
1856
+ projectName: string;
1857
+ issueType: string;
1858
+ };
1859
+
1860
+ export interface JigDeleteConflictResponse {
1861
+ error: string;
1862
+ code: "JIG_REFERENCED";
1863
+ references: JigReference[];
1864
+ }
1865
+
1866
+ export type JigDefaultSource = "issue-type-mapping" | "project" | "app" | "global";
1867
+
1868
+ export interface ProjectDefaultJigResponse {
1869
+ jigId: string;
1870
+ source: JigDefaultSource;
1871
+ }
1872
+
1873
+ export interface UpdateProjectDefaultJigRequest {
1874
+ jigId: string | null;
1875
+ }
1876
+
1877
+ export interface JigPreviewRequest {
1878
+ content: string;
1879
+ projectId?: string;
1880
+ benchId?: number;
1881
+ }
1882
+
1883
+ export interface JigPreviewResponse {
1884
+ resolved: string;
1885
+ unresolvedVariables: string[];
1886
+ }
1887
+
1888
+ export interface ProjectIssueTypeMappingsResponse {
1889
+ mappings: Record<string, string>;
1890
+ }
1891
+
1892
+ export interface UpdateProjectIssueTypeMappingsRequest {
1893
+ mappings: Record<string, string>;
1894
+ }
1895
+
1896
+ // ── User preferences types ──
1897
+
1898
+ export const THEME_MODES = ["light", "dark", "system"] as const;
1899
+ export type ThemeMode = (typeof THEME_MODES)[number];
1900
+
1901
+ export interface BenchSettings {
1902
+ enforceIssueDependencies: boolean;
1903
+ autoStartComponents: boolean;
1904
+ /** Application-wide cap on initialised benches. Positive integer (>= 1); absent means unlimited. */
1905
+ maxGlobal?: number;
1906
+ }
1907
+
1908
+ export const DEFAULT_BENCH_SETTINGS: BenchSettings = {
1909
+ enforceIssueDependencies: false,
1910
+ autoStartComponents: false,
1911
+ };
1912
+
1913
+ export interface TestBenchSettings {
1914
+ /** Master toggle for the TestBench feature. When false, no TestBench UI is offered. */
1915
+ enabled: boolean;
1916
+ }
1917
+
1918
+ export const DEFAULT_TESTBENCH_SETTINGS: TestBenchSettings = {
1919
+ enabled: true,
1920
+ };
1921
+
1922
+ export interface ClaudeCodeSettings {
1923
+ enableAutoMode: boolean;
1924
+ startInPlanMode: boolean;
1925
+ }
1926
+
1927
+ export const DEFAULT_CLAUDE_CODE_SETTINGS: ClaudeCodeSettings = {
1928
+ enableAutoMode: false,
1929
+ startInPlanMode: false,
1930
+ };
1931
+
1932
+ export interface GitHubSettings {
1933
+ issueTypesCacheTtlSeconds: number;
1934
+ }
1935
+
1936
+ export const DEFAULT_GITHUB_SETTINGS: GitHubSettings = {
1937
+ issueTypesCacheTtlSeconds: 300,
1938
+ };
1939
+
1940
+ export interface UserPreferences {
1941
+ theme: ThemeMode;
1942
+ jigs?: JigSettings;
1943
+ benches?: BenchSettings;
1944
+ testBench?: TestBenchSettings;
1945
+ claudeCode?: ClaudeCodeSettings;
1946
+ github?: GitHubSettings;
1947
+ }
1948
+
1949
+ export interface SettingsResponse extends UserPreferences {
1950
+ claudeCodeAutoModeAvailable: boolean;
1951
+ claudeCodeAutoModeReason?: string;
1952
+ contextWindow: number;
1953
+ }
1954
+
1955
+ // ── Combined create-and-assign types ──
1956
+
1957
+ export interface BranchConflictInfo {
1958
+ branchExists: boolean;
1959
+ workspaceExists: boolean;
1960
+ branchName: string;
1961
+ }
1962
+
1963
+ export type CreateBenchWithIssueResponse =
1964
+ | { status: "success"; bench: Bench; terminalSessionId: string | undefined }
1965
+ | { status: "conflict"; branchConflict: BranchConflictInfo };
1966
+
1967
+ export type GitHubErrorCode =
1968
+ | "NOT_CONNECTED"
1969
+ | "SCOPES_OUTDATED"
1970
+ | "ORG_APPROVAL_REQUIRED"
1971
+ | "SAML_SSO_REQUIRED"
1972
+ | "OWNER_NOT_FOUND"
1973
+ | "RATE_LIMITED"
1974
+ | "NETWORK"
1975
+ | "UNKNOWN";