wicked-crew 0.7.30 → 0.7.32

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 (115) hide show
  1. package/dist/api/chat-scope.d.ts +15 -2
  2. package/dist/api/chat-scope.d.ts.map +1 -1
  3. package/dist/api/chat-scope.js +7 -2
  4. package/dist/api/chat-scope.js.map +1 -1
  5. package/dist/api/eval-sample.d.ts +12 -12
  6. package/dist/api/routes.d.ts +10 -0
  7. package/dist/api/routes.d.ts.map +1 -1
  8. package/dist/api/routes.js +231 -37
  9. package/dist/api/routes.js.map +1 -1
  10. package/dist/api/run-files.d.ts +26 -0
  11. package/dist/api/run-files.d.ts.map +1 -1
  12. package/dist/api/run-files.js +78 -0
  13. package/dist/api/run-files.js.map +1 -1
  14. package/dist/api/seat-health.d.ts +33 -0
  15. package/dist/api/seat-health.d.ts.map +1 -1
  16. package/dist/api/seat-health.js +86 -2
  17. package/dist/api/seat-health.js.map +1 -1
  18. package/dist/api/seat-signin.d.ts +8 -0
  19. package/dist/api/seat-signin.d.ts.map +1 -1
  20. package/dist/api/seat-signin.js +48 -3
  21. package/dist/api/seat-signin.js.map +1 -1
  22. package/dist/api/seat-standing.d.ts +14 -2
  23. package/dist/api/seat-standing.d.ts.map +1 -1
  24. package/dist/api/seat-standing.js +14 -4
  25. package/dist/api/seat-standing.js.map +1 -1
  26. package/dist/api/server.d.ts.map +1 -1
  27. package/dist/api/server.js +41 -4
  28. package/dist/api/server.js.map +1 -1
  29. package/dist/api/skills.d.ts +2 -2
  30. package/dist/api/testing.d.ts +28 -1
  31. package/dist/api/testing.d.ts.map +1 -1
  32. package/dist/api/testing.js +190 -0
  33. package/dist/api/testing.js.map +1 -1
  34. package/dist/campaigns/rollup.d.ts +4 -0
  35. package/dist/campaigns/rollup.d.ts.map +1 -1
  36. package/dist/campaigns/rollup.js.map +1 -1
  37. package/dist/campaigns/routes.d.ts +3 -1
  38. package/dist/campaigns/routes.d.ts.map +1 -1
  39. package/dist/campaigns/routes.js +3 -0
  40. package/dist/campaigns/routes.js.map +1 -1
  41. package/dist/core/adapter.d.ts.map +1 -1
  42. package/dist/core/adapter.js +23 -2
  43. package/dist/core/adapter.js.map +1 -1
  44. package/dist/core/deliver-text.d.ts +7 -0
  45. package/dist/core/deliver-text.d.ts.map +1 -1
  46. package/dist/core/deliver-text.js +41 -2
  47. package/dist/core/deliver-text.js.map +1 -1
  48. package/dist/core/engine-roster.d.ts +68 -0
  49. package/dist/core/engine-roster.d.ts.map +1 -0
  50. package/dist/core/engine-roster.js +156 -0
  51. package/dist/core/engine-roster.js.map +1 -0
  52. package/dist/interactive/chat-events.d.ts.map +1 -1
  53. package/dist/interactive/chat-events.js +26 -4
  54. package/dist/interactive/chat-events.js.map +1 -1
  55. package/dist/interactive/council-outcome.d.ts +28 -1
  56. package/dist/interactive/council-outcome.d.ts.map +1 -1
  57. package/dist/interactive/council-outcome.js +51 -1
  58. package/dist/interactive/council-outcome.js.map +1 -1
  59. package/dist/interactive/demo-events.d.ts.map +1 -1
  60. package/dist/interactive/demo-events.js +26 -4
  61. package/dist/interactive/demo-events.js.map +1 -1
  62. package/dist/interactive/doc-run-index.d.ts +56 -0
  63. package/dist/interactive/doc-run-index.d.ts.map +1 -0
  64. package/dist/interactive/doc-run-index.js +100 -0
  65. package/dist/interactive/doc-run-index.js.map +1 -0
  66. package/dist/interactive/docs-index.d.ts +68 -0
  67. package/dist/interactive/docs-index.d.ts.map +1 -0
  68. package/dist/interactive/docs-index.js +114 -0
  69. package/dist/interactive/docs-index.js.map +1 -0
  70. package/dist/interactive/draft-events.d.ts +18 -0
  71. package/dist/interactive/draft-events.d.ts.map +1 -1
  72. package/dist/interactive/draft-events.js +36 -3
  73. package/dist/interactive/draft-events.js.map +1 -1
  74. package/dist/interactive/edit-events.d.ts.map +1 -1
  75. package/dist/interactive/edit-events.js +26 -4
  76. package/dist/interactive/edit-events.js.map +1 -1
  77. package/dist/interactive/ledger.d.ts +2 -0
  78. package/dist/interactive/ledger.d.ts.map +1 -1
  79. package/dist/interactive/ledger.js +4 -0
  80. package/dist/interactive/ledger.js.map +1 -1
  81. package/dist/qe/acceptance.d.ts +72 -3
  82. package/dist/qe/acceptance.d.ts.map +1 -1
  83. package/dist/qe/acceptance.js +137 -17
  84. package/dist/qe/acceptance.js.map +1 -1
  85. package/dist/qe/author-workflow.d.ts +165 -0
  86. package/dist/qe/author-workflow.d.ts.map +1 -0
  87. package/dist/qe/author-workflow.js +377 -0
  88. package/dist/qe/author-workflow.js.map +1 -0
  89. package/dist/qe/ledger.d.ts +128 -28
  90. package/dist/qe/ledger.d.ts.map +1 -1
  91. package/dist/qe/ledger.js +272 -42
  92. package/dist/qe/ledger.js.map +1 -1
  93. package/dist/qe/test-sets.d.ts +112 -0
  94. package/dist/qe/test-sets.d.ts.map +1 -0
  95. package/dist/qe/test-sets.js +170 -0
  96. package/dist/qe/test-sets.js.map +1 -0
  97. package/dist/skills/refs.d.ts +37 -0
  98. package/dist/skills/refs.d.ts.map +1 -1
  99. package/dist/skills/refs.js +22 -0
  100. package/dist/skills/refs.js.map +1 -1
  101. package/dist/skills/runtime.d.ts +13 -1
  102. package/dist/skills/runtime.d.ts.map +1 -1
  103. package/dist/skills/runtime.js +88 -1
  104. package/dist/skills/runtime.js.map +1 -1
  105. package/dist/skills/store.d.ts +82 -5
  106. package/dist/skills/store.d.ts.map +1 -1
  107. package/dist/skills/store.js +109 -21
  108. package/dist/skills/store.js.map +1 -1
  109. package/dist/studio/assets/{index-_ydd-jKd.css → index-C0UZ2BxT.css} +1 -1
  110. package/dist/studio/assets/index-Clm0MXDF.js +544 -0
  111. package/dist/studio/index.html +2 -2
  112. package/dist/studio/testid-inventory.json +251 -5
  113. package/endpoint-manifest.json +24 -1
  114. package/package.json +3 -3
  115. package/dist/studio/assets/index-CWUS9Ump.js +0 -542
