wicked-crew-api-types 0.25.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 +529 -4
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -1156,8 +1156,15 @@ export interface ConformanceRule {
1156
1156
  excludes?: string[];
1157
1157
  /** Ordering within a severity band + gate priority. Engine default: 1.0. */
1158
1158
  weight?: number;
1159
- /** Enforcement half (from the retired policy model). Absent ⇒ the rule is recall-only. */
1160
- 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';
1161
1168
  trigger?: { contains?: string };
1162
1169
  obligations?: string[];
1163
1170
  /** The frozen acceptance-criteria text (becomes a claim's `criteria` when the rule decides a gate). */
@@ -1319,6 +1326,371 @@ export interface SteeringLandingResult {
1319
1326
  error?: string;
1320
1327
  }
1321
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
+
1322
1694
  // ── Governance wiki management (scoreboard + meta) ─────────────────────────────
1323
1695
 
1324
1696
  /**
@@ -1465,7 +1837,17 @@ export interface GovernanceEvalNearestRule {
1465
1837
  * remediation pointer for "which rule needs sharpening".
1466
1838
  */
1467
1839
  export interface GovernanceEvalResult {
1468
- 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
+ };
1469
1851
  expected: 'deny' | 'allow';
1470
1852
  fired: string[];
1471
1853
  verdict: 'caught' | 'gap' | 'false_positive';
@@ -1480,15 +1862,73 @@ export interface GovernanceEvalSummary {
1480
1862
  false_positives: number;
1481
1863
  }
1482
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
+
1483
1918
  /**
1484
1919
  * The `POST /testing/evals/run` 200 body — the engine's serde report passed through VERBATIM
1485
1920
  * (snake_case field names, `degraded` spelled `null` when the run was full-fidelity;
1486
- * `'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.
1487
1926
  */
1488
1927
  export interface GovernanceEvalReport {
1489
1928
  results: GovernanceEvalResult[];
1490
1929
  summary: GovernanceEvalSummary;
1491
1930
  degraded: 'facet-only' | null;
1931
+ rule_coverage?: GovernanceEvalRuleCoverage;
1492
1932
  }
1493
1933
 
1494
1934
  /** The `POST /testing/corpora/import` request body — a named eval corpus for later runs. */
@@ -1553,6 +1993,12 @@ export interface EvalRunSummary {
1553
1993
  per_type: Partial<Record<SteeringType, EvalRunPerTypeCount>>;
1554
1994
  /** `'facet-only'` when the run degraded to facet matching; null when it ran full-fidelity. */
1555
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;
1556
2002
  }
1557
2003
 
1558
2004
  /**
@@ -1963,6 +2409,15 @@ export interface SystemSettings {
1963
2409
  * the engine default `~/.wicked-worker`.
1964
2410
  */
1965
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
+ */
1966
2421
  /**
1967
2422
  * The repo-scoped launch delivery DEFAULT (crew#393; api-types 0.18.0, additive). What a
1968
2423
  * `POST /runs` with `repoRef` + a CODE-WORK `workflow` (a def with at least one
@@ -2246,6 +2701,32 @@ export interface InteractiveDocDeleteResponse {
2246
2701
  ledger: InteractiveDocDeleteLedgerReport;
2247
2702
  }
2248
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
+
2249
2730
  /**
2250
2731
  * One normalized entry of the merged project activity feed
2251
2732
  * (`GET /projects/:id/activity`, ADR §5.2): core events of member runs/chats
@@ -2881,6 +3362,50 @@ export interface DiagnosticsResponse {
2881
3362
  /** Bounded tail of the daemon's own error-level log lines, newest first. */
2882
3363
  recentErrors: DiagnosticsRecentError[];
2883
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[];
2884
3409
  }
2885
3410
 
2886
3411
  // ── Memory proposal queue (DES-MEM-FACETED-001 §5.0, api-types 0.21.0) ─────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-crew-api-types",
3
- "version": "0.25.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",