wicked-crew-api-types 0.10.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.d.ts +1310 -11
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -190,6 +190,66 @@ export interface AgentSession {
190
190
  * pre-populate the steer textarea when a gate arrives.
191
191
  */
192
192
  guidance?: string;
193
+ /**
194
+ * The run's delivery state (crew#393; api-types 0.18.0) — derived at DTO assembly on BOTH
195
+ * `GET /runs` and `GET /runs/:id`, so a run whose reviewable work is sitting uncommitted in
196
+ * its worktree can no longer look identical to one that delivered or had nothing to deliver:
197
+ *
198
+ * - `'delivered'` — a PR was opened for this run (by the deliver phase, or post-hoc via
199
+ * `POST /runs/:id/deliver`); `deliverUrl` carries the PR URL.
200
+ * - `'stranded'` — a COMPLETED repo-scoped run with no recorded PR whose worktree still
201
+ * exists on disk AND carries work: reviewable work nobody lifted. Derived honestly for
202
+ * OLD runs too — records written before this field existed strand exactly the same way.
203
+ * - `'vacuous'` — a COMPLETED repo-scoped run whose surviving worktree carries NO
204
+ * contribution at all (crew#311: nothing uncommitted, no run-branch commit — units all
205
+ * reached "done" while producing nothing). There is nothing to deliver; the recovery is a
206
+ * retry launch (`POST /runs {"retryOf":"<id>"}`). Never silently green: this is the loud
207
+ * spelling of the vacuous-completion class. Derived with the same two read-only git
208
+ * instruments the engine's own evidence floor uses; any probe failure keeps `'stranded'`
209
+ * (vacuity is only ever asserted on positive reads).
210
+ * - `'none'` — everything else: repo-less runs, non-terminal runs, failed/cancelled
211
+ * runs, and completed runs whose worktree is gone.
212
+ *
213
+ * Always present on runs served by a 0.18.0+ daemon; absent only from an older server.
214
+ *
215
+ * ⚠ WIRE RESHAPE (0.17.0 → 0.18.0, NOT additive): 0.11.0–0.17.0 spelled this field as
216
+ * `delivery?: { kind: 'pull_request'; url: string }`. The object form is GONE — the state
217
+ * moved into this string and the URL into `deliverUrl`. A client reading `delivery?.url`
218
+ * must move to `deliverUrl`.
219
+ *
220
+ * FAILURE of a deliver phase is not spelled here; it is `units[].status === 'rejected'`
221
+ * plus `denial_reason` (crew#318's message), both already on the list wire.
222
+ */
223
+ delivery?: 'delivered' | 'stranded' | 'vacuous' | 'none';
224
+ /**
225
+ * The delivered PR's URL (crew#393; api-types 0.18.0) — present exactly when
226
+ * `delivery === 'delivered'`; absent otherwise (absence is the one spelling, never `null`).
227
+ *
228
+ * KNOWN LIMIT (inherited from 0.11.0): runs that delivered before a 0.11.0 daemon first saw
229
+ * their terminal frame have no durable `run.delivered` audit entry — they read as
230
+ * `'stranded'` while their worktree survives (post-hoc `POST /runs/:id/deliver` is
231
+ * idempotent against the existing branch) and `'none'` after it is cleaned up.
232
+ */
233
+ deliverUrl?: string;
234
+ /**
235
+ * The campaign this AD-HOC run was attached to at launch (`LaunchRunBody.campaignId`;
236
+ * wicked-studio#27, api-types 0.19.0) — daemon-joined at DTO assembly on `GET /runs` and
237
+ * `GET /runs/:id` from the launch record (audit trail), so it survives a daemon restart.
238
+ * ABSENT — never `null` — when the run was not attached (absence is the one spelling).
239
+ *
240
+ * NOT set on a campaign's own DAG-node runs: those are correlated through
241
+ * {@link Campaign.node_run_id} (their run ids are `{campaign}:{node}:a{attempt}`), and a
242
+ * caller-chosen `sessionId` may legally contain `:`, so id-shape parsing is unsound — read
243
+ * membership from the campaign, not from this field or the id.
244
+ */
245
+ campaign_id?: string;
246
+ /**
247
+ * The ad-hoc group label this run was launched under (`LaunchRunBody.groupLabel`;
248
+ * wicked-studio#27, api-types 0.19.0) — daemon-joined like {@link AgentSession.campaign_id}.
249
+ * ABSENT when the run is ungrouped. Runs sharing a label form one `RunGroup` on
250
+ * `GET /campaigns`.
251
+ */
252
+ group_label?: string;
193
253
  }
194
254
 
195
255
  /** An ordered unit of work within a run (`WorkUnit`). */
@@ -218,6 +278,14 @@ export interface WorkUnit {
218
278
  gate?: GateSpec | string;
219
279
  /** True when a pinned deterministic validator is attached to this unit. */
220
280
  has_validator_pin?: boolean;
281
+ /**
282
+ * True when this unit's backing phase declared `executes_code` (crew#311 / core#297 §2) —
283
+ * carried from the def at plan time. The engine's code-evidence floor holds such a unit to a
284
+ * worktree diff: a governed Creator unit with this flag cannot fold "done" over an untouched
285
+ * worktree. Additive and skip-if-false on the wire: absent on non-code units, on units planned
286
+ * before the field existed, and on an older engine.
287
+ */
288
+ executes_code?: boolean;
221
289
  }
222
290
 
223
291
  /** A run plus its ordered units (`SessionView`) — the shape `GET /runs` returns. */
@@ -359,7 +427,10 @@ export interface GateInfo {
359
427
  receivedAt: string;
360
428
  }
361
429
 
