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

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 (123) hide show
  1. package/cordis.patch.yml +15 -10
  2. package/lib/bootstrap.d.ts +9 -2
  3. package/lib/client/apply-guard.d.ts +14 -0
  4. package/lib/client/board.d.ts +5 -4
  5. package/lib/client/contract.d.ts +57 -24
  6. package/lib/client/host-api.d.ts +26 -0
  7. package/lib/client/index.d.ts +16 -9
  8. package/lib/client/inspector.d.ts +3 -3
  9. package/lib/client/slots.d.ts +4 -6
  10. package/lib/client/strip.d.ts +4 -3
  11. package/lib/client/use-live.d.ts +9 -0
  12. package/lib/client.js +252 -174
  13. package/lib/index.d.ts +11 -11
  14. package/lib/index.js +972 -4504
  15. package/lib/init-templates.d.ts +41 -0
  16. package/lib/live-route.d.ts +69 -0
  17. package/lib/phase-rules.d.ts +34 -0
  18. package/lib/runtime.d.ts +24 -17
  19. package/lib/snapshot.d.ts +11 -0
  20. package/package.json +4 -3
  21. package/preset/recursive/agent.cordis.yml +7 -4
  22. package/preset/recursive/preset.yml +1 -1
  23. package/references/agents-block.md +95 -0
  24. package/references/artifact-template.md +2574 -0
  25. package/references/bodies/claude.md +7 -0
  26. package/references/bodies/codex-agents.md +95 -0
  27. package/references/bodies/copilot.md +7 -0
  28. package/references/bodies/cursorrules.md +7 -0
  29. package/references/bodies/decisions.md +5 -0
  30. package/references/bodies/delegated-verification.md +49 -0
  31. package/references/bodies/memory-router.md +45 -0
  32. package/references/bodies/phase8-skill-memory.md +48 -0
  33. package/references/bodies/plans-bridge.md +43 -0
  34. package/references/bodies/recursive-agents-router.md +64 -0
  35. package/references/bodies/skill-discovery.md +51 -0
  36. package/references/bodies/skill-memory-router.md +42 -0
  37. package/references/bodies/state.md +5 -0
  38. package/references/bootstrap/RECURSIVE.md +2362 -0
  39. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  40. package/references/scripts/lint-recursive-run.ps1 +25 -0
  41. package/references/scripts/lint-recursive-run.py +2870 -0
  42. package/references/scripts/recursive-closeout.ps1 +38 -0
  43. package/references/scripts/recursive-closeout.py +541 -0
  44. package/references/scripts/recursive-init.ps1 +515 -0
  45. package/references/scripts/recursive-init.py +356 -0
  46. package/references/scripts/recursive-lock.ps1 +26 -0
  47. package/references/scripts/recursive-lock.py +302 -0
  48. package/references/scripts/recursive-review-bundle.ps1 +58 -0
  49. package/references/scripts/recursive-review-bundle.py +503 -0
  50. package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
  51. package/references/scripts/recursive-router-cli-configure.py +9 -0
  52. package/references/scripts/recursive-router-cli-init.ps1 +2 -0
  53. package/references/scripts/recursive-router-cli-init.py +9 -0
  54. package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
  55. package/references/scripts/recursive-router-cli-invoke.py +9 -0
  56. package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
  57. package/references/scripts/recursive-router-cli-probe.py +9 -0
  58. package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
  59. package/references/scripts/recursive-router-cli-resolve.py +9 -0
  60. package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
  61. package/references/scripts/recursive-router-cli-validate.py +9 -0
  62. package/references/scripts/recursive-router-configure.ps1 +27 -0
  63. package/references/scripts/recursive-router-configure.py +74 -0
  64. package/references/scripts/recursive-router-init.ps1 +17 -0
  65. package/references/scripts/recursive-router-init.py +29 -0
  66. package/references/scripts/recursive-router-invoke.ps1 +47 -0
  67. package/references/scripts/recursive-router-invoke.py +103 -0
  68. package/references/scripts/recursive-router-probe.ps1 +25 -0
  69. package/references/scripts/recursive-router-probe.py +44 -0
  70. package/references/scripts/recursive-router-resolve.ps1 +26 -0
  71. package/references/scripts/recursive-router-resolve.py +46 -0
  72. package/references/scripts/recursive-router-validate.ps1 +17 -0
  73. package/references/scripts/recursive-router-validate.py +27 -0
  74. package/references/scripts/recursive-status.ps1 +23 -0
  75. package/references/scripts/recursive-status.py +2124 -0
  76. package/references/scripts/recursive-subagent-action.ps1 +98 -0
  77. package/references/scripts/recursive-subagent-action.py +197 -0
  78. package/references/scripts/recursive-training-extract.ps1 +23 -0
  79. package/references/scripts/recursive-training-extract.py +99 -0
  80. package/references/scripts/recursive-training-grpo.ps1 +52 -0
  81. package/references/scripts/recursive-training-grpo.py +1341 -0
  82. package/references/scripts/recursive-training-loader.ps1 +78 -0
  83. package/references/scripts/recursive-training-loader.py +558 -0
  84. package/references/scripts/recursive-training-mcp.ps1 +29 -0
  85. package/references/scripts/recursive-training-mcp.py +261 -0
  86. package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
  87. package/references/scripts/recursive-training-phase8-trigger.py +137 -0
  88. package/references/scripts/recursive-training-sync.ps1 +29 -0
  89. package/references/scripts/recursive-training-sync.py +183 -0
  90. package/references/scripts/recursive_phase_rules.py +367 -0
  91. package/references/scripts/recursive_router_cli_lib.py +2 -0
  92. package/references/scripts/recursive_router_lib.py +2282 -0
  93. package/references/scripts/verify-locks.ps1 +25 -0
  94. package/references/scripts/verify-locks.py +353 -0
  95. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  96. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  97. package/scripts/install-preset.cmd +7 -0
  98. package/scripts/install-preset.js +101 -0
  99. package/scripts/lint-recursive-run.py +7 -5
  100. package/scripts/test-recursive-mode-smoke.ts +29 -28
  101. package/src/bootstrap.ts +253 -39
  102. package/src/client/apply-guard.ts +23 -0
  103. package/src/client/board.tsx +0 -0
  104. package/src/client/contract.ts +72 -24
  105. package/src/client/host-api.ts +90 -0
  106. package/src/client/index.ts +22 -11
  107. package/src/client/inspector.tsx +7 -9
  108. package/src/client/slots.ts +70 -26
  109. package/src/client/strip.tsx +9 -9
  110. package/src/client/use-live.ts +52 -0
  111. package/src/index.ts +89 -146
  112. package/src/init-templates.ts +276 -0
  113. package/src/live-route.ts +163 -0
  114. package/src/phase-rules.ts +262 -0
  115. package/src/policy.ts +28 -4
  116. package/src/runtime.ts +129 -77
  117. package/src/snapshot.ts +104 -0
  118. package/lib/client/node.d.ts +0 -71
  119. package/lib/events.d.ts +0 -173
  120. package/lib/projection.d.ts +0 -29
  121. package/src/client/node.ts +0 -156
  122. package/src/events.ts +0 -173
  123. package/src/projection.ts +0 -237
