@try-works/dsh-recursive-mode 0.1.4 → 0.1.6

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 (90) hide show
  1. package/lib/bootstrap.d.ts +6 -3
  2. package/lib/client/board.d.ts +4 -1
  3. package/lib/client/open-state.d.ts +20 -0
  4. package/lib/client/slots.d.ts +7 -1
  5. package/lib/client.js +135 -47
  6. package/lib/fs-intent.d.ts +34 -0
  7. package/lib/index.d.ts +1 -0
  8. package/lib/index.js +3429 -911
  9. package/lib/policy.d.ts +1 -1
  10. package/lib/runtime.d.ts +12 -5
  11. package/lib/ts-lint.d.ts +205 -0
  12. package/package.json +1 -1
  13. package/src/bootstrap.ts +338 -331
  14. package/src/client/board.tsx +0 -0
  15. package/src/client/open-state.ts +46 -0
  16. package/src/client/slots.ts +41 -20
  17. package/src/commands.ts +11 -0
  18. package/src/fs-intent.ts +84 -0
  19. package/src/index.ts +238 -226
  20. package/src/policy.ts +1 -1
  21. package/src/runtime.ts +19 -23
  22. package/src/ts-lint.ts +2110 -0
  23. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  24. package/references/scripts/lint-recursive-run.ps1 +0 -25
  25. package/references/scripts/lint-recursive-run.py +0 -2870
  26. package/references/scripts/recursive-closeout.ps1 +0 -38
  27. package/references/scripts/recursive-closeout.py +0 -541
  28. package/references/scripts/recursive-init.ps1 +0 -515
  29. package/references/scripts/recursive-init.py +0 -356
  30. package/references/scripts/recursive-lock.ps1 +0 -26
  31. package/references/scripts/recursive-lock.py +0 -302
  32. package/references/scripts/recursive-review-bundle.ps1 +0 -58
  33. package/references/scripts/recursive-review-bundle.py +0 -503
  34. package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
  35. package/references/scripts/recursive-router-cli-configure.py +0 -9
  36. package/references/scripts/recursive-router-cli-init.ps1 +0 -2
  37. package/references/scripts/recursive-router-cli-init.py +0 -9
  38. package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
  39. package/references/scripts/recursive-router-cli-invoke.py +0 -9
  40. package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
  41. package/references/scripts/recursive-router-cli-probe.py +0 -9
  42. package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
  43. package/references/scripts/recursive-router-cli-resolve.py +0 -9
  44. package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
  45. package/references/scripts/recursive-router-cli-validate.py +0 -9
  46. package/references/scripts/recursive-router-configure.ps1 +0 -27
  47. package/references/scripts/recursive-router-configure.py +0 -74
  48. package/references/scripts/recursive-router-init.ps1 +0 -17
  49. package/references/scripts/recursive-router-init.py +0 -29
  50. package/references/scripts/recursive-router-invoke.ps1 +0 -47
  51. package/references/scripts/recursive-router-invoke.py +0 -103
  52. package/references/scripts/recursive-router-probe.ps1 +0 -25
  53. package/references/scripts/recursive-router-probe.py +0 -44
  54. package/references/scripts/recursive-router-resolve.ps1 +0 -26
  55. package/references/scripts/recursive-router-resolve.py +0 -46
  56. package/references/scripts/recursive-router-validate.ps1 +0 -17
  57. package/references/scripts/recursive-router-validate.py +0 -27
  58. package/references/scripts/recursive-status.ps1 +0 -23
  59. package/references/scripts/recursive-status.py +0 -2124
  60. package/references/scripts/recursive-subagent-action.ps1 +0 -98
  61. package/references/scripts/recursive-subagent-action.py +0 -197
  62. package/references/scripts/recursive-training-extract.ps1 +0 -23
  63. package/references/scripts/recursive-training-extract.py +0 -99
  64. package/references/scripts/recursive-training-grpo.ps1 +0 -52
  65. package/references/scripts/recursive-training-grpo.py +0 -1341
  66. package/references/scripts/recursive-training-loader.ps1 +0 -78
  67. package/references/scripts/recursive-training-loader.py +0 -558
  68. package/references/scripts/recursive-training-mcp.ps1 +0 -29
  69. package/references/scripts/recursive-training-mcp.py +0 -261
  70. package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
  71. package/references/scripts/recursive-training-phase8-trigger.py +0 -137
  72. package/references/scripts/recursive-training-sync.ps1 +0 -29
  73. package/references/scripts/recursive-training-sync.py +0 -183
  74. package/references/scripts/recursive_phase_rules.py +0 -367
  75. package/references/scripts/recursive_router_cli_lib.py +0 -2
  76. package/references/scripts/recursive_router_lib.py +0 -2282
  77. package/references/scripts/verify-locks.ps1 +0 -25
  78. package/references/scripts/verify-locks.py +0 -353
  79. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  80. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  81. package/scripts/install-recursive-mode.ps1 +0 -956
  82. package/scripts/install-recursive-mode.py +0 -750
  83. package/scripts/lint-recursive-run.py +0 -2870
  84. package/scripts/recursive-closeout.py +0 -541
  85. package/scripts/recursive-init.py +0 -356
  86. package/scripts/recursive-lock.py +0 -302
  87. package/scripts/recursive-status.py +0 -2124
  88. package/scripts/recursive_phase_rules.py +0 -367
  89. package/scripts/recursive_router_lib.py +0 -2282
  90. package/scripts/verify-locks.py +0 -353
