wicked-crew-api-types 0.20.0 → 0.28.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 +844 -4
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -255,6 +255,33 @@ export interface AgentSession {
255
255
  * `GET /campaigns`.
256
256
  */
257
257
  group_label?: string;
258
+ /**
259
+ * When this run was LAUNCHED — unix SECONDS (home command-center run metrics; api-types 0.24.0).
260
+ * Daemon-derived at DTO assembly on BOTH `GET /runs` and `GET /runs/:id` from the `run.launched`
261
+ * audit entry's timestamp (the daemon's durable system-of-record for run provenance — task #88),
262
+ * joined exactly like {@link retry_of} / {@link guidance} and hydrated from the SAME trail scan
263
+ * at boot, so it survives a daemon restart.
264
+ *
265
+ * This is the ONE launch instant the daemon records: "created" and "started" are the SAME event
266
+ * here — the engine's `AgentSession` (wicked-core `domain.rs`) carries no launch time, so there
267
+ * is no separate queued-vs-started split to expose. Unlocks time-bucketed KPI deltas and a real
268
+ * 7/30/90-day range on the home dashboard.
269
+ *
270
+ * ABSENT — never `null`, never fabricated — when the daemon has no `run.launched` entry for the
271
+ * run: a run launched OFF the `POST /runs` path (a repo's onboarding run, a campaign's own
272
+ * DAG-node runs), a run predating this field, or a `--db`-isolated daemon reading a trail that
273
+ * never saw the launch. Read with `typeof === 'number'`; a bucketed KPI or a 7/30/90-day range
274
+ * must EXCLUDE an undated run, never date it with a fabricated now.
275
+ *
276
+ * COST (deferred, not faked): a per-run cost/spend figure is deliberately NOT joined onto this
277
+ * DTO. Cost exists only on the per-unit {@link CliUsageEvent} (`costUsd`), streamed over `/ws`
278
+ * and never accumulated per run daemon-side — a reliable per-run total would require replaying
279
+ * the durable event log per run on this hot path (the unbounded cost the GET /runs delivery-cache
280
+ * architecture forbids) or a new live cliUsage accumulator plus a durable per-run store. A burn
281
+ * chart should sum `cliUsage.costUsd` from the live `/ws` stream (or `GET /runs/:id/events`) for
282
+ * now; a joined `AgentSession.cost` is a follow-up once that store exists.
283
+ */
284
+ created_at?: number;
258
285
  }
259
286
 
260
287
  /** An ordered unit of work within a run (`WorkUnit`). */
@@ -448,6 +475,16 @@ export interface GateDecision {
448
475
  amend?: string;
449
476
  }
450
477
 
478
+ /**
479
+ * Body of `POST /runs/:id/reassign` — the manual operator lever that reassigns a wedged run's
480
+ * cursor unit through the same engine path the stall-watchdog's automatic escalation uses
481
+ * (crew#442). `cli` omitted lets the engine's council re-pick the seat; when present it must be
482
+ * a seat in the run's own pool (`SessionView.clis`) or the route answers 400.
483
+ */
484
+ export interface ReassignRequest {
485
+ cli?: string;
486
+ }
487
+
451
488
  /**
452
489
  * A CoreEvent frame as delivered verbatim over `/ws` — a tagged-JSON object
453
490
  * discriminated on `type` (wicked-core-ts `event_to_json`). The named optional
@@ -1119,12 +1156,34 @@ export interface ConformanceRule {
1119
1156
  excludes?: string[];
1120
1157
  /** Ordering within a severity band + gate priority. Engine default: 1.0. */
1121
1158
  weight?: number;
1122
- /** Enforcement half (from the retired policy model). Absent ⇒ the rule is recall-only. */
1123
- effect?: 'deny' | 'allow_with_conditions' | 'allow';
1159
+ /**
1160
+ * Enforcement half (from the retired policy model). Absent ⇒ the rule is RECALL-ONLY: it is
1161
+ * surfaced to workers but never enters decide()/select(), so it can neither block a gate nor
1162
+ * `catch` an eval sample — which is why a store of effect-less rules evals to "every bad
1163
+ * sample is a gap" (api-types 0.27.0, the #394/#395 companion). `deny` blocks (the only effect
1164
+ * the evals credit as a catch); `warn` surfaces without blocking (the operator-authorable
1165
+ * middle band); `allow_with_conditions` permits with obligations; `allow` permits outright.
1166
+ */
1167
+ effect?: 'deny' | 'warn' | 'allow_with_conditions' | 'allow';
1124
1168
  trigger?: { contains?: string };
1125
1169
  obligations?: string[];
1126
1170
  /** The frozen acceptance-criteria text (becomes a claim's `criteria` when the rule decides a gate). */
1127
1171
  criteria?: string;
1172
+ /**
1173
+ * Unix-SECONDS timestamp of when this rule FIRST entered the store — the "added" time the Steering
1174
+ * surface sorts + date-filters on (NOT the latest-edit time), stamped once by the engine on first
1175
+ * create and preserved across updates (api-types 0.23.0).
1176
+ *
1177
+ * WIRED-BUT-PENDING A CORE-TS BUMP: the field exists on the engine's `ConformanceRule`
1178
+ * (wicked-core `conformance.rs`, serialized with `skip_serializing_if = Option::is_none`), but it
1179
+ * landed AFTER the published `core-ts-v0.7.16` this daemon builds against — so a rule served by
1180
+ * the current binding carries NO `created_at`. The daemon passes the field through verbatim
1181
+ * (`listConformanceRules` returns the parsed rows unchanged), so it begins flowing the moment
1182
+ * crew re-pins to a core-ts that includes it — no daemon change needed. ABSENT — never
1183
+ * fabricated — on any rule the engine did not stamp (pre-field rows, and every row from a
1184
+ * pre-bump binding), so a `since`/`until` filter cannot assert an undated rule in range.
1185
+ */
1186
+ created_at?: number;
1128
1187
  }
1129
1188
 
1130
1189
  /** Facet query for `GET /governance/rules/preview`. All fields are optional. */
@@ -1160,6 +1219,21 @@ export interface RuleBrowseQuery {
1160
1219
  * contradict, and picking a winner silently would answer a question the caller didn't ask).
1161
1220
  */
1162
1221
  include_retired?: 'true' | 'false';
1222
+ /**
1223
+ * Inclusive LOWER date bound (api-types 0.23.0): keep only rules with `created_at >= since`.
1224
+ * Unix SECONDS, a non-negative integer (a present-but-non-integer value is a 400). Applied
1225
+ * server-side over the listed rows, alongside the facet filters. A rule with NO `created_at`
1226
+ * is EXCLUDED whenever `since` (or `until`) is set — see {@link ConformanceRule.created_at}: on
1227
+ * a pre-bump core-ts binding NO rule carries the field, so this filter narrows to empty until the
1228
+ * engine surfaces it. Omitted ⇒ no lower bound.
1229
+ */
1230
+ since?: string;
1231
+ /**
1232
+ * Inclusive UPPER date bound (api-types 0.23.0): keep only rules with `created_at <= until`.
1233
+ * Same units/validation/undated-exclusion as {@link RuleBrowseQuery.since}. Omitted ⇒ no upper
1234
+ * bound.
1235
+ */
1236
+ until?: string;
1163
1237
  }
1164
1238
 
1165
1239
  // ── Steering management (STEERING program) ──────────────────────────────────────