@@ -20,7 +20,10 @@ import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
20
20
  import { chatSeatAdmission, seatStanding } from './seat-standing.js';
21
21
  import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
22
22
  import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
23
- import { InvalidDiffBaseError, NotARegularFileError, UnresolvableDiffBaseError, readFileCapped, worktreeDiff, } from './run-files.js';
23
+ import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
24
+ import { DocRunIndex } from '../interactive/doc-run-index.js';
25
+ import { listInteractiveDocs } from '../interactive/docs-index.js';
26
+ import { TestSetIndex } from '../qe/test-sets.js';
24
27
  import { resolveProjectGraphBinding } from '../projects/graph.js';
25
28
  import { registerProjectRoutes } from '../projects/routes.js';
26
29
  import { registerCampaignRoutes } from '../campaigns/routes.js';
@@ -469,6 +472,10 @@ runtime = {}) {
469
472
  const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
470
473
  const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
471
474
  const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
475
+ // Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
476
+ // ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
477
+ const docRuns = runtime.docRuns ?? new DocRunIndex(() => []);
478
+ const testSets = runtime.testSets ?? new TestSetIndex();
472
479
  const worktreeExists = runtime.worktreeExists ?? ((p) => existsSync(p));
473
480
  const vacuityProbes = {
474
481
  worktreeExists,
@@ -557,6 +564,9 @@ runtime = {}) {
557
564
  const decorateRun = (view) => {
558
565
  const conflictStrand = normalizeStranded(view);
559
566
  view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
567
+ // Wave 6 (F-4R2-006): the interactive document this run answered, from the seams' handoff
568
+ // ledgers — `null` = genuinely not a document run, so the field is ALWAYS present on served runs.
569
+ view.session.document_id = docRuns.documentOf(view.session.id) ?? null;
560
570
  const retryOf = retryIndex.retryOfFor(view.session.id);
561
571
  if (retryOf !== undefined)
562
572
  view.session.retry_of = retryOf;
@@ -719,7 +729,11 @@ runtime = {}) {
719
729
  signed_in: signed,
720
730
  // The bench is THIS daemon's council evidence (councilSeatFailed, bounded window) — the
721
731
  // prediction learns from what the engine actually did with the seat (#533 review, F-1).
722
- ...seatStanding(seat, signed, health, seatHealth.councilBenchFor(key)),
732
+ ...seatStanding(seat, signed, health, seatHealth.councilBenchFor(key),
733
+ // F-A45-006: the seat's OWN "no credential" report (a ballot's "No API key found", a
734
+ // worker's 401, an auth ACP fallback) overrides the file probe — `auth` flips, not only
735
+ // `council_eligible`, and the evidence rides on the wire.
736
+ seatHealth.authFailureFor(key)),
723
737
  };
724
738
  });