362
- /** Approve / reject payload for the steering gate (`POST /runs/:id/gate`). */
430
+ /** Approve / reject payload for the steering gate (`POST /runs/:id/gate`). On a
431
+ * `steering-author` propose gate, an approve — amend note or not — also LANDS the proposal
432
+ * into the rules store and the 200 body carries a {@link SteeringLandingResult}; the amend
433
+ * steers the RUN's continuation, never the landed rule text (crew#388). */
363
434
  export interface GateDecision {
364
435
  approve: boolean;
365
436
  amend?: string;
@@ -503,10 +574,39 @@ export interface CoreEvent {
503
574
  latencyMs?: number;
504
575
  /** workerStalled: silent seconds before the stall event fired. */
505
576
  stalledSecs?: number;
577
+ /**
578
+ * `workerStalled` / `workerStallEscalated` (DAEMON-SYNTHETIC, crew#287/crew#341): how long the
579
+ * run had been silent on the daemon's CoreEvent relay when the frame fired, ms. Distinguishes
580
+ * the daemon's synthetic `workerStalled` frame (this field) from the engine's PTY-path
581
+ * `workerStalled` event (`stalledSecs` above) — same `type` tag, different producers.
582
+ */
583
+ quietForMs?: number;
584
+ /** `workerStallEscalated` (crew#341): what the watchdog did — `'reassign'` | `'notify'`. */
585
+ action?: string;
586
+ /** `workerStallEscalated`: how the action ended — `'ok'` | `'failed'` | `'exhausted'`. */
587
+ outcome?: string;
588
+ /**
589
+ * `workerStallEscalated`: `true` when a human should look — the action failed, the automatic
590
+ * recovery budget is exhausted, or the configured action is `'notify'` (surface, don't touch).
591
+ * `false` means the platform recovered on its own; the narrator shows it, nobody is paged.
592
+ */
593
+ needsYou?: boolean;
594
+ /** `workerStallEscalated` (action `'reassign'`): automatic reassigns consumed for this run, this one included. */
595
+ escalations?: number;
596
+ /** `workerStallEscalated` (outcome `'failed'`): bounded excerpt of what the recovery call threw. */
597
+ error?: string;
506
598
  /** failureTriaged: the triage judge's decision. */
507
599
  decision?: string;
508
600
  /** failureTriaged: the judge's bounded reasoning. */
509
601
  analysis?: string;
602
+ // campaign* frames (DES-CAMPAIGN-001 / TH-9) — camelCase per event_to_json; the exact
603
+ // per-variant shapes are the CampaignEvent union below.
604
+ /** campaign* frames: the campaign id. */
605
+ campaign?: string;
606
+ /** campaignNode* frames: the node id within the campaign. */
607
+ node?: string;
608
+ /** campaignNodeStarted / campaignNodeAwaitingHuman: the node's attempt-keyed Run id. */
609
+ runId?: string;
510
610
  // assumptionRecorded (external-transform convention) — camelCase per event_to_json
511
611
  kind?: string;
512
612
  library?: string;
@@ -526,6 +626,63 @@ export interface CoreEvent {
526
626
  * named live, and a bundle assembled after a run cannot describe it in different
527
627
  * words than the operator watched it happen in. (FINDING-014)
528
628
  */
629
+ /**
630
+ * The stall watchdog's DETECTION frame (crew#287; api-types 0.18.0 — previously daemon-local).
631
+ * DAEMON-SYNTHETIC: broadcast straight to `/ws` when a run in `executing` has been silent on the
632
+ * daemon's CoreEvent relay past `SystemSettings.workerStallMinutes` — never re-relayed through
633
+ * the engine, one frame per quiet period, re-armed by any new event for the run. Detection only:
634
+ * the run is not touched. (The engine's PTY-path event shares the `workerStalled` tag but
635
+ * carries `stalledSecs`; this frame carries `quietForMs`.)
636
+ *
637
+ * `type` aliases on purpose, not `interface`s: only anonymous object types satisfy `CoreEvent`'s
638
+ * index signature, which is what lets these frames flow through CoreEvent-typed broadcast seams.
639
+ */
640
+ export type WorkerStalledFrame = {
641
+ type: 'workerStalled';
642
+ /** The stalled run's id. */
643
+ session: string;
644
+ /** The current unit ord, when any observed event or the run header named one. */
645
+ ord?: number;
646
+ /** How long the run has been silent when the frame fired, ms. */
647
+ quietForMs: number;
648
+ };
649
+
650
+ /**
651
+ * The stall watchdog's ESCALATION frame (crew#341; api-types 0.18.0). DAEMON-SYNTHETIC, emitted
652
+ * only when escalation is armed (`SystemSettings.workerStallEscalateMinutes` > 0 — OFF by
653
+ * default) and a detected stall stays silent past that threshold. Reports what the watchdog DID
654
+ * and how it ended, one escalation per quiet period:
655
+ *
656
+ * - `action: 'reassign'` — the wedged cursor unit was recycled in place (engine `reassignUnit`:
657
+ * the stale turn is superseded, the session closed, the unit re-dispatched; queued operator
658
+ * injects survive into the fresh turn). `outcome: 'ok' | 'failed' | 'exhausted'`.
659
+ * - `action: 'notify'` — surface loudly without touching the run (the fail-loud rung).
660
+ *
661
+ * `needsYou: true` marks the frames a human should act on (failed / exhausted / notify).
662
+ */
663
+ export type WorkerStallEscalatedFrame = {
664
+ type: 'workerStallEscalated';
665
+ /** The stalled run's id. */
666
+ session: string;
667
+ /** The cursor unit's ord, when known. */
668
+ ord?: number;
669
+ /** How long the run had been silent when the watchdog escalated, ms. */
670
+ quietForMs: number;
671
+ /** What the watchdog did. */
672
+ action: 'reassign' | 'notify';
673
+ /** How it ended: `ok` (acted / surfaced), `failed` (the recovery call threw), `exhausted`
674
+ * (the per-run automatic budget was already spent — nothing was attempted). */
675
+ outcome: 'ok' | 'failed' | 'exhausted';
676
+ /** True when a human should look; false = the platform recovered on its own. */
677
+ needsYou: boolean;
678
+ /** action `reassign`: the seat the unit was re-dispatched to, when known. */
679
+ cli?: string;
680
+ /** action `reassign`: automatic reassigns consumed for this run, this one included. */
681
+ escalations?: number;
682
+ /** outcome `failed`: bounded excerpt of what the recovery call threw. */
683
+ error?: string;
684
+ };
685
+
529
686
  export interface RecordedEvent extends CoreEvent {
530
687
  /** Capture time, epoch millis — when core emitted the frame, not when it was read. */
531
688
  ts: number;
@@ -885,9 +1042,30 @@ export interface GovernancePolicy {
885
1042
  retired?: boolean;
886
1043
  }
887
1044
 
1045
+ /**
1046
+ * The steering-type vocabulary (STEERING program) — one sub-page per type in the studio's
1047
+ * Steering surface. Enum-as-string on the wire; the engine's serde default is `architecture`,
1048
+ * so a rule row written before the field existed reads back as an architecture rule.
1049
+ */
1050
+ export type SteeringType =
1051
+ | 'architecture'
1052
+ | 'development'
1053
+ | 'security'
1054
+ | 'testing'
1055
+ | 'operations'
1056
+ | 'compliance'
1057
+ | 'design-ux';
1058
+
888
1059
  /**
889
1060
  * A prescriptive conformance rule (`wicked-governance::ConformanceRule`).
890
1061
  * `rule_type`: `pattern` | `policy`. `severity`: `info` | `warn` | `error` | `critical`.
1062
+ *
1063
+ * STEERING (unified model): the wiki/rules model and the old policy model merged into ONE
1064
+ * steering-rule model. The steering fields below are all optional on the wire — additive for
1065
+ * older consumers, and ABSENT on rows served by a pre-steering engine (wicked-core-ts < 0.7.5),
1066
+ * where absence reads as the serde defaults (`steering_type` architecture, `weight` 1.0, no
1067
+ * enforcement half). A rule WITHOUT `effect` is recall-only, exactly as before the merge; a rule
1068
+ * WITH one participates in decide()/select() the way a `GovernancePolicy` used to.
891
1069
  */
892
1070
  export interface ConformanceRule {
893
1071
  id: string;
@@ -898,9 +1076,25 @@ export interface ConformanceRule {
898
1076
  targets: { language?: string; layer?: string; framework?: string };
899
1077
  symbol_ref?: string;
900
1078
  compliance?: { framework: string; control_id: string };
1079
+ /** Where the rule came from. Doc-ingested rules keep `path@sha#id` in `ref`; UI/chat-authored
1080
+ * rules carry `source: "ui"` / `source: "chat"` — first-class provenance, not second-class. */
901
1081
  provenance: { source: string; ref?: string; source_kinds: string[] };
902
1082
  /** Withdrawn from recall. Same contract as {@link GovernancePolicy.retired}. */
903
1083
  retired?: boolean;
1084
+ /** Which Steering sub-page this rule belongs to. Engine default: `architecture`. */
1085
+ steering_type?: SteeringType;
1086
+ /** Phases / tools this rule is selected for (inclusion — {@link GovernancePolicy.applies_to} semantics). */
1087
+ applies_to?: string[];
1088
+ /** Exclusion twin of `applies_to`: phases / tools this rule is NEVER selected for. */
1089
+ excludes?: string[];
1090
+ /** Ordering within a severity band + gate priority. Engine default: 1.0. */
1091
+ weight?: number;
1092
+ /** Enforcement half (from the retired policy model). Absent ⇒ the rule is recall-only. */
1093
+ effect?: 'deny' | 'allow_with_conditions' | 'allow';
1094
+ trigger?: { contains?: string };
1095
+ obligations?: string[];
1096
+ /** The frozen acceptance-criteria text (becomes a claim's `criteria` when the rule decides a gate). */
1097
+ criteria?: string;
904
1098
  }
905
1099
 
906
1100
  /** Facet query for `GET /governance/rules/preview`. All fields are optional. */
@@ -912,6 +1106,368 @@ export interface RulePreviewQuery {
912
1106
  rule_type?: string;
913
1107
  }
914
1108
 
1109
+ /**
1110
+ * Facet query for `GET /governance/rules` (the wiki BROWSE surface). Unlike
1111
+ * {@link RulePreviewQuery} — whose recall semantics treat an absent rule facet as a wildcard
1112
+ * match, enforcement's question — browse filters are EXACT matches over the listed rows
1113
+ * ("show me the rules tagged `layer=api`"), and `status` keeps retired rules reachable so the
1114
+ * kill switch stays visible (`all` is the default).
1115
+ */
1116
+ export interface RuleBrowseQuery {
1117
+ severity?: 'info' | 'warn' | 'error' | 'critical';
1118
+ layer?: string;
1119
+ rule_type?: 'pattern' | 'policy';
1120
+ status?: 'active' | 'retired' | 'all';
1121
+ /**
1122
+ * Steering-type facet (the Steering sub-page's filter). Answers 501 on a pre-steering engine
1123
+ * (wicked-core-ts < 0.7.5): rows there carry no `steering_type`, so an empty answer would
1124
+ * impersonate "no rules of that type".
1125
+ */
1126
+ type?: SteeringType;
1127
+ /**
1128
+ * Boolean spelling of the retire filter: `true` ⇒ `status=all`, `false` ⇒ `status=active`.
1129
+ * Mutually exclusive with `status` — sending both is a 400 (two spellings of one filter can
1130
+ * contradict, and picking a winner silently would answer a question the caller didn't ask).
1131
+ */
1132
+ include_retired?: 'true' | 'false';
1133
+ }
1134
+
1135
+ // ── Steering management (STEERING program) ──────────────────────────────────────
1136
+
1137
+ /**
1138
+ * One entry of a `POST /governance/steering/import` batch: either a frontmattered markdown
1139
+ * document (`kind: 'doc'` — the same format `rules ingest --dir` consumes) or a ready rule
1140
+ * object (`kind: 'rule'`). Every entry runs through the engine's ingest normalize/validate
1141
+ * path, fail-closed PER ENTRY: one bad entry rejects alone, the rest still land.
1142
+ */
1143
+ export type SteeringImportEntry =
1144
+ | { kind: 'doc'; name?: string; content: string }
1145
+ | { kind: 'rule'; rule: ConformanceRule };
1146
+
1147
+ /** The `POST /governance/steering/import` request body (JSON — this daemon speaks no multipart). */
1148
+ export interface SteeringImportBody {
1149
+ /** The page's inferred steering type, applied as the DEFAULT `steering_type` for entries that omit one. */
1150
+ type?: SteeringType;
1151
+ entries: SteeringImportEntry[];
1152
+ }
1153
+
1154
+ /** Per-entry outcome of a steering import (same order as the submitted batch). */
1155
+ export interface SteeringImportResult {
1156
+ /** Index into the submitted `entries` array. */
1157
+ index: number;
1158
+ /** The doc entry's `name`, when one was given. */
1159
+ name?: string;
1160
+ status: 'imported' | 'rejected';
1161
+ /** Rule ids the entry minted (a doc can mint several; a rejected entry mints none). */
1162
+ ids?: string[];
1163
+ /** Why the entry was rejected (present only on `rejected`). */
1164
+ error?: string;
1165
+ }
1166
+
1167
+ /** The `POST /governance/steering/import` 200 body — 200 even with rejections: per-entry results ARE the answer. */
1168
+ export interface SteeringImportResponse {
1169
+ results: SteeringImportResult[];
1170
+ imported: number;
1171
+ rejected: number;
1172
+ }
1173
+
1174
+ /**
1175
+ * The `POST /governance/steering/author` request body — "add with chat". Launches a governed
1176
+ * authoring run (the `steering-author` workflow) that analyzes the named files/directories plus
1177
+ * the operator's intent and emits PROPOSED steering rules at a human gate (the TH-12
1178
+ * propose-as-gate pattern): the run PAUSES `awaiting_human` after the propose phase, and the
1179
+ * operator approves/amends/rejects through the standard `POST /runs/:id/gate`. On APPROVE the
1180
+ * daemon lands the proposal into the rules store with `provenance.source: "chat"` and the gate
1181
+ * response carries a {@link SteeringLandingResult} — the run itself writes nothing to any store.
1182
+ */
1183
+ export interface SteeringAuthorBody {
1184
+ /** The operator's conversational intent — what steering to author, and why. */
1185
+ instructions: string;
1186
+ /** Default `steering_type` for the proposed rules (the page's inferred type). */
1187
+ type?: SteeringType;
1188
+ /** ABSOLUTE paths on the daemon host (files or directories) the run should analyze. */
1189
+ paths?: string[];
1190
+ /** Inline documents — written into the run's steering inbox on the daemon host and analyzed like `paths`. */
1191
+ documents?: { name: string; content: string }[];
1192
+ /** Registered repo to run within (the run analyzes that worktree's context too). */
1193
+ repoRef?: string;
1194
+ /** Stable run id; minted when omitted. */
1195
+ sessionId?: string;
1196
+ }
1197
+
1198
+ /** Where a landed steering proposal was read from, most-preferred first: the propose phase's
1199
+ * machine-readable artifact file, the propose unit's stored transcript, the cached gate prompt. */
1200
+ export type SteeringProposalSource = 'deliverable' | 'transcript' | 'gate-prompt';
1201
+
1202
+ /**
1203
+ * The `landing` field on the `POST /runs/:id/gate` (and gated `POST /runs/:id/resume`) 200 body
1204
+ * — present ONLY when the approved gate was a `steering-author` propose gate (crew#388). It is
1205
+ * how the skin learns the approved chat proposal actually reached the rules store — or, loudly,
1206
+ * that it did not: on `failed` the run still advanced (the approve stands) but `error` carries
1207
+ * the operator-readable reason, mirrored on the audit trail as
1208
+ * `governance.steering.landing_failed`. Landed rules are audited per rule as
1209
+ * `governance.rule.upserted` with the chat provenance. Idempotent: a replayed approve reports
1210
+ * `alreadyLanded: true` and re-lands nothing. An approve WITH an amend note lands the proposal
1211
+ * UNCHANGED — the amend steers the RUN, not the rule text. A reject lands nothing.
1212
+ */
1213
+ export interface SteeringLandingResult {
1214
+ outcome: 'landed' | 'failed';
1215
+ /** Rule ids actually upserted (all of them on `landed`; the partial set on a partial `failed`). */
1216
+ ruleIds: string[];
1217
+ /** Where the proposal was read from. Absent when no source yielded one. */
1218
+ source?: SteeringProposalSource;
1219
+ /** `true` when a replayed approve found the durable landing marker — nothing was re-landed. */
1220
+ alreadyLanded?: boolean;
1221
+ /** Present iff `outcome: "failed"` — the loud, operator-readable reason. */
1222
+ error?: string;
1223
+ }
1224
+
1225
+ // ── Governance wiki management (scoreboard + meta) ─────────────────────────────
1226
+
1227
+ /**
1228
+ * Typing-coverage half of the wiki scoreboard — % of doctrine statements typed into
1229
+ * enforcement classes. Doc-side by construction (the class lives in doc frontmatter, never on
1230
+ * the rule node), so it is measurable only when the daemon was pointed at the same docs root
1231
+ * `rules ingest --dir` used; otherwise `available` is `false` and `reason` says why.
1232
+ */
1233
+ export interface WikiTypingCoverage {
1234
+ available: boolean;
1235
+ /** Why typing could not be measured (present only when `available` is `false`). */
1236
+ reason?: string;
1237
+ docs_scanned: number;
1238
+ statements_total: number;
1239
+ statements_typed: number;
1240
+ /** `statements_typed / statements_total` in [0,100]; absent when the corpus mints no statements. */
1241
+ percent?: number;
1242
+ /** Typed statements per class (`policy` / `validator` / `guidance`). */
1243
+ by_class: Record<string, number>;
1244
+ /** Docs that mint statements but declare no class — the actionable backlog. */
1245
+ docs_untyped: string[];
1246
+ }
1247
+
1248
+ /** Connection-coverage half — do the active rules' `symbol_ref`s resolve, and are the links live? */
1249
+ export interface WikiConnectionCoverage {
1250
+ rules_with_ref: number;
1251
+ refs_resolving: number;
1252
+ refs_unresolvable: number;
1253
+ /** `refs_resolving / rules_with_ref` in [0,100]; absent when no rule carries a ref. */
1254
+ percent?: number;
1255
+ /** Active rules with at least one live `Governs` edge. */
1256
+ rules_linked: number;
1257
+ }
1258
+
1259
+ /** One rule's enforcement evidence (only rules with any evidence appear). */
1260
+ export interface WikiRuleEvidenceRow {
1261
+ rule_id: string;
1262
+ /** Distinct deny claims citing this rule (`evidenced_by` edges in). */
1263
+ denial_claims: number;
1264
+ /** Accumulated `evidence_count` across the rule's `Governs` edges. */
1265
+ governs_evidence: number;
1266
+ }
1267
+
1268
+ /** Enforcement evidence — gate denials citing wiki rules (retired rules included: a past denial
1269
+ * stays explicable after its rule retires, so its evidence stays countable). */
1270
+ export interface WikiEnforcementEvidence {
1271
+ denial_claims: number;
1272
+ rules_evidenced: number;
1273
+ evidenced_by_edges: number;
1274
+ governs_evidence_total: number;
1275
+ /** Per-rule breakdown, most-evidenced first — the "which rules actually fire" list. */
1276
+ per_rule: WikiRuleEvidenceRow[];
1277
+ }
1278
+
1279
+ /** Recall volume — documented UNAVAILABLE in-band (an honest "cannot measure" beats a
1280
+ * fabricated zero); `reason` says why. */
1281
+ export interface WikiRecallVolume {
1282
+ available: boolean;
1283
+ reason: string;
1284
+ }
1285
+
1286
+ /**
1287
+ * The wiki population/connection scoreboard (`wicked-governance::Scoreboard`, AW-23/arch-R23) —
1288
+ * the report that tells a populated wiki from an ingested-once-and-decaying one.
1289
+ * Served by `GET /governance/wiki/scoreboard`; 501 when the installed engine addon predates the
1290
+ * `governanceScoreboard` binding (wicked-core-ts ≥ 0.7.4).
1291
+ */
1292
+ export interface GovernanceScoreboard {
1293
+ rules_total: number;
1294
+ rules_active: number;
1295
+ rules_retired: number;
1296
+ typing: WikiTypingCoverage;
1297
+ connection: WikiConnectionCoverage;
1298
+ evidence: WikiEnforcementEvidence;
1299
+ recall_volume: WikiRecallVolume;
1300
+ }
1301
+
1302
+ /**
1303
+ * `GET /governance/wiki/meta` — the wiki's honest empty-state signal, cheap enough for the UI
1304
+ * to call on mount. `ruleset_count` is `null` (not 0) when the installed engine build cannot
1305
+ * count `RuleSet` rows: "no rulesets" is a real answer about a seeded store, and "cannot count"
1306
+ * must never impersonate it.
1307
+ */
1308
+ export interface GovernanceWikiMeta {
1309
+ /** Any doctrine on the store at all (rules listable, or RuleSet rows counted). */
1310
+ seeded: boolean;
1311
+ ruleset_count: number | null;
1312
+ /** Rules the engine lists (pre-0.7.4 addons list active rules only — the recall funnel). */
1313
+ rule_count: number;
1314
+ /** Whether `GET /governance/wiki/scoreboard` will answer 200 on this deployment. */
1315
+ scoreboard_available: boolean;
1316
+ /** The authoring guide / seed runbook the empty state points at. */
1317
+ doc: string;
1318
+ }
1319
+
1320
+ // ── Testing surface (crew-testing) — governance evals + eval corpora ───────────
1321
+
1322
+ /**
1323
+ * The behavior signals of one eval sample — the facts the governance decide path is asked to
1324
+ * judge (mirrors the engine's decision inputs: run phase, tool invoked, files touched, content).
1325
+ * All optional: a sample carries only the signals its behavior is about.
1326
+ */
1327
+ export interface GovernanceEvalSignals {
1328
+ phase?: string;
1329
+ tool?: string;
1330
+ files?: string[];
1331
+ content?: string;
1332
+ }
1333
+
1334
+ /**
1335
+ * One eval sample: a described behavior (`kind: 'bad'` ⇒ the steering corpus SHOULD deny it,
1336
+ * `'good'` ⇒ it should pass) tagged with the steering type it exercises. Field names are the
1337
+ * engine's serde spelling (snake_case) — the report echoes them back verbatim.
1338
+ */
1339
+ export interface GovernanceEvalSample {
1340
+ id: string;
1341
+ description: string;
1342
+ kind: 'good' | 'bad';
1343
+ steering_type: string;
1344
+ signals: GovernanceEvalSignals;
1345
+ }
1346
+
1347
+ /**
1348
+ * The `POST /testing/evals/run` request body. `type` narrows the run to one steering type;
1349
+ * `corpus` names an estate scope (`evals:<name>`, as minted by `POST /testing/corpora/import`);
1350
+ * omitted, the engine runs its built-in default corpus.
1351
+ */
1352
+ export interface RunGovernanceEvalsBody {
1353
+ type?: SteeringType;
1354
+ corpus?: string;
1355
+ }
1356
+
1357
+ /** A near-miss rule on a `gap` verdict — the rule that ALMOST fired, with its similarity score. */
1358
+ export interface GovernanceEvalNearestRule {
1359
+ rule_id: string;
1360
+ similarity: number;
1361
+ }
1362
+
1363
+ /**
1364
+ * One sample's outcome. `expected` is derived from the sample's `kind` (`bad` ⇒ `deny`);
1365
+ * `fired` lists the rule ids that actually fired; `verdict` is the comparison: `caught` (a bad
1366
+ * behavior denied), `gap` (a bad behavior that sailed through), `false_positive` (a good
1367
+ * behavior denied). `nearest_rules` is present on gaps (an empty array is allowed) — the
1368
+ * remediation pointer for "which rule needs sharpening".
1369
+ */
1370
+ export interface GovernanceEvalResult {
1371
+ sample: Pick<GovernanceEvalSample, 'id' | 'description' | 'kind' | 'steering_type'>;
1372
+ expected: 'deny' | 'allow';
1373
+ fired: string[];
1374
+ verdict: 'caught' | 'gap' | 'false_positive';
1375
+ nearest_rules?: GovernanceEvalNearestRule[];
1376
+ }
1377
+
1378
+ /** The report's roll-up counts (snake_case — the engine's serde output, passed through verbatim). */
1379
+ export interface GovernanceEvalSummary {
1380
+ total: number;
1381
+ caught: number;
1382
+ gaps: number;
1383
+ false_positives: number;
1384
+ }
1385
+
1386
+ /**
1387
+ * The `POST /testing/evals/run` 200 body — the engine's serde report passed through VERBATIM
1388
+ * (snake_case field names, `degraded` spelled `null` when the run was full-fidelity;
1389
+ * `'facet-only'` when the embedding side was unavailable and only facet matching ran).
1390
+ */
1391
+ export interface GovernanceEvalReport {
1392
+ results: GovernanceEvalResult[];
1393
+ summary: GovernanceEvalSummary;
1394
+ degraded: 'facet-only' | null;
1395
+ }
1396
+
1397
+ /** The `POST /testing/corpora/import` request body — a named eval corpus for later runs. */
1398
+ export interface ImportEvalCorpusBody {
1399
+ name: string;
1400
+ samples: GovernanceEvalSample[];
1401
+ }
1402
+
1403
+ /**
1404
+ * The `POST /testing/corpora/import` 200 body. `scope` is the estate scope the samples landed
1405
+ * under (`evals:<name>` — the string `RunGovernanceEvalsBody.corpus` names); `embedded` reports
1406
+ * whether the samples were embedded for similarity matching (false ⇒ later runs degrade to
1407
+ * facet-only).
1408
+ */
1409
+ export interface ImportEvalCorpusResponse {
1410
+ imported: number;
1411
+ scope: string;
1412
+ embedded: boolean;
1413
+ }
1414
+
1415
+ /**
1416
+ * The `POST /testing/recon` request body (api-types 0.15.0) — the Testing page's campaign-recon
1417
+ * trigger. `problem` is the recon brief, passed to every launched run VERBATIM (the client owns
1418
+ * the framing). The two optional fields are the multiscope wire, shared with
1419
+ * {@link LaunchCampaignBody}:
1420
+ *
1421
+ * - `repoRefs` — explicit codebase attachments: registered repo refs (ids; a unique repo NAME
1422
+ * also resolves), deduped; a ref that does not resolve fails the whole request with a 400
1423
+ * naming it.
1424
+ * - `projectId` — crew resolves the project's `crew.repo` members server-side (404 unknown
1425
+ * project; 400 when the project has zero repo members and no `repoRefs` cover for it) AND
1426
+ * files every launched run into the project (the `POST /runs` §2.2 semantics: atomic
1427
+ * membership + project-graph binding).
1428
+ *
1429
+ * BOTH ⇒ the union (`repoRefs` order first). NEITHER ⇒ one unscoped recon run — the launch the
1430
+ * Testing page sent before this wire existed, unchanged.
1431
+ *
1432
+ * One engine run carries ONE repo, so a multi-repo recon FANS: one governed run per resolved
1433
+ * repo, all under one shared campaign label (`TestingReconResponse.campaign`) — and a real fan
1434
+ * (>= 2 repos) registers an ENGINE campaign under that same id, so `GET /campaigns` serves it
1435
+ * (api-types 0.17.0, crew#390).
1436
+ *
1437
+ * Every launched sibling pauses at its INTAKE GATE (`human_confirm: before:1` — the launch
1438
+ * banner's promise) by default; `ungated: true` is the EXPLICIT opt-out for an unattended fan
1439
+ * (api-types 0.17.0, crew#391). It is never the silent default, and it is audited.
1440
+ */
1441
+ export interface TestingReconBody {
1442
+ problem: string;
1443
+ projectId?: string;
1444
+ repoRefs?: string[];
1445
+ /** EXPLICITLY launch the siblings unattended (no intake gate). Default `false`: each sibling
1446
+ * pauses at its intake gate before any unit runs. */
1447
+ ungated?: boolean;
1448
+ }
1449
+
1450
+ /**
1451
+ * The `POST /testing/recon` 201 body. `runIds` is the source of truth (`length >= 1` always —
1452
+ * one run per resolved repo in the caller's order; exactly one for an unscoped recon); `runId`
1453
+ * is its first entry, kept as the single-run spelling existing launch readers expect;
1454
+ * `campaign` is the shared label every fanned run's `run.launched` audit entry carries — and,
1455
+ * when `campaignRegistered` is `true`, the id of the ENGINE campaign the fan was filed under
1456
+ * (`GET /campaigns/:id` serves it; the `runIds` are its nodes' attempt-0 run ids,
1457
+ * `{campaign}:{node}:a0`). `campaignRegistered: false` = a single/unscoped recon, or an engine
1458
+ * addon without the campaign bindings — the launch will NOT appear on `GET /campaigns`.
1459
+ */
1460
+ export interface TestingReconResponse {
1461
+ runId: string;
1462
+ runIds: string[];
1463
+ campaign: string;
1464
+ /** Whether an engine campaign was registered for this launch (api-types 0.17.0). */
1465
+ campaignRegistered: boolean;
1466
+ /** Present only when the fan was campaign-registered with a `projectId` and filing a sibling
1467
+ * into the project failed — the runs are LIVE; re-attach via POST /projects/:id/members. */
1468
+ projectAttachError?: string;
1469
+ }
1470
+
915
1471
  // ── Governance claims (crew#40/43) ─────────────────────────────────────────────
916
1472
 
917
1473
  /**
@@ -949,17 +1505,29 @@ export interface LaunchRunBody {
949
1505
  */
950
1506
  projectId?: string;
951
1507
  /**
952
- * Delivery mode (crew#293). `"pr"` ⇒ the daemon appends a hardened deliver Tool phase (push
953
- * the run's branch, rebase onto origin's default branch, `gh pr create`; the PR URL is the
954
- * phase output's last line) to a PER-RUN copy of the selected workflow def — the shared def
955
- * is never mutated, and merge stays human. Requires `workflow`; sending `deliver` without it
956
- * is a 400. Omit ⇒ no delivery phase.
1508
+ * Delivery mode (crew#293, defaulting reworked by crew#393 / api-types 0.18.0).
1509
+ *
1510
+ * `"pr"` ⇒ the daemon appends a hardened deliver Tool phase (push the run's branch, rebase
1511
+ * onto origin's default branch, `gh pr create`; the PR URL is the phase output's last line)
1512
+ * to a PER-RUN copy of the selected workflow def — the shared def is never mutated, and
1513
+ * merge stays human. Requires `workflow`; sending `deliver: "pr"` without one is a 400.
1514
+ *
1515
+ * `"none"` ⇒ explicitly no delivery phase (the run's work stays in its worktree and the run
1516
+ * will read `delivery: 'stranded'` on the wire once completed — recoverable any time via
1517
+ * `POST /runs/:id/deliver`).
957
1518
  *
958
- * NOTE for the next api-types release: added daemon-side first (workspace copy, 0.6.0 +
959
- * crew#293) — fold into the published `LaunchRunBody` when 0.7.0 cuts. Optional, so the
960
- * field is forward-additive on the wire (DES-STUDIO-001 §5.1).
1519
+ * OMITTED ⇒ the daemon decides (crew#393): a repo-scoped launch (`repoRef` set) that names a
1520
+ * CODE-WORK `workflow` — a def carrying at least one `executes_code` phase (feature, bug,
1521
+ * migration; not chat/onboarding/recon defs, whose clean worktree the deliver script would
1522
+ * fail loudly) — DEFAULTS to `"pr"`: a completed code run must end with a reviewable
1523
+ * deliverable or an explicit decision not to. The daemon setting `deliverDefault: 'none'`
1524
+ * flips that default off. Repo-less launches, free-text launches (no `workflow`, so there is
1525
+ * no def to append the phase to), and read-only-workflow launches default to `"none"`.
1526
+ *
1527
+ * `"none"` is additive (0.18.0): an older daemon's launch schema rejects it with a 400 —
1528
+ * omit the field entirely against pre-0.18 servers.
961
1529
  */
962
- deliver?: 'pr';
1530
+ deliver?: 'pr' | 'none';
963
1531
  /**
964
1532
  * Retry lineage (DES-UX-001 §8.3, CREW-UX-3; api-types 0.8.0): the id of the run this
965
1533
  * launch retries. Must name an EXISTING run id — an unknown id fails the launch (400 with
@@ -969,6 +1537,28 @@ export interface LaunchRunBody {
969
1537
  * Omit when the launch is not a retry.
970
1538
  */
971
1539
  retryOf?: string;
1540
+ /**
1541
+ * Ad-hoc campaign attach (wicked-studio#27; api-types 0.19.0): file this run onto an EXISTING
1542
+ * campaign's surface. Validated at launch, loudly: an unknown campaign id fails the launch
1543
+ * with a 404 naming it (nothing is launched); a deployment whose engine addon lacks the
1544
+ * campaign bindings answers 501 ("upgrade the engine", never "fix your request").
1545
+ *
1546
+ * The attach is daemon-side provenance ONLY (recorded in the `run.launched` audit entry,
1547
+ * restart-durable, echoed as `AgentSession.campaign_id`): the run executes byte-identically
1548
+ * to an unattached launch — it does NOT become a DAG node, is never scheduled, gated, or
1549
+ * cancelled by the campaign, and the campaign's own lifecycle ignores it. It appears on the
1550
+ * campaigns surface as a `Campaign.attached_runs` entry. Mutually exclusive with
1551
+ * `groupLabel` (both ⇒ 400). Omit both for pre-0.19 behavior, byte for byte.
1552
+ */
1553
+ campaignId?: string;
1554
+ /**
1555
+ * Ad-hoc label grouping, created on first use (wicked-studio#27; api-types 0.19.0): runs
1556
+ * launched with the same label form one group — no pre-registration, no scheduler, no DAG,
1557
+ * no shared gates; pure provenance for "these sibling launches are one effort". 1–200
1558
+ * chars. Served as a `RunGroup` row beside the campaigns on `GET /campaigns` and echoed as
1559
+ * `AgentSession.group_label`. Mutually exclusive with `campaignId` (both ⇒ 400).
1560
+ */
1561
+ groupLabel?: string;
972
1562
  }
973
1563
 
974
1564
  /**
@@ -988,6 +1578,45 @@ export interface SetGuidanceResult {
988
1578
  guidance: string;
989
1579
  }
990
1580
 
1581
+ /**
1582
+ * Response of `POST /runs/:id/deliver` (crew#393; api-types 0.18.0) — post-hoc delivery: lift a
1583
+ * COMPLETED repo-scoped run's stranded worktree into a PR with the SAME hardened script the
1584
+ * deliver phase runs (crew#293/#317: commit the run's work, refuse the default branch, rebase
1585
+ * onto origin's default branch — a conflict fails LOUDLY with nothing pushed — push, `gh pr
1586
+ * create`, and re-derive success from a real PR URL). Takes no body.
1587
+ *
1588
+ * Idempotent: a run that already delivered answers 200 with the SAME recorded `prUrl` — a second
1589
+ * call never opens a second PR. Failure is loud, never silent: 404 (unknown run), 409 (run not
1590
+ * completed / repo-less / worktree gone / a delivery already in flight / the script itself failed
1591
+ * — the body's `error` carries the script's own words, e.g. the rebase-conflict message), 500
1592
+ * (the script produced no verifiable PR URL, or could not be spawned).
1593
+ */
1594
+ export interface DeliverRunResult {
1595
+ prUrl: string;
1596
+ }
1597
+
1598
+ /**
1599
+ * The 409 body of `POST /runs/:id/resume` on a TERMINAL run (crew#311; api-types 0.18.0).
1600
+ *
1601
+ * The engine's `resume_run` no-ops on a completed/cancelled run and answers the status token, so
1602
+ * the route used to reply `200 {"status":"cancelled"}` — on the exact runs an operator was trying
1603
+ * to rescue, the recovery affordance read as "resume destroyed my run". A terminal run is now
1604
+ * refused with 409 and the ACTUAL recovery named machine-readably:
1605
+ *
1606
+ * - `'retry'` — relaunch as a new run with `POST /runs {"retryOf":"<id>"}` (cancelled runs,
1607
+ * and completed runs with nothing liftable — including a `delivery: 'vacuous'` run, whose
1608
+ * units produced no work at all);
1609
+ * - `'deliver'` — the run completed with unlifted work in its worktree (`delivery:
1610
+ * 'stranded'`): lift it with `POST /runs/:id/deliver`.
1611
+ *
1612
+ * A FAILED run is not terminal for resume: it re-enters at the cursor as before (200 + status).
1613
+ */
1614
+ export interface ResumeRefusal {
1615
+ /** Human-readable refusal naming the run's terminal status and the recovery, verbatim. */
1616
+ error: string;
1617
+ recovery: 'retry' | 'deliver';
1618
+ }
1619
+
991
1620
  // ── Governance types (crew#40/41) ──────────────────────────────────────────────
992
1621
 
993
1622
  /** Per-app breakdown within a `CoverageReport`. */
@@ -1177,6 +1806,47 @@ export interface SystemSettings {
1177
1806
  * the engine default `~/.wicked-worker`.
1178
1807
  */
1179
1808
  worker_config_root?: string;
1809
+ /**
1810
+ * The repo-scoped launch delivery DEFAULT (crew#393; api-types 0.18.0, additive). What a
1811
+ * `POST /runs` with `repoRef` + a CODE-WORK `workflow` (a def with at least one
1812
+ * `executes_code` phase) and NO explicit `deliver` resolves to: `'pr'` (the shipped default —
1813
+ * completed code runs open their PR) or `'none'` (work stays in the worktree, surfaced as
1814
+ * `delivery: 'stranded'`). Absent reads as `'pr'`. Never consulted when the launch body sets
1815
+ * `deliver` explicitly, and never applied to repo-less, free-text, or read-only-workflow
1816
+ * launches (those are always `'none'`).
1817
+ */
1818
+ deliverDefault?: 'pr' | 'none';
1819
+ /**
1820
+ * The stall watchdog's DETECTION threshold (crew#287; api-types 0.18.0 — previously a
1821
+ * daemon-local extension): minutes a run in `executing` may go without ANY engine event on the
1822
+ * daemon's relay before one synthetic `workerStalled` frame per quiet period is broadcast on
1823
+ * `/ws`. Integer 1..1440; absent = the daemon default (15). Detection never touches the run.
1824
+ */
1825
+ workerStallMinutes?: number;
1826
+ /**
1827
+ * The stall watchdog's ESCALATION threshold (crew#341; api-types 0.18.0): quiet minutes before
1828
+ * the watchdog ACTS on a detected stall (see {@link WorkerStallEscalatedFrame}). Integer
1829
+ * 0..1440; **absent or 0 = escalation OFF — the shipped default** (the issue's design: automatic
1830
+ * recovery on a run that is merely slow would be worse than the wedge). Values below
1831
+ * `workerStallMinutes` escalate at the detection threshold — the ladder never acts before it
1832
+ * has notified.
1833
+ */
1834
+ workerStallEscalateMinutes?: number;
1835
+ /**
1836
+ * What an armed escalation DOES (crew#341): `'reassign'` (default) recycles the wedged cursor
1837
+ * unit in place — the engine supersedes the stale turn, closes the worker session, bumps the
1838
+ * attempt, and re-dispatches (queued operator injects survive into the fresh turn);
1839
+ * `'notify'` is the fail-loud rung — a `needsYou` `workerStallEscalated` frame + an audit
1840
+ * entry, the run untouched.
1841
+ */
1842
+ workerStallEscalateAction?: 'reassign' | 'notify';
1843
+ /**
1844
+ * Per-run budget of AUTOMATIC reassigns (crew#341): integer 1..10, default 2. Once spent, a
1845
+ * still-silent run gets `outcome: 'exhausted'` `needsYou` frames instead of further recovery —
1846
+ * a worker that wedges deterministically must reach a human, not loop through seats forever.
1847
+ * Ignored when `workerStallEscalateAction` is `'notify'`.
1848
+ */
1849
+ workerStallMaxEscalations?: number;
1180
1850
  /**
1181
1851
  * SKIN-OWNED preference blobs (crew#323). The settings store is shared: alongside the
1182
1852
  * engine's keys it carries the studio's own state — `studio.appearance` (accent/logo/theme),
@@ -1226,6 +1896,14 @@ export interface RequirementsPage {
1226
1896
  /** Which source served the corpus: the live estate store, or the evidence-gated
1227
1897
  * `requirements_graph.json` snapshot, which can lag it by hours (FINDING-065). */
1228
1898
  source: 'store' | 'artifact';
1899
+ /**
1900
+ * Keys in `requirements_overrides.json` that matched NO requirement in the corpus. Operator
1901
+ * edits are keyed by SymbolId (store) or `domain::reqId` (artifact); an estate id-scheme
1902
+ * migration re-mints method/field SymbolIds, so overrides keyed by the old ids stop matching and
1903
+ * would otherwise vanish silently. Non-zero means edits exist that no row is showing — re-run
1904
+ * the annotation workflow, then re-apply or delete the stale keys.
1905
+ */
1906
+ orphanedOverrides?: number;
1229
1907
  }
1230
1908
 
1231
1909
  export interface RequirementPatch {
@@ -1239,7 +1917,11 @@ export interface RequirementPatch {
1239
1917
  export interface BlastRadius {
1240
1918
  target: string;
1241
1919
  dependents: Array<{ id: string; name: string; kind: string; file: string; line: number }>;
1242
- /** Unresolved call-sites referencing the target — absence of dependents never means "safe". */
1920
+ /**
1921
+ * References to the target that no resolver bound (ENGINE-CONTRACT §2.1). Repeat call-sites of
1922
+ * an already-bound relationship are NOT counted, so 0 is a legitimate value for a fully-resolved
1923
+ * hot symbol. When non-zero, an empty `dependents` still never means "safe".
1924
+ */
1243
1925
  unresolved: number;
1244
1926
  }
1245
1927
 
@@ -1349,6 +2031,57 @@ export interface AttachMemberBody {
1349
2031
  attachedBy?: 'studio' | 'interactive' | 'cli' | 'api';
1350
2032
  }
1351
2033
 
2034
+ /**
2035
+ * The crew half of a governed doc delete (crew#338) — what
2036
+ * `DELETE /projects/:projectId/interactive/docs/:doc` did to crew's four handoff ledgers
2037
+ * (`interactive-{draft,edit,chat,demo}-ledger.json`). Present on every answer of that route so
2038
+ * a partial result can never hide.
2039
+ */
2040
+ export interface InteractiveDocDeleteLedgerReport {
2041
+ /** True iff every ledger was swept without error. On the 502 refusal paths this is false with
2042
+ * `skipped: true` — the sweep deliberately did not run because interactive's half did not
2043
+ * happen (nothing diverged). */
2044
+ ok: boolean;
2045
+ /** Every replay-dedup row key actually dropped (`<doc>`, `<doc>:v<n>`, `<doc>:m:<id>`, …).
2046
+ * Empty ⇒ nothing was there — deleting a never-drafted doc is a clean no-op on this side. */
2047
+ removed_keys: string[];
2048
+ /** The ledgers that could NOT be swept, by seam name (`draft`|`edit`|`chat`|`demo`) and cause.
2049
+ * Present only when `ok` is false and the sweep actually ran. */
2050
+ errors?: { ledger: string; error: string }[];
2051
+ /** True ⇒ the sweep was deliberately skipped (interactive refused/failed the retire, so
2052
+ * crew's rows are still doing their job and nothing diverged). */
2053
+ skipped?: boolean;
2054
+ }
2055
+
2056
+ /**
2057
+ * `DELETE /projects/:projectId/interactive/docs/:doc` (crew#338) — 200: the bridge's own retire
2058
+ * answer (interactive#189's wire, relayed verbatim) plus crew's ledger report. The route's
2059
+ * non-200 answers carry `error` plus the same `ledger` report (404 unknown doc — the ledger is
2060
+ * STILL swept, cleaning ghosts of hand-deleted workspaces, but ONLY on the retire wire's own
2061
+ * 404 body `{"error":"unknown doc"}`; 500 partial — interactive retired but the sweep failed,
2062
+ * the body names both halves; 502 — interactive did not retire (5xx, unreachable, or a 404
2063
+ * without the wire's body from a bridge predating the retire route), nothing swept; 409 — the
2064
+ * bridge's build-in-flight refusal relayed verbatim, no `ledger` field).
2065
+ */
2066
+ export interface InteractiveDocDeleteResponse {
2067
+ /** The doc name (slug). */
2068
+ name: string;
2069
+ kind: 'doc' | 'html' | 'source' | 'demo';
2070
+ retired: true;
2071
+ /** True on a repeat delete — idempotent, with the ORIGINAL `retired_at` and no `event_id`. */
2072
+ already_retired: boolean;
2073
+ /** ISO-8601 retirement timestamp. */
2074
+ retired_at: string;
2075
+ /** Head version at retirement. */
2076
+ head: number;
2077
+ /** Lineage size at retirement. */
2078
+ versions: number;
2079
+ /** The `wicked.interactive.doc.retired` bus event id — first retire only (no re-emit). */
2080
+ event_id?: number;
2081
+ /** What crew dropped from its handoff ledgers. */
2082
+ ledger: InteractiveDocDeleteLedgerReport;
2083
+ }
2084
+
1352
2085
  /**
1353
2086
  * One normalized entry of the merged project activity feed
1354
2087
  * (`GET /projects/:id/activity`, ADR §5.2): core events of member runs/chats
@@ -1419,3 +2152,569 @@ export interface ChatOpenBody {
1419
2152
  /** File the chat into a project (`crew.chat` membership, attached on open). */
1420
2153
  projectId?: string;
1421
2154
  }
2155
+
2156
+ // ── Project code graph (DES-PROJECT-001; the co-located multi-repo graph) ──────
2157
+ //
2158
+ // A project's `crew.repo` members indexed into ONE wicked-estate database, each under a stable
2159
+ // label, so one query answers over every repo the project contains.
2160
+ //
2161
+ // THE LIMIT, stated on the wire because it is not obvious from the results: this is CO-LOCATION,
2162
+ // NOT LINKAGE. estate resolves edges within a labelled repo's own nodes only, exactly as if each
2163
+ // repo sat in its own database. `studio → wicked-crew-api-types → crew` does NOT traverse; what
2164
+ // these shapes carry is per-repo results gathered in one place, with the repo named on every hit.
2165
+ // Every response repeats it in `linkage` + `note` so a consumer cannot read a cross-repo answer as
2166
+ // a cross-repo TRACE.
2167
+
2168
+ /** Co-location, never linkage — a single-valued field so a future linked mode is additive. */
2169
+ export type ProjectGraphLinkage = 'co-located';
2170
+
2171
+ /**
2172
+ * Why a project graph cannot answer, or the terms on which it can.
2173
+ *
2174
+ * - `ready` — indexed, two or more repos.
2175
+ * - `ready-single-repo` — indexed, exactly one repo: answers are correct but can never be
2176
+ * cross-repo, which a caller asking a project-scoped question needs told.
2177
+ * - `no-repo-members` — the project has no `crew.repo` member. Not an error and not an empty
2178
+ * graph: there is nothing to build one from.
2179
+ * - `not-indexed` — it has repo members and no database yet. `POST /projects/:id/graph/refresh`
2180
+ * is the fix, and the `detail` says so.
2181
+ * - `engine-too-old` — the running wicked-core addon does not publish `code_graph_db` on the repo
2182
+ * record (wicked-core#170), so its repo-record contract predates this surface.
2183
+ */
2184
+ export type ProjectGraphState =
2185
+ | 'ready'
2186
+ | 'ready-single-repo'
2187
+ | 'no-repo-members'
2188
+ | 'not-indexed'
2189
+ | 'engine-too-old';
2190
+
2191
+ /** One member repo's standing in the project graph. */
2192
+ export interface ProjectGraphRepo {
2193
+ /** Registry id (`RepoEntry.id`) — the member ref this came from. */
2194
+ repoId: string;
2195
+ /** The estate label its rows carry, and the prefix on every `file` path it contributes. */
2196
+ label: string;
2197
+ rootPath: string;
2198
+ /** `false` ⇒ its symbols are NOT in the graph; `reason` says why and results stay partial. */
2199
+ indexed: boolean;
2200
+ /** Commit indexed, when git could answer. */
2201
+ head?: string;
2202
+ /** Unix millis of the index that put these rows in. */
2203
+ indexedAt?: number;
2204
+ /**
2205
+ * Present when `indexed` is false: registry miss, index failure, never refreshed, or the rows
2206
+ * under this label were indexed from a DIFFERENT root than the registry now names (a repo that
2207
+ * moved — wicked-estate binds a label to the root it first saw and refuses to rebind it, so the
2208
+ * graph keeps the old checkout's symbols until the project graph is rebuilt).
2209
+ */
2210
+ reason?: string;
2211
+ /**
2212
+ * The action that actually resolves THIS cause, when one action does. Carried per-row because
2213
+ * the causes do not share a remedy: "never refreshed" and "attached since the last refresh" are
2214
+ * fixed by a refresh, a DANGLING member ref is not (the registry no longer knows the ref, so a
2215
+ * refresh indexes nothing new), and a MOVED root needs a rebuild when estate refuses to rebind
2216
+ * the label. Absent ⇒ no single action fixes it, or `reason` already spells the remedy out.
2217
+ * Never assume a refresh: telling an operator mid-incident to run one that cannot work costs
2218
+ * them the time it takes to run it and the trust to believe the next message.
2219
+ */
2220
+ remedy?: string;
2221
+ }
2222
+
2223
+ /** `GET /projects/:id/graph` — what the project graph holds and what it cannot answer. */
2224
+ export interface ProjectGraphStatus {
2225
+ projectId: string;
2226
+ state: ProjectGraphState;
2227
+ /** One sentence naming the state's cause and its remedy. Always present, never empty. */
2228
+ detail: string;
2229
+ /** Absolute path of the co-located database; `null` when there is none to point at. */
2230
+ dbPath: string | null;
2231
+ repos: ProjectGraphRepo[];
2232
+ /** Member repos absent from the graph — any answer is PARTIAL while this is non-empty. */
2233
+ missingRepos: string[];
2234
+ /** Labels still in the database whose repo is no longer a member; excluded from results. */
2235
+ staleRepos: string[];
2236
+ linkage: ProjectGraphLinkage;
2237
+ note: string;
2238
+ /** Unix millis of the last refresh; `null` when never refreshed. */
2239
+ updatedAt: number | null;
2240
+ }
2241
+
2242
+ /**
2243
+ * `POST /projects/:id/graph/refresh` body. Optional — an empty body is the plain incremental
2244
+ * refresh.
2245
+ *
2246
+ * `force: true` exists for MIGRATION, not for routine use: the plain refresh skips any member repo
2247
+ * whose clean checkout is still at the HEAD the manifest recorded, so after a wicked-estate upgrade
2248
+ * an UNCHANGED repo never re-indexes and its rows keep the old binary's id scheme and unresolved
2249
+ * accounting (ENGINE-CONTRACT §2.1) indefinitely. Force bypasses that skip for every member repo
2250
+ * AND passes `--force` to `wicked-estate index`, so estate performs a full re-extract even when its
2251
+ * own incremental digest would skip. Expect it to take as long as the project's first build.
2252
+ */
2253
+ export interface RefreshProjectGraphBody {
2254
+ /** Re-index every member repo, bypassing the clean-HEAD skip and estate's digest skip. */
2255
+ force?: boolean;
2256
+ }
2257
+
2258
+ /**
2259
+ * One member repo's outcome in a refresh — why it did or did not index. `skipped-head-unchanged`
2260
+ * is the row an operator reads when asking why a repo did not migrate after an estate upgrade:
2261
+ * the plain refresh trusts a clean checkout at the manifest's HEAD, so only `force` (or a new
2262
+ * commit) makes that repo re-index.
2263
+ */
2264
+ export interface ProjectGraphRefreshRepo {
2265
+ repoId: string;
2266
+ /** The estate label its rows carry. */
2267
+ label: string;
2268
+ /** What this refresh did with the repo. */
2269
+ action: 'indexed' | 'skipped-head-unchanged' | 'failed';
2270
+ /**
2271
+ * Bounded tail (last 2 KiB) of `wicked-estate index` stderr, present when the binary said
2272
+ * anything — notably estate's id_scheme migration notice explaining why a refresh that used to
2273
+ * skip in a second ran a full re-extract for minutes.
2274
+ */
2275
+ stderrTail?: string;
2276
+ /** Present when `action` is `failed` — the same message the `failed` array carries. */
2277
+ error?: string;
2278
+ }
2279
+
2280
+ /** `POST /projects/:id/graph/refresh` — an incremental (re)build, repo by repo. */
2281
+ export interface ProjectGraphRefreshResult {
2282
+ status: ProjectGraphStatus;
2283
+ /** Labels re-indexed this run. */
2284
+ indexed: string[];
2285
+ /** Labels skipped because the checkout is clean and its HEAD is already in the graph. */
2286
+ skipped: string[];
2287
+ /** Repos that failed to index, with the reason; the graph keeps whatever it already had. */
2288
+ failed: Array<{ repoId: string; label: string; error: string }>;
2289
+ /** Per-repo outcomes with the WHY (and any index stderr) attached — one row per member repo. */
2290
+ repos?: ProjectGraphRefreshRepo[];
2291
+ }
2292
+
2293
+ /** One hit, always attributed: a cross-repo answer that does not say WHERE is not an answer. */
2294
+ export interface ProjectGraphHit {
2295
+ /** The repo's registry id. */
2296
+ repoId: string;
2297
+ /** The repo's estate label. */
2298
+ repo: string;
2299
+ /** estate SymbolId — carries the label, so it is unique across the project. */
2300
+ id: string;
2301
+ /**
2302
+ * The graph node's name, VERBATIM from estate. For a `kind: 'file'` node that name IS the
2303
+ * label-prefixed path (`wicked-vault/src/vault/vault.mjs`) — left as estate minted it, because
2304
+ * rewriting it would produce a name that resolves to nothing. Use `repo` + `file` to display it.
2305
+ */
2306
+ name: string;
2307
+ kind: string;
2308
+ /** REPO-RELATIVE path, label stripped — the same spelling `/repos/:id/graph` returns. */
2309
+ file: string;
2310
+ line: number;
2311
+ }
2312
+
2313
+ /** Per-repo hit counts, so "which repo does this touch" is answerable without scanning hits. */
2314
+ export interface ProjectGraphRepoCount {
2315
+ repoId: string;
2316
+ repo: string;
2317
+ count: number;
2318
+ }
2319
+
2320
+ /** `GET /projects/:id/graph/blast-radius?name=` — dependents across every member repo. */
2321
+ export interface ProjectBlastRadius {
2322
+ projectId: string;
2323
+ target: string;
2324
+ dependents: ProjectGraphHit[];
2325
+ byRepo: ProjectGraphRepoCount[];
2326
+ /**
2327
+ * References to the target that no resolver bound (ENGINE-CONTRACT §2.1). Repeat call-sites of
2328
+ * an already-bound relationship are NOT counted, so 0 is a legitimate value for a fully-resolved
2329
+ * hot symbol. When non-zero, an empty `dependents` still never means "safe".
2330
+ */
2331
+ unresolved: number;
2332
+ /** Labels actually covered by this answer. */
2333
+ reposSearched: string[];
2334
+ /** Member repos NOT covered — the answer is partial while this is non-empty. */
2335
+ missingRepos: string[];
2336
+ linkage: ProjectGraphLinkage;
2337
+ note: string;
2338
+ }
2339
+
2340
+ /** `GET /projects/:id/graph/search?name=` — exact-name symbol resolution across member repos. */
2341
+ export interface ProjectGraphSearch {
2342
+ projectId: string;
2343
+ query: string;
2344
+ matches: ProjectGraphHit[];
2345
+ byRepo: ProjectGraphRepoCount[];
2346
+ reposSearched: string[];
2347
+ missingRepos: string[];
2348
+ linkage: ProjectGraphLinkage;
2349
+ /**
2350
+ * Carries the co-location limit AND the matching rule: this is EXACT-NAME resolution, so an
2351
+ * empty `matches` means "no symbol by that exact name" and never "not in this project". A
2352
+ * partial or fuzzy name returns nothing even when the symbol exists.
2353
+ */
2354
+ note: string;
2355
+ }
2356
+
2357
+ // ── Campaigns (DES-CAMPAIGN-001 / crew#342 slice 1 + TH-9) ───────────────────
2358
+ // The engine's Campaign scheduler — a durable, crash-resumable, dependency-aware parallel DAG of
2359
+ // governed Runs — exposed over `/api/v1/campaigns`. The GET routes return the ENGINE's persisted
2360
+ // shapes verbatim (wicked-core `src/campaign.rs` serde: snake_case fields, snake_case enum
2361
+ // tokens), same doctrine as `SessionView`: one producer of the wire shape, zero re-spelling in
2362
+ // the daemon. The POST body (`LaunchCampaignBody`) is the daemon's own camelCase request contract
2363
+ // — crew maps it onto the engine def, composing one Tool-executor workflow per deterministic
2364
+ // scenario (spec FILE PATHS on the argv, never scenario bodies — the 1022-byte PTY prompt trap).
2365
+
2366
+ /** Per-node lifecycle status. Terminal = `completed` | `failed` | `blocked` | `cancelled`. */
2367
+ export type CampaignNodeStatus =
2368
+ | 'pending'
2369
+ | 'ready'
2370
+ | 'running'
2371
+ | 'awaiting_human'
2372
+ | 'ready_to_resume'
2373
+ | 'completed'
2374
+ | 'failed'
2375
+ | 'blocked'
2376
+ | 'cancelled';
2377
+
2378
+ /** Campaign lifecycle status — also the token `POST /campaigns/:id/resume` / `/cancel` resolve to. */
2379
+ export type CampaignStatus =
2380
+ | 'running'
2381
+ | 'paused'
2382
+ | 'completed'
2383
+ | 'partially_completed'
2384
+ | 'failed'
2385
+ | 'cancelled';
2386
+
2387
+ /** When a dependency edge is satisfied: dep `completed` only, or any terminal outcome. */
2388
+ export type CampaignEdgeCondition = 'on_success' | 'on_terminal';
2389
+
2390
+ /** How a node failure propagates (DES-CAMPAIGN-001 §5.2). */
2391
+ export type CampaignFailurePolicy = 'fail_fast' | 'continue_independent' | 'human_gate_on_failure';
2392
+
2393
+ /**
2394
+ * What one campaign node runs — the engine's reusable Run specification (mirrors a launch minus
2395
+ * the session id, which the scheduler derives as `{campaign}:{node}:a{attempt}`). A node inherits
2396
+ * governance, worktree isolation, HITL gates, live output, and per-Run resume from the Run it is.
2397
+ */
2398
+ export interface CampaignRunSpec {
2399
+ /** The free-text problem this node's Run decomposes — for a scenario node, a short human label
2400
+ * (the scenario body lives in its spec FILE; it is never inlined here). */
2401
+ problem: string;
2402
+ /** The council roster (`AgenticCli` seats) convened for this node's Run. */
2403
+ clis: unknown[];
2404
+ entity_mode: EntityMode;
2405
+ /** The node's internal human-confirm gate policy (engine serde shape); absent = none. */
2406
+ human_confirm?: unknown;
2407
+ /** The registered repo the node's Run targets, if any (creates an isolated worktree). */
2408
+ repo_ref?: string | null;
2409
+ /** The registered `WorkflowDef` id this node's Run follows — for a deterministic scenario node,
2410
+ * the per-node composed Tool workflow (`campaign-<campaign>-<node>`). */
2411
+ workflow_id?: string | null;
2412
+ }
2413
+
2414
+ /** A schedulable unit of the DAG — one node = one governed core Run. */
2415
+ export interface CampaignNode {
2416
+ /** Stable node id, unique within the campaign (never contains `:` — it keys the run id). */
2417
+ node_id: string;
2418
+ run_spec: CampaignRunSpec;
2419
+ }
2420
+
2421
+ /** A dependency edge `from -> to`: `to` becomes eligible once `from` satisfies `condition`. */
2422
+ export interface CampaignEdge {
2423
+ from: string;
2424
+ to: string;
2425
+ condition: CampaignEdgeCondition;
2426
+ }
2427
+
2428
+ /** The static campaign definition — validated at launch (cycle / empty / duplicate-edge /
2429
+ * unknown-endpoint rejects) and persisted verbatim inside the live {@link Campaign}. */
2430
+ export interface CampaignDef {
2431
+ id: string;
2432
+ name: string;
2433
+ nodes: CampaignNode[];
2434
+ edges: CampaignEdge[];
2435
+ policy: CampaignFailurePolicy;
2436
+ /** Global concurrency cap (>= 1) — a resource guard on parallel worktrees + CLI subprocesses. */
2437
+ max_concurrency: number;
2438
+ }
2439
+
2440
+ /**
2441
+ * The live campaign instance (`GET /campaigns`, `GET /campaigns/:id`) — durable and
2442
+ * crash-resumable; the scheduler re-derives the ready set from these persisted statuses on
2443
+ * resume, never re-running a completed node.
2444
+ */
2445
+ export interface Campaign {
2446
+ id: string;
2447
+ def_id: string;
2448
+ status: CampaignStatus;
2449
+ /** The full definition, embedded so a resume needs no second store. */
2450
+ def: CampaignDef;
2451
+ node_status: Record<string, CampaignNodeStatus>;
2452
+ /**
2453
+ * node_id -> live Run id, always `{campaign}:{node}:a{attempt}` (attempt-keyed, idempotent).
2454
+ *
2455
+ * These values ARE the engine session ids: a node's run is an ordinary governed run, so it is
2456
+ * served by `GET /runs` / `GET /runs/:id` under exactly this id, and every session-scoped
2457
+ * CoreEvent frame on `/ws` carries it as `session` (the `campaignNodeStarted` /
2458
+ * `campaignNodeAwaitingHuman` frames additionally spell it as `runId`). Live narration for a
2459
+ * campaign card is therefore a pure client-side correlation:
2460
+ * `frame.session === campaign.node_run_id[node]` — no extra wire, no per-node fetch
2461
+ * (wicked-studio#27).
2462
+ */
2463
+ node_run_id: Record<string, string>;
2464
+ /** node_id -> 0-based attempt counter (a `human_gate_on_failure` Retry bumps it). */
2465
+ node_attempt: Record<string, number>;
2466
+ /** Persisted amend text of an approved-but-unslotted per-node gate decision (crash-safe). */
2467
+ pending_decision_amend: Record<string, string | null>;
2468
+ /** `human_gate_on_failure` queue — failed nodes awaiting a Retry/Skip/Abort decision. */
2469
+ pending_failure_gates: string[];
2470
+ /** Whether a fail-fast (or Abort) tripped — finalization lands on `failed`. */
2471
+ fail_fast_tripped: boolean;
2472
+ /**
2473
+ * Per-node delivery (wicked-studio#27; api-types 0.19.0) — DAEMON-JOINED at DTO assembly on
2474
+ * `GET /campaigns` and `GET /campaigns/:id`, never engine-persisted: the exact same
2475
+ * `delivery`/`deliverUrl` derivation every run already carries on the runs wire
2476
+ * (`AgentSession.delivery`, crew#393/#311), keyed by node_id like {@link Campaign.node_status}.
2477
+ * A node appears here once its current-attempt run (`node_run_id[node]`) exists on the store —
2478
+ * a still-pending node has no entry. With `node_run_id` + `node_status`, this is everything a
2479
+ * "N of M delivered" rollup needs from ONE campaigns fetch — no per-node run fetch, and no
2480
+ * dependence on the runs list's archived-run filtering. ABSENT only from a pre-0.19 daemon.
2481
+ */
2482
+ node_delivery?: Record<string, CampaignNodeDelivery>;
2483
+ /**
2484
+ * Ad-hoc runs attached to this campaign at launch (`LaunchRunBody.campaignId`;
2485
+ * wicked-studio#27, api-types 0.19.0) — DAEMON-JOINED like {@link Campaign.node_delivery},
2486
+ * launch order. These are NOT DAG nodes: the campaign's scheduler and lifecycle ignore them;
2487
+ * each entry is an independent governed run that an operator filed onto this campaign's
2488
+ * surface. Absent from a pre-0.19 daemon; `[]` when none. Narration correlation is the same
2489
+ * as for nodes: each `runId` is the engine session id on `/ws` frames (`frame.session`).
2490
+ */
2491
+ attached_runs?: AttachedRunView[];
2492
+ }
2493
+
2494
+ /**
2495
+ * One member's delivery snapshot on the campaigns surface (wicked-studio#27; api-types 0.19.0)
2496
+ * — the same tri-state + URL contract as `AgentSession.delivery`/`deliverUrl` (crew#393/#311),
2497
+ * derived by the same code at DTO assembly. `deliverUrl` present exactly when
2498
+ * `delivery === 'delivered'`.
2499
+ */
2500
+ export interface CampaignNodeDelivery {
2501
+ delivery: 'delivered' | 'stranded' | 'vacuous' | 'none';
2502
+ deliverUrl?: string;
2503
+ }
2504
+
2505
+ /**
2506
+ * An ad-hoc run on the campaigns surface (wicked-studio#27; api-types 0.19.0) — a member of
2507
+ * `Campaign.attached_runs` or `RunGroup.runs`. `runId` is the engine session id: the run is
2508
+ * served in full by `GET /runs/:id`, and live `/ws` CoreEvent frames for it carry it as
2509
+ * `session`. `status`/`delivery`/`deliverUrl` are snapshots of the same fields the run wire
2510
+ * serves, so a rollup needs no second fetch.
2511
+ */
2512
+ export interface AttachedRunView {
2513
+ runId: string;
2514
+ status: SessionStatus;
2515
+ delivery: 'delivered' | 'stranded' | 'vacuous' | 'none';
2516
+ deliverUrl?: string;
2517
+ }
2518
+
2519
+ /**
2520
+ * An ad-hoc label group (wicked-studio#27; api-types 0.19.0): the runs launched with the same
2521
+ * `LaunchRunBody.groupLabel`, in launch order. NOT an engine campaign — no DAG, no scheduler,
2522
+ * no status of its own (derive a rollup from the members); it exists the moment the first run
2523
+ * is launched under the label. Served beside the campaigns in `CampaignsListResponse.groups`.
2524
+ */
2525
+ export interface RunGroup {
2526
+ label: string;
2527
+ runs: AttachedRunView[];
2528
+ }
2529
+
2530
+ /**
2531
+ * `GET /campaigns` 200 body (api-types 0.19.0 — `groups` is ADDITIVE: a pre-0.19 daemon sends
2532
+ * only `campaigns`). One fetch answers the whole grouping surface: engine campaigns (each
2533
+ * carrying its own per-node rollup fields) plus the ad-hoc label groups.
2534
+ */
2535
+ export interface CampaignsListResponse {
2536
+ campaigns: Campaign[];
2537
+ groups: RunGroup[];
2538
+ }
2539
+
2540
+ /**
2541
+ * One scenario of `POST /campaigns` — the daemon maps each onto a {@link CampaignNode}.
2542
+ *
2543
+ * Exactly one of `tool` / `agent` per scenario:
2544
+ * - `tool` (deterministic): the daemon composes a single-phase Tool-executor workflow running
2545
+ * `cmd` verbatim (no council, no LLM). Every argv token must be a single line of ≤ 1022
2546
+ * bytes — pass the scenario/spec as a FILE PATH argument, never inline its body. A body
2547
+ * smuggled into the argv is rejected with a 400 naming the offending token.
2548
+ * - `agent` (exploratory/authoring): a governed agent run over `problem` — same byte
2549
+ * discipline, because the problem text reaches PTY workers as prompt material.
2550
+ */
2551
+ export interface CampaignScenario {
2552
+ /** Node id, unique within the campaign (letters/digits/dots/hyphens/underscores; no `:`). */
2553
+ id: string;
2554
+ /** Short human label for the scoreboard; defaults to the id. NOT the scenario body. */
2555
+ title?: string;
2556
+ /** Ids of scenarios that must reach `depsCondition` before this one dispatches. */
2557
+ deps?: string[];
2558
+ /** Edge condition applied to every dep edge of this scenario (default `on_success`). */
2559
+ depsCondition?: CampaignEdgeCondition;
2560
+ /** Deterministic execution: argv run by the engine's Tool executor, file paths not bodies. */
2561
+ tool?: { cmd: string[] };
2562
+ /** Governed agent execution: the problem statement (short — reaches workers as a prompt). */
2563
+ agent?: { problem: string; workflow?: string };
2564
+ /** Registered repo this node runs within (isolated worktree). Omit for a repo-less node. */
2565
+ repoRef?: string;
2566
+ }
2567
+
2568
+ /** `POST /campaigns` request body → 201 {@link LaunchCampaignResponse}. */
2569
+ export interface LaunchCampaignBody {
2570
+ /** Campaign id (also the def id). Defaults to a generated `campaign-<uuid>`. */
2571
+ id?: string;
2572
+ name?: string;
2573
+ scenarios: CampaignScenario[];
2574
+ /** Failure propagation policy (default `continue_independent`). */
2575
+ policy?: CampaignFailurePolicy;
2576
+ /** Global concurrency cap, >= 1 (default 2). */
2577
+ maxConcurrency?: number;
2578
+ /** JSON array of `AgenticCli` seats for agent scenarios; defaults to the daemon roster. */
2579
+ clisJson?: string;
2580
+ /**
2581
+ * The multiscope wire (api-types 0.15.0), shared with `POST /testing/recon`: scope the launch
2582
+ * to a PROJECT — crew resolves the project's `crew.repo` members server-side. Unknown project
2583
+ * ⇒ 404; a project with zero repo members (and no `repoRefs` to fall back on) ⇒ 400 naming
2584
+ * the fix. Combined with `repoRefs` ⇒ the union. Because one campaign node runs against ONE
2585
+ * repo, every scenario that does not pin its own `repoRef` is FANNED — one node per resolved
2586
+ * repo, same campaign — and the 201 body carries the fanned nodes' run ids (`runIds`).
2587
+ * Omit both fields ⇒ the launch behaves exactly as before (backward compatible).
2588
+ */
2589
+ projectId?: string;
2590
+ /**
2591
+ * The multiscope wire (api-types 0.15.0): explicit codebase attachments — registered repo
2592
+ * refs (ids; a unique repo NAME also resolves), deduped, min 1 when present. A ref that does
2593
+ * not resolve fails the WHOLE launch with a 400 naming it — never a silently narrowed scope.
2594
+ */
2595
+ repoRefs?: string[];
2596
+ }
2597
+
2598
+ /**
2599
+ * `POST /campaigns` 201 body. `runIds` is ADDITIVE (api-types 0.15.0): present exactly when
2600
+ * the launch used the multiscope fields (`projectId`/`repoRefs`), carrying the attempt-0 run
2601
+ * ids (`{campaign}:{node}:a0`) of the launch's nodes — repo-major over the fanned scenarios
2602
+ * (repos in resolved input order), then the scenarios that pinned their own `repoRef`.
2603
+ * Consumers treat `runIds.length >= 1` as the source of truth for what launched; a legacy
2604
+ * body keeps the legacy `{ campaignId }` answer.
2605
+ */
2606
+ export interface LaunchCampaignResponse {
2607
+ campaignId: string;
2608
+ runIds?: string[];
2609
+ }
2610
+
2611
+ /**
2612
+ * The 11 campaign frames of the `/ws` stream (wicked-core `event_to_json`, camelCase tags), as a
2613
+ * discriminated union for consumers that narrow on `type` — the TH-14 scoreboard renders node
2614
+ * status from exactly these. They also flow through the permissive {@link CoreEvent}; the daemon
2615
+ * relay is allowlist-free, so every frame below reaches `/ws` clients byte-for-byte.
2616
+ */
2617
+ export type CampaignEvent =
2618
+ | { type: 'campaignLaunched'; campaign: string }
2619
+ | { type: 'campaignNodeReady'; campaign: string; node: string }
2620
+ | { type: 'campaignNodeStarted'; campaign: string; node: string; runId: string }
2621
+ | { type: 'campaignNodeAwaitingHuman'; campaign: string; node: string; runId: string; prompt: string }
2622
+ | { type: 'campaignNodeCompleted'; campaign: string; node: string }
2623
+ | { type: 'campaignNodeFailed'; campaign: string; node: string }
2624
+ | { type: 'campaignNodeBlocked'; campaign: string; node: string }
2625
+ | { type: 'campaignPaused'; campaign: string }
2626
+ | { type: 'campaignCompleted'; campaign: string }
2627
+ | { type: 'campaignFailed'; campaign: string }
2628
+ | { type: 'campaignCancelled'; campaign: string };
2629
+
2630
+ // ── Diagnostics (api-types 0.16.0) ──────────────────────────────────────────────
2631
+
2632
+ /**
2633
+ * Component versions of the running deployment (`GET /diagnostics`). The honesty rule of the
2634
+ * whole diagnostics surface: a version the daemon cannot determine is `null` — never guessed.
2635
+ */
2636
+ export interface DiagnosticsComponents {
2637
+ /** The wicked-crew daemon's own package version (always known). */
2638
+ crew: string;
2639
+ /** The bundled studio SPA's version, read from the manifest the bundle ships
2640
+ * (`testid-inventory.json` → `studioVersion`); `null` on a headless daemon or a bundle
2641
+ * that predates the manifest. */
2642
+ studioBundle: string | null;
2643
+ /** The installed `wicked-core-ts` engine binding's version; `null` when unresolvable. */
2644
+ coreTs: string | null;
2645
+ /** Engine binary versions via `--version`, keyed by binary name (`wicked-core`,
2646
+ * `wicked-estate`) — probed ONLY through paths crew already resolves (`WICKED_CORE_EXE`,
2647
+ * bare PATH name); `null` per binary when unresolved or the probe fails. */
2648
+ engineBinaries: Record<string, string | null>;
2649
+ }
2650
+
2651
+ /** Daemon process runtime facts (`GET /diagnostics`). */
2652
+ export interface DiagnosticsDaemon {
2653
+ /** Milliseconds since this daemon process started. */
2654
+ uptimeMs: number;
2655
+ /** Epoch ms the process started (now − uptime). */
2656
+ startedAt: number;
2657
+ /** The actually-bound HTTP port (honours --port / CREW_PORT / port 0). */
2658
+ port: number;
2659
+ }
2660
+
2661
+ /** One store file (or dir, sized as a content total) under the daemon's state home. Paths and
2662
+ * sizes only — file contents never ride this wire. */
2663
+ export interface DiagnosticsStoreFile {
2664
+ /** Filename relative to the state home (`core.db`, `core.db-wal`, `core.db.events`, …). */
2665
+ name: string;
2666
+ /** Absolute path. */
2667
+ path: string;
2668
+ /** File size, or the total of a directory's contents (the events dir). */
2669
+ bytes: number;
2670
+ }
2671
+
2672
+ /** One captured error-level line from the daemon's own log ring (`GET /diagnostics`). */
2673
+ export interface DiagnosticsRecentError {
2674
+ /** Epoch ms the line was logged. */
2675
+ ts: number;
2676
+ /** Which record produced it (`daemon` = the in-process pino error ring). */
2677
+ source: string;
2678
+ /** The logged message, length-bounded. */
2679
+ line: string;
2680
+ }
2681
+
2682
+ /**
2683
+ * Per-CLI ACP health, folded from the durable run event logs
2684
+ * (`<state-home>/core.db.events/*.ndjson`, `acpSessionStarted` / `acpFallback` frames).
2685
+ * A CLI that never appears in the logs has no key — absence spells "no ACP traffic recorded",
2686
+ * and zero counts are never invented for it.
2687
+ */
2688
+ export interface AcpCliDiagnostics {
2689
+ /** Count of `acpSessionStarted` events for this cliKey. */
2690
+ sessionsStarted: number;
2691
+ /** Count of `acpFallback` events (ACP unavailable/failed; run continued single-shot). */
2692
+ fallbacks: number;
2693
+ /** Fallback counts by `fallbackKind` (`session_died`, `auth_required`, `binary_unavailable`, …). */
2694
+ fallbackKinds: Record<string, number>;
2695
+ /** Epoch ms of the newest `acpSessionStarted`, or `null` when none recorded. */
2696
+ lastStartedTs: number | null;
2697
+ /** Epoch ms of the newest `acpFallback`, or `null` when none recorded. */
2698
+ lastFallbackTs: number | null;
2699
+ }
2700
+
2701
+ /** The ACP fold of `GET /diagnostics` — keyed by `cliKey` (`claude`, `pi`, `codex`, …). */
2702
+ export interface AcpDiagnostics {
2703
+ byCli: Record<string, AcpCliDiagnostics>;
2704
+ }
2705
+
2706
+ /**
2707
+ * `GET /diagnostics` (api-types 0.16.0) — the daemon's read-only self-knowledge surface:
2708
+ * what is deployed, what it stores, what has gone wrong recently, and whether ACP is really
2709
+ * working across the CLIs. Every field is derived from records the daemon already owns;
2710
+ * anything it cannot answer is `null` / empty, never fabricated.
2711
+ */
2712
+ export interface DiagnosticsResponse {
2713
+ components: DiagnosticsComponents;
2714
+ daemon: DiagnosticsDaemon;
2715
+ /** `core.db` + sidecars + the events dir (as a total), sorted by name. */
2716
+ stores: DiagnosticsStoreFile[];
2717
+ /** Bounded tail of the daemon's own error-level log lines, newest first. */
2718
+ recentErrors: DiagnosticsRecentError[];
2719
+ acp: AcpDiagnostics;
2720
+ }