@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.
- 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/src/client/inspector.tsx
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Drill-down inspector (SP2 R1): the details-seat swap rendering one run's
|
|
3
|
+
* phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock badges,
|
|
4
|
+
* and tamper/gate facts — all from the live host-route snapshot, never disk.
|
|
5
5
|
*/
|
|
6
6
|
import { createElement } from 'react'
|
|
7
|
-
import type {
|
|
7
|
+
import type { LiveProjectionValue } from './contract.ts'
|
|
8
8
|
import { cardFacts, expandPhaseRows } from './derive.ts'
|
|
9
|
-
import type { RecursiveProjectionReader } from './contract.ts'
|
|
10
9
|
|
|
11
10
|
export interface InspectorProps {
|
|
12
11
|
runId: string
|
|
13
12
|
worktreeRoot: string
|
|
14
|
-
|
|
13
|
+
snapshot: LiveProjectionValue | null
|
|
15
14
|
onBackToToolDetails: () => void
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export function Inspector({ runId, worktreeRoot,
|
|
19
|
-
const
|
|
20
|
-
const card = projection?.[worktreeRoot]?.[runId]
|
|
17
|
+
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }: InspectorProps) {
|
|
18
|
+
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
21
19
|
if (card === undefined) {
|
|
22
20
|
return createElement('div', { className: 'rec-inspector' },
|
|
23
21
|
createElement('p', {}, 'Run not found: ' + runId),
|
package/src/client/slots.ts
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slot registrations (
|
|
3
|
-
*
|
|
4
|
-
* settings.section page. The details-seat inspector is a swap driven by the
|
|
5
|
-
* board cards' click (R6); the strip + board + settings are additive seats.
|
|
2
|
+
* Slot registrations (SP2 R1): the sidebar launcher, the shell.overlay board,
|
|
3
|
+
* the conversation.input.dock status strip, and the settings.section page.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* READ-ONLY (R9): the client only GETs the live host route; no mutation.
|
|
11
15
|
*/
|
|
12
|
-
import { createElement } from 'react'
|
|
13
|
-
import type { ClientContext,
|
|
14
|
-
import
|
|
16
|
+
import { createElement, type ReactNode } from 'react'
|
|
17
|
+
import type { ClientContext, ISessionsLike, SnapshotStoreLike, SessionListStateLike } from './contract.ts'
|
|
18
|
+
import { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
15
19
|
import { Board } from './board.tsx'
|
|
16
20
|
import { StatusStrip } from './strip.tsx'
|
|
17
21
|
import { RecursiveSettings } from './settings.tsx'
|
|
22
|
+
import { useLiveProjection } from './use-live.ts'
|
|
23
|
+
|
|
24
|
+
/** Structural standard-prop face for the root scope (useSessions only). */
|
|
25
|
+
interface RootSlotProps {
|
|
26
|
+
useSessions?: () => SessionListStateLike
|
|
27
|
+
useWorkspaces?: () => unknown
|
|
28
|
+
}
|
|
18
29
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
/** Structural standard-prop face for the session scope (sessionId + useSessions). */
|
|
31
|
+
interface SessionSlotProps {
|
|
32
|
+
sessionId?: string
|
|
33
|
+
useSessions?: () => SessionListStateLike
|
|
34
|
+
useProjection?: unknown
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export function registerSlots(ctx: ClientContext): () => void {
|
|
25
38
|
const disposers: (() => void)[] = []
|
|
26
39
|
|
|
27
|
-
// Board launcher in the sidebar footer action list (always visible).
|
|
40
|
+
// Board launcher in the sidebar footer action list (always visible; opens the board).
|
|
28
41
|
disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
29
42
|
name: 'sidebar.footer.action',
|
|
30
43
|
id: 'recursive',
|
|
@@ -32,27 +45,29 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
32
45
|
label: 'Recursive runs',
|
|
33
46
|
}, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs' }, '⧉'))))
|
|
34
47
|
|
|
35
|
-
// Board panel overlay (root scope, cross-session read).
|
|
48
|
+
// Board panel overlay (root scope, cross-session read) — gated on the recursive preset.
|
|
36
49
|
disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
37
50
|
name: 'shell.overlay',
|
|
38
51
|
id: 'recursive-board',
|
|
39
52
|
order: 10,
|
|
40
|
-
}, (props:
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
}, (props: RootSlotProps) => {
|
|
54
|
+
const useSessions = props?.useSessions
|
|
55
|
+
if (useSessions === undefined) return null
|
|
56
|
+
return createElement(RecursiveBoardGate, { useSessions })
|
|
43
57
|
})))
|
|
44
58
|
|
|
45
|
-
// Status strip in the composer input dock (session scope).
|
|
59
|
+
// Status strip in the composer input dock (session scope) — gated the same way.
|
|
46
60
|
disposers.push(ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
47
61
|
name: 'conversation.input.dock',
|
|
48
62
|
id: 'recursive-strip',
|
|
49
63
|
order: 20,
|
|
50
|
-
}, (props:
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
}, (props: SessionSlotProps) => {
|
|
65
|
+
const useSessions = props?.useSessions
|
|
66
|
+
if (useSessions === undefined) return null
|
|
67
|
+
return createElement(RecursiveStripGate, { useSessions, sessionId: props?.sessionId })
|
|
53
68
|
})))
|
|
54
69
|
|
|
55
|
-
// Settings section (root scope, always present).
|
|
70
|
+
// Settings section (root scope, always present; no gate — configuration is always available).
|
|
56
71
|
disposers.push(ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
57
72
|
name: 'settings.section',
|
|
58
73
|
id: 'recursive',
|
|
@@ -63,5 +78,34 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
63
78
|
return () => { for (const d of disposers) d() }
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
81
|
+
/**
|
|
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.
|
|
85
|
+
*/
|
|
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)
|
|
91
|
+
const scope = { sessionId: list.current, cwd }
|
|
92
|
+
const snapshot = useLiveProjection(scope)
|
|
93
|
+
return createElement(Board, { snapshot })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Strip gate: same preset gate, session-scoped sessionId; the strip renders
|
|
98
|
+
* nothing for a non-recursive session.
|
|
99
|
+
*/
|
|
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)
|
|
105
|
+
const scope = { sessionId, cwd }
|
|
106
|
+
const snapshot = useLiveProjection(scope)
|
|
107
|
+
return createElement(StatusStrip, { snapshot })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Export the gate helpers for tests. */
|
|
111
|
+
export { isRecursivePreset, currentSessionCwd }
|
package/src/client/strip.tsx
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Status strip (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Status strip (SP2 R1): a thin glance at the active run's phase, gate state,
|
|
3
|
+
* and tamper badge, docked at conversation.input.dock. Renders NOTHING when no
|
|
4
|
+
* run is current or the snapshot is null (the preset gate lives in slots.ts).
|
|
5
5
|
*/
|
|
6
6
|
import { createElement } from 'react'
|
|
7
7
|
import { cardFacts } from './derive.ts'
|
|
8
8
|
import { listRuns } from './board.tsx'
|
|
9
|
-
import type {
|
|
10
|
-
import type { RecursiveProjectionReader } from './contract.ts'
|
|
9
|
+
import type { LiveProjectionValue } from './contract.ts'
|
|
11
10
|
|
|
12
11
|
export interface StripProps {
|
|
13
|
-
|
|
12
|
+
/** The live host-route snapshot (null = not loaded / no recursive root). */
|
|
13
|
+
snapshot: LiveProjectionValue | null
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function StatusStrip({
|
|
17
|
-
|
|
18
|
-
const runs = listRuns(projection)
|
|
16
|
+
export function StatusStrip({ snapshot }: StripProps) {
|
|
17
|
+
if (snapshot === null || snapshot.root === null) return null
|
|
18
|
+
const runs = listRuns(snapshot.projection)
|
|
19
19
|
if (runs.length === 0) return null
|
|
20
20
|
const active = runs[runs.length - 1]
|
|
21
21
|
const facts = cardFacts(active)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLiveProjection(scope): the client bridge to the live host route (SP2 R1).
|
|
3
|
+
*
|
|
4
|
+
* Reads the per-workspace filesystem fold served by the host HTTP/SSE route
|
|
5
|
+
* instead of the (removed) 'recursive' session projection. The scope carries
|
|
6
|
+
* sessionId PRIMARY (the host resolves cwd from the attached session header)
|
|
7
|
+
* plus cwd as a fallback hint. Returns { root, projection, revision } or null
|
|
8
|
+
* while the first state fetch is in flight. The hook never mutates the host
|
|
9
|
+
* (R9 read-only).
|
|
10
|
+
*
|
|
11
|
+
* If the scope has neither sessionId nor cwd the hook renders nothing — the
|
|
12
|
+
* caller gates on the recursive preset BEFORE this so a non-recursive session
|
|
13
|
+
* shows NOTHING (acceptance #3).
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from 'react'
|
|
16
|
+
import { fetchLiveState, subscribeLiveEvents, type LiveRecursiveFrame, type LiveRecursiveState, type LiveScope } from './host-api.ts'
|
|
17
|
+
|
|
18
|
+
/** Snapshot shape the board/strip consume (null = not loaded / no recursive root). */
|
|
19
|
+
export type LiveProjectionSnapshot = LiveRecursiveState | null
|
|
20
|
+
|
|
21
|
+
/** Empty scope — nothing to key on. */
|
|
22
|
+
function isEmpty(scope: LiveScope): boolean {
|
|
23
|
+
return !scope.sessionId && !scope.cwd
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to the live route for one scope. Returns the current state. The SSE
|
|
28
|
+
* feed pushes full frames on every fs change; a dropped stream (sleep/error)
|
|
29
|
+
* triggers one refetch so the board never serves a stale fold.
|
|
30
|
+
*/
|
|
31
|
+
export function useLiveProjection(scope: LiveScope): LiveProjectionSnapshot {
|
|
32
|
+
const [state, setState] = useState<LiveRecursiveState | null>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Empty scope = no resolvable session row yet — render nothing, never fetch.
|
|
36
|
+
if (isEmpty(scope)) return
|
|
37
|
+
let disposed = false
|
|
38
|
+
const apply = (frame: LiveRecursiveFrame) => { if (!disposed) setState(frame) }
|
|
39
|
+
|
|
40
|
+
// Initial state + every refresh: a no-store fetch (cold resume = fresh fs read).
|
|
41
|
+
void fetchLiveState(scope).then(apply).catch(() => { /* no recursive root yet — stay empty */ })
|
|
42
|
+
|
|
43
|
+
// Live deltas over SSE.
|
|
44
|
+
const disposeEvents = subscribeLiveEvents(scope, apply, () => {
|
|
45
|
+
if (!disposed) void fetchLiveState(scope).then(apply).catch(() => {})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return () => { disposed = true; disposeEvents() }
|
|
49
|
+
}, [scope.sessionId, scope.cwd])
|
|
50
|
+
|
|
51
|
+
return state
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Context } from '@deepseek-ai/cordis'
|
|
2
|
-
import
|
|
2
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
3
5
|
import { RecursiveRuntime } from './runtime.ts'
|
|
4
6
|
import { createRecursiveStatusTool } from './recursive_status.tool.ts'
|
|
5
7
|
import { createRecursiveInitTool } from './recursive_init.tool.ts'
|
|
@@ -8,23 +10,14 @@ import { createRecursiveLintTool } from './recursive_lint.tool.ts'
|
|
|
8
10
|
import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
|
|
9
11
|
import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
|
|
10
12
|
import { registerRecursiveCommand } from './commands.ts'
|
|
11
|
-
import {
|
|
12
|
-
import { evaluatePreStepGate, evaluateToolGuard, detectTamper, resolveEnforcementConfig, DEFAULT_ENFORCEMENT, type EnforcementConfig } from './enforcement.ts'
|
|
13
|
+
import { evaluateToolGuard } from './enforcement.ts'
|
|
13
14
|
import { detectTransitionIntent } from './lifecycle.ts'
|
|
14
15
|
import { renderRecursivePolicy } from './policy.ts'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
phase as phaseEvent,
|
|
21
|
-
phaseLocked as phaseLockedEvent,
|
|
22
|
-
runState as runStateEvent,
|
|
23
|
-
runMerged as runMergedEvent,
|
|
24
|
-
subagentStart as subagentStartEvent,
|
|
25
|
-
subagentEnd as subagentEndEvent,
|
|
26
|
-
} from './events.ts'
|
|
27
|
-
import { recursiveProjectionUnit } from './projection.ts'
|
|
16
|
+
import { snapshotWorkspace } from './snapshot.ts'
|
|
17
|
+
import { mountRecursiveRoutesOnce, makeRecursiveRoutes, type RecursiveRouteHost } from './live-route.ts'
|
|
18
|
+
import { enumerateRuns } from './bootstrap.ts'
|
|
19
|
+
import { getNextLegalPhase, getLockStatus } from './lock.ts'
|
|
20
|
+
import { phaseLintRulesMessage } from './phase-rules.ts'
|
|
28
21
|
|
|
29
22
|
export const name = '@try-works/dsh-recursive-mode'
|
|
30
23
|
|
|
@@ -71,8 +64,8 @@ export * from './delegation.ts'
|
|
|
71
64
|
export * from './lifecycle.ts'
|
|
72
65
|
export * from './enforcement.ts'
|
|
73
66
|
export * from './policy.ts'
|
|
74
|
-
export * from './
|
|
75
|
-
export * from './
|
|
67
|
+
export * from './snapshot.ts'
|
|
68
|
+
export * from './live-route.ts'
|
|
76
69
|
|
|
77
70
|
/**
|
|
78
71
|
* Bundle plugin entry. The Loader activates this row once `tools` is available
|
|
@@ -83,16 +76,22 @@ export * from './projection.ts'
|
|
|
83
76
|
export const inject = ['tools']
|
|
84
77
|
|
|
85
78
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* inject a workspace-scoped notice. See stageBWorkflowInit + the
|
|
93
|
-
* agent/session-start listener below.
|
|
79
|
+
* Plugin entry (SP2 R1). Stage A (mount-time, this apply): register the
|
|
80
|
+
* isolated ctx.recursive service + the recursive_* tools + the /recursive
|
|
81
|
+
* command + the recursive:policy prompt section + the LIVE board/strip route
|
|
82
|
+
* (HTTP state + SSE), served per-workspace from the filesystem fold. No
|
|
83
|
+
* repo/run work and NO session-event emission here: zero recursive/* events
|
|
84
|
+
* are ever appended (resume-crash fix), and the board reads the live fs route.
|
|
94
85
|
*/
|
|
95
|
-
export function apply(ctx: Context) {
|
|
86
|
+
export function apply(ctx: Context, config?: { shellOnly?: boolean }) {
|
|
87
|
+
// R4 shell split (02-to-be-plan.addendum-r4-r2-mount-resolution.md): the
|
|
88
|
+
// global bare-name row in cordis.patch.yml mounts with config.shellOnly=true
|
|
89
|
+
// to expose ONLY the client bundle for client discovery. It must register
|
|
90
|
+
// NOTHING on the server root — no tools, no /recursive command, no
|
|
91
|
+
// recursive:policy, no projection (BUG 4 always-on leak). The full server
|
|
92
|
+
// surface is mounted ONLY by the recursive preset's isolated recursive-realm,
|
|
93
|
+
// whose rows carry no config (shellOnly undefined).
|
|
94
|
+
if (config?.shellOnly) return
|
|
96
95
|
ctx.effect(function* () {
|
|
97
96
|
// Workspace registry: optional host service (durable). Access via ctx.get —
|
|
98
97
|
// property access requires inject and would fail boot when undeclared.
|
|
@@ -100,27 +99,6 @@ export function apply(ctx: Context) {
|
|
|
100
99
|
const workspaceRegistry = ctx.get('workspaceRegistry') as never
|
|
101
100
|
const recursive = new RecursiveRuntime(ctx, { repoRoot: process.cwd(), workspaceRegistry })
|
|
102
101
|
|
|
103
|
-
/** Typed agent payload shape (session is a full Session). */
|
|
104
|
-
type AgentLike = { session?: Session | null }
|
|
105
|
-
|
|
106
|
-
/** Extract the session from a listener payload. */
|
|
107
|
-
const sessionOf = (p: unknown): Session | null => {
|
|
108
|
-
const a = (p as { agent?: AgentLike | null } | null)?.agent
|
|
109
|
-
return a?.session ?? null
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Resolve the session's control-plane root (registry first, cwd fallback). */
|
|
113
|
-
const rootOf = async (p: unknown): Promise<string> => {
|
|
114
|
-
const a = (p as { agent?: AgentLike | null } | null)?.agent
|
|
115
|
-
const root = await recursive.resolveRootFor(a as { session?: { header?: { cwd?: string } } } | null)
|
|
116
|
-
return root ?? a?.session?.header.cwd ?? process.cwd()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Emit one recursive/* event into the session log (log-only). */
|
|
120
|
-
const emit = (session: Session | null, type: string, data: unknown) => {
|
|
121
|
-
if (!session) return
|
|
122
|
-
try { (session.append as unknown as (t: string, d: unknown) => void)(type, data) } catch { /* log-only; never fail the gate on an emit error */ }
|
|
123
|
-
}
|
|
124
102
|
const disposers = [
|
|
125
103
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
126
104
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
@@ -156,30 +134,14 @@ export function apply(ctx: Context) {
|
|
|
156
134
|
}))
|
|
157
135
|
}
|
|
158
136
|
|
|
159
|
-
// Phase C R3
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const session = sessionOf(p)
|
|
168
|
-
const intent = recursive.detectTransitionIntent(events as never)
|
|
169
|
-
const decision = evaluatePreStepGate(events as never, recursive.enforcementConfig.preStep)
|
|
170
|
-
void (async () => {
|
|
171
|
-
// Emit a committed run-state (blocked) when a gate rejects the intent —
|
|
172
|
-
// the board then shows the run blocked, not merely a log message.
|
|
173
|
-
if (decision.gateBlocked && intent?.runId) {
|
|
174
|
-
const worktreeRoot = intent?.worktreeRoot ?? (await rootOf(p))
|
|
175
|
-
emit(session, 'recursive/gate-blocked', gateBlockedEvent({ runId: intent.runId, worktreeRoot, phase: intent?.targetArtifact ?? '', failures: decision.failures, kind: 'pre-step' }))
|
|
176
|
-
emit(session, 'recursive/run-state', runStateEvent({ runId: intent.runId, worktreeRoot, state: 'blocked', reason: 'gate-blocked: ' + decision.failures.join('; ') }))
|
|
177
|
-
}
|
|
178
|
-
})()
|
|
179
|
-
if (decision.kind === 'reject') return { kind: 'reject' }
|
|
180
|
-
return typeof next === 'function' ? next() : { kind: 'enter', messages: [] }
|
|
181
|
-
}))
|
|
182
|
-
}
|
|
137
|
+
// Phase C R3 (Layer 1, agent/pre-step proactive intent gate) is RETIRED
|
|
138
|
+
// under zero-emission (SP2 R1): its only signal was the recursive/phase-intent
|
|
139
|
+
// session event, whose emitter is now deleted, and it was ADVISORY by default
|
|
140
|
+
// (it never rejected; its sole side effect was the now-removed emission).
|
|
141
|
+
// Enforcement is preserved where it actually bites: Layer 2 (tools/pre-execute
|
|
142
|
+
// inspects the REAL tool call args below) plus the recursive_lock tool's own
|
|
143
|
+
// prerequisite validation in lockArtifact — strictly more reliable than the
|
|
144
|
+
// heuristic intent scan. See 03-implementation-summary.addendum-r1-*.md.
|
|
183
145
|
|
|
184
146
|
// Phase C R4: tools/pre-execute surgical guards (Layer 2, caller of the
|
|
185
147
|
// transition set). Scope-filtered to the active run's worktree.
|
|
@@ -195,87 +157,68 @@ export function apply(ctx: Context) {
|
|
|
195
157
|
if (decision.kind === 'allow') return typeof next === 'function' ? next() : { kind: 'allow' }
|
|
196
158
|
if (decision.kind === 'deny') return decision
|
|
197
159
|
// ask -> in advisory we let it through with a warn (the approval seam is Phase D)
|
|
198
|
-
return typeof next === 'function' ? next() : { kind: 'allow' }
|
|
199
160
|
}))
|
|
200
161
|
}
|
|
201
162
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
163
|
+
// SP3 R5: agent/pre-step lint-rules injection (pre-step contract verified: the
|
|
164
|
+
// listener returns { kind: 'enter', messages: [...messages, injected] } and the
|
|
165
|
+
// returned array REPLACES the default [...claimed, context]). When the session's
|
|
166
|
+
// control-plane root has an active recursive run whose current phase doc is
|
|
167
|
+
// DRAFT, prepend a compact system-reminder with THAT phase's required sections +
|
|
168
|
+
// gates. Pure fs read (zero-emission): never appends recursive/* events.
|
|
169
|
+
const agentRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
|
|
170
|
+
if (agentRuntime.on) {
|
|
171
|
+
disposers.push(agentRuntime.on('agent/pre-step', async (payload, next) => {
|
|
172
|
+
const p = payload as {
|
|
173
|
+
messages?: Array<{ content: Array<{ type: string; text?: string }> }>,
|
|
174
|
+
agent?: { session?: { header?: { cwd?: string } } } | null,
|
|
175
|
+
} | null
|
|
176
|
+
const messages = p?.messages ?? []
|
|
177
|
+
const agent = p?.agent ?? null
|
|
178
|
+
const cwd = agent?.session?.header?.cwd ?? ''
|
|
179
|
+
// delegate first so later listeners keep veto power, then fold ours on
|
|
180
|
+
if (typeof next === 'function') await next()
|
|
181
|
+
if (!cwd) return { kind: 'enter', messages } as const
|
|
182
|
+
const root = await recursive.resolveRootForRoute(undefined, cwd, undefined)
|
|
183
|
+
if (!root) return { kind: 'enter', messages } as const
|
|
184
|
+
const runs = enumerateRuns(root)
|
|
185
|
+
if (runs.length === 0) return { kind: 'enter', messages } as const
|
|
186
|
+
const runId = runs[runs.length - 1]
|
|
187
|
+
const runDir = join(root, '.recursive', 'run', runId)
|
|
188
|
+
if (!existsSync(runDir)) return { kind: 'enter', messages } as const
|
|
189
|
+
const phase = getNextLegalPhase(runDir)
|
|
190
|
+
if (!phase) return { kind: 'enter', messages } as const
|
|
191
|
+
// only inject when the phase doc is DRAFT (not locked/missing).
|
|
192
|
+
const phasePath = join(runDir, phase)
|
|
193
|
+
const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
|
|
194
|
+
if (status !== 'DRAFT') return { kind: 'enter', messages } as const
|
|
195
|
+
const reminder = phaseLintRulesMessage(phase)
|
|
196
|
+
return {
|
|
197
|
+
kind: 'enter',
|
|
198
|
+
messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
|
|
199
|
+
} as const
|
|
232
200
|
}))
|
|
233
201
|
}
|
|
234
202
|
|
|
235
|
-
// Phase
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
ctx.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
203
|
+
// Phase C R8: fs/observed lock-tamper WARNINGS are served to the board via the
|
|
204
|
+
// mountOnce-global: apply() runs per-session, but the route must register
|
|
205
|
+
// exactly once (WebServer.register throws on duplicate kind+path) and serve
|
|
206
|
+
// PER-WORKSPACE state. No-op when the host composes no webServer (headless).
|
|
207
|
+
const webServer = ctx.get('webServer') as
|
|
208
|
+
| { register: (route: { kind: string; path: string; handler: unknown }) => () => void }
|
|
209
|
+
| undefined
|
|
210
|
+
const sessionsStore = ctx.get('sessions') as
|
|
211
|
+
| { get?: (id: string) => { header?: { cwd?: string } } | undefined }
|
|
212
|
+
| undefined
|
|
213
|
+
if (webServer) {
|
|
214
|
+
const host: RecursiveRouteHost = {
|
|
215
|
+
// sessionId PRIMARY: the host resolves cwd from the attached session header;
|
|
216
|
+
// the client-passed cwd is a fallback hint (hydration / headless).
|
|
217
|
+
resolveRoot: async (sessionId: string | undefined, cwd: string) => recursive.resolveRootForRoute(sessionId, cwd, sessionsStore),
|
|
218
|
+
snapshot: async (root: string) => snapshotWorkspace(root),
|
|
219
|
+
revision: () => 1,
|
|
247
220
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Stage B (R6): agent/session-start workflow init, workspace-scoped.
|
|
251
|
-
// NEW sessions emit recursive/run-state('active') after init; resumed
|
|
252
|
-
// sessions replay the projection (already folded from the persisted log) —
|
|
253
|
-
// no duplicate run-created. run-created is emitted by the lifecycle at the
|
|
254
|
-
// init/lock boundary (the only place a run's files first exist).
|
|
255
|
-
const events = ctx as unknown as { on?: (event: string, listener: (payload: unknown) => void) => () => void }
|
|
256
|
-
if (events.on) {
|
|
257
|
-
disposers.push(events.on('agent/session-start', (payload) => {
|
|
258
|
-
const p = payload as { agent?: AgentLike | null; source?: 'new' | 'resume' }
|
|
259
|
-
const session = sessionOf(p)
|
|
260
|
-
const cwd = p?.agent?.session?.header?.cwd
|
|
261
|
-
if (!cwd) return
|
|
262
|
-
void (async () => {
|
|
263
|
-
const root = await recursive.resolveRootFor(p?.agent as { session?: { header?: { cwd?: string } } } | null)
|
|
264
|
-
if (!root) return
|
|
265
|
-
const result = stageBWorkflowInit({ root, source: p.source === 'resume' ? 'resume' : 'new' })
|
|
266
|
-
// Emit run-state for the active run (from the projected wire value, the
|
|
267
|
-
// one same-source view) when the projection registry is present.
|
|
268
|
-
if (session) {
|
|
269
|
-
const runId = result.activeRunId ?? result.runs[result.runs.length - 1] ?? ''
|
|
270
|
-
if (runId) {
|
|
271
|
-
const snap = sessionProjections?.snapshot?.(session)
|
|
272
|
-
const proj = (snap?.values as { recursive?: Record<string, Record<string, { state?: string }>> } | undefined)?.recursive
|
|
273
|
-
const state = proj?.[root]?.[runId]?.state ?? 'active'
|
|
274
|
-
emit(session, 'recursive/run-state', runStateEvent({ runId, worktreeRoot: root, state: state as 'new' | 'active' | 'paused' | 'blocked' | 'complete' }))
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
})()
|
|
278
|
-
}))
|
|
221
|
+
disposers.push(mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes(host), webServer))
|
|
279
222
|
}
|
|
280
223
|
|
|
281
224
|
yield () => { for (const d of disposers) d() }
|