@try-works/dsh-recursive-mode 0.1.5 → 0.1.7

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 (89) hide show
  1. package/lib/bootstrap.d.ts +6 -3
  2. package/lib/client/board.d.ts +4 -1
  3. package/lib/client/contract.d.ts +16 -13
  4. package/lib/client/index.d.ts +1 -1
  5. package/lib/client/open-state.d.ts +20 -0
  6. package/lib/client/slots.d.ts +13 -1
  7. package/lib/client.js +160 -71
  8. package/lib/index.js +3391 -906
  9. package/lib/runtime.d.ts +12 -5
  10. package/lib/ts-lint.d.ts +205 -0
  11. package/package.json +1 -1
  12. package/src/bootstrap.ts +338 -331
  13. package/src/client/board.tsx +0 -0
  14. package/src/client/contract.ts +19 -22
  15. package/src/client/index.ts +1 -1
  16. package/src/client/open-state.ts +46 -0
  17. package/src/client/slots.ts +67 -33
  18. package/src/commands.ts +11 -0
  19. package/src/index.ts +238 -228
  20. package/src/runtime.ts +19 -23
  21. package/src/ts-lint.ts +2110 -0
  22. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  23. package/references/scripts/lint-recursive-run.ps1 +0 -25
  24. package/references/scripts/lint-recursive-run.py +0 -2870
  25. package/references/scripts/recursive-closeout.ps1 +0 -38
  26. package/references/scripts/recursive-closeout.py +0 -541
  27. package/references/scripts/recursive-init.ps1 +0 -515
  28. package/references/scripts/recursive-init.py +0 -356
  29. package/references/scripts/recursive-lock.ps1 +0 -26
  30. package/references/scripts/recursive-lock.py +0 -302
  31. package/references/scripts/recursive-review-bundle.ps1 +0 -58
  32. package/references/scripts/recursive-review-bundle.py +0 -503
  33. package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
  34. package/references/scripts/recursive-router-cli-configure.py +0 -9
  35. package/references/scripts/recursive-router-cli-init.ps1 +0 -2
  36. package/references/scripts/recursive-router-cli-init.py +0 -9
  37. package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
  38. package/references/scripts/recursive-router-cli-invoke.py +0 -9
  39. package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
  40. package/references/scripts/recursive-router-cli-probe.py +0 -9
  41. package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
  42. package/references/scripts/recursive-router-cli-resolve.py +0 -9
  43. package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
  44. package/references/scripts/recursive-router-cli-validate.py +0 -9
  45. package/references/scripts/recursive-router-configure.ps1 +0 -27
  46. package/references/scripts/recursive-router-configure.py +0 -74
  47. package/references/scripts/recursive-router-init.ps1 +0 -17
  48. package/references/scripts/recursive-router-init.py +0 -29
  49. package/references/scripts/recursive-router-invoke.ps1 +0 -47
  50. package/references/scripts/recursive-router-invoke.py +0 -103
  51. package/references/scripts/recursive-router-probe.ps1 +0 -25
  52. package/references/scripts/recursive-router-probe.py +0 -44
  53. package/references/scripts/recursive-router-resolve.ps1 +0 -26
  54. package/references/scripts/recursive-router-resolve.py +0 -46
  55. package/references/scripts/recursive-router-validate.ps1 +0 -17
  56. package/references/scripts/recursive-router-validate.py +0 -27
  57. package/references/scripts/recursive-status.ps1 +0 -23
  58. package/references/scripts/recursive-status.py +0 -2124
  59. package/references/scripts/recursive-subagent-action.ps1 +0 -98
  60. package/references/scripts/recursive-subagent-action.py +0 -197
  61. package/references/scripts/recursive-training-extract.ps1 +0 -23
  62. package/references/scripts/recursive-training-extract.py +0 -99
  63. package/references/scripts/recursive-training-grpo.ps1 +0 -52
  64. package/references/scripts/recursive-training-grpo.py +0 -1341
  65. package/references/scripts/recursive-training-loader.ps1 +0 -78
  66. package/references/scripts/recursive-training-loader.py +0 -558
  67. package/references/scripts/recursive-training-mcp.ps1 +0 -29
  68. package/references/scripts/recursive-training-mcp.py +0 -261
  69. package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
  70. package/references/scripts/recursive-training-phase8-trigger.py +0 -137
  71. package/references/scripts/recursive-training-sync.ps1 +0 -29
  72. package/references/scripts/recursive-training-sync.py +0 -183
  73. package/references/scripts/recursive_phase_rules.py +0 -367
  74. package/references/scripts/recursive_router_cli_lib.py +0 -2
  75. package/references/scripts/recursive_router_lib.py +0 -2282
  76. package/references/scripts/verify-locks.ps1 +0 -25
  77. package/references/scripts/verify-locks.py +0 -353
  78. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  79. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  80. package/scripts/install-recursive-mode.ps1 +0 -956
  81. package/scripts/install-recursive-mode.py +0 -750
  82. package/scripts/lint-recursive-run.py +0 -2870
  83. package/scripts/recursive-closeout.py +0 -541
  84. package/scripts/recursive-init.py +0 -356
  85. package/scripts/recursive-lock.py +0 -302
  86. package/scripts/recursive-status.py +0 -2124
  87. package/scripts/recursive_phase_rules.py +0 -367
  88. package/scripts/recursive_router_lib.py +0 -2282
  89. package/scripts/verify-locks.py +0 -353