@@ -1252,6 +1326,371 @@ export interface SteeringLandingResult {
1252
1326
  error?: string;
1253
1327
  }
1254
1328
 
1329
+ // ── Skills — the daemon-owned garden plugin root, published as immutable snapshots (api-types 0.28.0) ──
1330
+ //
1331
+ // Skills are files (skills keystone, design v3 + amendments v3.1/v3.2). The daemon owns ONE
1332
+ // effective `wicked-garden`-shaped plugin root — `<state home>/skills/effective/`, the dependency
1333
+ // closure of the installed plugin: `.claude-plugin/{plugin.json,archetypes.json,components.json}`,
1334
+ // `skills/**` (nested layout verbatim), `scripts/**` minus CI + dev tools, `schemas/`,
1335
+ // `docs/examples/`, `pyproject.toml`, `uv.lock` — seeded from the LIVE installed plugin into a
1336
+ // content-addressed `baseline/<contentHash>/`. The operator edits files in place, replaces a
1337
+ // skill, adds one, disables one (manifest state — the files stay), resets one (content from the
1338
+ // baseline; never enablement). Nothing a worker runs is read from `effective/`: PUBLISHING
1339
+ // validates the whole tree and writes an IMMUTABLE, read-only `snapshots/<gen>/` (enabled skills
1340
+ // only, closure included, `snapshot.json`, and the generated delivery views —
1341
+ // `views/copilot/.github/skills/<name>/` holding the enabled PORTABLE skills, part of the content
1342
+ // hash), then flips `current -> snapshots/<gen>`; the engine receives the resolved snapshot path
1343
+ // as `WICKED_SKILLS_SNAPSHOT`. The daemon NEVER writes into the user's own CLI directories
1344
+ // (`~/.codex`, `~/.pi`, `~/.copilot`, `~/.config/opencode`, `~/.claude`): skills reach non-Claude
1345
+ // workers only through per-launch, wicked-owned delivery core performs from the snapshot (v3.2) —
1346
+ // a CLI without a lever (codex today) runs without wicked skills, and a unit on such a seat that
1347
+ // requires one is REFUSED at launch (no proceed-with-disclosure setting exists).
1348
+ // `/skills` is a file manager whose EVERY mutation is CAS-guarded (`expectedRevision`) and
1349
+ // answers 2xx `{verdict, findings[], revision}` — a `blocked` verdict is a normal response,
1350
+ // including a publish refused because one is in flight (`publish-in-flight`) or aborted because the
1351
+ // root changed under it (`root-changed`), neither of which wrote anything. 409 (`{error, revision}`)
1352
+ // is EXCLUSIVELY a stale `expectedRevision` (a CAS conflict).
1353
+
1354
+ /** What a skill IS, from its frontmatter — fork-first: `context: fork` → fork worker (a subagent
1355
+ * body); else `user-invocable: true` → router (an operator-facing entry point); else module (a
1356
+ * nested reference skill routers/workers pull in). */
1357
+ export type SkillKind = 'router' | 'fork-worker' | 'module';
1358
+
1359
+ /** `shipped` = every own file byte-identical to the baseline; `override` = a shipped skill with
1360
+ * edited/replaced files; `user-added` = no baseline (added through the API, or upstream dropped
1361
+ * it while the operator's edits were kept). */
1362
+ export type SkillProvenance = 'shipped' | 'override' | 'user-added';
1363
+
1364
+ /** Where a baseline was captured from. `claude-plugin-cache` covers the marketplace cache AND the
1365
+ * hand-installed `plugins/wicked-garden` copy (both are "the installed plugin"); `checkout` is a
1366
+ * git working tree; `directory` any other explicit plugin-shaped directory. */
1367
+ export type SkillSourceKind = 'claude-plugin-cache' | 'checkout' | 'directory';
1368
+
1369
+ /** The per-baseline `uv sync` state (`<baseline>/.venv`, provisioned once per content hash — the
1370
+ * publish that needs it AWAITS it — and shared read-only by every snapshot that links it):
1371
+ * `pending` until a successful publish records it, `synced` on success (the snapshot carries a
1372
+ * `.venv` link), `skipped` when the bundle carries no `pyproject.toml` (nothing to provision; no
1373
+ * link). `failed` (uv missing or a sync error) is BLOCKING for the publish (`venv-failed`) and is
1374
+ * therefore never the recorded state of a published baseline — the record keeps its previous
1375
+ * value; a later publish retries. */
1376
+ export type SkillVenvState = 'pending' | 'synced' | 'failed' | 'skipped';
1377
+
1378
+ /** One captured baseline — keyed in `SkillManifest.baselines` by the content hash of its bundle
1379
+ * (sorted relative paths + sha256 digests), never by the version string alone: two installs of
1380
+ * "12.32.0" with different bytes are two baselines. */
1381
+ export interface SkillBaselineRecord {
1382
+ /** `version` from the source's `.claude-plugin/plugin.json`. */
1383
+ plugin_version: string;
1384
+ source: { kind: SkillSourceKind; path: string };
1385
+ /** HEAD sha for a `checkout` source; `null` otherwise (or when git could not answer). */
1386
+ git_sha: string | null;
1387
+ /** ISO-8601 instant the baseline was captured. */
1388
+ captured_at: string;
1389
+ venv: SkillVenvState;
1390
+ }
1391
+
1392
+ export interface SkillEntry {
1393
+ /** Plugin-relative directory, nested layout preserved (`skills/engineering/frontend`). Never
1394
+ * renamed — sibling `../` links depend on it. */
1395
+ dir: string;
1396
+ kind: SkillKind;
1397
+ /** Core-by-reference: in the registered-reference closure — a `skill_ref` of a workflow the
1398
+ * daemon knows (core drop-ins, crew-generated, user-registered) or a skill one of those names in
1399
+ * its SKILL.md (repo-learn → search, mem). Disabling or renaming it is blocking. */
1400
+ core: boolean;
1401
+ /** `false` when the skill's files resolve `${CLAUDE_PLUGIN_ROOT}`, invoke a script relative to
1402
+ * the cwd (`python3 -u scripts/x.py`, `./scripts/x`, …), or link `../` — Claude-only by nature:
1403
+ * excluded from the snapshot's `views/copilot/` and from the per-launch skill lists core builds
1404
+ * for the other CLIs. `portable` is the admission key for every non-Claude view (design v3.2). */
1405
+ portable: boolean;
1406
+ /** Manifest state, orthogonal to content: a disabled skill's files stay in `effective/` and are
1407
+ * excluded from the next published snapshot. Reset never flips it. */
1408
+ enabled: boolean;
1409
+ provenance: SkillProvenance;
1410
+ /** ISO-8601 instant of the last edit/replace/add through the API; `null` when untouched. */
1411
+ editedAt: string | null;
1412
+ /** A refreshed baseline changed a file this skill overrides — the new side is readable as
1413
+ * `?side=baseline` for diffing; the operator's content is kept. */
1414
+ upgradeAvailable: boolean;
1415
+ /** `upgradeAvailable`, or the last refresh found a name collision (upstream now ships a skill
1416
+ * under this user-added name at another dir). Cleared by reset / a later refresh. */
1417
+ conflict: boolean;
1418
+ /** The held-back UPSTREAM skill's directory in the current baseline when the last refresh found a
1419
+ * name collision (upstream ships this name at another dir than the operator's skill); `null`
1420
+ * otherwise. `GET /skills/:name/files/*path?side=baseline` reads THIS directory for such a skill,
1421
+ * so the two sides of the collision are comparable — the answer's `path` names the file actually
1422
+ * read (api-types 0.28.0). Re-derived by every refresh. */
1423
+ upstreamDir: string | null;
1424
+ }
1425
+
1426
+ /** One managed file (plugin-relative path → hashes). `baselineHash` `null` = user-added file;
1427
+ * `effectiveHash` `null` = a baseline file absent from `effective/` (removed by a direct
1428
+ * filesystem edit — reset restores it; the skill reads `override` until then). */
1429
+ export interface SkillFileRecord {
1430
+ baselineHash: string | null;
1431
+ effectiveHash: string | null;
1432
+ /** The hash this file had in the most recent published snapshot; `null` = never published. */
1433
+ lastPublishedHash: string | null;
1434
+ /** The last refresh saw BOTH sides change (kept the effective side). */
1435
+ conflict: boolean;
1436
+ }
1437
+
1438
+ /** The most recent publish. */
1439
+ export interface SkillPublishedRecord {
1440
+ gen: number;
1441
+ /** Hash over the snapshot's files (sorted relative paths + sha256 digests), `snapshot.json` excluded. */
1442
+ contentHash: string;
1443
+ /** ISO-8601 instant. */
1444
+ at: string;
1445
+ /** sha256 of the exact `snapshot.json` bytes publish wrote. `snapshot.json` is excluded from the
1446
+ * content hash, so the crew-owned manifest AUTHENTICATES it: `current` verifies only the
1447
+ * generation this record names, with metadata hashing to this value (api-types 0.28.0). */
1448
+ snapshotHash: string;
1449
+ }
1450
+
1451
+ /** `<skills root>/manifest.json` — the state of the daemon-owned root. (The v3 mirror ledger is
1452
+ * withdrawn with the mirror — design v3.2 §1: the daemon never writes into the user's CLI dirs.) */
1453
+ export interface SkillManifest {
1454
+ version: 2;
1455
+ /** Monotonic; bumped by EVERY mutation. Every mutating request carries it as `expectedRevision`. */
1456
+ revision: number;
1457
+ /** Content hash of the current baseline (`baseline/<hash>/`). */
1458
+ baseline: string;
1459
+ baselines: Record<string, SkillBaselineRecord>;
1460
+ /** Keyed by frontmatter `name` (`wicked-garden-<dir segments joined by '-'>`). */
1461
+ skills: Record<string, SkillEntry>;
1462
+ /** Every managed file under `effective/`, plugin-relative. */
1463
+ files: Record<string, SkillFileRecord>;
1464
+ published: SkillPublishedRecord | null;
1465
+ }
1466
+
1467
+ /** `GET /skills` 200 body. 503 when the root is not seeded (no installed plugin was found) or when
1468
+ * `current` exists but fails verification (realpath outside `snapshots/`, malformed
1469
+ * `snapshot.json`, content hash mismatch) — a corrupt root is a loud error, never an empty catalog. */
1470
+ export interface SkillsManifestResponse {
1471
+ manifest: SkillManifest;
1472
+ revision: number;
1473
+ /** The resolved skills root on the daemon host (`<state home>/skills` by default). */
1474
+ root: string;
1475
+ /** The VERIFIED published snapshot `current` resolves to, or `null` before the first publish.
1476
+ * `path` is the absolute REAL path of `snapshots/<gen>` — byte-identical to the one input the
1477
+ * engine is handed (`WICKED_SKILLS_SNAPSHOT`; design v3.1 §2). */
1478
+ current: { gen: number; path: string } | null;
1479
+ }
1480
+
1481
+ export interface SkillFileEntry {
1482
+ /** POSIX path relative to the skill dir. */
1483
+ path: string;
1484
+ size: number;
1485
+ sha256: string;
1486
+ /** This file's manifest record (`null` for a file present on disk but not yet recorded). */
1487
+ record: SkillFileRecord | null;
1488
+ }
1489
+
1490
+ /** `GET /skills/:name/files` 200 body — the skill's OWN files (a nested skill's files are its own). */
1491
+ export interface SkillFileTree {
1492
+ name: string;
1493
+ dir: string;
1494
+ enabled: boolean;
1495
+ files: SkillFileEntry[];
1496
+ }
1497
+
1498
+ /** `GET /skills/:name/files/*path` and `GET /skills/support/*path` 200 body — a typed, capped read.
1499
+ * Save is disabled on `truncated` or `binary`. `?side=baseline` reads the baseline copy instead
1500
+ * (the "new side" of a refresh conflict). */
1501
+ export interface SkillReadResult {
1502
+ /** Plugin-relative path served. */
1503
+ path: string;
1504
+ /** UTF-8 text — the first 512 KB when `truncated`; `null` when `binary` (there is no text to show). */
1505
+ content: string | null;
1506
+ /** The file's FULL size in bytes. */
1507
+ size: number;
1508
+ truncated: boolean;
1509
+ binary: boolean;
1510
+ }
1511
+
1512
+ export type SkillFindingKind =
1513
+ | 'name-invalid'
1514
+ | 'name-collision'
1515
+ | 'name-mismatch'
1516
+ | 'frontmatter-invalid'
1517
+ | 'missing-skill-md'
1518
+ | 'unregistered-skill'
1519
+ | 'nested-skill-create'
1520
+ | 'core-disable'
1521
+ | 'core-rename'
1522
+ | 'core-missing'
1523
+ | 'support-file-edit'
1524
+ | 'non-portable'
1525
+ /** A `${CLAUDE_PLUGIN_ROOT}/<p>` or `../<p>` reference in an enabled skill's files that does not
1526
+ * resolve inside the would-be snapshot. Severity follows the TARGET (design v3.4 §1): a reference
1527
+ * that ESCAPES the plugin root (`..` climbing out — it reaches whatever lies beside the snapshot on
1528
+ * the worker host) is `blocking`; one whose target is MISSING (it normalizes inside the root but
1529
+ * the snapshot does not carry it — a file the bundle omits, or a skill that is disabled) is a
1530
+ * `warning`: a content bug the skill's author owns, published as found — a publish with only
1531
+ * warnings answers `verdict: 'warnings'` with the findings AND a written snapshot (api-types
1532
+ * 0.28.0). */
1533
+ | 'unresolved-ref'
1534
+ /** A path the store refuses BY NAME rather than reads through: a shape that could leave its root
1535
+ * (`..`, an absolute piece), a component that crosses a symlink, or — under `effective/`, where
1536
+ * every entry is classified — a symlink or a special node (socket, fifo, device) anywhere,
1537
+ * the contents of a pruned directory (`.venv`, `node_modules`, `__pycache__`) INCLUDED: such a
1538
+ * directory is never copied or hashed, but what it holds is still judged, and a link inside one
1539
+ * blocks with the reason (a provisioned environment lives under `baseline/`, not the editable
1540
+ * root). Blocking; `file` names the entry, `skill` its owning skill when it has one. */
1541
+ | 'path-invalid'
1542
+ | 'unknown-skill'
1543
+ | 'no-baseline'
1544
+ | 'fs-drift'
1545
+ | 'refresh-conflict'
1546
+ | 'empty-snapshot'
1547
+ /** `.claude-plugin/plugin.json`, `archetypes.json` or `components.json` is absent from `effective/`
1548
+ * — the plugin manifest + the runtime catalogs are REQUIRED snapshot members (blocking at
1549
+ * publish; api-types 0.28.0). */
1550
+ | 'missing-plugin-manifest'
1551
+ /** The plugin manifest or a runtime catalog is present but not what its reader expects — does
1552
+ * not parse as JSON, is not a JSON object, or `archetypes.json` lacks its `archetypes`
1553
+ * collection (blocking at publish; api-types 0.28.0). A manifest without `name` is
1554
+ * `name-mismatch`. */
1555
+ | 'catalog-invalid'
1556
+ /** The baseline's shared read-only Python env could not be provisioned (uv missing, `uv sync`
1557
+ * failed, or the env could not be locked) while the bundle carries a `pyproject.toml` — the env
1558
+ * is REQUIRED, so the publish is blocked and nothing (the provisioning state included) is
1559
+ * persisted (api-types 0.28.0). */
1560
+ | 'venv-failed'
1561
+ /** A publish was refused because one is already running (one at a time) — nothing was written, so
1562
+ * it is a 2xx `blocked` envelope, not a 409 (409 is only a stale `expectedRevision`; api-types
1563
+ * 0.28.0). Re-read `GET /skills` and retry against the revision it answers. */
1564
+ | 'publish-in-flight'
1565
+ /** A publish was aborted because the skills root changed under it — nothing was written to either
1566
+ * root; a 2xx `blocked` envelope (api-types 0.28.0). Re-read `GET /skills` and retry. */
1567
+ | 'root-changed'
1568
+ /** A path outside the bundle closure — the ONE allowlist of what the seed copies, what the support
1569
+ * API may address, and what a snapshot may carry: the five runtime catalogs under `.claude-plugin`
1570
+ * BY NAME (`plugin.json`, `archetypes.json`, `components.json`, `specialist.json`,
1571
+ * `stack-registry.json` — `marketplace.json`, a publish-time listing, is outside), everything under
1572
+ * `skills`, `schemas` and `docs/examples`, everything under `scripts` except the `ci` and `wg`
1573
+ * directories and any `wg-`-prefixed dev tooling, plus `pyproject.toml` and `uv.lock`. A support
1574
+ * add/PUT there is a 2xx `blocked` envelope (nothing written); a file found there under
1575
+ * `effective/` at publish/analyze (a direct filesystem edit) is a BLOCKING finding naming the path
1576
+ * — a snapshot never ships it (api-types 0.28.0). */
1577
+ | 'outside-closure'
1578
+ /** A content-addressed `baseline/<hash>` whose tree does not hash to its name (a bundle file
1579
+ * modified, planted or removed, or a symlink inside), or a baseline file whose bytes do not match
1580
+ * the hash the manifest recorded for it. Baselines are re-verified before EVERY reuse: a refresh
1581
+ * refuses to reuse it (2xx `blocked`, nothing copied), reset refuses to restore from it (nothing
1582
+ * written), publish/analyze report it blocking (before AND after the env was provisioned in it).
1583
+ * Read-only mode bits on the baseline are a guard, never the integrity boundary — the hash is
1584
+ * (api-types 0.28.0). */
1585
+ | 'baseline-corrupt';
1586
+
1587
+ export type SkillFindingSeverity = 'warning' | 'blocking';
1588
+
1589
+ /** `blocked` = at least one blocking finding (nothing was written / published); `warnings` =
1590
+ * proceeded with warnings; `clear` = nothing to say. */
1591
+ export type SkillVerdict = 'clear' | 'warnings' | 'blocked';
1592
+
1593
+ export interface SkillConflictFinding {
1594
+ kind: SkillFindingKind;
1595
+ severity: SkillFindingSeverity;
1596
+ /** The skill this finding is about, or `null` for a catalog / support-file finding. */
1597
+ skill: string | null;
1598
+ /** Plugin-relative file the finding names, or `null`. */
1599
+ file: string | null;
1600
+ /** 1-based line in `file`, when the finding is anchored to one. */
1601
+ line: number | null;
1602
+ /** The OTHER catalog skill this finding is against (a collision, a core guard), or `null`. */
1603
+ againstSkill: string | null;
1604
+ againstIsCore: boolean;
1605
+ /** The concrete fact (names, paths, the parse error). */
1606
+ evidence: string;
1607
+ /** Why it matters. */
1608
+ explanation: string;
1609
+ }
1610
+
1611
+ /** `POST /skills/analyze` 200 body (a PURE dry run of the publish validation: nothing persisted,
1612
+ * `revision` unchanged — drift it observes is reported, and recorded only by the publish that
1613
+ * ships it) — and the base of every mutation result. */
1614
+ export interface SkillAnalyzeResult {
1615
+ verdict: SkillVerdict;
1616
+ findings: SkillConflictFinding[];
1617
+ revision: number;
1618
+ }
1619
+
1620
+ /** Every `/skills` mutation's 200 body. `blocked` ⇒ nothing was written and `revision` is unchanged. */
1621
+ export interface SkillMutationResult extends SkillAnalyzeResult {
1622
+ skill?: SkillEntry & { name: string };
1623
+ }
1624
+
1625
+ /** `POST /skills/publish` 200 body. `snapshot` is `null` when the verdict is `blocked` — and then
1626
+ * `revision` is unchanged (a blocked publish persists nothing — not the drift it observed, not the
1627
+ * baseline's provisioning state). `path` is the absolute REAL path of the locked, read-only
1628
+ * generation; its `snapshot.json` carries the skill rows and the `views` block (`views.copilot`:
1629
+ * the `views/copilot` dir + the portable skills laid out in it). One publish runs at a time — a
1630
+ * concurrent one wrote nothing and answers a 2xx `blocked` `publish-in-flight` envelope
1631
+ * (`snapshot: null`), not a 409. */
1632
+ export interface SkillPublishResult extends SkillAnalyzeResult {
1633
+ snapshot: { gen: number; path: string; contentHash: string; skills: number } | null;
1634
+ }
1635
+
1636
+ /** `POST /skills/refresh-baseline` 200 body — the three-way merge per FILE (baseline_old /
1637
+ * baseline_new / effective): unchanged → take new; user-modified & upstream-unchanged → keep;
1638
+ * both changed → keep + `conflict` (new side readable as `?side=baseline`); user-DELETED &
1639
+ * upstream-changed → the deletion is kept + `conflict` (a deletion is a modification; reset
1640
+ * restores upstream); upstream-deleted & user-unmodified → remove; upstream-deleted &
1641
+ * user-modified → keep as user-added; a new upstream skill whose path-derived name is a
1642
+ * user-added skill's → conflict. 502 when no plugin is installed. */
1643
+ export interface SkillRefreshResult extends SkillAnalyzeResult {
1644
+ previous_baseline: string;
1645
+ baseline: string;
1646
+ plugin_version: string;
1647
+ /** Skills whose files were replaced from the new baseline. */
1648
+ taken: string[];
1649
+ /** Skills that kept operator content (upstream unchanged, or a flagged conflict). */
1650
+ kept: string[];
1651
+ added: string[];
1652
+ removed: string[];
1653
+ /** Skills flagged `conflict` by this refresh. */
1654
+ conflicts: string[];
1655
+ }
1656
+
1657
+ /** The 409 body of a `/skills` mutation whose `expectedRevision` is stale — a CAS conflict, the
1658
+ * ONLY thing that answers 409. A publish refused because another is in flight, or aborted because
1659
+ * the skills root changed under it, wrote nothing and instead answers a 2xx `blocked` findings
1660
+ * envelope (`publish-in-flight` / `root-changed`; api-types 0.28.0), never a 409. */
1661
+ export interface SkillRevisionConflict {
1662
+ error: string;
1663
+ /** The current revision — re-read `GET /skills` (or use this) and retry. */
1664
+ revision: number;
1665
+ }
1666
+
1667
+ /** The body of `POST /skills/:name/{enable,disable,reset}`, `POST /skills/publish` and
1668
+ * `POST /skills/refresh-baseline`. */
1669
+ export interface SkillRevisionBody {
1670
+ expectedRevision: number;
1671
+ }
1672
+
1673
+ /** `PUT /skills/:name/files/*path` and `PUT /skills/support/*path` body. `content` is UTF-8 text,
1674
+ * at most 512 KB. */
1675
+ export interface PutSkillFileBody {
1676
+ content: string;
1677
+ expectedRevision: number;
1678
+ }
1679
+
1680
+ /** `POST /skills` body — add a user skill. `files` maps skill-relative POSIX paths to UTF-8 text
1681
+ * and must include `SKILL.md`; the skill lands at `skills/<name minus "wicked-garden-">`. */
1682
+ export interface AddSkillBody {
1683
+ name: string;
1684
+ files: Record<string, string>;
1685
+ expectedRevision: number;
1686
+ }
1687
+
1688
+ /** `POST /skills/:name/replace` body — replace the skill's OWN files wholesale (nested skills untouched). */
1689
+ export interface ReplaceSkillBody {
1690
+ files: Record<string, string>;
1691
+ expectedRevision: number;
1692
+ }
1693
+
1255
1694
  // ── Governance wiki management (scoreboard + meta) ─────────────────────────────