725
739
  };
@@ -868,26 +882,8 @@ runtime = {}) {
868
882
  if (resolved === null)
869
883
  return reply;
870
884
  const workdir = resolved.session.workdir;
871
- if (typeof workdir !== 'string' || workdir.length === 0) {
872
- return reply.code(409).send({ error: `run ${id} has no workdir — nothing to diff` });
873
- }
874
- if (!existsSync(workdir)) {
875
- return reply.code(409).send({ error: `run ${id}'s workdir no longer exists: ${workdir}` });
876
- }
877
- // Narrowing is WORKTREE-scoped: a contained-but-outside-the-worktree path (extra write
878
- // root / repo root) is a valid FILE read but has no meaning as a diff pathspec — rejected
879
- // explicitly here rather than handing git a `../`-prefixed pathspec and surfacing its
880
- // "outside repository" error as a 500 (Copilot, #305).
881
- if (resolved.target !== undefined && !isInsideRoot(workdir, resolved.target)) {
882
- return reply.code(400).send({
883
- error: `\`path\` must be inside the run's worktree to diff: ${workdir}`,
884
- });
885
- }
886
- const rel = resolved.target === undefined ? undefined : relative(workdir, resolved.target);
887
- try {
888
- return await worktreeDiff(workdir, rel, rawBase);
889
- }
890
- catch (err) {
885
+ /** The route's one error mapping, shared by the worktree and the branch reads. */
886
+ const diffError = (err) => {
891
887
  // Named 400s (§8.1): malformed base (not a plain ref) and well-formed-but-unresolvable
892
888
  // base are both client errors, each with its error name in the body — never a git 500.
893
889
  if (err instanceof InvalidDiffBaseError || err instanceof UnresolvableDiffBaseError) {
@@ -905,6 +901,75 @@ runtime = {}) {
905
901
  });
906
902
  }
907
903
  return reply.code(500).send({ error: message(err) });
904
+ };
905
+ const worktreeLive = typeof workdir === 'string' && workdir.length > 0 && existsSync(workdir);
906
+ if (!worktreeLive) {
907
+ // Wave 6 (F-7R2-013): the engine reaps a completed run's worktree, and the run page went dark
908
+ // (`409 workdir no longer exists`). The run's WORK is not gone — it lives on the `wicked/<id>`
909
+ // branch of the registered repo, and the wave-6 engine records `run_branch` + `base_commit`
910
+ // on the session. Serve the diff from the BRANCH (`source: 'branch'`): the recorded branch
911
+ // (else `wicked/<id>`) against the recorded base (else its merge-base with the default
912
+ // branch), `?base=<ref>` overriding the base with a plain in-repo ref. Only when there is
913
+ // neither a worktree nor a branch does the 409 stand — and then it says so.
914
+ const repoRef = resolved.session.repo_ref;
915
+ const root = repoRef !== null ? await repoRootOf(repoRef) : undefined;
916
+ if (root !== undefined) {
917
+ const session = resolved.session;
918
+ const branch = typeof session.run_branch === 'string' && session.run_branch !== '' ? session.run_branch : `wicked/${id}`;
919
+ let base = typeof session.base_commit === 'string' && session.base_commit !== '' ? session.base_commit : null;
920
+ if (rawBase !== undefined && rawBase !== '' && rawBase !== MERGE_BASE_LITERAL) {
921
+ if (!isPlainRef(rawBase)) {
922
+ return reply.code(400).send({
923
+ error: `InvalidDiffBaseError: \`base\` must be the literal '${MERGE_BASE_LITERAL}' or a plain git ref`,
924
+ });
925
+ }
926
+ base = rawBase;
927
+ }
928
+ // Narrowing: a path under the (gone) worktree or under the repo root, made repo-relative.
929
+ let rel;
930
+ if (resolved.target !== undefined) {
931
+ if (isInsideRoot(root, resolved.target))
932
+ rel = relative(root, resolved.target);
933
+ else if (typeof workdir === 'string' && workdir.length > 0 && isInsideRoot(workdir, resolved.target)) {
934
+ rel = relative(workdir, resolved.target);
935
+ }
936
+ else {
937
+ return reply.code(400).send({
938
+ error: `\`path\` must be inside the run's worktree or its repository to diff: ${root}`,
939
+ });
940
+ }
941
+ }
942
+ try {
943
+ const fromBranch = await branchDiff(root, branch, base, rel);
944
+ if (fromBranch !== null)
945
+ return fromBranch;
946
+ }
947
+ catch (err) {
948
+ return diffError(err);
949
+ }
950
+ }
951
+ if (typeof workdir !== 'string' || workdir.length === 0) {
952
+ return reply.code(409).send({ error: `run ${id} has no workdir and no run branch — nothing to diff` });
953
+ }
954
+ return reply.code(409).send({
955
+ error: `run ${id}'s workdir no longer exists (${workdir}) and its run branch holds no commits — nothing to diff`,
956
+ });
957
+ }
958
+ // Narrowing is WORKTREE-scoped: a contained-but-outside-the-worktree path (extra write
959
+ // root / repo root) is a valid FILE read but has no meaning as a diff pathspec — rejected
960
+ // explicitly here rather than handing git a `../`-prefixed pathspec and surfacing its
961
+ // "outside repository" error as a 500 (Copilot, #305).
962
+ if (resolved.target !== undefined && !isInsideRoot(workdir, resolved.target)) {
963
+ return reply.code(400).send({
964
+ error: `\`path\` must be inside the run's worktree to diff: ${workdir}`,
965
+ });
966
+ }
967
+ const rel = resolved.target === undefined ? undefined : relative(workdir, resolved.target);
968
+ try {
969
+ return { ...(await worktreeDiff(workdir, rel, rawBase)), source: 'worktree' };
970
+ }
971
+ catch (err) {
972
+ return diffError(err);
908
973
  }