Binary file
@@ -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,15 +1,15 @@
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).
8
- *
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.
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. The
9
+ * board reads cwd from the ROOT-scope standard prop useSessions (the scoped-
10
+ * slots root branch provides ONLY useSessions/useWorkspaces useProjection is
11
+ * session-scope-only). The strip reads its sessionId from the session-scope
12
+ * standard prop.
13
13
  *
14
14
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
15
15
  */
@@ -17,9 +17,11 @@ import { createElement, type ReactNode } from 'react'
17
17
  import type { ClientContext, ISessionsLike, SnapshotStoreLike, SessionListStateLike } from './contract.ts'
18
18
  import { isRecursivePreset, currentSessionCwd } from './contract.ts'
19
19
  import { Board } from './board.tsx'
20
+ import { Inspector } from './inspector.tsx'
20
21
  import { StatusStrip } from './strip.tsx'
21
22
  import { RecursiveSettings } from './settings.tsx'
22
23
  import { useLiveProjection } from './use-live.ts'
24
+ import { boardState, useBoardState } from './open-state.ts'
23
25
 
24
26
  /** Structural standard-prop face for the root scope (useSessions only). */
25
27
  interface RootSlotProps {
@@ -34,18 +36,28 @@ interface SessionSlotProps {
34
36
  useProjection?: unknown
35
37
  }
36
38
 
39
+ export type OverlayContent = 'hidden' | 'board' | 'inspector'
40
+
41
+ /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
42
+ export function overlayContent(state: { open: boolean; selection: unknown }, sessions: ISessionsLike): OverlayContent {
43
+ if (!state.open) return 'hidden'
44
+ if (!isRecursivePreset(sessions)) return 'hidden'
45
+ if (state.selection !== null) return 'inspector'
46
+ return 'board'
47
+ }
48
+
37
49
  export function registerSlots(ctx: ClientContext): () => void {
38
50
  const disposers: (() => void)[] = []
39
51
 
40
- // Board launcher in the sidebar footer action list (always visible; opens the board).
52
+ // Board launcher in the sidebar footer action list OPENS the shared board store (run 08 R1).
41
53
  disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
42
54
  name: 'sidebar.footer.action',
43
55
  id: 'recursive',
44
56
  order: 50,
45
57
  label: 'Recursive runs',
46
- }, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs' }, '⧉'))))
58
+ }, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉'))))
47
59
 
48
- // Board panel overlay (root scope, cross-session read) — gated on the recursive preset.
60
+ // Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
49
61
  disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
50
62
  name: 'shell.overlay',
51
63
  id: 'recursive-board',
@@ -53,7 +65,7 @@ export function registerSlots(ctx: ClientContext): () => void {
53
65
  }, (props: RootSlotProps) => {
54
66
  const useSessions = props?.useSessions
55
67
  if (useSessions === undefined) return null
56
- return createElement(RecursiveBoardGate, { useSessions })
68
+ return createElement(RecursiveBoardOverlay, { useSessions })
57
69
  })))
58
70
 
59
71
  // Status strip in the composer input dock (session scope) — gated the same way.
@@ -79,18 +91,27 @@ export function registerSlots(ctx: ClientContext): () => void {
79
91
  }
80
92
 
81
93
  /**
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.
94
+ * Board overlay: subscribes to the shared board store, gates on open + recursive
95
+ * preset, and swaps board <-> inspector (run 08 R1/R3).
85
96
  */
