wicked-crew-api-types 0.8.0 → 0.18.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 +1234 -11
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -178,6 +178,59 @@ export interface AgentSession {
178
178
  * spelling of "no lineage", so `'retry_of' in session` and `!== undefined` agree.
179
179
  */
180
180
  retry_of?: string;
181
+ /**
182
+ * Durable operator guidance (DES-UX-002 §7.2 — spec'd there as CREW-UX-4, shipped as
183
+ * CREW-UX-7; api-types 0.9.0): the ONE pre-gate note upserted via `PUT /runs/:id/guidance`,
184
+ * populated at DTO assembly on both `GET /runs` and `GET /runs/:id`. ABSENT — never `null`
185
+ * or `''` — when no note is set (never set, cleared, or a pre-0.9.0 server), so old clients
186
+ * are unaffected and `'guidance' in session` and `!== undefined` agree.
187
+ *
188
+ * Operator-visible context ONLY: the governance gate never reads this field; the amend text
189
+ * at gate decision (`GateDecision.amend`) remains the one injection point. Skins use it to
190
+ * pre-populate the steer textarea when a gate arrives.
191
+ */
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;
181
234
  }
182
235
 
183
236
  /** An ordered unit of work within a run (`WorkUnit`). */
@@ -206,6 +259,14 @@ export interface WorkUnit {
206
259
  gate?: GateSpec | string;
207
260
  /** True when a pinned deterministic validator is attached to this unit. */
208
261
  has_validator_pin?: boolean;
262
+ /**
263
+ * True when this unit's backing phase declared `executes_code` (crew#311 / core#297 §2) —
264
+ * carried from the def at plan time. The engine's code-evidence floor holds such a unit to a
265
+ * worktree diff: a governed Creator unit with this flag cannot fold "done" over an untouched
266
+ * worktree. Additive and skip-if-false on the wire: absent on non-code units, on units planned
267
+ * before the field existed, and on an older engine.
268
+ */
269
+ executes_code?: boolean;
209
270
  }
210
271
 
211
272
  /** A run plus its ordered units (`SessionView`) — the shape `GET /runs` returns. */
@@ -347,7 +408,10 @@ export interface GateInfo {
347
408
  receivedAt: string;
348
409
  }
349
410
 