909
974
  });
910
975
  // Registered repos → target-repo picker.
@@ -981,7 +1046,10 @@ runtime = {}) {
981
1046
  const input = {
982
1047
  problem: b.problem,
983
1048
  sessionId: b.sessionId ?? randomUUID(),
984
- clisJson: b.clisJson ?? JSON.stringify(CoreAdapter.roster()),
1049
+ // The default roster carries crew's STANDING (wave 6, the crew half of F-7R2-006): the adapter
1050
+ // translates `council_eligible: false` into the engine's per-seat bench verdict at launch
1051
+ // (`core/engine-roster.ts`), so a signed-out seat is never convened, never a judge.
1052
+ clisJson: b.clisJson ?? JSON.stringify(rosterWithStanding()),
985
1053
  };
986
1054
  if (b.entityMode !== undefined)
987
1055
  input.entityMode = b.entityMode;
@@ -1152,8 +1220,17 @@ runtime = {}) {
1152
1220
  // resolve, not leak.
1153
1221
  // Fastify parses a REPEATED query param as string[] — normalize so `?include=archived`
1154
1222
  // and `?include=archived&include=archived` behave identically (Copilot).
1155
- const { include, limit } = req.query;
1223
+ const { include, limit, doc } = req.query;
1156
1224
  const includeArchived = (Array.isArray(include) ? include : [include]).includes('archived');
1225
+ // `?doc=<document id>` (wave 6, F-4R2-006): only the runs the interactive seams launched for
1226
+ // that document — the doc↔run binding as a direct read (the run DTO carries `document_id`).
1227
+ // A repeated `?doc` is an ambiguity, not a repetition — refused like a repeated `?limit`.
1228
+ if (Array.isArray(doc)) {
1229
+ return reply.code(400).send({ error: '`doc` may be given at most once' });
1230
+ }
1231
+ if (doc !== undefined && doc.trim() === '') {
1232
+ return reply.code(400).send({ error: '`doc` must name a document id' });
1233
+ }
1157
1234
  // `?limit=N` — the top N AFTER the actionable-first sort below, so a capped poll still sees
1158
1235
  // the runs that need a human before the terminal sediment. The default stays UNBOUNDED: the
1159
1236
  // param used to be read by nobody (silently ignored — the full payload regardless), so an
@@ -1176,9 +1253,10 @@ runtime = {}) {
1176
1253
  }