@@ -35,8 +35,11 @@ export interface StageBInput {
35
35
  }
36
36
  /**
37
37
  * Stage B workflow init at agent/session-start. New: bootstrap if missing +
38
- * enumerate runs. Resume: never re-bootstrap; enumerate + note the active run
39
- * (the caller re-reads only that run's current phase doc). Always scoped to
40
- * the given control-plane root.
38
+ * enumerate runs. Resume: idempotent REPAIR (not a full re-bootstrap) the
39
+ * upsert-only bootstrapScaffold adds missing control-plane files/dirs and
40
+ * re-upserts marked blocks WITHOUT overwriting user content or touching run/
41
+ * artifacts, then enumerates + notes the active run. The repair is safe on
42
+ * fresh, partial, AND complete scaffolds (run 09 R3/R6). Always scoped to the
43
+ * given control-plane root.
41
44
  */
42
45
  export declare function stageBWorkflowInit(input: StageBInput): StageBResult;
@@ -1,11 +1,14 @@
1
1
  import type { RecursiveProjection, RecursiveRunCard } from '../types.ts';
2
2
  import type { LiveProjectionValue } from './contract.ts';
3
+ import type { BoardSelection } from './open-state.ts';
3
4
  /** Flatten the worktree-grouped projection into a stable run list. */
4
5
  export declare function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[];
5
6
  export interface BoardProps {
6
7
  /** The live host-route snapshot (null = not loaded / no recursive root). */
7
8
  snapshot: LiveProjectionValue | null;
9
+ /** Opens the drill-down inspector for a run (R3). */
10
+ onOpenInspector?: (selection: BoardSelection) => void;
8
11
  }
