@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
Binary file
@@ -33,23 +33,25 @@ export interface SessionSummaryRow {
33
33
  agentPreset?: string
34
34
  }
35
35
 
36
- /** The session-list snapshot (mirrors SessionListState). */
36
+ /**
37
+ * The session-list snapshot (mirrors the REAL SessionListState). useSessions is
38
+ * a SnapshotSelectorHook over this state; the identity selector `(s) => s`
39
+ * yields the full value, which the gate reads directly (ids/byId/current).
40
+ */
37
41
  export interface SessionListStateLike {
38
42
  ids: string[]
39
43
  byId: Record<string, SessionSummaryRow>
40
44
  current: string | undefined
41
45
  }
42
46
 
43
- /** A snapshot store the client runtime hands out (mirrors SnapshotStore<T>). */
44
- export interface SnapshotStoreLike<T> {
45
- getSnapshot(): T
46
- subscribe(fn: () => void): () => void
47
- }
48
-
49
- /** The sessions service surface (mirrors ISessions.list). */
50
- export interface ISessionsLike {
51
- list: SnapshotStoreLike<SessionListStateLike>
52
- }
47
+ /**
48
+ * The REAL selector-hook shape (ui-slots store.ts):
49
+ * SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?) => S
50
+ * The selector is REQUIRED — useSyncExternalStoreWithSelector needs it. Calling
51
+ * the hook with no selector passes sel=undefined and breaks the board mount.
52
+ * This is the run 10 live bug.
53
+ */
54
+ export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S
53
55
 
54
56
  /** The slots registry surface the client injects into. */
