@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/config-schema.ts +432 -0
- package/deep-merge.ts +38 -0
- package/gate-overrides-contract.ts +120 -0
- package/integration-types.ts +124 -0
- package/package.json +39 -0
- package/plugin-consent-schema.ts +80 -0
- package/plugin-enable-state-schema.ts +30 -0
- package/plugin-manifest-schema.ts +179 -0
- package/plugin-manifest.ts +55 -0
- package/plugin-runtime-types.ts +50 -0
- package/provision-descriptor-schema.ts +103 -0
- package/testbench-canonicalize.ts +129 -0
- package/testbench-contracts.ts +271 -0
- package/testbench-domain-types.ts +128 -0
- package/testbench-domain.ts +232 -0
- package/testbench-targeting-schema.ts +132 -0
- package/types.ts +1975 -0
- package/work-units-contract.ts +155 -0
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";
|