86
- function RecursiveBoardGate({ useSessions }: { useSessions: () => SessionListStateLike }): ReactNode {
97
+ function RecursiveBoardOverlay({ useSessions }: { useSessions: () => SessionListStateLike }): ReactNode {
98
+ const board = useBoardState()
87
99
  const list = useSessions()
88
100
  const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
89
- if (!isRecursivePreset(sessions)) return null
101
+ const content = overlayContent(board, sessions)
102
+ if (content === 'hidden') return null
90
103
  const cwd = currentSessionCwd(sessions)
91
104
  const scope = { sessionId: list.current, cwd }
92
105
  const snapshot = useLiveProjection(scope)
93
- return createElement(Board, { snapshot })
106
+ if (content === 'inspector' && board.selection !== null) {
107
+ return createElement(Inspector, {
108
+ runId: board.selection.runId,
109
+ worktreeRoot: board.selection.worktreeRoot,
110
+ snapshot,
111
+ onBackToToolDetails: () => boardState.backToBoard(),
112
+ })
113
+ }
114
+ return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
94
115
  }
95
116
 
96
117
  /**
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] ?? ''
@@ -0,0 +1,84 @@
1
+ /**
2
+ * fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
3
+ *
4
+ * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
5
+ * so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
6
+ * prompt section rendered ''. This module derives the SAME intent from the
7
+ * filesystem instead: session cwd -> control-plane root -> enumerate runs ->
8
+ * latest run -> current phase (foldRun). Pure read-only fs folding, zero
9
+ * emission — consistent with the per-workspace design + pre-step listener.
10
+ */
11
+ import { existsSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+ import { enumerateRuns } from './bootstrap.ts'
14
+ import { foldRun } from './status.ts'
15
+ import type { WorkspaceRegistryLike } from './workspace.ts'
16
+ import { resolveControlPlaneRoot } from './workspace.ts'
17
+
18
+ export interface FsPolicyIntent {
19
+ worktreeRoot: string
20
+ runId: string
21
+ /** The current phase doc file name (e.g. 03-implementation-summary.md), when determinable. */
22
+ currentPhaseFile: string | null
23
+ }
24
+
25
+ /**
26
+ * Resolve the recursive policy intent for one agent from the FILESYSTEM.
27
+ * Returns null when no control-plane root or run is present (callers defer,
28
+ * never fail hard). The registry path is async-only in the harness, so when a
29
+ * registry is present this resolves the session cwd directly (B4: the session
30
+ * cwd IS the workspace root when unregistered; a registered workspace resolves
31
+ * to the same cwd via resolveControlPlaneRoot's sync fallback).
32
+ */
33
+ export function fsPolicyIntent(
34
+ agent: { session?: { header?: { cwd?: string } } } | null | undefined,
35
+ registry?: WorkspaceRegistryLike | null,
36
+ cwdFallback?: string,
37
+ ): FsPolicyIntent | null {
38
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
39
+ if (!cwd) return null
40
+ // B4 sync path: when the registry is absent the session cwd IS the root;
41
+ // when present, resolveControlPlaneRoot canonicalizes through the registry
42
+ // (async). For the sync prompt-section callback we take the B4 shortcut: the
43
+ // session cwd is authoritative per the workspace-scoping invariant. This is
44
+ // exactly what the pre-step listener's resolveRootForRoute falls back to.
45
+ const root = registry ? cwd : cwd
46
+ const runs = enumerateRuns(root)
47
+ if (runs.length === 0) return null
48
+ const runId = runs[runs.length - 1]
49
+ const runDir = join(root, '.recursive', 'run', runId)
50
+ if (!existsSync(runDir)) return null
51
+ const folded = foldRun(runDir, runId)
52
+ return {
53
+ worktreeRoot: root,
54
+ runId,
55
+ currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Async variant for callers that CAN await (pre-step listener, routes): uses
61
+ * resolveControlPlaneRoot for registry canonicalization when a registry is
62
+ * present, falling back to the session cwd (B4).
63
+ */
64
+ export async function fsPolicyIntentAsync(
65
+ agent: { session?: { header?: { cwd?: string } } } | null | undefined,
66
+ registry?: WorkspaceRegistryLike | null,
67
+ cwdFallback?: string,
68
+ ): Promise<FsPolicyIntent | null> {
69
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
70
+ if (!cwd) return null
71
+ const root = await resolveControlPlaneRoot(agent, registry, cwdFallback)
72
+ if (!root) return null
73
+ const runs = enumerateRuns(root)
74
+ if (runs.length === 0) return null
75
+ const runId = runs[runs.length - 1]
76
+ const runDir = join(root, '.recursive', 'run', runId)
77
+ if (!existsSync(runDir)) return null
78
+ const folded = foldRun(runDir, runId)
79
+ return {
80
+ worktreeRoot: root,
81
+ runId,
82
+ currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
83
+ }
84
+ }