1256
1695
 
1257
1696
  /**
@@ -1398,7 +1837,17 @@ export interface GovernanceEvalNearestRule {
1398
1837
  * remediation pointer for "which rule needs sharpening".
1399
1838
  */
1400
1839
  export interface GovernanceEvalResult {
1401
- sample: Pick<GovernanceEvalSample, 'id' | 'description' | 'kind' | 'steering_type'>;
1840
+ sample: Pick<GovernanceEvalSample, 'id' | 'description' | 'kind' | 'steering_type'> & {
1841
+ /**
1842
+ * The sample's PAYLOAD identity — `sha256:` over the canonical JSON of its full payload (id,
1843
+ * description, kind, steering_type, signals). The engine echoes only the four fields above
1844
+ * (input `signals` never ride a result row), so this is stamped by a PRODUCER that held the
1845
+ * samples it staged (the internal-corpus `run`); a row without it cannot be proven to be the
1846
+ * same action as a row with the same id in another run, and the offline comparison reports
1847
+ * such a pair as unverified rather than comparable.
1848
+ */
1849
+ payload_hash?: string;
1850
+ };
1402
1851
  expected: 'deny' | 'allow';
1403
1852
  fired: string[];
1404
1853
  verdict: 'caught' | 'gap' | 'false_positive';
@@ -1413,15 +1862,73 @@ export interface GovernanceEvalSummary {
1413
1862
  false_positives: number;
1414
1863
  }
1415
1864
 
1865
+ /**
1866
+ * One steering rule in the judged store that NO sample of the run exercised — the inverse blind
1867
+ * spot of `summary.gaps` (core #394): a gap is a sample nothing caught, but a rule with zero
1868
+ * exercising samples produces no result row at all and so is invisible to the summary. Each
1869
+ * entry names the rule and the Steering sub-page it belongs to, so the corpus can grow a sample
1870
+ * for it (or the rule can be retired as untestable).
1871
+ */
1872
+ export interface GovernanceEvalUnexercisedRule {
1873
+ rule_id: string;
1874
+ steering_type: SteeringType;
1875
+ }
1876
+
1877
+ /**
1878
+ * One steering type's row of {@link GovernanceEvalRuleCoverage.per_type} (evals.rs `TypeCoverage`):
1879
+ * how many of the run's eligible decide-lane rules OF THAT TYPE fired for at least one sample, and
1880
+ * how many fired for none. `unexercised` here is a COUNT — the ids are the parent's `unexercised`
1881
+ * list, whose rows each carry their `steering_type`, so the two reconcile per type.
1882
+ */
1883
+ export interface GovernanceEvalTypeCoverage {
1884
+ exercised: number;
1885
+ unexercised: number;
1886
+ }
1887
+
1888
+ /**
1889
+ * Rule-side coverage of an eval run (core #394/#395 — evals.rs `RuleCoverage`): the decide-lane
1890
+ * rules ELIGIBLE for the run (every active effect-bearing rule, narrowed to the run's `--type`
1891
+ * slice when one was given) partitioned into `exercised` (fired — blocking or not — for at least
1892
+ * one sample; a COUNT) and `unexercised` (fired for none; the ids, each with its steering type).
1893
+ * Together with `summary` it separates "the corpus lacks a behavior" from "the store lacks a rule"
1894
+ * — the two readings a bare gap count conflates. `recall_only` counts the active rules in the slice
1895
+ * carrying NO effect — outside the partition because the gate never fires them (core #395).
1896
+ * `per_type` is the same partition per steering type — all seven keys, zeros included (the engine
1897
+ * pins the shape).
1898
+ *
1899
+ * Every engine that emits `rule_coverage` at all (core #394 onward) serializes all four fields on
1900
+ * every report (serde, no skip). `recall_only` and `per_type` are declared optional on the CONTRACT
1901
+ * only because the daemon persists a run's coverage VERBATIM and validates none of its fields, so a
1902
+ * stored record is exactly as complete as its producer made it and the contract never promises what
1903
+ * was never checked: a consumer reads an absent `per_type` as "the engine reports no per-type
1904
+ * coverage" (the offline comparison says exactly that), never as zeros. `exercised` and
1905
+ * `unexercised` are required — no engine ever emitted one without the other.
1906
+ *
1907
+ * A run's `results[].fired` is NOT this partition's evidence: `fired` is the BLOCKING subset of the
1908
+ * firings, and under a type filter it may name rules of OTHER types — the filter slices the samples
1909
+ * and this denominator, never the gate (evals.rs `run_evals` / `evaluate_sample`).
1910
+ */
1911
+ export interface GovernanceEvalRuleCoverage {
1912
+ exercised: number;
1913
+ unexercised: GovernanceEvalUnexercisedRule[];
1914
+ recall_only?: number;
1915
+ per_type?: Record<SteeringType, GovernanceEvalTypeCoverage>;
1916
+ }
1917
+
1416
1918
  /**
1417
1919
  * The `POST /testing/evals/run` 200 body — the engine's serde report passed through VERBATIM
1418
1920
  * (snake_case field names, `degraded` spelled `null` when the run was full-fidelity;
1419
- * `'facet-only'` when the embedding side was unavailable and only facet matching ran).
1921
+ * `'facet-only'` when the gap-hint embedder was unavailable and hints fell back to lexical
1922
+ * matching — the verdicts are the same either way, only `nearest_rules` degrades).
1923
+ *
1924
+ * `rule_coverage` is OPTIONAL on purpose: an engine that predates core #394 emits a report
1925
+ * without it, and that report still validates — the daemon never fabricates the field.
1420
1926
  */
1421
1927
  export interface GovernanceEvalReport {
1422
1928
  results: GovernanceEvalResult[];
1423
1929
  summary: GovernanceEvalSummary;
1424
1930
  degraded: 'facet-only' | null;
1931
+ rule_coverage?: GovernanceEvalRuleCoverage;
1425
1932
  }
1426
1933
 
1427
1934
  /** The `POST /testing/corpora/import` request body — a named eval corpus for later runs. */
@@ -1442,6 +1949,72 @@ export interface ImportEvalCorpusResponse {
1442
1949
  embedded: boolean;
1443
1950
  }
1444
1951
 
1952
+ // ── Eval run history (crew-side EvalRunStore) — the Evals section's real list ───
1953
+
1954
+ /**
1955
+ * One steering type's slice of an eval run's rollup — the same four counts as
1956
+ * {@link GovernanceEvalSummary}, derived from the run's results grouped by `sample.steering_type`.
1957
+ * This is what makes "which steering type carries the gaps" a stored field, not a per-request
1958
+ * recompute over the full results.
1959
+ */
1960
+ export interface EvalRunPerTypeCount {
1961
+ total: number;
1962
+ caught: number;
1963
+ gaps: number;
1964
+ false_positives: number;
1965
+ }
1966
+
1967
+ /**
1968
+ * One PERSISTED eval run — the listable rollup row (`GET /testing/evals`). A run is one
1969
+ * `POST /testing/evals/run` and its report, stamped with WHO ran it, WHAT it ran against, and
1970
+ * WHEN, so the Evals section is a real history instead of a single session-local report that a
1971
+ * reload loses. The per-sample `results` are NOT here — they live on {@link EvalRunDetail} (the
1972
+ * drilldown), so listing the history stays cheap no matter how large a custom corpus is.
1973
+ *
1974
+ * Evals take no repo/project (they judge a STORE, not a workspace), so this is a single GLOBAL
1975
+ * feed per daemon — unlike recon campaign runs, which file into projects.
1976
+ */
1977
+ export interface EvalRunSummary {
1978
+ /** Server-minted run id. */
1979
+ id: string;
1980
+ /** Unix SECONDS the run was recorded (the crew `created_at` convention — see AgentSession). */
1981
+ created_at: number;
1982
+ /** The authenticated actor who ran it. */
1983
+ actor: string;
1984
+ /** The corpus scope (`evals:<name>`), or null for the engine's built-in dev-behaviors corpus. */
1985
+ corpus: string | null;
1986
+ /** The single steering type the run was narrowed to, or null when it ran all seven. */
1987
+ type_filter: SteeringType | null;
1988
+ /** The steering rule store the run was judged against (the daemon's `--db` steering store). */
1989
+ rule_store: string;
1990
+ /** The report's rollup, verbatim ({@link GovernanceEvalSummary}). */
1991
+ summary: GovernanceEvalSummary;
1992
+ /** Per-steering-type rollup, derived from the results — present only for the types the run covered. */
1993
+ per_type: Partial<Record<SteeringType, EvalRunPerTypeCount>>;
1994
+ /** `'facet-only'` when the run degraded to facet matching; null when it ran full-fidelity. */
1995
+ degraded: 'facet-only' | null;
1996
+ /**
1997
+ * The report's {@link GovernanceEvalReport.rule_coverage}, persisted VERBATIM (like `degraded`,
1998
+ * never recomputed daemon-side). ABSENT — not null — when the engine that ran the eval predates
1999
+ * core #394 and emitted no coverage, so a history row honestly shows which runs measured it.
2000
+ */
2001
+ rule_coverage?: GovernanceEvalRuleCoverage;
2002
+ }
2003
+
2004
+ /**
2005
+ * One eval run WITH its full per-sample results (`GET /testing/evals/:id`) — the drilldown behind a
2006
+ * history row. `results` is the same verbatim {@link GovernanceEvalResult} array the run's original
2007
+ * `POST /testing/evals/run` returned, so the existing report view renders it unchanged.
2008
+ */
2009
+ export interface EvalRunDetail extends EvalRunSummary {
2010
+ results: GovernanceEvalResult[];
2011
+ }
2012
+
2013
+ /** The `GET /testing/evals` 200 body — the eval run history, newest first. */
2014
+ export interface ListEvalRunsResponse {
2015
+ runs: EvalRunSummary[];
2016
+ }
2017
+
1445
2018
  /**
1446
2019
  * The `POST /testing/recon` request body (api-types 0.15.0) — the Testing page's campaign-recon
1447
2020
  * trigger. `problem` is the recon brief, passed to every launched run VERBATIM (the client owns
@@ -1836,6 +2409,15 @@ export interface SystemSettings {
1836
2409
  * the engine default `~/.wicked-worker`.
1837
2410
  */
1838
2411
  worker_config_root?: string;
2412
+ /*
2413
+ * There is NO skills setting (skills keystone, api-types 0.28.0). The daemon-owned skills root is
2414
+ * `<state home>/skills` — the directory holding `manifest.json`, `baseline/<hash>/`, `effective/`,
2415
+ * `snapshots/<gen>/` and the `current` symlink — full stop: not configurable (a `skills_root` a
2416
+ * client sends is dropped and named in the audit entry's `ignored`; design v3.1 §1 one storage
2417
+ * root, v3.2 §1 never a user CLI directory), and there is no mirror knob either (the daemon never
2418
+ * writes into the user's own CLI directories). `GET /skills` → `root` and `GET /diagnostics` →
2419
+ * `skills.root` report where it is.
2420
+ */
1839
2421
  /**
1840
2422
  * The repo-scoped launch delivery DEFAULT (crew#393; api-types 0.18.0, additive). What a
1841
2423
  * `POST /runs` with `repoRef` + a CODE-WORK `workflow` (a def with at least one
@@ -2119,6 +2701,32 @@ export interface InteractiveDocDeleteResponse {
2119
2701
  ledger: InteractiveDocDeleteLedgerReport;
2120
2702
  }
2121
2703
 
2704
+ /**
2705
+ * One row of `GET /projects/:projectId/interactive/api/docs` (crew#472) — the bridge's own
2706
+ * `GET /api/docs` row (interactive's `listDocs` shape, relayed field-for-field) stamped with the
2707
+ * project whose mount it was listed under. Docs roots are partitioned per project (the `default`
2708
+ * project keeps the legacy shared root), so `projectId` is the attribution a client rendering
2709
+ * docs from several projects at once cannot otherwise recover.
2710
+ */
2711
+ export interface InteractiveDocSummary {
2712
+ /** The doc name (slug). */
2713
+ name: string;
2714
+ /** Manifest `kind`; a manifest without one lists as `doc`. */
2715
+ kind: 'doc' | 'html' | 'source' | 'demo';
2716
+ /** Head version. */
2717
+ head: number;
2718
+ /** Lineage size. */
2719
+ versions: number;
2720
+ /** ISO-8601 timestamp of the head version, or null when the lineage is empty. */
2721
+ updated_at: string | null;
2722
+ /** Present only on a retired (tombstoned) row, which lists only with `?includeRetired=1`. */
2723
+ retired?: true;
2724
+ /** ISO-8601 retirement timestamp; present with `retired`. */
2725
+ retired_at?: string;
2726
+ /** The crew project this row was listed under — the mount's `:projectId`. */
2727
+ projectId: string;
2728
+ }
2729
+
2122
2730
  /**
2123
2731
  * One normalized entry of the merged project activity feed
2124
2732
  * (`GET /projects/:id/activity`, ADR §5.2): core events of member runs/chats
@@ -2754,4 +3362,236 @@ export interface DiagnosticsResponse {
2754
3362
  /** Bounded tail of the daemon's own error-level log lines, newest first. */
2755
3363
  recentErrors: DiagnosticsRecentError[];
2756
3364
  acp: AcpDiagnostics;
3365
+ /** The skills seam's last outcome (api-types 0.28.0) — see `DiagnosticsSkills`. */
3366
+ skills: DiagnosticsSkills;
3367
+ }
3368
+
3369
+ /** The skills seam's state as `GET /diagnostics` reports it (skills keystone, api-types 0.28.0). */
3370
+ export type DiagnosticsSkillsState = 'published' | 'fallback' | 'blocked' | 'config-error' | 'disabled';
3371
+
3372
+ /** One finding the skills degradation ladder produced (design v3 §3). */
3373
+ export interface DiagnosticsSkillsFinding {
3374
+ /** `skills.fallback` = no wicked-garden installed (engine input left unset / restored to the boot
3375
+ * value — the engine resolves the live cache itself); `skills.blocked` = the first publish is
3376
+ * blocked (engine input points at a non-existent refusal path — launches fail loudly until the
3377
+ * catalog is fixed); `skills.config` = the configured root is corrupt/unusable (same refusal;
3378
+ * `error`), or — as a `warning` on the `published` state — the root lies OUTSIDE the daemon
3379
+ * state home, so core's fence cross-check (`WICKED_CREW_STATE_HOME`) refuses every launch. */
3380
+ kind: 'skills.fallback' | 'skills.blocked' | 'skills.config';
3381
+ severity: 'warning' | 'error';
3382
+ message: string;
3383
+ }
3384
+
3385
+ /**
3386
+ * `GET /diagnostics` → `skills`: whether the engine is being handed a verified snapshot, and if not,
3387
+ * why — the one read-only answer to "why do launches refuse the skills snapshot". `disabled` is a
3388
+ * daemon booted without the seam (the manifest collector, some tests).
3389
+ */
3390
+ export interface DiagnosticsSkills {
3391
+ state: DiagnosticsSkillsState;
3392
+ /** The resolved skills root on the daemon host; `null` when `disabled`. */
3393
+ root: string | null;
3394
+ /** The VERIFIED published snapshot (`current` realpath-contained, `snapshot.json` + content hash
3395
+ * checked), or `null`. `path` is the absolute REAL path of `snapshots/<gen>`. */
3396
+ current: { gen: number; path: string } | null;
3397
+ /** What `WICKED_SKILLS_SNAPSHOT` — the engine's ONE skills input (v3.1 §2, v3.4 §2;
3398
+ * `WICKED_SKILLS_CURRENT` is never set, `WICKED_CREW_STATE_HOME` is not exported) — is exported as
3399
+ * right now: the real snapshot path, a `<root>/refused/…` refusal path, `""` when the process
3400
+ * booted with an explicitly EMPTY value (preserved — a configuration error core refuses, state
3401
+ * `config-error`; design v3.5 §4), or `null` = unset. */
3402
+ engineInput: string | null;
3403
+ /** DIAGNOSTICS-ONLY: the canonical realpath of the daemon state home, reported for humans. It is
3404
+ * NOT an engine input — `WICKED_CREW_STATE_HOME` is retired (v3.4 §2): core reads only
3405
+ * `WICKED_SKILLS_SNAPSHOT` and derives the state home from its `<state home>/skills/snapshots/<gen>`
3406
+ * layout. `null` only when `disabled`. */
3407
+ stateHome: string | null;
3408
+ findings: DiagnosticsSkillsFinding[];
3409
+ }
3410
+
3411
+ // ── Memory proposal queue (DES-MEM-FACETED-001 §5.0, api-types 0.21.0) ─────────
3412
+
3413
+ /**
3414
+ * A proposal's lifecycle state. A proposal is INERT while `pending` (never recalled/applied);
3415
+ * `approve` promotes its payload to the active store and marks it `approved`, `reject` marks it
3416
+ * `rejected`.
3417
+ */
3418
+ export type ProposalState = 'pending' | 'approved' | 'rejected';
3419
+
3420
+ /**
3421
+ * One entry in the estate proposal queue — a type-generic, inert write agents submit and operators
3422
+ * decide (estate #162). The shape is the estate MCP `proposal.list` wire form verbatim.
3423
+ */
3424
+ export interface Proposal {
3425
+ /** Stable proposal-node id. */
3426
+ id: string;
3427
+ /** `"memory"` | `"policy:<steering_type>"` | future — the router key `approve` promotes by. */
3428
+ kind_type: string;
3429
+ /** The proposed content (a memory body, a policy rule, …). Opaque here; narrowed by `kind_type`. */
3430
+ payload: unknown;
3431
+ /** Agent-declared orthogonal facets (axis → value). */
3432
+ facets: Record<string, string>;
3433
+ /** Run/unit/agent attribution — AUTHORITY-STAMPED by the estate MCP from its launch env, never
3434
+ * taken from the submitter. Empty when the server had no `WICKED_RUN_*` env. */
3435
+ provenance: Record<string, string>;
3436
+ state: ProposalState;
3437
+ /** Unix seconds. */
3438
+ created_at: number;
3439
+ }
3440
+
3441
+ /** `GET /proposals` query filters — both optional; omitting a filter lists across that dimension. */
3442
+ export interface ListProposalsQuery {
3443
+ kind_type?: string;
3444
+ state?: ProposalState;
3445
+ }
3446
+
3447
+ /** `GET /proposals` → 200. */
3448
+ export interface ListProposalsResponse {
3449
+ proposals: Proposal[];
3450
+ }
3451
+
3452
+ /**
3453
+ * The result of landing an approved POLICY proposal as a steering rule (DES-MEM-FACETED-001 §5.2).
3454
+ * estate performs no rules-write for a policy proposal (the AW-11 invariant) — it hands the payload
3455
+ * to crew, the one governed rules-write path, which shapes a `ConformanceRule` and upserts it. Like
3456
+ * the steering-author landing (crew#388) the estate decision (approve) stands regardless, so a
3457
+ * landing that could not complete reports `outcome:"failed"` with a loud, operator-readable `error`
3458
+ * (recoverable by hand via `POST /governance/rules`) rather than turning the approve into a 500.
3459
+ */
3460
+ export interface PolicyLandingResult {
3461
+ /** `landed` = the steering rule is in the store; `failed` = the proposal is approved but no rule
3462
+ * was written — `error` says why. */
3463
+ outcome: 'landed' | 'failed';
3464
+ /** The upserted rule's id (deterministic `proposal:<proposalId>`, idempotent on replay). Present on `landed`. */
3465
+ ruleId?: string;
3466
+ /** The steering type derived from the proposal's `kind_type` (`policy:<type>`). Present on `landed`. */
3467
+ steering_type?: SteeringType;
3468
+ /** Present iff `outcome:"failed"` — the loud reason the rule did not land. */
3469
+ error?: string;
3470
+ }
3471
+
3472
+ /**
3473
+ * `POST /proposals/:id/approve` → 200. A MEMORY proposal is `promoted` (its payload is now an
3474
+ * active memory, `active_id`) and is complete. A POLICY proposal is `handed_off` — its `payload`
3475
+ * (`{ rule, severity }`) is still returned verbatim, and `landing` now reports the steering rule
3476
+ * crew created from it (`landing.outcome:"landed"` with the `ruleId`), or the loud reason it could
3477
+ * not (`landing.outcome:"failed"`). `landing` is absent only from responses served by a daemon
3478
+ * predating the policy→steering landing (forward-additive, DES-MEM-FACETED-001 §5.2).
3479
+ */
3480
+ export type ApproveProposalResponse =
3481
+ | { outcome: 'promoted'; active_id: string }
3482
+ | { outcome: 'handed_off'; payload: unknown; landing?: PolicyLandingResult };
3483
+
3484
+ /** `POST /proposals/:id/reject` → 200. */
3485
+ export interface RejectProposalResponse {
3486
+ ok: true;
3487
+ }
3488
+
3489
+ // ── Memory management (governed-knowledge surface, DES-MEM-FACETED-001, api-types 0.22.0) ──────
3490
+ //
3491
+ // The studio's surface for managing the memories that ALREADY exist in the operator store — the
3492
+ // counterpart to the proposal queue (which decides learnings not yet stored). Backed by the estate
3493
+ // MCP memory tools (memory.recall / memory.coverage / memory.erase) exposed over crew's /api/v1.
3494
+ // The granularity is estate's, surfaced honestly: browse is query-based (no "list all"), and
3495
+ // retire is SUBTREE-scoped (no per-id delete).
3496
+
3497
+ /**
3498
+ * One memory as browsed from the store (`GET /memory`, from estate `memory.recall`).
3499
+ *
3500
+ * estate `memory.recall` is query-based and token-budgeted — there is NO "list all" — and returns
3501
+ * `{ memory_id, scope, content, tier, score }` per item. It does NOT surface per-item `facets`, so
3502
+ * `facets` is presently always `{}` (kept in the shape for forward-compatibility and to mirror
3503
+ * `Proposal.facets`); `score` is the recall relevance score, absent when estate omits it.
3504
+ */
3505
+ export interface MemoryItem {
3506
+ /** Stable memory-node id (estate `memory_id`). */
3507
+ id: string;
3508
+ /** The memory body. */
3509
+ content: string;
3510
+ /** Tier: `working` | `episodic` | `semantic` | `procedural` | `archival`. */
3511
+ tier: string;
3512
+ /** The memory's own hierarchical scope path (slash-separated `kind:id` segments; `""` = root). */
3513
+ scope: string;
3514
+ /** Orthogonal facets (axis → value) the memory is tagged with. Populated by estate `memory.list`
3515
+ * (the management browse) — this is what powers the surface's facet filter. `{}` for an
3516
+ * unfaceted memory (and for a `memory.recall`-sourced item, which carries no facets). */
3517
+ facets: Record<string, string>;
3518
+ /** Relevance score — only present on a `memory.recall` item; `memory.list` (the browse) is not
3519
+ * relevance-ranked, so it is absent there. */
3520
+ score?: number;
3521
+ /**
3522
+ * Unix-SECONDS timestamp of when this memory FIRST entered the store — the "added" time the
3523
+ * governance surface sorts + date-filters on (api-types 0.23.0). Populated from estate
3524
+ * `memory.list`, which returns `created_at` per item; ABSENT — never fabricated — on a memory
3525
+ * whose creation time the store did not report (and on a `memory.recall`-sourced item, which
3526
+ * carries none). Additive: absence is the one spelling of "unknown", so a `since`/`until` filter
3527
+ * cannot assert an undated memory in range.
3528
+ */
3529
+ created_at?: number;
3530
+ }
3531
+
3532
+ /**
3533
+ * `GET /memory` query filters. All optional. The browse itself lists the COMPLETE in-scope set via
3534
+ * estate `memory.list`; `query`, `facets`, and `limit` are applied server-side as post-filters over
3535
+ * that set (they are NOT a relevance query). `scope_prefix` is the only estate-side argument — a
3536
+ * subtree filter where `""`/omitted matches EVERY memory (the same predicate as retire). `query` is
3537
+ * a case-insensitive CONTENT substring match. `facets` is a JSON-encoded object of axis→value
3538
+ * strings; an item is kept only if it carries every pair. `limit` caps the returned ROW count.
3539
+ */
3540
+ export interface ListMemoriesQuery {
3541
+ /** Case-insensitive content substring filter over the complete set. */
3542
+ query?: string;
3543
+ scope_prefix?: string;
3544
+ /** JSON-encoded `Record<string, string>` (axis→value); a malformed value is a 400. */
3545
+ facets?: string;
3546
+ /** Positive integer; caps the returned row count. */
3547
+ limit?: string;
3548
+ /**
3549
+ * Inclusive LOWER date bound (api-types 0.23.0): keep only memories with `created_at >= since`.
3550
+ * Unix SECONDS, a non-negative integer (a present-but-non-integer value is a 400). Applied
3551
+ * server-side over the loaded set, alongside `query`/`facets`. A memory with NO `created_at`
3552
+ * is EXCLUDED whenever `since` (or `until`) is set — an unknown creation time is not asserted
3553
+ * in range. Omitted ⇒ no lower bound.
3554
+ */
3555
+ since?: string;
3556
+ /**
3557
+ * Inclusive UPPER date bound (api-types 0.23.0): keep only memories with `created_at <= until`.
3558
+ * Same units/validation/undated-exclusion as {@link ListMemoriesQuery.since}. Omitted ⇒ no
3559
+ * upper bound.
3560
+ */
3561
+ until?: string;
3562
+ }
3563
+
3564
+ /** `GET /memory` → 200. */
3565
+ export interface ListMemoriesResponse {
3566
+ memories: MemoryItem[];
3567
+ }
3568
+
3569
+ /** `GET /memory/coverage` query — an optional subtree filter (`""`/omitted = global totals). */
3570
+ export interface MemoryCoverageQuery {
3571
+ scope_prefix?: string;
3572
+ }
3573
+
3574
+ /**
3575
+ * `GET /memory/coverage` → 200 (estate `memory.coverage`). Memory-node counts, optionally scoped to
3576
+ * a subtree: `total`, plus per-tier and per-kind breakdowns.
3577
+ */
3578
+ export interface MemoryCoverageResponse {
3579
+ total: number;
3580
+ by_tier: Record<string, number>;
3581
+ by_kind: Record<string, number>;
3582
+ }
3583
+
3584
+ /**
3585
+ * `POST /memory/retire` body. Retire is SUBTREE-scoped, never per-id: estate `memory.erase`
3586
+ * hard-deletes EVERY memory whose scope equals or descends from `scope_prefix`. `scope_prefix` is
3587
+ * required and must be non-empty (estate refuses a total wipe) — the surface must show the operator
3588
+ * the whole subtree it will remove, not a single row.
3589
+ */
3590
+ export interface RetireMemoryBody {
3591
+ scope_prefix: string;
3592
+ }
3593
+
3594
+ /** `POST /memory/retire` → 200. The number of memories the subtree wipe deleted. */
3595
+ export interface RetireMemoryResponse {
3596
+ erased: number;
2757
3597
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-crew-api-types",
3
- "version": "0.20.0",
3
+ "version": "0.28.0",
4
4
  "description": "The wire contract of the wicked-crew daemon's /api/v1 REST surface and /ws CoreEvent frames \u2014 types only, zero runtime",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",