@try-works/dsh-recursive-mode 0.1.2 → 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.
- package/cordis.patch.yml +15 -10
- package/lib/bootstrap.d.ts +9 -2
- package/lib/client/apply-guard.d.ts +14 -0
- package/lib/client/board.d.ts +5 -4
- package/lib/client/contract.d.ts +57 -24
- package/lib/client/host-api.d.ts +26 -0
- package/lib/client/index.d.ts +16 -9
- package/lib/client/inspector.d.ts +3 -3
- package/lib/client/slots.d.ts +4 -6
- package/lib/client/strip.d.ts +4 -3
- package/lib/client/use-live.d.ts +9 -0
- package/lib/client.js +252 -174
- package/lib/index.d.ts +11 -11
- package/lib/index.js +972 -4504
- package/lib/init-templates.d.ts +41 -0
- package/lib/live-route.d.ts +69 -0
- package/lib/phase-rules.d.ts +34 -0
- package/lib/runtime.d.ts +24 -17
- package/lib/snapshot.d.ts +11 -0
- package/package.json +4 -3
- package/preset/recursive/agent.cordis.yml +7 -4
- package/preset/recursive/preset.yml +1 -1
- package/references/agents-block.md +95 -0
- package/references/artifact-template.md +2574 -0
- package/references/bodies/claude.md +7 -0
- package/references/bodies/codex-agents.md +95 -0
- package/references/bodies/copilot.md +7 -0
- package/references/bodies/cursorrules.md +7 -0
- package/references/bodies/decisions.md +5 -0
- package/references/bodies/delegated-verification.md +49 -0
- package/references/bodies/memory-router.md +45 -0
- package/references/bodies/phase8-skill-memory.md +48 -0
- package/references/bodies/plans-bridge.md +43 -0
- package/references/bodies/recursive-agents-router.md +64 -0
- package/references/bodies/skill-discovery.md +51 -0
- package/references/bodies/skill-memory-router.md +42 -0
- package/references/bodies/state.md +5 -0
- package/references/bootstrap/RECURSIVE.md +2362 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +25 -0
- package/references/scripts/lint-recursive-run.py +2870 -0
- package/references/scripts/recursive-closeout.ps1 +38 -0
- package/references/scripts/recursive-closeout.py +541 -0
- package/references/scripts/recursive-init.ps1 +515 -0
- package/references/scripts/recursive-init.py +356 -0
- package/references/scripts/recursive-lock.ps1 +26 -0
- package/references/scripts/recursive-lock.py +302 -0
- package/references/scripts/recursive-review-bundle.ps1 +58 -0
- package/references/scripts/recursive-review-bundle.py +503 -0
- package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
- package/references/scripts/recursive-router-cli-configure.py +9 -0
- package/references/scripts/recursive-router-cli-init.ps1 +2 -0
- package/references/scripts/recursive-router-cli-init.py +9 -0
- package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
- package/references/scripts/recursive-router-cli-invoke.py +9 -0
- package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
- package/references/scripts/recursive-router-cli-probe.py +9 -0
- package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
- package/references/scripts/recursive-router-cli-resolve.py +9 -0
- package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
- package/references/scripts/recursive-router-cli-validate.py +9 -0
- package/references/scripts/recursive-router-configure.ps1 +27 -0
- package/references/scripts/recursive-router-configure.py +74 -0
- package/references/scripts/recursive-router-init.ps1 +17 -0
- package/references/scripts/recursive-router-init.py +29 -0
- package/references/scripts/recursive-router-invoke.ps1 +47 -0
- package/references/scripts/recursive-router-invoke.py +103 -0
- package/references/scripts/recursive-router-probe.ps1 +25 -0
- package/references/scripts/recursive-router-probe.py +44 -0
- package/references/scripts/recursive-router-resolve.ps1 +26 -0
- package/references/scripts/recursive-router-resolve.py +46 -0
- package/references/scripts/recursive-router-validate.ps1 +17 -0
- package/references/scripts/recursive-router-validate.py +27 -0
- package/references/scripts/recursive-status.ps1 +23 -0
- package/references/scripts/recursive-status.py +2124 -0
- package/references/scripts/recursive-subagent-action.ps1 +98 -0
- package/references/scripts/recursive-subagent-action.py +197 -0
- package/references/scripts/recursive-training-extract.ps1 +23 -0
- package/references/scripts/recursive-training-extract.py +99 -0
- package/references/scripts/recursive-training-grpo.ps1 +52 -0
- package/references/scripts/recursive-training-grpo.py +1341 -0
- package/references/scripts/recursive-training-loader.ps1 +78 -0
- package/references/scripts/recursive-training-loader.py +558 -0
- package/references/scripts/recursive-training-mcp.ps1 +29 -0
- package/references/scripts/recursive-training-mcp.py +261 -0
- package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
- package/references/scripts/recursive-training-phase8-trigger.py +137 -0
- package/references/scripts/recursive-training-sync.ps1 +29 -0
- package/references/scripts/recursive-training-sync.py +183 -0
- package/references/scripts/recursive_phase_rules.py +367 -0
- package/references/scripts/recursive_router_cli_lib.py +2 -0
- package/references/scripts/recursive_router_lib.py +2282 -0
- package/references/scripts/verify-locks.ps1 +25 -0
- package/references/scripts/verify-locks.py +353 -0
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-preset.cmd +7 -0
- package/scripts/install-preset.js +101 -0
- package/scripts/lint-recursive-run.py +7 -5
- package/scripts/test-recursive-mode-smoke.ts +29 -28
- package/src/bootstrap.ts +253 -39
- package/src/client/apply-guard.ts +23 -0
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +72 -24
- package/src/client/host-api.ts +90 -0
- package/src/client/index.ts +22 -11
- package/src/client/inspector.tsx +7 -9
- package/src/client/slots.ts +70 -26
- package/src/client/strip.tsx +9 -9
- package/src/client/use-live.ts +52 -0
- package/src/index.ts +89 -146
- package/src/init-templates.ts +276 -0
- package/src/live-route.ts +163 -0
- package/src/phase-rules.ts +262 -0
- package/src/policy.ts +28 -4
- package/src/runtime.ts +129 -77
- package/src/snapshot.ts +104 -0
- package/lib/client/node.d.ts +0 -71
- package/lib/events.d.ts +0 -173
- package/lib/projection.d.ts +0 -29
- package/src/client/node.ts +0 -156
- package/src/events.ts +0 -173
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
121
|
+
const g = phaseGroupOf(key);
|
|
101
122
|
if (g === null) continue;
|
|
102
|
-
const idx = GROUP_INDEX
|
|
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
|
|
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
|
|
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
|
|
155
|
+
const g = phaseGroupOf(key);
|
|
135
156
|
if (g === null) continue;
|
|
136
|
-
const idx = GROUP_INDEX
|
|
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
|
|
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
|
|
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 (
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
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({
|
|
219
|
-
|
|
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 (
|
|
248
|
-
*
|
|
249
|
-
*
|
|
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({
|
|
252
|
-
|
|
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 (
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* board
|
|
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
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
/**
|
|
379
|
-
function
|
|
380
|
-
|
|
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 (
|
|
455
|
-
*
|
|
456
|
-
*
|
|
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,
|
|
459
|
-
const card =
|
|
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
|
-
/**
|
|
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.
|
|
559
|
+
exports.currentSessionCwd = currentSessionCwd;
|
|
560
|
+
exports.fetchLiveState = fetchLiveState;
|
|
561
|
+
exports.inject = inject;
|
|
562
|
+
exports.isRecursivePreset = isRecursivePreset;
|
|
485
563
|
exports.listRuns = listRuns;
|
|
486
|
-
exports.
|
|
487
|
-
exports.
|
|
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 './
|
|
22
|
-
export * from './
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
38
|
+
export declare function apply(ctx: Context, config?: {
|
|
39
|
+
shellOnly?: boolean;
|
|
40
|
+
}): void;
|