package/lib/client.js CHANGED
@@ -5,6 +5,27 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ //#region src/client/contract.ts
9
+ /**
10
+ * The client-side preset gate (acceptance #3): the board/strip render ONLY when
11
+ * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
12
+ * session (or a session whose preset is undefined) shows NOTHING — not an empty
13
+ * board, not a spinner.
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";
20
+ }
21
+ /** 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 ?? "";
27
+ }
28
+ //#endregion
8
29
  //#region src/client/derive.ts
9
30
  const KANBAN_LANES = [
10
31
  {
@@ -74,17 +95,17 @@ window.__ModuleLoader__.load({
74
95
  "08"
75
96
  ];
76
97
  /** Map a group id to its ordinal for stable ordering. */
77
- const GROUP_INDEX$1 = new Map(GROUP_ORDER.map((g, i) => [g, i]));
98
+ const GROUP_INDEX = new Map(GROUP_ORDER.map((g, i) => [g, i]));
78
99
  /** Group id for a phase filename (e.g. '03.5-code-review.md' -> '03.5'); null for non-phase names. */
79
- function phaseGroupOf$1(phase) {
100
+ function phaseGroupOf(phase) {
80
101
  const m = /^(\d{2}(?:\.\d)?)(?:-|$)/.exec(phase);
81
102
  if (m === null) return null;
82
103
  const g = m[1];
83
- return GROUP_INDEX$1.has(g) ? g : null;
104
+ return GROUP_INDEX.has(g) ? g : null;
84
105
  }
85
106
  /** Kanban lane id for a phase filename. */
86
107
  function laneOf(phase) {
87
- const g = phaseGroupOf$1(phase);
108
+ const g = phaseGroupOf(phase);
88
109
  if (g === null) return null;
89
110
  for (const lane of KANBAN_LANES) if (lane.groups.includes(g)) return lane.id;
90
111
  return null;
@@ -97,9 +118,9 @@ window.__ModuleLoader__.load({
97
118
  let best = null;
98
119
  let bestIndex = -1;
99
120
  for (const key of Object.keys(card.phases)) {
100
- const g = phaseGroupOf$1(key);
121
+ const g = phaseGroupOf(key);
101
122
  if (g === null) continue;
102
- const idx = GROUP_INDEX$1.get(g) ?? -1;
123
+ const idx = GROUP_INDEX.get(g) ?? -1;
103
124
  if (idx > bestIndex) {
104
125
  bestIndex = idx;
105
126
  best = g;
@@ -112,12 +133,12 @@ window.__ModuleLoader__.load({
112
133
  function cardFacts(card) {
113
134
  const groups = /* @__PURE__ */ new Set();
114
135
  for (const key of Object.keys(card.phases)) {
115
- const g = phaseGroupOf$1(key);
136
+ const g = phaseGroupOf(key);
116
137
  if (g !== null) groups.add(g);
117
138
  }
118
139
  const lockedGroups = /* @__PURE__ */ new Set();
119
140
  for (const key of Object.keys(card.phases)) {
120
- const g = phaseGroupOf$1(key);
141
+ const g = phaseGroupOf(key);
121
142
  if (g !== null && card.phases[key].status === "LOCKED") lockedGroups.add(g);
122
143
  }
123
144
  const lockedCount = lockedGroups.size;
@@ -131,9 +152,9 @@ window.__ModuleLoader__.load({
131
152
  let currentPhase = null;
132
153
  let currentIdx = -1;
133
154
  for (const key of Object.keys(card.phases)) {
134
- const g = phaseGroupOf$1(key);
155
+ const g = phaseGroupOf(key);
135
156
  if (g === null) continue;
136
- const idx = GROUP_INDEX$1.get(g) ?? -1;
157
+ const idx = GROUP_INDEX.get(g) ?? -1;
137
158
  if (idx > currentIdx) {
138
159
  currentIdx = idx;
139
160
  currentPhase = key;
@@ -141,7 +162,7 @@ window.__ModuleLoader__.load({
141
162
  }
142
163
  const tampered = Object.keys(card.tampers).length > 0;
143
164
  const allMandatoryLocked = MANDATORY_GROUPS.every((g) => {
144
- const row = Object.entries(card.phases).find(([key]) => phaseGroupOf$1(key) === g);
165
+ const row = Object.entries(card.phases).find(([key]) => phaseGroupOf(key) === g);
145
166
  return row !== void 0 && row[1].status === "LOCKED";
146
167
  });
147
168
  const lockValidity = tampered ? "tampered" : allMandatoryLocked ? "locked" : "in-progress";
@@ -173,7 +194,7 @@ window.__ModuleLoader__.load({
173
194
  function expandPhaseRows(card) {
174
195
  const byGroup = /* @__PURE__ */ new Map();
175
196
  for (const [fileName, row] of Object.entries(card.phases)) {
176
- const g = phaseGroupOf$1(fileName);
197
+ const g = phaseGroupOf(fileName);
177
198
  if (g !== null) byGroup.set(g, row);
178
199
  }
179
200
  const rows = [];
@@ -203,10 +224,13 @@ window.__ModuleLoader__.load({
203
224
  //#endregion
204
225
  //#region src/client/board.tsx
205
226
  /**
206
- * Board overlay (Phase D R5, §11.8): the kanban view over the 'recursive'
207
- * projection. Pure read-only renderer — no filesystem, no run-file scraping.
208
- * Cards are grouped by worktree root (the projection's outer key) and mapped
209
- * to their current-phase lane via derive.columnForRun.
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").
210
234
  */
211
235
  /** Flatten the worktree-grouped projection into a stable run list. */
212
236
  function listRuns(projection) {
@@ -215,8 +239,9 @@ window.__ModuleLoader__.load({
215
239
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
216
240
  return runs;
217
241
  }
218
- function Board({ useProjection }) {
219
- const runs = listRuns(useProjection("recursive"));
242
+ function Board({ snapshot }) {
243
+ if (snapshot === null || snapshot.root === null) return null;
244
+ const runs = listRuns(snapshot.projection);
220
245
  if (runs.length === 0) return (0, react.createElement)("div", {
221
246
  className: "rec-board",
222
247
  "data-empty": true
@@ -244,12 +269,13 @@ window.__ModuleLoader__.load({
244
269
  //#endregion
245
270
  //#region src/client/strip.tsx
246
271
  /**
247
- * Status strip (Phase D R7, §11.6): a thin glance at the active run's phase,
248
- * gate state, and tamper badge, docked at conversation.input.dock. Renders
249
- * nothing when no run is current.
272
+ * Status strip (SP2 R1): a thin glance at the active run's phase, gate state,
273
+ * and tamper badge, docked at conversation.input.dock. Renders NOTHING when no
274
+ * run is current or the snapshot is null (the preset gate lives in slots.ts).
250
275
  */
251
- function StatusStrip({ useProjection }) {
252
- const runs = listRuns(useProjection("recursive"));
276
+ function StatusStrip({ snapshot }) {
277
+ if (snapshot === null || snapshot.root === null) return null;
278
+ const runs = listRuns(snapshot.projection);
253
279
  if (runs.length === 0) return null;
254
280
  const active = runs[runs.length - 1];
255
281
  const facts = cardFacts(active);
@@ -266,17 +292,138 @@ window.__ModuleLoader__.load({
266
292
  return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("h2", {}, "Recursive"), (0, react.createElement)("p", {}, "Enforcement policy, scratch format, provider defaults, and preset install path are configured on the host. The client is read-only (§11.9)."), (0, react.createElement)("button", { onClick: close }, "Close"));
267
293
  }
268
294
  //#endregion
295
+ //#region src/client/host-api.ts
296
+ /**
297
+ * Live recursive board/strip feed (SP2 R1): the browser half of the host
298
+ * HTTP/SSE route. Replaces useProjection('recursive') — the board reads the
299
+ * FILESYSTEM FOLD served by the host, keyed per-WORKSPACE. The client passes
300
+ * its sessionId (PRIMARY — the host resolves the cwd from the attached
301
+ * session header) plus cwd as a fallback hint while hydrating; the host
302
+ * resolves the recursive root server-side, never trusting a client path.
303
+ *
304
+ * READ-ONLY (R9): GET state + GET events only; this module never mutates the
305
+ * host and never touches the filesystem.
306
+ *
307
+ * Mirrors the dsh-web-ui task-board + DSH-better-sidebar host-api pattern:
308
+ * fetch /state with no-store, EventSource /events with a visibilitychange
309
+ * re-listener, and an AbortController timeout on the initial fetch.
310
+ */
311
+ const FETCH_TIMEOUT_MS = 15e3;
312
+ /** Query-string encoder for the two params. */
313
+ function scopeQuery(scope) {
314
+ const parts = [];
315
+ if (scope.sessionId) parts.push("sessionId=" + encodeURIComponent(scope.sessionId));
316
+ if (scope.cwd) parts.push("cwd=" + encodeURIComponent(scope.cwd));
317
+ return parts.length === 0 ? "" : "?" + parts.join("&");
318
+ }
319
+ /** Fetch the current state for one session scope (host resolves the workspace root). */
320
+ async function fetchLiveState(scope, signal) {
321
+ const controller = new AbortController();
322
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
323
+ try {
324
+ const res = await fetch("/.recursive/api/state" + scopeQuery(scope), {
325
+ signal: controller.signal,
326
+ cache: "no-store"
327
+ });
328
+ if (!res.ok) throw new Error("recursive live state: HTTP " + res.status);
329
+ return await res.json();
330
+ } finally {
331
+ clearTimeout(timer);
332
+ }
333
+ }
334
+ /**
335
+ * Subscribe to the SSE events feed. Returns a disposer. The host pushes a full
336
+ * {revision, root, projection} frame on every fs change + a 15s heartbeat.
337
+ *
338
+ * @param scope - sessionId primary + cwd fallback (host resolves the root).
339
+ * @param onFrame - called per data frame.
340
+ * @param onError - called when the stream fails/ends (the caller re-subscribes).
341
+ */
342
+ function subscribeLiveEvents(scope, onFrame, onError) {
343
+ const url = "/.recursive/api/events" + scopeQuery(scope);
344
+ const es = new EventSource(url);
345
+ const onMessage = (event) => {
346
+ try {
347
+ onFrame(JSON.parse(event.data));
348
+ } catch {}
349
+ };
350
+ es.addEventListener("message", onMessage);
351
+ es.onerror = () => {
352
+ es.close();
353
+ onError?.();
354
+ };
355
+ const onVisible = () => {
356
+ if (document.visibilityState === "visible") {
357
+ es.close();
358
+ onError?.();
359
+ }
360
+ };
361
+ document.addEventListener("visibilitychange", onVisible);
362
+ return () => {
363
+ document.removeEventListener("visibilitychange", onVisible);
364
+ es.close();
365
+ };
366
+ }
367
+ //#endregion
368
+ //#region src/client/use-live.ts
369
+ /**
370
+ * useLiveProjection(scope): the client bridge to the live host route (SP2 R1).
371
+ *
372
+ * Reads the per-workspace filesystem fold served by the host HTTP/SSE route
373
+ * instead of the (removed) 'recursive' session projection. The scope carries
374
+ * sessionId PRIMARY (the host resolves cwd from the attached session header)
375
+ * plus cwd as a fallback hint. Returns { root, projection, revision } or null
376
+ * while the first state fetch is in flight. The hook never mutates the host
377
+ * (R9 read-only).
378
+ *
379
+ * If the scope has neither sessionId nor cwd the hook renders nothing — the
380
+ * caller gates on the recursive preset BEFORE this so a non-recursive session
381
+ * shows NOTHING (acceptance #3).
382
+ */
383
+ /** Empty scope — nothing to key on. */
384
+ function isEmpty(scope) {
385
+ return !scope.sessionId && !scope.cwd;
386
+ }
387
+ /**
388
+ * Subscribe to the live route for one scope. Returns the current state. The SSE
389
+ * feed pushes full frames on every fs change; a dropped stream (sleep/error)
390
+ * triggers one refetch so the board never serves a stale fold.
391
+ */
392
+ function useLiveProjection(scope) {
393
+ const [state, setState] = (0, react.useState)(null);
394
+ (0, react.useEffect)(() => {
395
+ if (isEmpty(scope)) return;
396
+ let disposed = false;
397
+ const apply = (frame) => {
398
+ if (!disposed) setState(frame);
399
+ };
400
+ fetchLiveState(scope).then(apply).catch(() => {});
401
+ const disposeEvents = subscribeLiveEvents(scope, apply, () => {
402
+ if (!disposed) fetchLiveState(scope).then(apply).catch(() => {});
403
+ });
404
+ return () => {
405
+ disposed = true;
406
+ disposeEvents();
407
+ };
408
+ }, [scope.sessionId, scope.cwd]);
409
+ return state;
410
+ }
411
+ //#endregion
269
412
  //#region src/client/slots.ts
270
413
  /**
271
- * Slot registrations (Phase D R5/R6/R7/R8, §11.8): the sidebar launcher, the
272
- * shell.overlay board, the conversation.input.dock status strip, and the
273
- * settings.section page. The details-seat inspector is a swap driven by the
274
- * board cards' click (R6); the strip + board + settings are additive seats.
414
+ * Slot registrations (SP2 R1): the sidebar launcher, the shell.overlay board,
415
+ * the conversation.input.dock status strip, and the settings.section page.
416
+ *
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).
275
420
  *
276
- * Self-contained structural seam: the real slot registry + runtime standard
277
- * kit arrive from @deepseek-ai/dsh-client-ui-slots + @deepseek-ai/dsh-client-runtime
278
- * at compose time; this module consumes the minimal ClientContext shape
279
- * (contract.ts). The client is READ-ONLY (R9) — no filesystem, no mutation.
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.
425
+ *
426
+ * READ-ONLY (R9): the client only GETs the live host route; no mutation.
280
427
  */
281
428
  function registerSlots(ctx) {
282
429
  const disposers = [];
@@ -294,16 +441,21 @@ window.__ModuleLoader__.load({
294
441
  id: "recursive-board",
295
442
  order: 10
296
443
  }, (props) => {
297
- if (props?.useProjection === void 0) return null;
298
- return (0, react.createElement)(Board, { useProjection: props.useProjection });
444
+ const useSessions = props?.useSessions;
445
+ if (useSessions === void 0) return null;
446
+ return (0, react.createElement)(RecursiveBoardGate, { useSessions });
299
447
  })));
300
448
  disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
301
449
  name: "conversation.input.dock",
302
450
  id: "recursive-strip",
303
451
  order: 20
304
452
  }, (props) => {
305
- if (props?.useProjection === void 0) return null;
306
- return (0, react.createElement)(StatusStrip, { useProjection: props.useProjection });
453
+ const useSessions = props?.useSessions;
454
+ if (useSessions === void 0) return null;
455
+ return (0, react.createElement)(RecursiveStripGate, {
456
+ useSessions,
457
+ sessionId: props?.sessionId
458
+ });
307
459
  })));
308
460
  disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
309
461
  name: "settings.section",
@@ -315,148 +467,63 @@ window.__ModuleLoader__.load({
315
467
  for (const d of disposers) d();
316
468
  };
317
469
  }
318
- //#endregion
319
- //#region src/client/node.ts
320
- const RECURSIVE_NODE_KIND = "recursive/run";
321
- const GROUP_INDEX = new Map([
322
- "00",
323
- "01",
324
- "01.5",
325
- "02",
326
- "03",
327
- "03.5",
328
- "04",
329
- "05",
330
- "06",
331
- "07",
332
- "08"
333
- ].map((g, i) => [g, i]));
334
- function phaseGroupOf(phase) {
335
- const m = /^(\d{2}(?:\.\d)?)(?:-|$)/.exec(phase);
336
- return m ? m[1] : null;
337
- }
338
- function currentPhaseOf(phases) {
339
- let best = null;
340
- let bestIdx = -1;
341
- for (const key of Object.keys(phases)) {
342
- const g = phaseGroupOf(key);
343
- if (g === null) continue;
344
- const idx = GROUP_INDEX.get(g) ?? -1;
345
- if (idx > bestIdx) {
346
- bestIdx = idx;
347
- best = key;
348
- }
349
- }
350
- return best;
470
+ /**
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.
474
+ */
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);
483
+ const snapshot = useLiveProjection({
484
+ sessionId: list.current,
485
+ cwd
486
+ });
487
+ return (0, react.createElement)(Board, { snapshot });
351
488
  }
352
- function nodeDataOf(s) {
353
- const lockedCount = Object.values(s.phases).filter((p) => p.status === "LOCKED").length;
354
- return {
355
- runId: s.runId,
356
- worktreeRoot: s.worktreeRoot,
357
- state: s.state,
358
- currentPhase: currentPhaseOf(s.phases),
359
- lockedCount,
360
- totalPhases: Object.keys(s.phases).length,
361
- tampered: s.tampered,
362
- gateBlocked: s.gateBlocked,
363
- ...s.mergedToRepoRoot === void 0 ? {} : { mergedToRepoRoot: s.mergedToRepoRoot }
364
- };
489
+ /**
490
+ * Strip gate: same preset gate, session-scoped sessionId; the strip renders
491
+ * nothing for a non-recursive session.
492
+ */
493
+ function RecursiveStripGate({ useSessions, sessionId }) {
494
+ const list = useSessions();
495
+ const sessions = { list: {
496
+ getSnapshot: () => list,
497
+ subscribe: () => () => {}
498
+ } };
499
+ if (!isRecursivePreset(sessions)) return null;
500
+ const snapshot = useLiveProjection({
501
+ sessionId,
502
+ cwd: currentSessionCwd(sessions)
503
+ });
504
+ return (0, react.createElement)(StatusStrip, { snapshot });
365
505
  }
366
- /** Extract a stable (runId, worktreeRoot) business id + lifecycle role from one event. */
367
- function matchRecursiveNode(event) {
368
- if (typeof event.type !== "string" || !event.type.startsWith("recursive/")) return null;
369
- const data = event.data ?? {};
370
- const runId = typeof data.runId === "string" ? data.runId : "";
371
- const worktreeRoot = typeof data.worktreeRoot === "string" ? data.worktreeRoot : "";
372
- if (runId === "" || worktreeRoot === "") return null;
373
- return {
374
- id: worktreeRoot + "\0" + runId,
375
- role: event.type === "recursive/run-created" ? "start" : "update"
376
- };
506
+ //#endregion
507
+ //#region src/client/apply-guard.ts
508
+ /** Claims the client apply slot. Returns true when this call won the slot. */
509
+ function claimClientApply() {
510
+ if (globalThis.__dshRecursiveModeClientApplied === true) return false;
511
+ globalThis.__dshRecursiveModeClientApplied = true;
512
+ return true;
377
513
  }
378
- /** Fold one recursive/* event into the node state. */
379
- function foldRecursiveNode(s, event) {
380
- const data = event.data ?? {};
381
- const runId = typeof data.runId === "string" ? data.runId : "";
382
- const worktreeRoot = typeof data.worktreeRoot === "string" ? data.worktreeRoot : "";
383
- const base = s ?? {
384
- runId,
385
- worktreeRoot,
386
- state: "active",
387
- phases: {},
388
- tampered: false,
389
- gateBlocked: false
390
- };
391
- switch (event.type) {
392
- case "recursive/run-created":
393
- base.runId = runId;
394
- base.worktreeRoot = worktreeRoot;
395
- return base;
396
- case "recursive/phase": {
397
- const phase = String(data.phase ?? "");
398
- if (phase !== "") base.phases[phase] = {
399
- phase,
400
- status: String(data.status ?? "")
401
- };
402
- return base;
403
- }
404
- case "recursive/phase-locked": {
405
- const phase = String(data.phase ?? "");
406
- if (phase !== "") base.phases[phase] = {
407
- phase,
408
- status: "LOCKED"
409
- };
410
- return base;
411
- }
412
- case "recursive/gate-blocked":
413
- base.gateBlocked = true;
414
- return base;
415
- case "recursive/tamper":
416
- base.tampered = true;
417
- return base;
418
- case "recursive/run-state":
419
- base.state = typeof data.state === "string" ? data.state : base.state;
420
- return base;
421
- case "recursive/run-merged": {
422
- const repoRoot = typeof data.repoRoot === "string" ? data.repoRoot : "";
423
- if (repoRoot !== "") {
424
- base.mergedToRepoRoot = repoRoot;
425
- base.worktreeRoot = repoRoot;
426
- }
427
- return base;
428
- }
429
- default: return base;
430
- }
514
+ /** Releases the claim (called from the client fiber cleanup on unload/hot-reload). */
515
+ function releaseClientApply() {
516
+ globalThis.__dshRecursiveModeClientApplied = void 0;
431
517
  }
432
- /** The structural ConversationNodeDefinition (host links to the real contract). */
433
- const recursiveNodeDefinition = {
434
- kind: RECURSIVE_NODE_KIND,
435
- target: "chat",
436
- match: matchRecursiveNode,
437
- start: (_ctx, match) => foldRecursiveNode(void 0, match.event),
438
- update: (ctx, match) => foldRecursiveNode(ctx.state, match.event),
439
- buildViewNode: (ctx) => {
440
- const state = ctx.state;
441
- if (state === void 0) return null;
442
- return {
443
- key: ctx.key,
444
- kind: RECURSIVE_NODE_KIND,
445
- id: ctx.id,
446
- target: "chat",
447
- data: nodeDataOf(state)
448
- };
449
- }
450
- };
451
518
  //#endregion
452
519
  //#region src/client/inspector.tsx
453
520
  /**
454
- * Drill-down inspector (Phase D R6, §11.5): the details-seat swap rendering
455
- * one run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows),
456
- * lock badges, and tamper/gate facts — all from the projection, never disk.
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.
457
524
  */
458
- function Inspector({ runId, worktreeRoot, useProjection, onBackToToolDetails }) {
459
- const card = useProjection("recursive")?.[worktreeRoot]?.[runId];
525
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }) {
526
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
460
527
  if (card === void 0) return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("p", {}, "Run not found: " + runId));
461
528
  const facts = cardFacts(card);
462
529
  const rows = expandPhaseRows(card);
@@ -470,21 +537,32 @@ window.__ModuleLoader__.load({
470
537
  }
471
538
  //#endregion
472
539
  //#region src/client/index.ts
473
- /** Browser-half plugin entry (R4). */
540
+ /** Required services (fiber inject waiting — the runtime must be up first). */
541
+ const inject = [
542
+ "slots",
543
+ "sessions",
544
+ "workspaces",
545
+ "connection"
546
+ ];
547
+ /** Browser-half plugin entry (R4 + SP2 R1). */
474
548
  function apply(ctx) {
549
+ if (!claimClientApply()) return;
550
+ ctx.effect(() => releaseClientApply, "recursive: client apply claim");
475
551
  registerSlots(ctx);
476
552
  }
477
553
  //#endregion
478
554
  exports.Board = Board;
479
555
  exports.Inspector = Inspector;
480
- exports.RECURSIVE_NODE_KIND = RECURSIVE_NODE_KIND;
481
556
  exports.RecursiveSettings = RecursiveSettings;
482
557
  exports.StatusStrip = StatusStrip;
483
558
  exports.apply = apply;
484
- exports.foldRecursiveNode = foldRecursiveNode;
559
+ exports.currentSessionCwd = currentSessionCwd;
560
+ exports.fetchLiveState = fetchLiveState;
561
+ exports.inject = inject;
562
+ exports.isRecursivePreset = isRecursivePreset;
485
563
  exports.listRuns = listRuns;
486
- exports.matchRecursiveNode = matchRecursiveNode;
487
- exports.recursiveNodeDefinition = recursiveNodeDefinition;
564
+ exports.subscribeLiveEvents = subscribeLiveEvents;
565
+ exports.useLiveProjection = useLiveProjection;
488
566
  return module.exports;
489
567
  }
490
568
  });
package/lib/index.d.ts CHANGED
@@ -18,8 +18,8 @@ export * from './delegation.ts';
18
18
  export * from './lifecycle.ts';
19
19
  export * from './enforcement.ts';
20
20
  export * from './policy.ts';
21
- export * from './events.ts';
22
- export * from './projection.ts';
21
+ export * from './snapshot.ts';
22
+ export * from './live-route.ts';
23
23
  /**
24
24
  * Bundle plugin entry. The Loader activates this row once `tools` is available
25
25
  * (`inject` below); the RecursiveRuntime service is constructed directly so it
@@ -28,13 +28,13 @@ export * from './projection.ts';
28
28
  */
29
29
  export declare const inject: string[];
30
30
  /**
31
- * Two-stage init (PROPOSAL §5.10):
32
- * - Stage A (mount-time, this apply): register the isolated ctx.recursive
33
- * service + the recursive_* tools + the /recursive command + the
34
- * recursive:policy prompt section. No repo/run work here.
35
- * - Stage B (agent/session-start): resolve the session's workspace root (R1),
36
- * bootstrap the scaffold if missing, enumerate runs as dir names only, and
37
- * inject a workspace-scoped notice. See stageBWorkflowInit + the
38
- * agent/session-start listener below.
31
+ * Plugin entry (SP2 R1). Stage A (mount-time, this apply): register the
32
+ * isolated ctx.recursive service + the recursive_* tools + the /recursive
33
+ * command + the recursive:policy prompt section + the LIVE board/strip route
34
+ * (HTTP state + SSE), served per-workspace from the filesystem fold. No
35
+ * repo/run work and NO session-event emission here: zero recursive/* events
36
+ * are ever appended (resume-crash fix), and the board reads the live fs route.
39
37
  */
40
- export declare function apply(ctx: Context): void;
38
+ export declare function apply(ctx: Context, config?: {
39
+ shellOnly?: boolean;
40
+ }): void;