55
57
  export interface ClientSlots {
@@ -88,19 +90,14 @@ export interface LiveProjectionValue {
88
90
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
89
91
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
90
92
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
91
- * board, not a spinner.
93
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
92
94
  */
93
- export function isRecursivePreset(sessions: ISessionsLike): boolean {
94
- const snapshot = sessions.list.getSnapshot()
95
- const current = snapshot.current
96
- if (current === undefined) return false
97
- return snapshot.byId[current]?.agentPreset === 'recursive'
95
+ export function isRecursivePreset(state: SessionListStateLike): boolean {
96
+ if (state.current === undefined) return false
97
+ return state.byId[state.current]?.agentPreset === 'recursive'
98
98
  }
99
99
 
100
100
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
101
- export function currentSessionCwd(sessions: ISessionsLike): string {
102
- const snapshot = sessions.list.getSnapshot()
103
- const current = snapshot.current
104
- if (current === undefined) return ''
105
- return snapshot.byId[current]?.cwd ?? ''
101
+ export function currentSessionCwd(state: SessionListStateLike): string {
102
+ return state.current === undefined ? '' : (state.byId[state.current]?.cwd ?? '')
106
103
  }
@@ -24,7 +24,7 @@ export type { LiveProjectionSnapshot } from './use-live.ts'
24
24
  export { fetchLiveState, subscribeLiveEvents } from './host-api.ts'
25
25
  export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts'
26
26
  export { isRecursivePreset, currentSessionCwd } from './contract.ts'
27
- export type { SessionSummaryRow, SessionListStateLike, ISessionsLike, ClientContext } from './contract.ts'
27
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts'
28
28
 
29
29
  /** Required services (fiber inject waiting — the runtime must be up first). */
30
30
  export const inject = ['slots', 'sessions', 'workspaces', 'connection']
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared board open-state (run 08 R1): module-level store behind the launcher,
3
+ * the shell.overlay entry, and the board<->inspector swap. Zero session events,
4
+ * zero host mutation — the client remains a read-only projection (R5/R9).
5
+ */
6
+ import { useSyncExternalStore } from 'react'
7
+
8
+ export interface BoardSelection {
9
+ worktreeRoot: string;
10
+ runId: string;
11
+ }
12
+
13
+ export interface BoardState {
14
+ open: boolean;
15
+ selection: BoardSelection | null;
16
+ }
17
+
18
+ export interface BoardOpenState {
19
+ get(): BoardState;
20
+ openBoard(): void;
21
+ close(): void;
22
+ openInspector(selection: BoardSelection): void;
23
+ backToBoard(): void;
24
+ subscribe(fn: () => void): () => void;
25
+ }
26
+
27
+ export function createBoardState(initial: BoardState = { open: false, selection: null }): BoardOpenState {
28
+ let state: BoardState = initial;
29
+ const listeners = new Set<() => void>();
30
+ const emit = (): void => { for (const fn of listeners) fn() };
31
+ return {
32
+ get: () => state,
33
+ openBoard: () => { state = { ...state, open: true }; emit(); },
34
+ close: () => { state = { ...state, open: false, selection: null }; emit(); },
35
+ openInspector: (selection) => { state = { ...state, open: true, selection }; emit(); },
36
+ backToBoard: () => { state = { ...state, selection: null }; emit(); },
37
+ subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn) }; },
38
+ };
39
+ }
40
+
41
+ /** App-wide singleton shared by the launcher, overlay, board, and inspector. */
42
+ export const boardState = createBoardState();
43
+
44
+ export function useBoardState(store: BoardOpenState = boardState): BoardState {
45
+ return useSyncExternalStore(store.subscribe, store.get, store.get);
46
+ }
@@ -1,51 +1,75 @@
1
1
  /**
2
- * Slot registrations (SP2 R1): the sidebar launcher, the shell.overlay board,
3
- * the conversation.input.dock status strip, and the settings.section page.
2
+ * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
3
+ * shell.overlay board, the conversation.input.dock status strip, and the
4
+ * settings.section page.
4
5
  *
5
- * The board (root scope) and strip (session scope) render ONLY for the
6
- * recursive preset the gate reads the CURRENT session's agentPreset from the
7
- * sessions list snapshot (acceptance #3: a non-recursive session shows NOTHING).
6
+ * Run 08: the launcher now OPENS the shared board store (onClick), and the
7
+ * shell.overlay entry renders the board gated on that store + the recursive
8
+ * preset, swapping to the drill-down inspector when a run is selected.
8
9
  *
9
- * The board reads cwd from the ROOT-scope standard prop useSessions (the
10
- * scoped-slots root branch provides ONLY useSessions/useWorkspaces useProjection
11
- * is session-scope-only, which is why the board never reads it). The strip reads
12
- * its sessionId from the session-scope standard prop.
10
+ * Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
11
+ * thunk) it is called WITH a selector, e.g. useSessions((s) => s) for the
12
+ * full SessionListStateLike. The old code called useSessions() with NO selector
13
+ * and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
14
+ * never saw the recursive preset and the board never mounted (launcher click
15
+ * did nothing). The board/strip now consume the SessionListStateLike directly.
13
16
  *
14
17
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
15
18
  */
16
19
  import { createElement, type ReactNode } from 'react'
17
- import type { ClientContext, ISessionsLike, SnapshotStoreLike, SessionListStateLike } from './contract.ts'
20
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts'
18
21
  import { isRecursivePreset, currentSessionCwd } from './contract.ts'
19
22
  import { Board } from './board.tsx'
23
+ import { Inspector } from './inspector.tsx'
20
24
  import { StatusStrip } from './strip.tsx'
21
25
  import { RecursiveSettings } from './settings.tsx'
22
26
  import { useLiveProjection } from './use-live.ts'
27
+ import { boardState, useBoardState } from './open-state.ts'
23
28
 
24
- /** Structural standard-prop face for the root scope (useSessions only). */
29
+ /** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
25
30
  interface RootSlotProps {
26
- useSessions?: () => SessionListStateLike
27
- useWorkspaces?: () => unknown
31
+ useSessions?: SnapshotSelectorHook<SessionListStateLike>
32
+ useWorkspaces?: SnapshotSelectorHook<unknown>
28
33
  }
29
34
 
30
35
  /** Structural standard-prop face for the session scope (sessionId + useSessions). */
31
36
  interface SessionSlotProps {
32
37
  sessionId?: string
33
- useSessions?: () => SessionListStateLike
38
+ useSessions?: SnapshotSelectorHook<SessionListStateLike>
34
39
  useProjection?: unknown
35
40
  }
36
41
 
42
+ export type OverlayContent = 'hidden' | 'board' | 'inspector'
43
+
44
+ /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
45
+ export function overlayContent(state: { open: boolean; selection: unknown }, sessions: SessionListStateLike): OverlayContent {
46
+ if (!state.open) return 'hidden'
47
+ if (!isRecursivePreset(sessions)) return 'hidden'
48
+ if (state.selection !== null) return 'inspector'
49
+ return 'board'
50
+ }
51
+
52
+ /**
53
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
54
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
55
+ * Exported so the spec can assert the selector is actually passed.
56
+ */
57
+ export function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike {
58
+ return useSessions((s) => s)
59
+ }
60
+
37
61
  export function registerSlots(ctx: ClientContext): () => void {
38
62
  const disposers: (() => void)[] = []
39
63
 
40
- // Board launcher in the sidebar footer action list (always visible; opens the board).
64
+ // Board launcher in the sidebar footer action list OPENS the shared board store (run 08 R1).
41
65
  disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
42
66
  name: 'sidebar.footer.action',
43
67
  id: 'recursive',
44
68
  order: 50,
45
69
  label: 'Recursive runs',
46
- }, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs' }, '⧉'))))
70
+ }, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉'))))
47
71
 
48
- // Board panel overlay (root scope, cross-session read) — gated on the recursive preset.
72
+ // Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
49
73
  disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
