@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.
Files changed (126) 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/fs-intent.d.ts +34 -0
  14. package/lib/index.d.ts +12 -11
  15. package/lib/index.js +1008 -4507
  16. package/lib/init-templates.d.ts +41 -0
  17. package/lib/live-route.d.ts +69 -0
  18. package/lib/phase-rules.d.ts +34 -0
  19. package/lib/policy.d.ts +1 -1
  20. package/lib/runtime.d.ts +24 -17
  21. package/lib/snapshot.d.ts +11 -0
  22. package/package.json +4 -3
  23. package/preset/recursive/agent.cordis.yml +7 -4
  24. package/preset/recursive/preset.yml +1 -1
  25. package/references/agents-block.md +95 -0
  26. package/references/artifact-template.md +2574 -0
  27. package/references/bodies/claude.md +7 -0
  28. package/references/bodies/codex-agents.md +95 -0
  29. package/references/bodies/copilot.md +7 -0
  30. package/references/bodies/cursorrules.md +7 -0
  31. package/references/bodies/decisions.md +5 -0
  32. package/references/bodies/delegated-verification.md +49 -0
  33. package/references/bodies/memory-router.md +45 -0
  34. package/references/bodies/phase8-skill-memory.md +48 -0
  35. package/references/bodies/plans-bridge.md +43 -0
  36. package/references/bodies/recursive-agents-router.md +64 -0
  37. package/references/bodies/skill-discovery.md +51 -0
  38. package/references/bodies/skill-memory-router.md +42 -0
  39. package/references/bodies/state.md +5 -0
  40. package/references/bootstrap/RECURSIVE.md +2362 -0
  41. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  42. package/references/scripts/lint-recursive-run.ps1 +25 -0
  43. package/references/scripts/lint-recursive-run.py +2870 -0
  44. package/references/scripts/recursive-closeout.ps1 +38 -0
  45. package/references/scripts/recursive-closeout.py +541 -0
  46. package/references/scripts/recursive-init.ps1 +515 -0
  47. package/references/scripts/recursive-init.py +356 -0
  48. package/references/scripts/recursive-lock.ps1 +26 -0
  49. package/references/scripts/recursive-lock.py +302 -0
  50. package/references/scripts/recursive-review-bundle.ps1 +58 -0
  51. package/references/scripts/recursive-review-bundle.py +503 -0
  52. package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
  53. package/references/scripts/recursive-router-cli-configure.py +9 -0
  54. package/references/scripts/recursive-router-cli-init.ps1 +2 -0
  55. package/references/scripts/recursive-router-cli-init.py +9 -0
  56. package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
  57. package/references/scripts/recursive-router-cli-invoke.py +9 -0
  58. package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
  59. package/references/scripts/recursive-router-cli-probe.py +9 -0
  60. package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
  61. package/references/scripts/recursive-router-cli-resolve.py +9 -0
  62. package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
  63. package/references/scripts/recursive-router-cli-validate.py +9 -0
  64. package/references/scripts/recursive-router-configure.ps1 +27 -0
  65. package/references/scripts/recursive-router-configure.py +74 -0
  66. package/references/scripts/recursive-router-init.ps1 +17 -0
  67. package/references/scripts/recursive-router-init.py +29 -0
  68. package/references/scripts/recursive-router-invoke.ps1 +47 -0
  69. package/references/scripts/recursive-router-invoke.py +103 -0
  70. package/references/scripts/recursive-router-probe.ps1 +25 -0
  71. package/references/scripts/recursive-router-probe.py +44 -0
  72. package/references/scripts/recursive-router-resolve.ps1 +26 -0
  73. package/references/scripts/recursive-router-resolve.py +46 -0
  74. package/references/scripts/recursive-router-validate.ps1 +17 -0
  75. package/references/scripts/recursive-router-validate.py +27 -0
  76. package/references/scripts/recursive-status.ps1 +23 -0
  77. package/references/scripts/recursive-status.py +2124 -0
  78. package/references/scripts/recursive-subagent-action.ps1 +98 -0
  79. package/references/scripts/recursive-subagent-action.py +197 -0
  80. package/references/scripts/recursive-training-extract.ps1 +23 -0
  81. package/references/scripts/recursive-training-extract.py +99 -0
  82. package/references/scripts/recursive-training-grpo.ps1 +52 -0
  83. package/references/scripts/recursive-training-grpo.py +1341 -0
  84. package/references/scripts/recursive-training-loader.ps1 +78 -0
  85. package/references/scripts/recursive-training-loader.py +558 -0
  86. package/references/scripts/recursive-training-mcp.ps1 +29 -0
  87. package/references/scripts/recursive-training-mcp.py +261 -0
  88. package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
  89. package/references/scripts/recursive-training-phase8-trigger.py +137 -0
  90. package/references/scripts/recursive-training-sync.ps1 +29 -0
  91. package/references/scripts/recursive-training-sync.py +183 -0
  92. package/references/scripts/recursive_phase_rules.py +367 -0
  93. package/references/scripts/recursive_router_cli_lib.py +2 -0
  94. package/references/scripts/recursive_router_lib.py +2282 -0
  95. package/references/scripts/verify-locks.ps1 +25 -0
  96. package/references/scripts/verify-locks.py +353 -0
  97. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  98. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  99. package/scripts/install-preset.cmd +7 -0
  100. package/scripts/install-preset.js +101 -0
  101. package/scripts/lint-recursive-run.py +7 -5
  102. package/scripts/test-recursive-mode-smoke.ts +29 -28
  103. package/src/bootstrap.ts +253 -39
  104. package/src/client/apply-guard.ts +23 -0
  105. package/src/client/board.tsx +0 -0
  106. package/src/client/contract.ts +72 -24
  107. package/src/client/host-api.ts +90 -0
  108. package/src/client/index.ts +22 -11
  109. package/src/client/inspector.tsx +7 -9
  110. package/src/client/slots.ts +70 -26
  111. package/src/client/strip.tsx +9 -9
  112. package/src/client/use-live.ts +52 -0
  113. package/src/fs-intent.ts +84 -0
  114. package/src/index.ts +100 -155
  115. package/src/init-templates.ts +276 -0
  116. package/src/live-route.ts +163 -0
  117. package/src/phase-rules.ts +262 -0
  118. package/src/policy.ts +29 -5
  119. package/src/runtime.ts +129 -77
  120. package/src/snapshot.ts +104 -0
  121. package/lib/client/node.d.ts +0 -71
  122. package/lib/events.d.ts +0 -173
  123. package/lib/projection.d.ts +0 -29
  124. package/src/client/node.ts +0 -156
  125. package/src/events.ts +0 -173
  126. package/src/projection.ts +0 -237