1177
1254
  cap = Number(limit);
1178
1255
  }
1179
- const visible = includeArchived
1256
+ const unarchived = includeArchived
1180
1257
  ? views
1181
1258
  : views.filter((v) => v.session.archived_at == null);
1259
+ const visible = doc === undefined ? unarchived : unarchived.filter((v) => docRuns.documentOf(v.session.id) === doc.trim());
1182
1260
  const ordered = sortActionableFirst(visible);
1183
1261
  return { runs: (cap !== undefined ? ordered.slice(0, cap) : ordered).map(decorateRun) };
1184
1262
  });
@@ -1577,19 +1655,23 @@ runtime = {}) {
1577
1655
  // refuses per seat with its reason, which is copied into `refused` too.
1578
1656
  const scoped = scope.kind !== 'none';
1579
1657
  const refused = [];
1658
+ // The standing roster, read ONCE: the default admission below and the engine-drop
1659
+ // attribution after `chatOpen` both consult it (F-A45-011).
1660
+ const standing = rosterWithStanding();
1661
+ const standingOf = (key) => standing.find((s) => String(s.key) === key);
1580
1662
  let clis;
1581
1663
  if (b.clis !== undefined) {
1582
1664
  clis = b.clis;
1583
1665
  }
1584
1666
  else {
1585
1667
  clis = [];
1586
- for (const seat of rosterWithStanding()) {
1668
+ for (const seat of standing) {
1587
1669
  const key = String(seat.key);
1588
1670
  const admission = chatSeatAdmission(seat, seat.auth ?? 'unknown', scoped);
1589
1671
  if (admission.ok)
1590
1672
  clis.push(key);
1591
1673
  else
1592
- refused.push({ cliKey: key, reason: admission.reason });
1674
+ refused.push({ cliKey: key, reason: admission.reason, source: admission.source });
1593
1675
  }
1594
1676
  }
1595
1677
  if (clis.length === 0) {
@@ -1613,7 +1695,44 @@ runtime = {}) {
1613
1695
  // A REQUESTED seat the engine refused joins the same list, with the engine's reason.
1614
1696
  for (const s of seats) {
1615
1697
  if (!s.ok)
1616
- refused.push({ cliKey: s.cliKey, reason: s.error ?? 'the engine refused the seat' });
1698
+ refused.push({ cliKey: s.cliKey, reason: s.error ?? 'the engine refused the seat', source: 'engine' });
1699
+ }
1700
+ // F-A45-011 (F-2R2-007 still open): a seat the engine DROPPED — absent from `seats`
1701
+ // altogether, neither ok nor refused — used to vanish without a trace (the fresh rig's pi:
1702
+ // default chips claude·pi·opencode, 201 `seats:[claude, opencode]`, `refused: []`). It was
1703
+ // taken out by the council-bench / dispatch-timeout path, not by admission, so admission's
1704
+ // list never named it. Every requested-or-defaulted seat that is not in `seats` is named
1705
+ // here with the most specific cause the daemon knows: its own "no credential" report
1706
+ // (`auth`), this daemon's council bench (`bench`), else the dispatch budget (`budget`).
1707
+ for (const key of clis) {
1708
+ if (seats.some((s) => s.cliKey === key) || refused.some((r) => r.cliKey === key))
1709
+ continue;
1710
+ const st = standingOf(key);
1711
+ const authFailure = seatHealth.authFailureFor(key);
1712
+ if (authFailure !== null || st?.auth === 'signed_out') {
1713
+ refused.push({
1714
+ cliKey: key,
1715
+ reason: authFailure !== null
1716
+ ? `not seated — the seat itself reported no credential (${authFailure.source}: ${authFailure.detail}); sign it in from the System page`
1717
+ : 'not seated — signed out; sign it in from the System page',
1718
+ source: 'auth',
1719
+ });
1720
+ }
1721
+ else if (st?.council_bench !== undefined) {
1722
+ refused.push({
1723
+ cliKey: key,
1724
+ reason: `not seated — ${st.council_ineligible_reason ?? 'benched by this daemon’s recent councils'}`,
1725
+ source: 'bench',
1726
+ });
1727
+ }
1728
+ else {
1729
+ refused.push({
1730
+ cliKey: key,
1731
+ reason: 'not seated — the engine did not warm it within its dispatch budget (the seat timed out or ' +
1732
+ 'was dropped at dispatch); check GET /roster and try again, or name seats with `clis`',
1733
+ source: 'budget',
1734
+ });
1735
+ }
1617
1736
  }
1618
1737
  // Nothing warmed (independent review, W1): the engine holds no pool row and has dropped the
1619
1738
  // scope itself (no `chatClosed` will come) — report the PER-SEAT reasons, never an
@@ -1679,7 +1798,7 @@ runtime = {}) {
1679
1798
  projectAttachError = message(err);
1680
1799
  }
1681
1800
  }
1682
- if (!chatScopes.set(chatId, scope, token)) {
1801
+ if (!chatScopes.set(chatId, scope, token, refused)) {
1683
1802
  // The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
1684
1803
  // `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
1685
1804
  // session, scratch root, and the filing just made) instead of returning a stale 201.
@@ -1702,6 +1821,7 @@ runtime = {}) {
1702
1821
  chat: chatId,
1703
1822
  cliKey: r.cliKey,
1704
1823
  reason: r.reason,
1824
+ source: r.source,
1705
1825
  ...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
1706
1826
  });
1707
1827
  }