350
- /** Approve / reject payload for the steering gate (`POST /runs/:id/gate`). */
411
+ /** Approve / reject payload for the steering gate (`POST /runs/:id/gate`). On a
412
+ * `steering-author` propose gate, an approve — amend note or not — also LANDS the proposal
413
+ * into the rules store and the 200 body carries a {@link SteeringLandingResult}; the amend
414
+ * steers the RUN's continuation, never the landed rule text (crew#388). */
351
415
  export interface GateDecision {
352
416
  approve: boolean;
353
417
  amend?: string;
@@ -491,10 +555,39 @@ export interface CoreEvent {
491
555
  latencyMs?: number;
492
556
  /** workerStalled: silent seconds before the stall event fired. */
493
557
  stalledSecs?: number;
558
+ /**
559
+ * `workerStalled` / `workerStallEscalated` (DAEMON-SYNTHETIC, crew#287/crew#341): how long the
560
+ * run had been silent on the daemon's CoreEvent relay when the frame fired, ms. Distinguishes
561
+ * the daemon's synthetic `workerStalled` frame (this field) from the engine's PTY-path
562
+ * `workerStalled` event (`stalledSecs` above) — same `type` tag, different producers.
563
+ */
564
+ quietForMs?: number;
565
+ /** `workerStallEscalated` (crew#341): what the watchdog did — `'reassign'` | `'notify'`. */
566
+ action?: string;
567
+ /** `workerStallEscalated`: how the action ended — `'ok'` | `'failed'` | `'exhausted'`. */
568
+ outcome?: string;
569
+ /**
570
+ * `workerStallEscalated`: `true` when a human should look — the action failed, the automatic
571
+ * recovery budget is exhausted, or the configured action is `'notify'` (surface, don't touch).
572
+ * `false` means the platform recovered on its own; the narrator shows it, nobody is paged.
573
+ */
574
+ needsYou?: boolean;
575
+ /** `workerStallEscalated` (action `'reassign'`): automatic reassigns consumed for this run, this one included. */
576
+ escalations?: number;
577
+ /** `workerStallEscalated` (outcome `'failed'`): bounded excerpt of what the recovery call threw. */
578
+ error?: string;
494
579
  /** failureTriaged: the triage judge's decision. */
495
580
  decision?: string;
496
581
  /** failureTriaged: the judge's bounded reasoning. */
497
582
  analysis?: string;
583
+ // campaign* frames (DES-CAMPAIGN-001 / TH-9) — camelCase per event_to_json; the exact
584
+ // per-variant shapes are the CampaignEvent union below.
585
+ /** campaign* frames: the campaign id. */
586
+ campaign?: string;
587
+ /** campaignNode* frames: the node id within the campaign. */
588
+ node?: string;
589
+ /** campaignNodeStarted / campaignNodeAwaitingHuman: the node's attempt-keyed Run id. */
590
+ runId?: string;
498
591
  // assumptionRecorded (external-transform convention) — camelCase per event_to_json
499
592
  kind?: string;
500
593
  library?: string;
@@ -514,6 +607,63 @@ export interface CoreEvent {
514
607
  * named live, and a bundle assembled after a run cannot describe it in different
515
608
  * words than the operator watched it happen in. (FINDING-014)
516
609
  */
610
+ /**
611
+ * The stall watchdog's DETECTION frame (crew#287; api-types 0.18.0 — previously daemon-local).
612
+ * DAEMON-SYNTHETIC: broadcast straight to `/ws` when a run in `executing` has been silent on the
613
+ * daemon's CoreEvent relay past `SystemSettings.workerStallMinutes` — never re-relayed through
614
+ * the engine, one frame per quiet period, re-armed by any new event for the run. Detection only:
615
+ * the run is not touched. (The engine's PTY-path event shares the `workerStalled` tag but
616
+ * carries `stalledSecs`; this frame carries `quietForMs`.)
617
+ *
618
+ * `type` aliases on purpose, not `interface`s: only anonymous object types satisfy `CoreEvent`'s
619
+ * index signature, which is what lets these frames flow through CoreEvent-typed broadcast seams.
620
+ */
621
+ export type WorkerStalledFrame = {
622
+ type: 'workerStalled';
623
+ /** The stalled run's id. */
624
+ session: string;
625
+ /** The current unit ord, when any observed event or the run header named one. */
626
+ ord?: number;
627
+ /** How long the run has been silent when the frame fired, ms. */
628
+ quietForMs: number;
629
+ };
630
+
631
+ /**
632
+ * The stall watchdog's ESCALATION frame (crew#341; api-types 0.18.0). DAEMON-SYNTHETIC, emitted
633
+ * only when escalation is armed (`SystemSettings.workerStallEscalateMinutes` > 0 — OFF by
634
+ * default) and a detected stall stays silent past that threshold. Reports what the watchdog DID
635
+ * and how it ended, one escalation per quiet period:
636
+ *
637
+ * - `action: 'reassign'` — the wedged cursor unit was recycled in place (engine `reassignUnit`:
638
+ * the stale turn is superseded, the session closed, the unit re-dispatched; queued operator
639
+ * injects survive into the fresh turn). `outcome: 'ok' | 'failed' | 'exhausted'`.
640
+ * - `action: 'notify'` — surface loudly without touching the run (the fail-loud rung).
641
+ *
642
+ * `needsYou: true` marks the frames a human should act on (failed / exhausted / notify).
643
+ */
644
+ export type WorkerStallEscalatedFrame = {
645
+ type: 'workerStallEscalated';
646
+ /** The stalled run's id. */
647
+ session: string;
648
+ /** The cursor unit's ord, when known. */
649
+ ord?: number;
650
+ /** How long the run had been silent when the watchdog escalated, ms. */
651
+ quietForMs: number;
652
+ /** What the watchdog did. */
653
+ action: 'reassign' | 'notify';
654
+ /** How it ended: `ok` (acted / surfaced), `failed` (the recovery call threw), `exhausted`
655
+ * (the per-run automatic budget was already spent — nothing was attempted). */
656
+ outcome: 'ok' | 'failed' | 'exhausted';
657
+ /** True when a human should look; false = the platform recovered on its own. */
658
+ needsYou: boolean;
659
+ /** action `reassign`: the seat the unit was re-dispatched to, when known. */
660
+ cli?: string;
661
+ /** action `reassign`: automatic reassigns consumed for this run, this one included. */
662
+ escalations?: number;
663
+ /** outcome `failed`: bounded excerpt of what the recovery call threw. */
664
+ error?: string;
665
+ };
666
+
517
667
  export interface RecordedEvent extends CoreEvent {
518
668
  /** Capture time, epoch millis — when core emitted the frame, not when it was read. */
519
669
  ts: number;
@@ -873,9 +1023,30 @@ export interface GovernancePolicy {
873
1023
  retired?: boolean;
874
1024
  }
875
1025
 
1026
+ /**
1027
+ * The steering-type vocabulary (STEERING program) — one sub-page per type in the studio's
1028
+ * Steering surface. Enum-as-string on the wire; the engine's serde default is `architecture`,
1029
+ * so a rule row written before the field existed reads back as an architecture rule.
1030
+ */
1031
+ export type SteeringType =
1032
+ | 'architecture'
1033
+ | 'development'
1034
+ | 'security'
1035
+ | 'testing'
1036
+ | 'operations'
1037
+ | 'compliance'
1038
+ | 'design-ux';
1039
+
876
1040
  /**
877
1041
  * A prescriptive conformance rule (`wicked-governance::ConformanceRule`).
878
1042
  * `rule_type`: `pattern` | `policy`. `severity`: `info` | `warn` | `error` | `critical`.
1043
+ *
1044
+ * STEERING (unified model): the wiki/rules model and the old policy model merged into ONE
1045
+ * steering-rule model. The steering fields below are all optional on the wire — additive for
1046
+ * older consumers, and ABSENT on rows served by a pre-steering engine (wicked-core-ts < 0.7.5),
1047
+ * where absence reads as the serde defaults (`steering_type` architecture, `weight` 1.0, no
1048
+ * enforcement half). A rule WITHOUT `effect` is recall-only, exactly as before the merge; a rule
1049
+ * WITH one participates in decide()/select() the way a `GovernancePolicy` used to.
879
1050
  */
880
1051
  export interface ConformanceRule {
881
1052
  id: string;
@@ -886,9 +1057,25 @@ export interface ConformanceRule {
886
1057
  targets: { language?: string; layer?: string; framework?: string };
887
1058
  symbol_ref?: string;
888
1059
  compliance?: { framework: string; control_id: string };
1060
+ /** Where the rule came from. Doc-ingested rules keep `path@sha#id` in `ref`; UI/chat-authored
1061
+ * rules carry `source: "ui"` / `source: "chat"` — first-class provenance, not second-class. */
889
1062
  provenance: { source: string; ref?: string; source_kinds: string[] };
890
1063
  /** Withdrawn from recall. Same contract as {@link GovernancePolicy.retired}. */
891
1064
  retired?: boolean;
1065
+ /** Which Steering sub-page this rule belongs to. Engine default: `architecture`. */
1066
+ steering_type?: SteeringType;
1067
+ /** Phases / tools this rule is selected for (inclusion — {@link GovernancePolicy.applies_to} semantics). */
1068
+ applies_to?: string[];
1069
+ /** Exclusion twin of `applies_to`: phases / tools this rule is NEVER selected for. */
1070
+ excludes?: string[];
1071
+ /** Ordering within a severity band + gate priority. Engine default: 1.0. */
1072
+ weight?: number;
1073
+ /** Enforcement half (from the retired policy model). Absent ⇒ the rule is recall-only. */
1074
+ effect?: 'deny' | 'allow_with_conditions' | 'allow';
1075
+ trigger?: { contains?: string };
1076
+ obligations?: string[];
1077
+ /** The frozen acceptance-criteria text (becomes a claim's `criteria` when the rule decides a gate). */
1078
+ criteria?: string;
892
1079
  }
893
1080
 
894
1081
  /** Facet query for `GET /governance/rules/preview`. All fields are optional. */
@@ -900,6 +1087,368 @@ export interface RulePreviewQuery {
900
1087
  rule_type?: string;
901
1088
  }
902
1089
 
1090
+ /**
1091
+ * Facet query for `GET /governance/rules` (the wiki BROWSE surface). Unlike
1092
+ * {@link RulePreviewQuery} — whose recall semantics treat an absent rule facet as a wildcard
1093
+ * match, enforcement's question — browse filters are EXACT matches over the listed rows
1094
+ * ("show me the rules tagged `layer=api`"), and `status` keeps retired rules reachable so the
1095
+ * kill switch stays visible (`all` is the default).
1096
+ */
1097
+ export interface RuleBrowseQuery {
1098
+ severity?: 'info' | 'warn' | 'error' | 'critical';
1099
+ layer?: string;
1100
+ rule_type?: 'pattern' | 'policy';
1101
+ status?: 'active' | 'retired' | 'all';
1102
+ /**
1103
+ * Steering-type facet (the Steering sub-page's filter). Answers 501 on a pre-steering engine
1104
+ * (wicked-core-ts < 0.7.5): rows there carry no `steering_type`, so an empty answer would
1105
+ * impersonate "no rules of that type".
1106
+ */
1107
+ type?: SteeringType;
1108
+ /**
1109
+ * Boolean spelling of the retire filter: `true` ⇒ `status=all`, `false` ⇒ `status=active`.
1110
+ * Mutually exclusive with `status` — sending both is a 400 (two spellings of one filter can
1111
+ * contradict, and picking a winner silently would answer a question the caller didn't ask).
1112
+ */
1113
+ include_retired?: 'true' | 'false';
1114
+ }
1115
+
1116
+ // ── Steering management (STEERING program) ──────────────────────────────────────
1117
+
1118
+ /**
1119
+ * One entry of a `POST /governance/steering/import` batch: either a frontmattered markdown
1120
+ * document (`kind: 'doc'` — the same format `rules ingest --dir` consumes) or a ready rule
1121
+ * object (`kind: 'rule'`). Every entry runs through the engine's ingest normalize/validate
1122
+ * path, fail-closed PER ENTRY: one bad entry rejects alone, the rest still land.
1123
+ */
1124
+ export type SteeringImportEntry =
1125
+ | { kind: 'doc'; name?: string; content: string }
1126
+ | { kind: 'rule'; rule: ConformanceRule };
1127
+
1128
+ /** The `POST /governance/steering/import` request body (JSON — this daemon speaks no multipart). */
1129
+ export interface SteeringImportBody {
1130
+ /** The page's inferred steering type, applied as the DEFAULT `steering_type` for entries that omit one. */
1131
+ type?: SteeringType;
1132
+ entries: SteeringImportEntry[];
1133
+ }
1134
+
1135
+ /** Per-entry outcome of a steering import (same order as the submitted batch). */
1136
+ export interface SteeringImportResult {
1137
+ /** Index into the submitted `entries` array. */
1138
+ index: number;
1139
+ /** The doc entry's `name`, when one was given. */
1140
+ name?: string;
1141
+ status: 'imported' | 'rejected';
1142
+ /** Rule ids the entry minted (a doc can mint several; a rejected entry mints none). */
1143
+ ids?: string[];
1144
+ /** Why the entry was rejected (present only on `rejected`). */
1145
+ error?: string;
1146
+ }
1147
+
1148
+ /** The `POST /governance/steering/import` 200 body — 200 even with rejections: per-entry results ARE the answer. */
1149
+ export interface SteeringImportResponse {
1150
+ results: SteeringImportResult[];
1151
+ imported: number;
1152
+ rejected: number;
1153
+ }
1154
+
1155
+ /**
1156
+ * The `POST /governance/steering/author` request body — "add with chat". Launches a governed
1157
+ * authoring run (the `steering-author` workflow) that analyzes the named files/directories plus
1158
+ * the operator's intent and emits PROPOSED steering rules at a human gate (the TH-12
1159
+ * propose-as-gate pattern): the run PAUSES `awaiting_human` after the propose phase, and the
1160
+ * operator approves/amends/rejects through the standard `POST /runs/:id/gate`. On APPROVE the
1161
+ * daemon lands the proposal into the rules store with `provenance.source: "chat"` and the gate
1162
+ * response carries a {@link SteeringLandingResult} — the run itself writes nothing to any store.
1163
+ */
1164
+ export interface SteeringAuthorBody {
1165
+ /** The operator's conversational intent — what steering to author, and why. */
1166
+ instructions: string;
1167
+ /** Default `steering_type` for the proposed rules (the page's inferred type). */
1168
+ type?: SteeringType;
1169
+ /** ABSOLUTE paths on the daemon host (files or directories) the run should analyze. */
1170
+ paths?: string[];
1171
+ /** Inline documents — written into the run's steering inbox on the daemon host and analyzed like `paths`. */
1172
+ documents?: { name: string; content: string }[];
1173
+ /** Registered repo to run within (the run analyzes that worktree's context too). */
1174
+ repoRef?: string;
1175
+ /** Stable run id; minted when omitted. */
1176
+ sessionId?: string;
1177
+ }
1178
+
1179
+ /** Where a landed steering proposal was read from, most-preferred first: the propose phase's
1180
+ * machine-readable artifact file, the propose unit's stored transcript, the cached gate prompt. */
1181
+ export type SteeringProposalSource = 'deliverable' | 'transcript' | 'gate-prompt';
1182
+
1183
+ /**
1184
+ * The `landing` field on the `POST /runs/:id/gate` (and gated `POST /runs/:id/resume`) 200 body
1185
+ * — present ONLY when the approved gate was a `steering-author` propose gate (crew#388). It is
1186
+ * how the skin learns the approved chat proposal actually reached the rules store — or, loudly,
1187
+ * that it did not: on `failed` the run still advanced (the approve stands) but `error` carries
1188
+ * the operator-readable reason, mirrored on the audit trail as
1189
+ * `governance.steering.landing_failed`. Landed rules are audited per rule as
1190
+ * `governance.rule.upserted` with the chat provenance. Idempotent: a replayed approve reports
1191
+ * `alreadyLanded: true` and re-lands nothing. An approve WITH an amend note lands the proposal
1192
+ * UNCHANGED — the amend steers the RUN, not the rule text. A reject lands nothing.
1193
+ */
1194
+ export interface SteeringLandingResult {
1195
+ outcome: 'landed' | 'failed';
1196
+ /** Rule ids actually upserted (all of them on `landed`; the partial set on a partial `failed`). */
1197
+ ruleIds: string[];
1198
+ /** Where the proposal was read from. Absent when no source yielded one. */
1199
+ source?: SteeringProposalSource;
1200
+ /** `true` when a replayed approve found the durable landing marker — nothing was re-landed. */
1201
+ alreadyLanded?: boolean;
1202
+ /** Present iff `outcome: "failed"` — the loud, operator-readable reason. */
1203
+ error?: string;
1204
+ }
1205
+
1206
+ // ── Governance wiki management (scoreboard + meta) ─────────────────────────────
1207
+
1208
+ /**
1209
+ * Typing-coverage half of the wiki scoreboard — % of doctrine statements typed into
1210
+ * enforcement classes. Doc-side by construction (the class lives in doc frontmatter, never on
1211
+ * the rule node), so it is measurable only when the daemon was pointed at the same docs root
1212
+ * `rules ingest --dir` used; otherwise `available` is `false` and `reason` says why.
1213
+ */
1214
+ export interface WikiTypingCoverage {
1215
+ available: boolean;
1216
+ /** Why typing could not be measured (present only when `available` is `false`). */
1217
+ reason?: string;
1218
+ docs_scanned: number;
1219
+ statements_total: number;
1220
+ statements_typed: number;
1221
+ /** `statements_typed / statements_total` in [0,100]; absent when the corpus mints no statements. */
1222
+ percent?: number;
1223
+ /** Typed statements per class (`policy` / `validator` / `guidance`). */
1224
+ by_class: Record<string, number>;
1225
+ /** Docs that mint statements but declare no class — the actionable backlog. */
1226
+ docs_untyped: string[];
1227
+ }
1228
+
1229
+ /** Connection-coverage half — do the active rules' `symbol_ref`s resolve, and are the links live? */
1230
+ export interface WikiConnectionCoverage {
1231
+ rules_with_ref: number;
1232
+ refs_resolving: number;
1233
+ refs_unresolvable: number;
1234
+ /** `refs_resolving / rules_with_ref` in [0,100]; absent when no rule carries a ref. */
1235
+ percent?: number;
1236
+ /** Active rules with at least one live `Governs` edge. */
1237
+ rules_linked: number;
1238
+ }
1239
+
1240
+ /** One rule's enforcement evidence (only rules with any evidence appear). */
1241
+ export interface WikiRuleEvidenceRow {
1242
+ rule_id: string;
1243
+ /** Distinct deny claims citing this rule (`evidenced_by` edges in). */
1244
+ denial_claims: number;
1245
+ /** Accumulated `evidence_count` across the rule's `Governs` edges. */
1246
+ governs_evidence: number;
1247
+ }
1248
+
1249
+ /** Enforcement evidence — gate denials citing wiki rules (retired rules included: a past denial
1250
+ * stays explicable after its rule retires, so its evidence stays countable). */
1251
+ export interface WikiEnforcementEvidence {
1252
+ denial_claims: number;
1253
+ rules_evidenced: number;
1254
+ evidenced_by_edges: number;
1255
+ governs_evidence_total: number;
1256
+ /** Per-rule breakdown, most-evidenced first — the "which rules actually fire" list. */
1257
+ per_rule: WikiRuleEvidenceRow[];
1258
+ }
1259
+
1260
+ /** Recall volume — documented UNAVAILABLE in-band (an honest "cannot measure" beats a
1261
+ * fabricated zero); `reason` says why. */
1262
+ export interface WikiRecallVolume {
1263
+ available: boolean;
1264
+ reason: string;
1265
+ }
1266
+
1267
+ /**
1268
+ * The wiki population/connection scoreboard (`wicked-governance::Scoreboard`, AW-23/arch-R23) —
1269
+ * the report that tells a populated wiki from an ingested-once-and-decaying one.
1270
+ * Served by `GET /governance/wiki/scoreboard`; 501 when the installed engine addon predates the
1271
+ * `governanceScoreboard` binding (wicked-core-ts ≥ 0.7.4).
1272
+ */
1273
+ export interface GovernanceScoreboard {
1274
+ rules_total: number;
1275
+ rules_active: number;
1276
+ rules_retired: number;
1277
+ typing: WikiTypingCoverage;
1278
+ connection: WikiConnectionCoverage;
1279
+ evidence: WikiEnforcementEvidence;
1280
+ recall_volume: WikiRecallVolume;
1281
+ }
1282
+
1283
+ /**
1284
+ * `GET /governance/wiki/meta` — the wiki's honest empty-state signal, cheap enough for the UI
1285
+ * to call on mount. `ruleset_count` is `null` (not 0) when the installed engine build cannot
1286
+ * count `RuleSet` rows: "no rulesets" is a real answer about a seeded store, and "cannot count"
1287
+ * must never impersonate it.
1288
+ */
1289
+ export interface GovernanceWikiMeta {
1290
+ /** Any doctrine on the store at all (rules listable, or RuleSet rows counted). */
1291
+ seeded: boolean;
1292
+ ruleset_count: number | null;
1293
+ /** Rules the engine lists (pre-0.7.4 addons list active rules only — the recall funnel). */
1294
+ rule_count: number;
1295
+ /** Whether `GET /governance/wiki/scoreboard` will answer 200 on this deployment. */
1296
+ scoreboard_available: boolean;
1297
+ /** The authoring guide / seed runbook the empty state points at. */
1298
+ doc: string;
1299
+ }
1300
+
1301
+ // ── Testing surface (crew-testing) — governance evals + eval corpora ───────────
1302
+
1303
+ /**
1304
+ * The behavior signals of one eval sample — the facts the governance decide path is asked to
1305
+ * judge (mirrors the engine's decision inputs: run phase, tool invoked, files touched, content).
1306
+ * All optional: a sample carries only the signals its behavior is about.
1307
+ */
1308
+ export interface GovernanceEvalSignals {
1309
+ phase?: string;
1310
+ tool?: string;
1311
+ files?: string[];
1312
+ content?: string;
1313
+ }
1314
+
1315
+ /**
1316
+ * One eval sample: a described behavior (`kind: 'bad'` ⇒ the steering corpus SHOULD deny it,
1317
+ * `'good'` ⇒ it should pass) tagged with the steering type it exercises. Field names are the
1318
+ * engine's serde spelling (snake_case) — the report echoes them back verbatim.
1319
+ */
1320
+ export interface GovernanceEvalSample {
1321
+ id: string;
1322
+ description: string;
1323
+ kind: 'good' | 'bad';
1324
+ steering_type: string;
1325
+ signals: GovernanceEvalSignals;
1326
+ }
1327
+
1328
+ /**
1329
+ * The `POST /testing/evals/run` request body. `type` narrows the run to one steering type;
1330
+ * `corpus` names an estate scope (`evals:<name>`, as minted by `POST /testing/corpora/import`);
1331
+ * omitted, the engine runs its built-in default corpus.
1332
+ */
1333
+ export interface RunGovernanceEvalsBody {
1334
+ type?: SteeringType;
1335
+ corpus?: string;
1336
+ }
1337
+
1338
+ /** A near-miss rule on a `gap` verdict — the rule that ALMOST fired, with its similarity score. */
1339
+ export interface GovernanceEvalNearestRule {
1340
+ rule_id: string;
1341
+ similarity: number;
1342
+ }
1343
+
1344
+ /**
1345
+ * One sample's outcome. `expected` is derived from the sample's `kind` (`bad` ⇒ `deny`);
1346
+ * `fired` lists the rule ids that actually fired; `verdict` is the comparison: `caught` (a bad
1347
+ * behavior denied), `gap` (a bad behavior that sailed through), `false_positive` (a good
1348
+ * behavior denied). `nearest_rules` is present on gaps (an empty array is allowed) — the
1349
+ * remediation pointer for "which rule needs sharpening".
1350
+ */
1351
+ export interface GovernanceEvalResult {
1352
+ sample: Pick<GovernanceEvalSample, 'id' | 'description' | 'kind' | 'steering_type'>;
1353
+ expected: 'deny' | 'allow';
1354
+ fired: string[];
1355
+ verdict: 'caught' | 'gap' | 'false_positive';
1356
+ nearest_rules?: GovernanceEvalNearestRule[];
1357
+ }
1358
+
1359
+ /** The report's roll-up counts (snake_case — the engine's serde output, passed through verbatim). */
1360
+ export interface GovernanceEvalSummary {
1361
+ total: number;
1362
+ caught: number;
1363
+ gaps: number;
1364
+ false_positives: number;
1365
+ }
1366
+
1367
+ /**
1368
+ * The `POST /testing/evals/run` 200 body — the engine's serde report passed through VERBATIM
1369
+ * (snake_case field names, `degraded` spelled `null` when the run was full-fidelity;
1370
+ * `'facet-only'` when the embedding side was unavailable and only facet matching ran).
1371
+ */
1372
+ export interface GovernanceEvalReport {
1373
+ results: GovernanceEvalResult[];
1374
+ summary: GovernanceEvalSummary;
1375
+ degraded: 'facet-only' | null;
1376
+ }
1377
+
1378
+ /** The `POST /testing/corpora/import` request body — a named eval corpus for later runs. */
1379
+ export interface ImportEvalCorpusBody {
1380
+ name: string;
1381
+ samples: GovernanceEvalSample[];
1382
+ }
1383
+
1384
+ /**
1385
+ * The `POST /testing/corpora/import` 200 body. `scope` is the estate scope the samples landed
1386
+ * under (`evals:<name>` — the string `RunGovernanceEvalsBody.corpus` names); `embedded` reports
1387
+ * whether the samples were embedded for similarity matching (false ⇒ later runs degrade to
1388
+ * facet-only).
1389
+ */
1390
+ export interface ImportEvalCorpusResponse {
1391
+ imported: number;
1392
+ scope: string;
1393
+ embedded: boolean;
1394
+ }
1395
+
1396
+ /**
1397
+ * The `POST /testing/recon` request body (api-types 0.15.0) — the Testing page's campaign-recon
1398
+ * trigger. `problem` is the recon brief, passed to every launched run VERBATIM (the client owns
1399
+ * the framing). The two optional fields are the multiscope wire, shared with
1400
+ * {@link LaunchCampaignBody}:
1401
+ *
1402
+ * - `repoRefs` — explicit codebase attachments: registered repo refs (ids; a unique repo NAME
1403
+ * also resolves), deduped; a ref that does not resolve fails the whole request with a 400
1404
+ * naming it.
1405
+ * - `projectId` — crew resolves the project's `crew.repo` members server-side (404 unknown
1406
+ * project; 400 when the project has zero repo members and no `repoRefs` cover for it) AND
1407
+ * files every launched run into the project (the `POST /runs` §2.2 semantics: atomic
1408
+ * membership + project-graph binding).
1409
+ *
1410
+ * BOTH ⇒ the union (`repoRefs` order first). NEITHER ⇒ one unscoped recon run — the launch the
1411
+ * Testing page sent before this wire existed, unchanged.
1412
+ *
1413
+ * One engine run carries ONE repo, so a multi-repo recon FANS: one governed run per resolved
1414
+ * repo, all under one shared campaign label (`TestingReconResponse.campaign`) — and a real fan
1415
+ * (>= 2 repos) registers an ENGINE campaign under that same id, so `GET /campaigns` serves it
1416
+ * (api-types 0.17.0, crew#390).
1417
+ *
1418
+ * Every launched sibling pauses at its INTAKE GATE (`human_confirm: before:1` — the launch
1419
+ * banner's promise) by default; `ungated: true` is the EXPLICIT opt-out for an unattended fan
1420
+ * (api-types 0.17.0, crew#391). It is never the silent default, and it is audited.
1421
+ */
1422
+ export interface TestingReconBody {
1423
+ problem: string;
1424
+ projectId?: string;
1425
+ repoRefs?: string[];
1426
+ /** EXPLICITLY launch the siblings unattended (no intake gate). Default `false`: each sibling
1427
+ * pauses at its intake gate before any unit runs. */
1428
+ ungated?: boolean;
1429
+ }
1430
+
1431
+ /**
1432
+ * The `POST /testing/recon` 201 body. `runIds` is the source of truth (`length >= 1` always —
1433
+ * one run per resolved repo in the caller's order; exactly one for an unscoped recon); `runId`
1434
+ * is its first entry, kept as the single-run spelling existing launch readers expect;
1435
+ * `campaign` is the shared label every fanned run's `run.launched` audit entry carries — and,
1436
+ * when `campaignRegistered` is `true`, the id of the ENGINE campaign the fan was filed under
1437
+ * (`GET /campaigns/:id` serves it; the `runIds` are its nodes' attempt-0 run ids,
1438
+ * `{campaign}:{node}:a0`). `campaignRegistered: false` = a single/unscoped recon, or an engine
1439
+ * addon without the campaign bindings — the launch will NOT appear on `GET /campaigns`.
1440
+ */
1441
+ export interface TestingReconResponse {
1442
+ runId: string;
1443
+ runIds: string[];
1444
+ campaign: string;
1445
+ /** Whether an engine campaign was registered for this launch (api-types 0.17.0). */
1446
+ campaignRegistered: boolean;
1447
+ /** Present only when the fan was campaign-registered with a `projectId` and filing a sibling
1448
+ * into the project failed — the runs are LIVE; re-attach via POST /projects/:id/members. */
1449
+ projectAttachError?: string;
1450
+ }
1451
+
903
1452
  // ── Governance claims (crew#40/43) ─────────────────────────────────────────────
904
1453
 
905
1454
  /**
@@ -937,17 +1486,29 @@ export interface LaunchRunBody {
937
1486
  */
938
1487
  projectId?: string;
939
1488
  /**
940
- * Delivery mode (crew#293). `"pr"` ⇒ the daemon appends a hardened deliver Tool phase (push
941
- * the run's branch, rebase onto origin's default branch, `gh pr create`; the PR URL is the
942
- * phase output's last line) to a PER-RUN copy of the selected workflow def — the shared def
943
- * is never mutated, and merge stays human. Requires `workflow`; sending `deliver` without it
944
- * is a 400. Omit ⇒ no delivery phase.
1489
+ * Delivery mode (crew#293, defaulting reworked by crew#393 / api-types 0.18.0).
1490
+ *
1491
+ * `"pr"` ⇒ the daemon appends a hardened deliver Tool phase (push the run's branch, rebase
1492
+ * onto origin's default branch, `gh pr create`; the PR URL is the phase output's last line)
1493
+ * to a PER-RUN copy of the selected workflow def — the shared def is never mutated, and
1494
+ * merge stays human. Requires `workflow`; sending `deliver: "pr"` without one is a 400.
1495
+ *
1496
+ * `"none"` ⇒ explicitly no delivery phase (the run's work stays in its worktree and the run
1497
+ * will read `delivery: 'stranded'` on the wire once completed — recoverable any time via
1498
+ * `POST /runs/:id/deliver`).
945
1499
  *
946
- * NOTE for the next api-types release: added daemon-side first (workspace copy, 0.6.0 +
947
- * crew#293) — fold into the published `LaunchRunBody` when 0.7.0 cuts. Optional, so the
948
- * field is forward-additive on the wire (DES-STUDIO-001 §5.1).
1500
+ * OMITTED ⇒ the daemon decides (crew#393): a repo-scoped launch (`repoRef` set) that names a
1501
+ * CODE-WORK `workflow` — a def carrying at least one `executes_code` phase (feature, bug,
1502
+ * migration; not chat/onboarding/recon defs, whose clean worktree the deliver script would
1503
+ * fail loudly) — DEFAULTS to `"pr"`: a completed code run must end with a reviewable
1504
+ * deliverable or an explicit decision not to. The daemon setting `deliverDefault: 'none'`
1505
+ * flips that default off. Repo-less launches, free-text launches (no `workflow`, so there is
1506
+ * no def to append the phase to), and read-only-workflow launches default to `"none"`.
1507
+ *
1508
+ * `"none"` is additive (0.18.0): an older daemon's launch schema rejects it with a 400 —
1509
+ * omit the field entirely against pre-0.18 servers.
949
1510
  */
950
- deliver?: 'pr';
1511
+ deliver?: 'pr' | 'none';
951
1512
  /**
952
1513
  * Retry lineage (DES-UX-001 §8.3, CREW-UX-3; api-types 0.8.0): the id of the run this
953
1514
  * launch retries. Must name an EXISTING run id — an unknown id fails the launch (400 with
@@ -959,6 +1520,62 @@ export interface LaunchRunBody {
959
1520
  retryOf?: string;
960
1521
  }
961
1522
 
1523
+ /**
1524
+ * Body for `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7; api-types 0.9.0) — upsert the
1525
+ * run's ONE durable operator guidance note. `text: ''` CLEARS the note (the DTO field returns
1526
+ * to absent). Capped at 8192 UTF-8 bytes — beyond it the daemon answers 400 with a named
1527
+ * error. An unknown run is a 404. The write is audit-trailed (`action: 'guidance.set'` with
1528
+ * the authenticated actor) and survives daemon restart.
1529
+ */
1530
+ export interface SetGuidanceBody {
1531
+ text: string;
1532
+ }
1533
+
1534
+ /** Response of `PUT /runs/:id/guidance` — echoes what was stored (`''` after a clear). */
1535
+ export interface SetGuidanceResult {
1536
+ runId: string;
1537
+ guidance: string;
1538
+ }
1539
+
1540
+ /**
1541
+ * Response of `POST /runs/:id/deliver` (crew#393; api-types 0.18.0) — post-hoc delivery: lift a
1542
+ * COMPLETED repo-scoped run's stranded worktree into a PR with the SAME hardened script the
1543
+ * deliver phase runs (crew#293/#317: commit the run's work, refuse the default branch, rebase
1544
+ * onto origin's default branch — a conflict fails LOUDLY with nothing pushed — push, `gh pr
1545
+ * create`, and re-derive success from a real PR URL). Takes no body.
1546
+ *
1547
+ * Idempotent: a run that already delivered answers 200 with the SAME recorded `prUrl` — a second
1548
+ * call never opens a second PR. Failure is loud, never silent: 404 (unknown run), 409 (run not
1549
+ * completed / repo-less / worktree gone / a delivery already in flight / the script itself failed
1550
+ * — the body's `error` carries the script's own words, e.g. the rebase-conflict message), 500
1551
+ * (the script produced no verifiable PR URL, or could not be spawned).
1552
+ */
1553
+ export interface DeliverRunResult {
1554
+ prUrl: string;
1555
+ }
1556
+
1557
+ /**
1558
+ * The 409 body of `POST /runs/:id/resume` on a TERMINAL run (crew#311; api-types 0.18.0).
1559
+ *
1560
+ * The engine's `resume_run` no-ops on a completed/cancelled run and answers the status token, so
1561
+ * the route used to reply `200 {"status":"cancelled"}` — on the exact runs an operator was trying
1562
+ * to rescue, the recovery affordance read as "resume destroyed my run". A terminal run is now
1563
+ * refused with 409 and the ACTUAL recovery named machine-readably:
1564
+ *
1565
+ * - `'retry'` — relaunch as a new run with `POST /runs {"retryOf":"<id>"}` (cancelled runs,
1566
+ * and completed runs with nothing liftable — including a `delivery: 'vacuous'` run, whose
1567
+ * units produced no work at all);
1568
+ * - `'deliver'` — the run completed with unlifted work in its worktree (`delivery:
1569
+ * 'stranded'`): lift it with `POST /runs/:id/deliver`.
1570
+ *
1571
+ * A FAILED run is not terminal for resume: it re-enters at the cursor as before (200 + status).
1572
+ */
1573
+ export interface ResumeRefusal {
1574
+ /** Human-readable refusal naming the run's terminal status and the recovery, verbatim. */
1575
+ error: string;
1576
+ recovery: 'retry' | 'deliver';
1577
+ }
1578
+
962
1579
  // ── Governance types (crew#40/41) ──────────────────────────────────────────────
963
1580
 
964
1581
  /** Per-app breakdown within a `CoverageReport`. */
@@ -1148,6 +1765,59 @@ export interface SystemSettings {
1148
1765
  * the engine default `~/.wicked-worker`.
1149
1766
  */
1150
1767
  worker_config_root?: string;
1768
+ /**
1769
+ * The repo-scoped launch delivery DEFAULT (crew#393; api-types 0.18.0, additive). What a
1770
+ * `POST /runs` with `repoRef` + a CODE-WORK `workflow` (a def with at least one
1771
+ * `executes_code` phase) and NO explicit `deliver` resolves to: `'pr'` (the shipped default —
1772
+ * completed code runs open their PR) or `'none'` (work stays in the worktree, surfaced as
1773
+ * `delivery: 'stranded'`). Absent reads as `'pr'`. Never consulted when the launch body sets
1774
+ * `deliver` explicitly, and never applied to repo-less, free-text, or read-only-workflow
1775
+ * launches (those are always `'none'`).
1776
+ */
1777
+ deliverDefault?: 'pr' | 'none';
1778
+ /**
1779
+ * The stall watchdog's DETECTION threshold (crew#287; api-types 0.18.0 — previously a
1780
+ * daemon-local extension): minutes a run in `executing` may go without ANY engine event on the
1781
+ * daemon's relay before one synthetic `workerStalled` frame per quiet period is broadcast on
1782
+ * `/ws`. Integer 1..1440; absent = the daemon default (15). Detection never touches the run.
1783
+ */
1784
+ workerStallMinutes?: number;
1785
+ /**
1786
+ * The stall watchdog's ESCALATION threshold (crew#341; api-types 0.18.0): quiet minutes before
1787
+ * the watchdog ACTS on a detected stall (see {@link WorkerStallEscalatedFrame}). Integer
1788
+ * 0..1440; **absent or 0 = escalation OFF — the shipped default** (the issue's design: automatic
1789
+ * recovery on a run that is merely slow would be worse than the wedge). Values below
1790
+ * `workerStallMinutes` escalate at the detection threshold — the ladder never acts before it
1791
+ * has notified.
1792
+ */
1793
+ workerStallEscalateMinutes?: number;
1794
+ /**
1795
+ * What an armed escalation DOES (crew#341): `'reassign'` (default) recycles the wedged cursor
1796
+ * unit in place — the engine supersedes the stale turn, closes the worker session, bumps the
1797
+ * attempt, and re-dispatches (queued operator injects survive into the fresh turn);
1798
+ * `'notify'` is the fail-loud rung — a `needsYou` `workerStallEscalated` frame + an audit
1799
+ * entry, the run untouched.
1800
+ */
1801
+ workerStallEscalateAction?: 'reassign' | 'notify';
1802
+ /**
1803
+ * Per-run budget of AUTOMATIC reassigns (crew#341): integer 1..10, default 2. Once spent, a
1804
+ * still-silent run gets `outcome: 'exhausted'` `needsYou` frames instead of further recovery —
1805
+ * a worker that wedges deterministically must reach a human, not loop through seats forever.
1806
+ * Ignored when `workerStallEscalateAction` is `'notify'`.
1807
+ */
1808
+ workerStallMaxEscalations?: number;
1809
+ /**
1810
+ * SKIN-OWNED preference blobs (crew#323). The settings store is shared: alongside the
1811
+ * engine's keys it carries the studio's own state — `studio.appearance` (accent/logo/theme),
1812
+ * `studio.notifications` (desktop-notification opt-in) — which the daemon persists and
1813
+ * returns VERBATIM without ever interpreting them.
1814
+ *
1815
+ * `PUT /settings` accepts a key matching `^studio\.[a-z][a-z0-9-]*$` whose value is
1816
+ * JSON-serializable and at most 512KB of JSON; it answers 400 naming the key otherwise.
1817
+ * Any other unrecognized key is still dropped from the patch (request bodies are
1818
+ * forward-additive, §5.1), so a skin's own state MUST ride this namespace to persist.
1819
+ */
1820
+ [key: `studio.${string}`]: unknown;
1151
1821
  }
1152
1822
 
1153
1823
  // ── Requirements management (server-side search + overrides; crew api/requirements.ts) ──
@@ -1185,6 +1855,14 @@ export interface RequirementsPage {
1185
1855
  /** Which source served the corpus: the live estate store, or the evidence-gated
1186
1856
  * `requirements_graph.json` snapshot, which can lag it by hours (FINDING-065). */
1187
1857
  source: 'store' | 'artifact';
1858
+ /**
1859
+ * Keys in `requirements_overrides.json` that matched NO requirement in the corpus. Operator
1860
+ * edits are keyed by SymbolId (store) or `domain::reqId` (artifact); an estate id-scheme
1861
+ * migration re-mints method/field SymbolIds, so overrides keyed by the old ids stop matching and
1862
+ * would otherwise vanish silently. Non-zero means edits exist that no row is showing — re-run
1863
+ * the annotation workflow, then re-apply or delete the stale keys.
1864
+ */
1865
+ orphanedOverrides?: number;
1188
1866
  }
1189
1867
 
1190
1868
  export interface RequirementPatch {
@@ -1198,7 +1876,11 @@ export interface RequirementPatch {
1198
1876
  export interface BlastRadius {
1199
1877
  target: string;
1200
1878
  dependents: Array<{ id: string; name: string; kind: string; file: string; line: number }>;
1201
- /** Unresolved call-sites referencing the target — absence of dependents never means "safe". */
1879
+ /**
1880
+ * References to the target that no resolver bound (ENGINE-CONTRACT §2.1). Repeat call-sites of
1881
+ * an already-bound relationship are NOT counted, so 0 is a legitimate value for a fully-resolved
1882
+ * hot symbol. When non-zero, an empty `dependents` still never means "safe".
1883
+ */
1202
1884
  unresolved: number;
1203
1885
  }
1204
1886
 
@@ -1308,6 +1990,57 @@ export interface AttachMemberBody {
1308
1990
  attachedBy?: 'studio' | 'interactive' | 'cli' | 'api';
1309
1991
  }
1310
1992
 
1993
+ /**
1994
+ * The crew half of a governed doc delete (crew#338) — what
1995
+ * `DELETE /projects/:projectId/interactive/docs/:doc` did to crew's four handoff ledgers
1996
+ * (`interactive-{draft,edit,chat,demo}-ledger.json`). Present on every answer of that route so
1997
+ * a partial result can never hide.
1998
+ */
1999
+ export interface InteractiveDocDeleteLedgerReport {
2000
+ /** True iff every ledger was swept without error. On the 502 refusal paths this is false with
2001
+ * `skipped: true` — the sweep deliberately did not run because interactive's half did not
2002
+ * happen (nothing diverged). */
2003
+ ok: boolean;
2004
+ /** Every replay-dedup row key actually dropped (`<doc>`, `<doc>:v<n>`, `<doc>:m:<id>`, …).
2005
+ * Empty ⇒ nothing was there — deleting a never-drafted doc is a clean no-op on this side. */
2006
+ removed_keys: string[];
2007
+ /** The ledgers that could NOT be swept, by seam name (`draft`|`edit`|`chat`|`demo`) and cause.
2008
+ * Present only when `ok` is false and the sweep actually ran. */
2009
+ errors?: { ledger: string; error: string }[];
2010
+ /** True ⇒ the sweep was deliberately skipped (interactive refused/failed the retire, so
2011
+ * crew's rows are still doing their job and nothing diverged). */
2012
+ skipped?: boolean;
2013
+ }
2014
+
2015
+ /**
2016
+ * `DELETE /projects/:projectId/interactive/docs/:doc` (crew#338) — 200: the bridge's own retire
2017
+ * answer (interactive#189's wire, relayed verbatim) plus crew's ledger report. The route's
2018
+ * non-200 answers carry `error` plus the same `ledger` report (404 unknown doc — the ledger is
2019
+ * STILL swept, cleaning ghosts of hand-deleted workspaces, but ONLY on the retire wire's own
2020
+ * 404 body `{"error":"unknown doc"}`; 500 partial — interactive retired but the sweep failed,
2021
+ * the body names both halves; 502 — interactive did not retire (5xx, unreachable, or a 404
2022
+ * without the wire's body from a bridge predating the retire route), nothing swept; 409 — the
2023
+ * bridge's build-in-flight refusal relayed verbatim, no `ledger` field).
2024
+ */
2025
+ export interface InteractiveDocDeleteResponse {
2026
+ /** The doc name (slug). */
2027
+ name: string;
2028
+ kind: 'doc' | 'html' | 'source' | 'demo';
2029
+ retired: true;
2030
+ /** True on a repeat delete — idempotent, with the ORIGINAL `retired_at` and no `event_id`. */
2031
+ already_retired: boolean;
2032
+ /** ISO-8601 retirement timestamp. */
2033
+ retired_at: string;
2034
+ /** Head version at retirement. */
2035
+ head: number;
2036
+ /** Lineage size at retirement. */
2037
+ versions: number;
2038
+ /** The `wicked.interactive.doc.retired` bus event id — first retire only (no re-emit). */
2039
+ event_id?: number;
2040
+ /** What crew dropped from its handoff ledgers. */
2041
+ ledger: InteractiveDocDeleteLedgerReport;
2042
+ }
2043
+
1311
2044
  /**
1312
2045
  * One normalized entry of the merged project activity feed
1313
2046
  * (`GET /projects/:id/activity`, ADR §5.2): core events of member runs/chats
@@ -1378,3 +2111,493 @@ export interface ChatOpenBody {
1378
2111
  /** File the chat into a project (`crew.chat` membership, attached on open). */
1379
2112
  projectId?: string;
1380
2113
  }
2114
+
2115
+ // ── Project code graph (DES-PROJECT-001; the co-located multi-repo graph) ──────
2116
+ //
2117
+ // A project's `crew.repo` members indexed into ONE wicked-estate database, each under a stable
2118
+ // label, so one query answers over every repo the project contains.
2119
+ //
2120
+ // THE LIMIT, stated on the wire because it is not obvious from the results: this is CO-LOCATION,
2121
+ // NOT LINKAGE. estate resolves edges within a labelled repo's own nodes only, exactly as if each
2122
+ // repo sat in its own database. `studio → wicked-crew-api-types → crew` does NOT traverse; what
2123
+ // these shapes carry is per-repo results gathered in one place, with the repo named on every hit.
2124
+ // Every response repeats it in `linkage` + `note` so a consumer cannot read a cross-repo answer as
2125
+ // a cross-repo TRACE.
2126
+
2127
+ /** Co-location, never linkage — a single-valued field so a future linked mode is additive. */
2128
+ export type ProjectGraphLinkage = 'co-located';
2129
+
2130
+ /**
2131
+ * Why a project graph cannot answer, or the terms on which it can.
2132
+ *
2133
+ * - `ready` — indexed, two or more repos.
2134
+ * - `ready-single-repo` — indexed, exactly one repo: answers are correct but can never be
2135
+ * cross-repo, which a caller asking a project-scoped question needs told.
2136
+ * - `no-repo-members` — the project has no `crew.repo` member. Not an error and not an empty
2137
+ * graph: there is nothing to build one from.
2138
+ * - `not-indexed` — it has repo members and no database yet. `POST /projects/:id/graph/refresh`
2139
+ * is the fix, and the `detail` says so.
2140
+ * - `engine-too-old` — the running wicked-core addon does not publish `code_graph_db` on the repo
2141
+ * record (wicked-core#170), so its repo-record contract predates this surface.
2142
+ */
2143
+ export type ProjectGraphState =
2144
+ | 'ready'
2145
+ | 'ready-single-repo'
2146
+ | 'no-repo-members'
2147
+ | 'not-indexed'
2148
+ | 'engine-too-old';
2149
+
2150
+ /** One member repo's standing in the project graph. */
2151
+ export interface ProjectGraphRepo {
2152
+ /** Registry id (`RepoEntry.id`) — the member ref this came from. */
2153
+ repoId: string;
2154
+ /** The estate label its rows carry, and the prefix on every `file` path it contributes. */
2155
+ label: string;
2156
+ rootPath: string;
2157
+ /** `false` ⇒ its symbols are NOT in the graph; `reason` says why and results stay partial. */
2158
+ indexed: boolean;
2159
+ /** Commit indexed, when git could answer. */
2160
+ head?: string;
2161
+ /** Unix millis of the index that put these rows in. */
2162
+ indexedAt?: number;
2163
+ /**
2164
+ * Present when `indexed` is false: registry miss, index failure, never refreshed, or the rows
2165
+ * under this label were indexed from a DIFFERENT root than the registry now names (a repo that
2166
+ * moved — wicked-estate binds a label to the root it first saw and refuses to rebind it, so the
2167
+ * graph keeps the old checkout's symbols until the project graph is rebuilt).
2168
+ */
2169
+ reason?: string;
2170
+ /**
2171
+ * The action that actually resolves THIS cause, when one action does. Carried per-row because
2172
+ * the causes do not share a remedy: "never refreshed" and "attached since the last refresh" are
2173
+ * fixed by a refresh, a DANGLING member ref is not (the registry no longer knows the ref, so a
2174
+ * refresh indexes nothing new), and a MOVED root needs a rebuild when estate refuses to rebind
2175
+ * the label. Absent ⇒ no single action fixes it, or `reason` already spells the remedy out.
2176
+ * Never assume a refresh: telling an operator mid-incident to run one that cannot work costs
2177
+ * them the time it takes to run it and the trust to believe the next message.
2178
+ */
2179
+ remedy?: string;
2180
+ }
2181
+
2182
+ /** `GET /projects/:id/graph` — what the project graph holds and what it cannot answer. */
2183
+ export interface ProjectGraphStatus {
2184
+ projectId: string;
2185
+ state: ProjectGraphState;
2186
+ /** One sentence naming the state's cause and its remedy. Always present, never empty. */
2187
+ detail: string;
2188
+ /** Absolute path of the co-located database; `null` when there is none to point at. */
2189
+ dbPath: string | null;
2190
+ repos: ProjectGraphRepo[];
2191
+ /** Member repos absent from the graph — any answer is PARTIAL while this is non-empty. */
2192
+ missingRepos: string[];
2193
+ /** Labels still in the database whose repo is no longer a member; excluded from results. */
2194
+ staleRepos: string[];
2195
+ linkage: ProjectGraphLinkage;
2196
+ note: string;
2197
+ /** Unix millis of the last refresh; `null` when never refreshed. */
2198
+ updatedAt: number | null;
2199
+ }
2200
+
2201
+ /**
2202
+ * `POST /projects/:id/graph/refresh` body. Optional — an empty body is the plain incremental
2203
+ * refresh.
2204
+ *
2205
+ * `force: true` exists for MIGRATION, not for routine use: the plain refresh skips any member repo
2206
+ * whose clean checkout is still at the HEAD the manifest recorded, so after a wicked-estate upgrade
2207
+ * an UNCHANGED repo never re-indexes and its rows keep the old binary's id scheme and unresolved
2208
+ * accounting (ENGINE-CONTRACT §2.1) indefinitely. Force bypasses that skip for every member repo
2209
+ * AND passes `--force` to `wicked-estate index`, so estate performs a full re-extract even when its
2210
+ * own incremental digest would skip. Expect it to take as long as the project's first build.
2211
+ */
2212
+ export interface RefreshProjectGraphBody {
2213
+ /** Re-index every member repo, bypassing the clean-HEAD skip and estate's digest skip. */
2214
+ force?: boolean;
2215
+ }
2216
+
2217
+ /**
2218
+ * One member repo's outcome in a refresh — why it did or did not index. `skipped-head-unchanged`
2219
+ * is the row an operator reads when asking why a repo did not migrate after an estate upgrade:
2220
+ * the plain refresh trusts a clean checkout at the manifest's HEAD, so only `force` (or a new
2221
+ * commit) makes that repo re-index.
2222
+ */
2223
+ export interface ProjectGraphRefreshRepo {
2224
+ repoId: string;
2225
+ /** The estate label its rows carry. */
2226
+ label: string;
2227
+ /** What this refresh did with the repo. */
2228
+ action: 'indexed' | 'skipped-head-unchanged' | 'failed';
2229
+ /**
2230
+ * Bounded tail (last 2 KiB) of `wicked-estate index` stderr, present when the binary said
2231
+ * anything — notably estate's id_scheme migration notice explaining why a refresh that used to
2232
+ * skip in a second ran a full re-extract for minutes.
2233
+ */
2234
+ stderrTail?: string;
2235
+ /** Present when `action` is `failed` — the same message the `failed` array carries. */
2236
+ error?: string;
2237
+ }
2238
+
2239
+ /** `POST /projects/:id/graph/refresh` — an incremental (re)build, repo by repo. */
2240
+ export interface ProjectGraphRefreshResult {
2241
+ status: ProjectGraphStatus;
2242
+ /** Labels re-indexed this run. */
2243
+ indexed: string[];
2244
+ /** Labels skipped because the checkout is clean and its HEAD is already in the graph. */
2245
+ skipped: string[];
2246
+ /** Repos that failed to index, with the reason; the graph keeps whatever it already had. */
2247
+ failed: Array<{ repoId: string; label: string; error: string }>;
2248
+ /** Per-repo outcomes with the WHY (and any index stderr) attached — one row per member repo. */
2249
+ repos?: ProjectGraphRefreshRepo[];
2250
+ }
2251
+
2252
+ /** One hit, always attributed: a cross-repo answer that does not say WHERE is not an answer. */
2253
+ export interface ProjectGraphHit {
2254
+ /** The repo's registry id. */
2255
+ repoId: string;
2256
+ /** The repo's estate label. */
2257
+ repo: string;
2258
+ /** estate SymbolId — carries the label, so it is unique across the project. */
2259
+ id: string;
2260
+ /**
2261
+ * The graph node's name, VERBATIM from estate. For a `kind: 'file'` node that name IS the
2262
+ * label-prefixed path (`wicked-vault/src/vault/vault.mjs`) — left as estate minted it, because
2263
+ * rewriting it would produce a name that resolves to nothing. Use `repo` + `file` to display it.
2264
+ */
2265
+ name: string;
2266
+ kind: string;
2267
+ /** REPO-RELATIVE path, label stripped — the same spelling `/repos/:id/graph` returns. */
2268
+ file: string;
2269
+ line: number;
2270
+ }
2271
+
2272
+ /** Per-repo hit counts, so "which repo does this touch" is answerable without scanning hits. */
2273
+ export interface ProjectGraphRepoCount {
2274
+ repoId: string;
2275
+ repo: string;
2276
+ count: number;
2277
+ }
2278
+
2279
+ /** `GET /projects/:id/graph/blast-radius?name=` — dependents across every member repo. */
2280
+ export interface ProjectBlastRadius {
2281
+ projectId: string;
2282
+ target: string;
2283
+ dependents: ProjectGraphHit[];
2284
+ byRepo: ProjectGraphRepoCount[];
2285
+ /**
2286
+ * References to the target that no resolver bound (ENGINE-CONTRACT §2.1). Repeat call-sites of
2287
+ * an already-bound relationship are NOT counted, so 0 is a legitimate value for a fully-resolved
2288
+ * hot symbol. When non-zero, an empty `dependents` still never means "safe".
2289
+ */
2290
+ unresolved: number;
2291
+ /** Labels actually covered by this answer. */
2292
+ reposSearched: string[];
2293
+ /** Member repos NOT covered — the answer is partial while this is non-empty. */
2294
+ missingRepos: string[];
2295
+ linkage: ProjectGraphLinkage;
2296
+ note: string;
2297
+ }
2298
+
2299
+ /** `GET /projects/:id/graph/search?name=` — exact-name symbol resolution across member repos. */
2300
+ export interface ProjectGraphSearch {
2301
+ projectId: string;
2302
+ query: string;
2303
+ matches: ProjectGraphHit[];
2304
+ byRepo: ProjectGraphRepoCount[];
2305
+ reposSearched: string[];
2306
+ missingRepos: string[];
2307
+ linkage: ProjectGraphLinkage;
2308
+ /**
2309
+ * Carries the co-location limit AND the matching rule: this is EXACT-NAME resolution, so an
2310
+ * empty `matches` means "no symbol by that exact name" and never "not in this project". A
2311
+ * partial or fuzzy name returns nothing even when the symbol exists.
2312
+ */
2313
+ note: string;
2314
+ }
2315
+
2316
+ // ── Campaigns (DES-CAMPAIGN-001 / crew#342 slice 1 + TH-9) ───────────────────
2317
+ // The engine's Campaign scheduler — a durable, crash-resumable, dependency-aware parallel DAG of
2318
+ // governed Runs — exposed over `/api/v1/campaigns`. The GET routes return the ENGINE's persisted
2319
+ // shapes verbatim (wicked-core `src/campaign.rs` serde: snake_case fields, snake_case enum
2320
+ // tokens), same doctrine as `SessionView`: one producer of the wire shape, zero re-spelling in
2321
+ // the daemon. The POST body (`LaunchCampaignBody`) is the daemon's own camelCase request contract
2322
+ // — crew maps it onto the engine def, composing one Tool-executor workflow per deterministic
2323
+ // scenario (spec FILE PATHS on the argv, never scenario bodies — the 1022-byte PTY prompt trap).
2324
+
2325
+ /** Per-node lifecycle status. Terminal = `completed` | `failed` | `blocked` | `cancelled`. */
2326
+ export type CampaignNodeStatus =
2327
+ | 'pending'
2328
+ | 'ready'
2329
+ | 'running'
2330
+ | 'awaiting_human'
2331
+ | 'ready_to_resume'
2332
+ | 'completed'
2333
+ | 'failed'
2334
+ | 'blocked'
2335
+ | 'cancelled';
2336
+
2337
+ /** Campaign lifecycle status — also the token `POST /campaigns/:id/resume` / `/cancel` resolve to. */
2338
+ export type CampaignStatus =
2339
+ | 'running'
2340
+ | 'paused'
2341
+ | 'completed'
2342
+ | 'partially_completed'
2343
+ | 'failed'
2344
+ | 'cancelled';
2345
+
2346
+ /** When a dependency edge is satisfied: dep `completed` only, or any terminal outcome. */
2347
+ export type CampaignEdgeCondition = 'on_success' | 'on_terminal';
2348
+
2349
+ /** How a node failure propagates (DES-CAMPAIGN-001 §5.2). */
2350
+ export type CampaignFailurePolicy = 'fail_fast' | 'continue_independent' | 'human_gate_on_failure';
2351
+
2352
+ /**
2353
+ * What one campaign node runs — the engine's reusable Run specification (mirrors a launch minus
2354
+ * the session id, which the scheduler derives as `{campaign}:{node}:a{attempt}`). A node inherits
2355
+ * governance, worktree isolation, HITL gates, live output, and per-Run resume from the Run it is.
2356
+ */
2357
+ export interface CampaignRunSpec {
2358
+ /** The free-text problem this node's Run decomposes — for a scenario node, a short human label
2359
+ * (the scenario body lives in its spec FILE; it is never inlined here). */
2360
+ problem: string;
2361
+ /** The council roster (`AgenticCli` seats) convened for this node's Run. */
2362
+ clis: unknown[];
2363
+ entity_mode: EntityMode;
2364
+ /** The node's internal human-confirm gate policy (engine serde shape); absent = none. */
2365
+ human_confirm?: unknown;
2366
+ /** The registered repo the node's Run targets, if any (creates an isolated worktree). */
2367
+ repo_ref?: string | null;
2368
+ /** The registered `WorkflowDef` id this node's Run follows — for a deterministic scenario node,
2369
+ * the per-node composed Tool workflow (`campaign-<campaign>-<node>`). */
2370
+ workflow_id?: string | null;
2371
+ }
2372
+
2373
+ /** A schedulable unit of the DAG — one node = one governed core Run. */
2374
+ export interface CampaignNode {
2375
+ /** Stable node id, unique within the campaign (never contains `:` — it keys the run id). */
2376
+ node_id: string;
2377
+ run_spec: CampaignRunSpec;
2378
+ }
2379
+
2380
+ /** A dependency edge `from -> to`: `to` becomes eligible once `from` satisfies `condition`. */
2381
+ export interface CampaignEdge {
2382
+ from: string;
2383
+ to: string;
2384
+ condition: CampaignEdgeCondition;
2385
+ }
2386
+
2387
+ /** The static campaign definition — validated at launch (cycle / empty / duplicate-edge /
2388
+ * unknown-endpoint rejects) and persisted verbatim inside the live {@link Campaign}. */
2389
+ export interface CampaignDef {
2390
+ id: string;
2391
+ name: string;
2392
+ nodes: CampaignNode[];
2393
+ edges: CampaignEdge[];
2394
+ policy: CampaignFailurePolicy;
2395
+ /** Global concurrency cap (>= 1) — a resource guard on parallel worktrees + CLI subprocesses. */
2396
+ max_concurrency: number;
2397
+ }
2398
+
2399
+ /**
2400
+ * The live campaign instance (`GET /campaigns`, `GET /campaigns/:id`) — durable and
2401
+ * crash-resumable; the scheduler re-derives the ready set from these persisted statuses on
2402
+ * resume, never re-running a completed node.
2403
+ */
2404
+ export interface Campaign {
2405
+ id: string;
2406
+ def_id: string;
2407
+ status: CampaignStatus;
2408
+ /** The full definition, embedded so a resume needs no second store. */
2409
+ def: CampaignDef;
2410
+ node_status: Record<string, CampaignNodeStatus>;
2411
+ /** node_id -> live Run id, always `{campaign}:{node}:a{attempt}` (attempt-keyed, idempotent). */
2412
+ node_run_id: Record<string, string>;
2413
+ /** node_id -> 0-based attempt counter (a `human_gate_on_failure` Retry bumps it). */
2414
+ node_attempt: Record<string, number>;
2415
+ /** Persisted amend text of an approved-but-unslotted per-node gate decision (crash-safe). */
2416
+ pending_decision_amend: Record<string, string | null>;
2417
+ /** `human_gate_on_failure` queue — failed nodes awaiting a Retry/Skip/Abort decision. */
2418
+ pending_failure_gates: string[];
2419
+ /** Whether a fail-fast (or Abort) tripped — finalization lands on `failed`. */
2420
+ fail_fast_tripped: boolean;
2421
+ }
2422
+
2423
+ /**
2424
+ * One scenario of `POST /campaigns` — the daemon maps each onto a {@link CampaignNode}.
2425
+ *
2426
+ * Exactly one of `tool` / `agent` per scenario:
2427
+ * - `tool` (deterministic): the daemon composes a single-phase Tool-executor workflow running
2428
+ * `cmd` verbatim (no council, no LLM). Every argv token must be a single line of ≤ 1022
2429
+ * bytes — pass the scenario/spec as a FILE PATH argument, never inline its body. A body
2430
+ * smuggled into the argv is rejected with a 400 naming the offending token.
2431
+ * - `agent` (exploratory/authoring): a governed agent run over `problem` — same byte
2432
+ * discipline, because the problem text reaches PTY workers as prompt material.
2433
+ */
2434
+ export interface CampaignScenario {
2435
+ /** Node id, unique within the campaign (letters/digits/dots/hyphens/underscores; no `:`). */
2436
+ id: string;
2437
+ /** Short human label for the scoreboard; defaults to the id. NOT the scenario body. */
2438
+ title?: string;
2439
+ /** Ids of scenarios that must reach `depsCondition` before this one dispatches. */
2440
+ deps?: string[];
2441
+ /** Edge condition applied to every dep edge of this scenario (default `on_success`). */
2442
+ depsCondition?: CampaignEdgeCondition;
2443
+ /** Deterministic execution: argv run by the engine's Tool executor, file paths not bodies. */
2444
+ tool?: { cmd: string[] };
2445
+ /** Governed agent execution: the problem statement (short — reaches workers as a prompt). */
2446
+ agent?: { problem: string; workflow?: string };
2447
+ /** Registered repo this node runs within (isolated worktree). Omit for a repo-less node. */
2448
+ repoRef?: string;
2449
+ }
2450
+
2451
+ /** `POST /campaigns` request body → 201 {@link LaunchCampaignResponse}. */
2452
+ export interface LaunchCampaignBody {
2453
+ /** Campaign id (also the def id). Defaults to a generated `campaign-<uuid>`. */
2454
+ id?: string;
2455
+ name?: string;
2456
+ scenarios: CampaignScenario[];
2457
+ /** Failure propagation policy (default `continue_independent`). */
2458
+ policy?: CampaignFailurePolicy;
2459
+ /** Global concurrency cap, >= 1 (default 2). */
2460
+ maxConcurrency?: number;
2461
+ /** JSON array of `AgenticCli` seats for agent scenarios; defaults to the daemon roster. */
2462
+ clisJson?: string;
2463
+ /**
2464
+ * The multiscope wire (api-types 0.15.0), shared with `POST /testing/recon`: scope the launch
2465
+ * to a PROJECT — crew resolves the project's `crew.repo` members server-side. Unknown project
2466
+ * ⇒ 404; a project with zero repo members (and no `repoRefs` to fall back on) ⇒ 400 naming
2467
+ * the fix. Combined with `repoRefs` ⇒ the union. Because one campaign node runs against ONE
2468
+ * repo, every scenario that does not pin its own `repoRef` is FANNED — one node per resolved
2469
+ * repo, same campaign — and the 201 body carries the fanned nodes' run ids (`runIds`).
2470
+ * Omit both fields ⇒ the launch behaves exactly as before (backward compatible).
2471
+ */
2472
+ projectId?: string;
2473
+ /**
2474
+ * The multiscope wire (api-types 0.15.0): explicit codebase attachments — registered repo
2475
+ * refs (ids; a unique repo NAME also resolves), deduped, min 1 when present. A ref that does
2476
+ * not resolve fails the WHOLE launch with a 400 naming it — never a silently narrowed scope.
2477
+ */
2478
+ repoRefs?: string[];
2479
+ }
2480
+
2481
+ /**
2482
+ * `POST /campaigns` 201 body. `runIds` is ADDITIVE (api-types 0.15.0): present exactly when
2483
+ * the launch used the multiscope fields (`projectId`/`repoRefs`), carrying the attempt-0 run
2484
+ * ids (`{campaign}:{node}:a0`) of the launch's nodes — repo-major over the fanned scenarios
2485
+ * (repos in resolved input order), then the scenarios that pinned their own `repoRef`.
2486
+ * Consumers treat `runIds.length >= 1` as the source of truth for what launched; a legacy
2487
+ * body keeps the legacy `{ campaignId }` answer.
2488
+ */
2489
+ export interface LaunchCampaignResponse {
2490
+ campaignId: string;
2491
+ runIds?: string[];
2492
+ }
2493
+
2494
+ /**
2495
+ * The 11 campaign frames of the `/ws` stream (wicked-core `event_to_json`, camelCase tags), as a
2496
+ * discriminated union for consumers that narrow on `type` — the TH-14 scoreboard renders node
2497
+ * status from exactly these. They also flow through the permissive {@link CoreEvent}; the daemon
2498
+ * relay is allowlist-free, so every frame below reaches `/ws` clients byte-for-byte.
2499
+ */
2500
+ export type CampaignEvent =
2501
+ | { type: 'campaignLaunched'; campaign: string }
2502
+ | { type: 'campaignNodeReady'; campaign: string; node: string }
2503
+ | { type: 'campaignNodeStarted'; campaign: string; node: string; runId: string }
2504
+ | { type: 'campaignNodeAwaitingHuman'; campaign: string; node: string; runId: string; prompt: string }
2505
+ | { type: 'campaignNodeCompleted'; campaign: string; node: string }
2506
+ | { type: 'campaignNodeFailed'; campaign: string; node: string }
2507
+ | { type: 'campaignNodeBlocked'; campaign: string; node: string }
2508
+ | { type: 'campaignPaused'; campaign: string }
2509
+ | { type: 'campaignCompleted'; campaign: string }
2510
+ | { type: 'campaignFailed'; campaign: string }
2511
+ | { type: 'campaignCancelled'; campaign: string };
2512
+
2513
+ // ── Diagnostics (api-types 0.16.0) ──────────────────────────────────────────────
2514
+
2515
+ /**
2516
+ * Component versions of the running deployment (`GET /diagnostics`). The honesty rule of the
2517
+ * whole diagnostics surface: a version the daemon cannot determine is `null` — never guessed.
2518
+ */
2519
+ export interface DiagnosticsComponents {
2520
+ /** The wicked-crew daemon's own package version (always known). */
2521
+ crew: string;
2522
+ /** The bundled studio SPA's version, read from the manifest the bundle ships
2523
+ * (`testid-inventory.json` → `studioVersion`); `null` on a headless daemon or a bundle
2524
+ * that predates the manifest. */
2525
+ studioBundle: string | null;
2526
+ /** The installed `wicked-core-ts` engine binding's version; `null` when unresolvable. */
2527
+ coreTs: string | null;
2528
+ /** Engine binary versions via `--version`, keyed by binary name (`wicked-core`,
2529
+ * `wicked-estate`) — probed ONLY through paths crew already resolves (`WICKED_CORE_EXE`,
2530
+ * bare PATH name); `null` per binary when unresolved or the probe fails. */
2531
+ engineBinaries: Record<string, string | null>;
2532
+ }
2533
+
2534
+ /** Daemon process runtime facts (`GET /diagnostics`). */
2535
+ export interface DiagnosticsDaemon {
2536
+ /** Milliseconds since this daemon process started. */
2537
+ uptimeMs: number;
2538
+ /** Epoch ms the process started (now − uptime). */
2539
+ startedAt: number;
2540
+ /** The actually-bound HTTP port (honours --port / CREW_PORT / port 0). */
2541
+ port: number;
2542
+ }
2543
+
2544
+ /** One store file (or dir, sized as a content total) under the daemon's state home. Paths and
2545
+ * sizes only — file contents never ride this wire. */
2546
+ export interface DiagnosticsStoreFile {
2547
+ /** Filename relative to the state home (`core.db`, `core.db-wal`, `core.db.events`, …). */
2548
+ name: string;
2549
+ /** Absolute path. */
2550
+ path: string;
2551
+ /** File size, or the total of a directory's contents (the events dir). */
2552
+ bytes: number;
2553
+ }
2554
+
2555
+ /** One captured error-level line from the daemon's own log ring (`GET /diagnostics`). */
2556
+ export interface DiagnosticsRecentError {
2557
+ /** Epoch ms the line was logged. */
2558
+ ts: number;
2559
+ /** Which record produced it (`daemon` = the in-process pino error ring). */
2560
+ source: string;
2561
+ /** The logged message, length-bounded. */
2562
+ line: string;
2563
+ }
2564
+
2565
+ /**
2566
+ * Per-CLI ACP health, folded from the durable run event logs
2567
+ * (`<state-home>/core.db.events/*.ndjson`, `acpSessionStarted` / `acpFallback` frames).
2568
+ * A CLI that never appears in the logs has no key — absence spells "no ACP traffic recorded",
2569
+ * and zero counts are never invented for it.
2570
+ */
2571
+ export interface AcpCliDiagnostics {
2572
+ /** Count of `acpSessionStarted` events for this cliKey. */
2573
+ sessionsStarted: number;
2574
+ /** Count of `acpFallback` events (ACP unavailable/failed; run continued single-shot). */
2575
+ fallbacks: number;
2576
+ /** Fallback counts by `fallbackKind` (`session_died`, `auth_required`, `binary_unavailable`, …). */
2577
+ fallbackKinds: Record<string, number>;
2578
+ /** Epoch ms of the newest `acpSessionStarted`, or `null` when none recorded. */
2579
+ lastStartedTs: number | null;
2580
+ /** Epoch ms of the newest `acpFallback`, or `null` when none recorded. */
2581
+ lastFallbackTs: number | null;
2582
+ }
2583
+
2584
+ /** The ACP fold of `GET /diagnostics` — keyed by `cliKey` (`claude`, `pi`, `codex`, …). */
2585
+ export interface AcpDiagnostics {
2586
+ byCli: Record<string, AcpCliDiagnostics>;
2587
+ }
2588
+
2589
+ /**
2590
+ * `GET /diagnostics` (api-types 0.16.0) — the daemon's read-only self-knowledge surface:
2591
+ * what is deployed, what it stores, what has gone wrong recently, and whether ACP is really
2592
+ * working across the CLIs. Every field is derived from records the daemon already owns;
2593
+ * anything it cannot answer is `null` / empty, never fabricated.
2594
+ */
2595
+ export interface DiagnosticsResponse {
2596
+ components: DiagnosticsComponents;
2597
+ daemon: DiagnosticsDaemon;
2598
+ /** `core.db` + sidecars + the events dir (as a total), sorted by name. */
2599
+ stores: DiagnosticsStoreFile[];
2600
+ /** Bounded tail of the daemon's own error-level log lines, newest first. */
2601
+ recentErrors: DiagnosticsRecentError[];
2602
+ acp: AcpDiagnostics;
2603
+ }