50
74
  name: 'shell.overlay',
51
75
  id: 'recursive-board',
@@ -53,7 +77,7 @@ export function registerSlots(ctx: ClientContext): () => void {
53
77
  }, (props: RootSlotProps) => {
54
78
  const useSessions = props?.useSessions
55
79
  if (useSessions === undefined) return null
56
- return createElement(RecursiveBoardGate, { useSessions })
80
+ return createElement(RecursiveBoardOverlay, { useSessions })
57
81
  })))
58
82
 
59
83
  // Status strip in the composer input dock (session scope) — gated the same way.
@@ -79,29 +103,39 @@ export function registerSlots(ctx: ClientContext): () => void {
79
103
  }
80
104
 
81
105
  /**
82
- * Board gate: renders the board ONLY when the current session is on the
83
- * recursive preset. The gate itself reads the live sessions snapshot via the
84
- * standard useSessions hook and re-renders on change.
106
+ * Board overlay: subscribes to the shared board store, gates on open + recursive
107
+ * preset, and swaps board <-> inspector (run 08 R1/R3).
108
+ *
109
+ * Run 10: consumes the full SessionListStateLike through the identity selector
110
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
85
111
  */
86
- function RecursiveBoardGate({ useSessions }: { useSessions: () => SessionListStateLike }): ReactNode {
87
- const list = useSessions()
88
- const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
89
- if (!isRecursivePreset(sessions)) return null
90
- const cwd = currentSessionCwd(sessions)
112
+ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
113
+ const board = useBoardState()
114
+ const list = useRecursiveSessions(useSessions)
115
+ const content = overlayContent(board, list)
116
+ if (content === 'hidden') return null
117
+ const cwd = currentSessionCwd(list)
91
118
  const scope = { sessionId: list.current, cwd }
92
119
  const snapshot = useLiveProjection(scope)
93
- return createElement(Board, { snapshot })
120
+ if (content === 'inspector' && board.selection !== null) {
121
+ return createElement(Inspector, {
122
+ runId: board.selection.runId,
123
+ worktreeRoot: board.selection.worktreeRoot,
124
+ snapshot,
125
+ onBackToToolDetails: () => boardState.backToBoard(),
126
+ })
127
+ }
128
+ return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
94
129
  }
95
130
 
96
131
  /**
97
132
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
98
- * nothing for a non-recursive session.
133
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
99
134
  */
100
- function RecursiveStripGate({ useSessions, sessionId }: { useSessions: () => SessionListStateLike; sessionId?: string }): ReactNode {
101
- const list = useSessions()
102
- const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
103
- if (!isRecursivePreset(sessions)) return null
104
- const cwd = currentSessionCwd(sessions)
135
+ function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
136
+ const list = useRecursiveSessions(useSessions)
137
+ if (!isRecursivePreset(list)) return null
138
+ const cwd = currentSessionCwd(list)
105
139
  const scope = { sessionId, cwd }
106
140
  const snapshot = useLiveProjection(scope)
107
141
  return createElement(StatusStrip, { snapshot })
package/src/commands.ts CHANGED
@@ -13,6 +13,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs'
13
13
  import { join } from 'node:path'
14
14
  import type { RecursiveRuntime } from './runtime.ts'
15
15
  import { resolveControlPlaneRoot } from './workspace.ts'
16
+ import { bootstrapScaffold } from './bootstrap.ts'
16
17
 
17
18
  export type RecursiveVerb = 'status' | 'spec' | 'worktree' | 'init' | 'lock' | 'qa' | 'closeout' | 'addendum' | 'review' | 'scratch' | 'bootstrap' | 'list' | 'help'
18
19
 
@@ -65,6 +66,16 @@ export function executeRecursiveCommand(root: string, rawInput: string): Recursi
65
66
  case 'help': {
66
67
  return { kind: 'success', text: HELP_TEXT + ' — see /.recursive/RECURSIVE.md' }
67
68
  }
69
+ case 'bootstrap': {
70
+ // R6 (run 09): manual repair path — idempotent upsert scaffold repair.
71
+ // Adds missing control-plane files/dirs + re-upserts marked blocks;
72
+ // never overwrites user content or touches run/ artifacts.
73
+ const result = bootstrapScaffold(root)
74
+ const summary = result.created.length === 0
75
+ ? 'scaffold already complete (no changes)'
76
+ : 'created ' + result.created.length + ' item(s), ' + result.existing.length + ' existing'
77
+ return { kind: 'success', text: 'recursive-mode scaffold repair: ' + summary + ' — root ' + root }
78
+ }
68
79
  case 'closeout': {
69
80
  const phaseMatch = arg.match(/--phase\s+(\d{2})/)
70
81
  const phase = phaseMatch?.[1] ?? ''