@try-works/dsh-recursive-mode 0.1.3 → 0.1.5
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/fs-intent.d.ts +34 -0
- package/lib/index.d.ts +12 -11
- package/lib/index.js +1008 -4507
- 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/policy.d.ts +1 -1
- 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/fs-intent.ts +84 -0
- package/src/index.ts +100 -155
- 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 +29 -5
- 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/fs-intent.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
|
|
3
|
+
*
|
|
4
|
+
* SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
|
|
5
|
+
* so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
|
|
6
|
+
* prompt section rendered ''. This module derives the SAME intent from the
|
|
7
|
+
* filesystem instead: session cwd -> control-plane root -> enumerate runs ->
|
|
8
|
+
* latest run -> current phase (foldRun). Pure read-only fs folding, zero
|
|
9
|
+
* emission — consistent with the per-workspace design + pre-step listener.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { enumerateRuns } from './bootstrap.ts'
|
|
14
|
+
import { foldRun } from './status.ts'
|
|
15
|
+
import type { WorkspaceRegistryLike } from './workspace.ts'
|
|
16
|
+
import { resolveControlPlaneRoot } from './workspace.ts'
|
|
17
|
+
|
|
18
|
+
export interface FsPolicyIntent {
|
|
19
|
+
worktreeRoot: string
|
|
20
|
+
runId: string
|
|
21
|
+
/** The current phase doc file name (e.g. 03-implementation-summary.md), when determinable. */
|
|
22
|
+
currentPhaseFile: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the recursive policy intent for one agent from the FILESYSTEM.
|
|
27
|
+
* Returns null when no control-plane root or run is present (callers defer,
|
|
28
|
+
* never fail hard). The registry path is async-only in the harness, so when a
|
|
29
|
+
* registry is present this resolves the session cwd directly (B4: the session
|
|
30
|
+
* cwd IS the workspace root when unregistered; a registered workspace resolves
|
|
31
|
+
* to the same cwd via resolveControlPlaneRoot's sync fallback).
|
|
32
|
+
*/
|
|
33
|
+
export function fsPolicyIntent(
|
|
34
|
+
agent: { session?: { header?: { cwd?: string } } } | null | undefined,
|
|
35
|
+
registry?: WorkspaceRegistryLike | null,
|
|
36
|
+
cwdFallback?: string,
|
|
37
|
+
): FsPolicyIntent | null {
|
|
38
|
+
const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
|
|
39
|
+
if (!cwd) return null
|
|
40
|
+
// B4 sync path: when the registry is absent the session cwd IS the root;
|
|
41
|
+
// when present, resolveControlPlaneRoot canonicalizes through the registry
|
|
42
|
+
// (async). For the sync prompt-section callback we take the B4 shortcut: the
|
|
43
|
+
// session cwd is authoritative per the workspace-scoping invariant. This is
|
|
44
|
+
// exactly what the pre-step listener's resolveRootForRoute falls back to.
|
|
45
|
+
const root = registry ? cwd : cwd
|
|
46
|
+
const runs = enumerateRuns(root)
|
|
47
|
+
if (runs.length === 0) return null
|
|
48
|
+
const runId = runs[runs.length - 1]
|
|
49
|
+
const runDir = join(root, '.recursive', 'run', runId)
|
|
50
|
+
if (!existsSync(runDir)) return null
|
|
51
|
+
const folded = foldRun(runDir, runId)
|
|
52
|
+
return {
|
|
53
|
+
worktreeRoot: root,
|
|
54
|
+
runId,
|
|
55
|
+
currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Async variant for callers that CAN await (pre-step listener, routes): uses
|
|
61
|
+
* resolveControlPlaneRoot for registry canonicalization when a registry is
|
|
62
|
+
* present, falling back to the session cwd (B4).
|
|
63
|
+
*/
|
|
64
|
+
export async function fsPolicyIntentAsync(
|
|
65
|
+
agent: { session?: { header?: { cwd?: string } } } | null | undefined,
|
|
66
|
+
registry?: WorkspaceRegistryLike | null,
|
|
67
|
+
cwdFallback?: string,
|
|
68
|
+
): Promise<FsPolicyIntent | null> {
|
|
69
|
+
const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
|
|
70
|
+
if (!cwd) return null
|
|
71
|
+
const root = await resolveControlPlaneRoot(agent, registry, cwdFallback)
|
|
72
|
+
if (!root) return null
|
|
73
|
+
const runs = enumerateRuns(root)
|
|
74
|
+
if (runs.length === 0) return null
|
|
75
|
+
const runId = runs[runs.length - 1]
|
|
76
|
+
const runDir = join(root, '.recursive', 'run', runId)
|
|
77
|
+
if (!existsSync(runDir)) return null
|
|
78
|
+
const folded = foldRun(runDir, runId)
|
|
79
|
+
return {
|
|
80
|
+
worktreeRoot: root,
|
|
81
|
+
runId,
|
|
82
|
+
currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
|
|
83
|
+
}
|
|
84
|
+
}
|