@@ -1766,11 +1886,33 @@ runtime = {}) {
1766
1886
  .send({ error: message(err) });
1767
1887
  }
1768
1888
  });
1889
+ // ── Interactive documents, daemon-wide, WITHOUT spawning a bridge (wave 6, studio #263) ──────
1890
+ // The per-project listing (`GET /projects/:id/interactive/api/docs`) asks the project's bridge —
1891
+ // one `wicked-interactive serve` per project root, ≈60 s cold start — so a skin must never fan
1892
+ // it out on mount. This reads what the bridge reads (every project's docs root, each slug child
1893
+ // carrying a `versions.json`) plus what only the daemon knows (the seams that answered each doc
1894
+ // and the runs they launched, from the handoff ledgers). `?includeRetired=1` lists tombstones.
1895
+ app.get(`${V}/interactive/docs`, { config: { manifest: { responseType: 'InteractiveDocsListing', statusCodes: [200] } } }, async (req) => {
1896
+ const q = req.query;
1897
+ const flag = Array.isArray(q.includeRetired) ? q.includeRetired[0] : q.includeRetired;
1898
+ return listInteractiveDocs({
1899
+ adapter,
1900
+ settings: projectSettings,
1901
+ docRuns,
1902
+ includeRetired: flag === '1' || flag === 'true',
1903
+ });
1904
+ });
1769
1905
  app.get(`${V}/chats/:id`, { config: { manifest: { responseType: 'ChatDetailResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
1770
1906
  const { id } = req.params;
1771
1907
  try {
1772
- // The scope recorded at open (crew#502) — `null` for a chat this daemon did not open.
1773
- return { chatId: id, seats: await adapter.chatSeats(id), scope: chatScopes.get(id) ?? null };
1908
+ // The scope recorded at open (crew#502) — `null` for a chat this daemon did not open — and
1909
+ // the seats refused at open (F-A45-011), so the studio's admission copy survives a reload.
1910
+ return {
1911
+ chatId: id,
1912
+ seats: await adapter.chatSeats(id),
1913
+ scope: chatScopes.get(id) ?? null,
1914
+ refused: chatScopes.refusedOf(id) ?? null,
1915
+ };
1774
1916
  }
1775
1917
  catch (err) {
1776
1918
  return reply.code(400).send({ error: message(err) });
@@ -2051,11 +2193,39 @@ runtime = {}) {
2051
2193
  const run = views.find((v) => v.session.id === id);
2052
2194
  if (!run)
2053
2195
  return reply.code(404).send({ error: 'Run not found' });
2054
- if (run.session.status !== 'executing') {
2196
+ // Wave 6 (F-7R2-007 root fix): a run PARKED at a gate on a dead seat — the stall watchdog's
2197
+ // escalation gate, or a verdict escalation — used to answer 409 here, so the studio's control
2198
+ // needed two calls (approve, then race the engine's re-dispatch of the same dead seat with a
2199
+ // reassign). The ENGINE's `reassign_unit` accepts ONLY an Executing run (wicked-core actor.rs:
2200
+ // "run X is not Executing (status: AwaitingHuman)") — there is no reassign-in-place for a gated
2201
+ // run — so this route performs APPROVE-THEN-REASSIGN in one call: the approve resumes the run
2202
+ // (the engine re-dispatches the cursor unit to its current seat), and the reassign IMMEDIATELY
2203
+ // supersedes that turn (cancels its epoch, closes the seat's session, bumps the attempt) and
2204
+ // re-dispatches to the requested seat or the council's pick. The dead seat is dispatched for
2205
+ // the gap between the two engine calls — a bounded window the engine contract leaves open (a
2206
+ // reassign-as-approval engine op would close it; documented in the PR). Audited as BOTH a gate
2207
+ // decision (`via: 'reassign'`) and a reassign, so the "who approved" trail has no side door.
2208
+ // A steering-author propose gate is NOT approvable this way: its approve LANDS the proposal
2209
+ // through `POST /runs/:id/gate` (crew#388), and a reassign is not a review of the rules.
2210
+ const gated = run.session.status === 'awaiting_human';
2211
+ if (run.session.status !== 'executing' && !gated) {
2055
2212
  return reply.code(409).send({
2056
2213
  error: `run ${id} is ${run.session.status}, not executing — only an executing run has a cursor unit to reassign`,
2057
2214
  });
2058
2215
  }
2216
+ if (gated && typeof adapter.confirmGate !== 'function') {
2217
+ // A partial adapter (a directly-driven route set) cannot approve the gate on the caller's
2218
+ // behalf — say so by name, never a TypeError dressed as a refusal.
2219
+ return reply.code(409).send({
2220
+ error: `run ${id} is awaiting_human, not executing — the engine reassigns only an executing run, and ` +
2221
+ `this build cannot approve the gate on your behalf (no confirmGate); approve via POST /runs/${id}/gate, then reassign`,
2222
+ });
2223
+ }
2224
+ if (gated && typeof adapter.listWorkflows === 'function' && isSteeringAuthorRun(run, adapter.listWorkflows())) {
2225
+ return reply.code(409).send({
2226
+ error: `run ${id} is awaiting a steering-author propose gate — approve or reject it via POST /runs/${id}/gate (the approve lands the proposal); reassign is not a review of the rules`,
2227
+ });
2228
+ }
2059
2229
  const requestedCli = parsed.data.cli;
2060
2230
  if (requestedCli !== undefined) {
2061
2231
  const pool = Array.isArray(run.session.clis) ? run.session.clis : [];
@@ -2068,12 +2238,24 @@ runtime = {}) {
2068
2238
  const cursor = resolveCursorUnit(run);
2069
2239
  const ord = cursor?.ord ?? run.session.unit_ix;
2070
2240
  try {
2241
+ if (gated) {
2242
+ const status = await adapter.confirmGate(id, true);
2243
+ audit.record('gate.decided', actorOf(req), {
2244
+ runId: id,
2245
+ detail: { approve: true, via: 'reassign', status },
2246
+ });
2247
+ }
2071
2248
  await adapter.reassignUnit(id, ord, requestedCli ?? null);
2072
2249
  audit.record('run.reassigned', actorOf(req), {
2073
2250
  runId: id,
2074
- detail: { ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}) },
2251
+ detail: { ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}), ...(gated ? { approved: true } : {}) },
2252
+ });
2253
+ return reply.send({
2254
+ status: 'ok',
2255
+ ord,
2256
+ ...(requestedCli !== undefined ? { cli: requestedCli } : {}),
2257
+ ...(gated ? { approved: true } : {}),
2075
2258
  });
2076
- return reply.send({ status: 'ok', ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}) });
2077
2259
  }
2078
2260
  catch (err) {
2079
2261
  return reply.code(409).send({ error: message(err) });
@@ -3496,8 +3678,14 @@ runtime = {}) {
3496
3678
  registerCampaignRoutes(app, adapter, {
3497
3679
  audit,
3498
3680
  actorOf,
3499
- roster: () => CoreAdapter.roster(),
3681
+ // The roster WITH crew's standing (F-086 — parity with POST /runs and POST /testing/*): the
3682
+ // adapter turns `council_eligible` into the engine's per-seat bench at launch, per campaign
3683
+ // node (`core/engine-roster.ts` `engineCampaignDef`), so a signed-out seat is never convened
3684
+ // by a campaign node either.
3685
+ roster: () => rosterWithStanding(),
3500
3686
  groupIndex,
3687
+ // Wave 6 (F-7R2-014): the registered test sets ride beside the campaigns + groups.
3688
+ testSets,
3501
3689
  deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
3502
3690
  vacuity: vacuityProbes,
3503
3691
  // A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
@@ -3514,7 +3702,9 @@ runtime = {}) {
3514
3702
  registerGovernanceSteeringRoutes(app, adapter, {
3515
3703
  audit,
3516
3704
  actorOf,
3517
- roster: () => CoreAdapter.roster(),
3705
+ // Standing too (F-086 parity): the steering-author run launches through `launchRun`, whose
3706
+ // roster translation (`engineRosterJson`) already benches `council_eligible: false` seats.
3707
+ roster: () => rosterWithStanding(),
3518
3708
  // So the steering-author run's `run.launched` entry stamps `created_at` live too (Copilot #466).
3519
3709
  runTimingIndex,
3520
3710
  });
@@ -3526,8 +3716,12 @@ runtime = {}) {
3526
3716
  registerTestingRoutes(app, adapter, {
3527
3717
  audit,
3528
3718
  actorOf,
3529
- roster: () => CoreAdapter.roster(),
3719
+ // The roster WITH crew's standing (wave 6): the adapter turns `council_eligible` into the
3720
+ // engine's per-seat bench at launch, and the author response's `plan.seats` reads it.
3721
+ roster: () => rosterWithStanding(),
3530
3722
  projects: { bus: projects.bus, index: projects.index },
3723
+ // The author launch files each run under its repo's `qe-tests-<repo>` label (F-7R2-014).
3724
+ groupIndex,
3531
3725
  // So the recon fan's `run.launched` entries stamp `created_at` live too (Copilot #466), not
3532
3726
  // just after a restart re-hydrates the trail.
3533
3727
  runTimingIndex,