9
- export declare function Board({ snapshot }: BoardProps): import("react").DetailedReactHTMLElement<{
12
+ export declare function Board({ snapshot, onOpenInspector }: BoardProps): import("react").DetailedReactHTMLElement<{
10
13
  className: string;
11
14
  }, HTMLElement> | null;
@@ -29,21 +29,24 @@ export interface SessionSummaryRow {
29
29
  /** Agent preset this session was composed from; absent when the deployment has none. */
30
30
  agentPreset?: string;
31
31
  }
32
- /** The session-list snapshot (mirrors SessionListState). */
32
+ /**
33
+ * The session-list snapshot (mirrors the REAL SessionListState). useSessions is
34
+ * a SnapshotSelectorHook over this state; the identity selector `(s) => s`
35
+ * yields the full value, which the gate reads directly (ids/byId/current).
36
+ */
33
37
  export interface SessionListStateLike {
34
38
  ids: string[];
35
39
  byId: Record<string, SessionSummaryRow>;
36
40
  current: string | undefined;
37
41
  }
38
- /** A snapshot store the client runtime hands out (mirrors SnapshotStore<T>). */
39
- export interface SnapshotStoreLike<T> {
40
- getSnapshot(): T;
41
- subscribe(fn: () => void): () => void;
42
- }
43
- /** The sessions service surface (mirrors ISessions.list). */
44
- export interface ISessionsLike {
45
- list: SnapshotStoreLike<SessionListStateLike>;
46
- }
42
+ /**
43
+ * The REAL selector-hook shape (ui-slots store.ts):
44
+ * SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?) => S
45
+ * The selector is REQUIRED — useSyncExternalStoreWithSelector needs it. Calling
46
+ * the hook with no selector passes sel=undefined and breaks the board mount.
47
+ * This is the run 10 live bug.
48
+ */
49
+ export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S;
47
50
  /** The slots registry surface the client injects into. */
48
51
  export interface ClientSlots {
49
52
  inject(seat: string, factory: (ctx: unknown) => () => void): () => void;
@@ -77,8 +80,8 @@ export interface LiveProjectionValue {
77
80
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
78
81
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
79
82
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
80
- * board, not a spinner.
83
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
81
84
  */
82
- export declare function isRecursivePreset(sessions: ISessionsLike): boolean;
85
+ export declare function isRecursivePreset(state: SessionListStateLike): boolean;
83
86
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
84
- export declare function currentSessionCwd(sessions: ISessionsLike): string;
87
+ export declare function currentSessionCwd(state: SessionListStateLike): string;
@@ -21,7 +21,7 @@ export type { LiveProjectionSnapshot } from './use-live.ts';
21
21
  export { fetchLiveState, subscribeLiveEvents } from './host-api.ts';
22
22
  export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts';
23
23
  export { isRecursivePreset, currentSessionCwd } from './contract.ts';
24
- export type { SessionSummaryRow, SessionListStateLike, ISessionsLike, ClientContext } from './contract.ts';
24
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts';
25
25
  /** Required services (fiber inject waiting — the runtime must be up first). */
26
26
  export declare const inject: string[];
27
27
  /** Browser-half plugin entry (R4 + SP2 R1). */
@@ -0,0 +1,20 @@
1
+ export interface BoardSelection {
2
+ worktreeRoot: string;
3
+ runId: string;
4
+ }
5
+ export interface BoardState {
6
+ open: boolean;
7
+ selection: BoardSelection | null;
8
+ }
9
+ export interface BoardOpenState {
10
+ get(): BoardState;
11
+ openBoard(): void;
12
+ close(): void;
13
+ openInspector(selection: BoardSelection): void;
14
+ backToBoard(): void;
15
+ subscribe(fn: () => void): () => void;
16
+ }
17
+ export declare function createBoardState(initial?: BoardState): BoardOpenState;
18
+ /** App-wide singleton shared by the launcher, overlay, board, and inspector. */
19
+ export declare const boardState: BoardOpenState;
20
+ export declare function useBoardState(store?: BoardOpenState): BoardState;
@@ -1,5 +1,17 @@
1
- import type { ClientContext } from './contract.ts';
1
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts';
2
2
  import { isRecursivePreset, currentSessionCwd } from './contract.ts';
3
+ export type OverlayContent = 'hidden' | 'board' | 'inspector';
4
+ /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
5
+ export declare function overlayContent(state: {
6
+ open: boolean;
7
+ selection: unknown;
8
+ }, sessions: SessionListStateLike): OverlayContent;
9
+ /**
10
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
11
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
12
+ * Exported so the spec can assert the selector is actually passed.
13
+ */
14
+ export declare function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike;
3
15
  export declare function registerSlots(ctx: ClientContext): () => void;
4
16
  /** Export the gate helpers for tests. */
5
17
  export { isRecursivePreset, currentSessionCwd };
package/lib/client.js CHANGED
@@ -10,20 +10,15 @@ window.__ModuleLoader__.load({
10
10
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
11
11
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
12
12
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
13
- * board, not a spinner.
13
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
14
14
  */
15
- function isRecursivePreset(sessions) {
16
- const snapshot = sessions.list.getSnapshot();
17
- const current = snapshot.current;
18
- if (current === void 0) return false;
19
- return snapshot.byId[current]?.agentPreset === "recursive";
15
+ function isRecursivePreset(state) {
16
+ if (state.current === void 0) return false;
17
+ return state.byId[state.current]?.agentPreset === "recursive";
20
18
  }
21
19
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
22
- function currentSessionCwd(sessions) {
23
- const snapshot = sessions.list.getSnapshot();
24
- const current = snapshot.current;
25
- if (current === void 0) return "";
26
- return snapshot.byId[current]?.cwd ?? "";
20
+ function currentSessionCwd(state) {
21
+ return state.current === void 0 ? "" : state.byId[state.current]?.cwd ?? "";
27
22
  }
28
23
  //#endregion
29
24
  //#region src/client/derive.ts
@@ -224,13 +219,17 @@ window.__ModuleLoader__.load({
224
219
  //#endregion
225
220
  //#region src/client/board.tsx
226
221
  /**
227
- * Board overlay (SP2 R1): the kanban view over the live host-route projection.
228
- * Pure read-only renderer — no filesystem, no run-file scraping. Cards are
229
- * grouped by worktree root (the projection's outer key) and mapped to their
230
- * current-phase lane via derive.columnForRun. Renders NOTHING when the snapshot
231
- * is null (no recursive root / not loaded) or the current session is not on the
232
- * recursive preset (the gate lives in slots.ts — here an empty projection with
233
- * a null root is a no-op, not an "empty board").
222
+ * Board overlay (SP2 R1 + run 08 R3): the kanban view over the live host-route
223
+ * projection. Pure read-only renderer — no filesystem, no run-file scraping.
224
+ * Cards are grouped by worktree root (the projection's outer key) and mapped to
225
+ * their current-phase lane via derive.columnForRun. Renders NOTHING when the
226
+ * snapshot is null (no recursive root / not loaded) or the current session is
227
+ * not on the recursive preset (the gate lives in slots.ts — here an empty
228
+ * projection with a null root is a no-op, not an "empty board").
229
+ *
230
+ * Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
231
+ * (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
232
+ * callback only selects a run for display; no mutation.
234
233
  */
235
234
  /** Flatten the worktree-grouped projection into a stable run list. */
236
235
  function listRuns(projection) {
@@ -239,13 +238,17 @@ window.__ModuleLoader__.load({
239
238
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
240
239
  return runs;
241
240
  }
242
- function Board({ snapshot }) {
241
+ function Board({ snapshot, onOpenInspector }) {
243
242
  if (snapshot === null || snapshot.root === null) return null;
244
243
  const runs = listRuns(snapshot.projection);
245
244
  if (runs.length === 0) return (0, react.createElement)("div", {
246
245
  className: "rec-board",
247
246
  "data-empty": true
248
247
  }, (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
248
+ const open = (run) => () => onOpenInspector?.({
249
+ worktreeRoot: run.worktreeRoot,
250
+ runId: run.runId
251
+ });
249
252
  return (0, react.createElement)("div", { className: "rec-board" }, KANBAN_LANES.map((lane) => {
250
253
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
251
254
  return (0, react.createElement)("section", {
@@ -255,7 +258,8 @@ window.__ModuleLoader__.load({
255
258
  const facts = cardFacts(run);
256
259
  return (0, react.createElement)("article", {
257
260
  key: run.worktreeRoot + "\0" + run.runId,
258
- className: "rec-card"
261
+ className: "rec-card",
262
+ onClick: open(run)
259
263
  }, (0, react.createElement)("div", { className: "rec-card-head" }, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
260
264
  className: "rec-badge rec-badge-tamper",
261
265
  title: "tampered"
@@ -267,6 +271,26 @@ window.__ModuleLoader__.load({
267
271
  }));
268
272
  }
269
273
  //#endregion
274
+ //#region src/client/inspector.tsx
275
+ /**
276
+ * Drill-down inspector (SP2 R1): the details-seat swap rendering one run's
277
+ * phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock badges,
278
+ * and tamper/gate facts — all from the live host-route snapshot, never disk.
279
+ */
280
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }) {
281
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
282
+ if (card === void 0) return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("p", {}, "Run not found: " + runId));
283
+ const facts = cardFacts(card);
284
+ const rows = expandPhaseRows(card);
285
+ return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-inspector-head" }, (0, react.createElement)("button", {
286
+ onClick: onBackToToolDetails,
287
+ className: "rec-back"
288
+ }, "← back to tool details"), (0, react.createElement)("h2", {}, runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.state), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered")), (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
289
+ key: row.phase,
290
+ className: "rec-phase-row"
291
+ }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", { className: "rec-badge" }, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
292
+ }
293
+ //#endregion
270
294
  //#region src/client/strip.tsx
271
295
  /**
272
296
  * Status strip (SP2 R1): a thin glance at the active run's phase, gate state,
@@ -409,22 +433,101 @@ window.__ModuleLoader__.load({
409
433
  return state;
410
434
  }
411
435
  //#endregion
436
+ //#region src/client/open-state.ts
437
+ /**
438
+ * Shared board open-state (run 08 R1): module-level store behind the launcher,
439
+ * the shell.overlay entry, and the board<->inspector swap. Zero session events,
440
+ * zero host mutation — the client remains a read-only projection (R5/R9).
441
+ */
442
+ function createBoardState(initial = {
443
+ open: false,
444
+ selection: null
445
+ }) {
446
+ let state = initial;
447
+ const listeners = /* @__PURE__ */ new Set();
448
+ const emit = () => {
449
+ for (const fn of listeners) fn();
450
+ };
451
+ return {
452
+ get: () => state,
453
+ openBoard: () => {
454
+ state = {
455
+ ...state,
456
+ open: true
457
+ };
458
+ emit();
459
+ },
460
+ close: () => {
461
+ state = {
462
+ ...state,
463
+ open: false,
464
+ selection: null
465
+ };
466
+ emit();
467
+ },
468
+ openInspector: (selection) => {
469
+ state = {
470
+ ...state,
471
+ open: true,
472
+ selection
473
+ };
474
+ emit();
475
+ },
476
+ backToBoard: () => {
477
+ state = {
478
+ ...state,
479
+ selection: null
480
+ };
481
+ emit();
482
+ },
483
+ subscribe: (fn) => {
484
+ listeners.add(fn);
485
+ return () => {
486
+ listeners.delete(fn);
487
+ };
488
+ }
489
+ };
490
+ }
491
+ /** App-wide singleton shared by the launcher, overlay, board, and inspector. */
492
+ const boardState = createBoardState();
493
+ function useBoardState(store = boardState) {
494
+ return (0, react.useSyncExternalStore)(store.subscribe, store.get, store.get);
495
+ }
496
+ //#endregion
412
497
  //#region src/client/slots.ts
413
498
  /**
414
- * Slot registrations (SP2 R1): the sidebar launcher, the shell.overlay board,
415
- * the conversation.input.dock status strip, and the settings.section page.
499
+ * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
500
+ * shell.overlay board, the conversation.input.dock status strip, and the
501
+ * settings.section page.
416
502
  *
417
- * The board (root scope) and strip (session scope) render ONLY for the
418
- * recursive preset the gate reads the CURRENT session's agentPreset from the
419
- * sessions list snapshot (acceptance #3: a non-recursive session shows NOTHING).
503
+ * Run 08: the launcher now OPENS the shared board store (onClick), and the
504
+ * shell.overlay entry renders the board gated on that store + the recursive
505
+ * preset, swapping to the drill-down inspector when a run is selected.
420
506
  *
421
- * The board reads cwd from the ROOT-scope standard prop useSessions (the
422
- * scoped-slots root branch provides ONLY useSessions/useWorkspaces useProjection
423
- * is session-scope-only, which is why the board never reads it). The strip reads
424
- * its sessionId from the session-scope standard prop.
507
+ * Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
508
+ * thunk) it is called WITH a selector, e.g. useSessions((s) => s) for the
509
+ * full SessionListStateLike. The old code called useSessions() with NO selector
510
+ * and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
511
+ * never saw the recursive preset and the board never mounted (launcher click
512
+ * did nothing). The board/strip now consume the SessionListStateLike directly.
425
513
  *
426
514
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
427
515
  */
516
+ /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
517
+ function overlayContent(state, sessions) {
518
+ if (!state.open) return "hidden";
519
+ if (!isRecursivePreset(sessions)) return "hidden";
520
+ if (state.selection !== null) return "inspector";
521
+ return "board";
522
+ }
523
+ /**
524
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
525
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
526
+ * Exported so the spec can assert the selector is actually passed.
527
+ */
528
+ function useRecursiveSessions(useSessions) {
529
+ return useSessions((s) => s);
530
+ }
428
531
  function registerSlots(ctx) {
429
532
  const disposers = [];
430
533
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
@@ -434,7 +537,8 @@ window.__ModuleLoader__.load({
434
537
  label: "Recursive runs"
435
538
  }, () => (0, react.createElement)("button", {
436
539
  className: "rec-launcher",
437
- title: "Recursive runs"
540
+ title: "Recursive runs",
541
+ onClick: () => boardState.openBoard()
438
542
  }, "⧉"))));
439
543
  disposers.push(ctx.slots.inject("shell.overlay", () => ctx.slots.register({
440
544
  name: "shell.overlay",
@@ -443,7 +547,7 @@ window.__ModuleLoader__.load({
443
547
  }, (props) => {
444
548
  const useSessions = props?.useSessions;
445
549
  if (useSessions === void 0) return null;
446
- return (0, react.createElement)(RecursiveBoardGate, { useSessions });
550
+ return (0, react.createElement)(RecursiveBoardOverlay, { useSessions });
447
551
  })));
448
552
  disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
449
553
  name: "conversation.input.dock",
@@ -468,38 +572,43 @@ window.__ModuleLoader__.load({
468
572
  };
469
573
  }
470
574
  /**
471
- * Board gate: renders the board ONLY when the current session is on the
472
- * recursive preset. The gate itself reads the live sessions snapshot via the
473
- * standard useSessions hook and re-renders on change.
575
+ * Board overlay: subscribes to the shared board store, gates on open + recursive
576
+ * preset, and swaps board <-> inspector (run 08 R1/R3).
577
+ *
578
+ * Run 10: consumes the full SessionListStateLike through the identity selector
579
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
474
580
  */
475
- function RecursiveBoardGate({ useSessions }) {
476
- const list = useSessions();
477
- const sessions = { list: {
478
- getSnapshot: () => list,
479
- subscribe: () => () => {}
480
- } };
481
- if (!isRecursivePreset(sessions)) return null;
482
- const cwd = currentSessionCwd(sessions);
581
+ function RecursiveBoardOverlay({ useSessions }) {
582
+ const board = useBoardState();
583
+ const list = useRecursiveSessions(useSessions);
584
+ const content = overlayContent(board, list);
585
+ if (content === "hidden") return null;
586
+ const cwd = currentSessionCwd(list);
483
587
  const snapshot = useLiveProjection({
484
588
  sessionId: list.current,
485
589
  cwd
486
590
  });
487
- return (0, react.createElement)(Board, { snapshot });
591
+ if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
592
+ runId: board.selection.runId,
593
+ worktreeRoot: board.selection.worktreeRoot,
594
+ snapshot,
595
+ onBackToToolDetails: () => boardState.backToBoard()
596
+ });
597
+ return (0, react.createElement)(Board, {
598
+ snapshot,
599
+ onOpenInspector: (sel) => boardState.openInspector(sel)
600
+ });
488
601
  }
489
602
  /**
490
603
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
491
- * nothing for a non-recursive session.
604
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
492
605
  */
493
606
  function RecursiveStripGate({ useSessions, sessionId }) {
494
- const list = useSessions();
495
- const sessions = { list: {
496
- getSnapshot: () => list,
497
- subscribe: () => () => {}
498
- } };
499
- if (!isRecursivePreset(sessions)) return null;
607
+ const list = useRecursiveSessions(useSessions);
608
+ if (!isRecursivePreset(list)) return null;
500
609
  const snapshot = useLiveProjection({
501
610
  sessionId,
502
- cwd: currentSessionCwd(sessions)
611
+ cwd: currentSessionCwd(list)
503
612
  });
504
613
  return (0, react.createElement)(StatusStrip, { snapshot });
505
614
  }
@@ -516,26 +625,6 @@ window.__ModuleLoader__.load({
516
625
  globalThis.__dshRecursiveModeClientApplied = void 0;
517
626
  }
518
627
  //#endregion
519
- //#region src/client/inspector.tsx
520
- /**
521
- * Drill-down inspector (SP2 R1): the details-seat swap rendering one run's
522
- * phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock badges,
523
- * and tamper/gate facts — all from the live host-route snapshot, never disk.
524
- */
525
- function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }) {
526
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
527
- if (card === void 0) return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("p", {}, "Run not found: " + runId));
528
- const facts = cardFacts(card);
529
- const rows = expandPhaseRows(card);
530
- return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-inspector-head" }, (0, react.createElement)("button", {
531
- onClick: onBackToToolDetails,
532
- className: "rec-back"
533
- }, "← back to tool details"), (0, react.createElement)("h2", {}, runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.state), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered")), (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
534
- key: row.phase,
535
- className: "rec-phase-row"
536
- }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", { className: "rec-badge" }, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
537
- }
538
- //#endregion
539
628
  //#region src/client/index.ts
540
629
  /** Required services (fiber inject waiting — the runtime must be up first). */
541
630
  const inject = [