@@ -1,23 +1,21 @@
1
1
  /**
2
- * Drill-down inspector (Phase D R6, §11.5): the details-seat swap rendering
3
- * one run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows),
4
- * lock badges, and tamper/gate facts — all from the projection, never disk.
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 { RecursiveProjection } from '../types.ts'
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
- useProjection: RecursiveProjectionReader
13
+ snapshot: LiveProjectionValue | null
15
14
  onBackToToolDetails: () => void
16
15
  }
17
16
 
18
- export function Inspector({ runId, worktreeRoot, useProjection, onBackToToolDetails }: InspectorProps) {
19
- const projection = useProjection('recursive') as RecursiveProjection | undefined
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),
@@ -1,30 +1,43 @@
1
1
  /**
2
- * Slot registrations (Phase D R5/R6/R7/R8, §11.8): the sidebar launcher, the
3
- * shell.overlay board, the conversation.input.dock status strip, and the
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
- * Self-contained structural seam: the real slot registry + runtime standard
8
- * kit arrive from @deepseek-ai/dsh-client-ui-slots + @deepseek-ai/dsh-client-runtime
9
- * at compose time; this module consumes the minimal ClientContext shape
10
- * (contract.ts). The client is READ-ONLY (R9) — no filesystem, no mutation.
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, RecursiveProjectionReader } from './contract.ts'
14
- import type { RecursiveProjection } from '../types.ts'
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
- /** Narrow the projection whole value to the worktree-grouped run map. */
20
- function projectionOf(reader: RecursiveProjectionReader): RecursiveProjection | undefined {
21
- return reader('recursive') as RecursiveProjection | undefined
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: { useProjection?: RecursiveProjectionReader }) => {
41
- if (props?.useProjection === undefined) return null
42
- return createElement(Board, { useProjection: props.useProjection })
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: { useProjection?: RecursiveProjectionReader }) => {
51
- if (props?.useProjection === undefined) return null
52
- return createElement(StatusStrip, { useProjection: props.useProjection })
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
- /** Export the projection reader narrow for the board/inspector/strip. */
67
- export { projectionOf }
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 }
@@ -1,21 +1,21 @@
1
1
  /**
2
- * Status strip (Phase D R7, §11.6): a thin glance at the active run's phase,
3
- * gate state, and tamper badge, docked at conversation.input.dock. Renders
4
- * nothing when no run is current.
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 { RecursiveProjection } from '../types.ts'
10
- import type { RecursiveProjectionReader } from './contract.ts'
9
+ import type { LiveProjectionValue } from './contract.ts'
11
10
 
12
11
  export interface StripProps {
13
- useProjection: RecursiveProjectionReader
12
+ /** The live host-route snapshot (null = not loaded / no recursive root). */
13
+ snapshot: LiveProjectionValue | null
14
14
  }
15
15
 
16
- export function StatusStrip({ useProjection }: StripProps) {
17
- const projection = useProjection('recursive') as RecursiveProjection | undefined
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
+ }
@@ -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
+ }