@try-works/dsh-recursive-mode 0.1.0

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 (88) hide show
  1. package/cordis.patch.yml +12 -0
  2. package/lib/bootstrap.d.ts +35 -0
  3. package/lib/client/board.d.ts +10 -0
  4. package/lib/client/contract.d.ts +51 -0
  5. package/lib/client/derive.d.ts +92 -0
  6. package/lib/client/index.d.ts +21 -0
  7. package/lib/client/inspector.d.ts +10 -0
  8. package/lib/client/node.d.ts +71 -0
  9. package/lib/client/settings.d.ts +6 -0
  10. package/lib/client/slots.d.ts +7 -0
  11. package/lib/client/strip.d.ts +7 -0
  12. package/lib/client.d.ts +10 -0
  13. package/lib/client.js +490 -0
  14. package/lib/closeout.d.ts +23 -0
  15. package/lib/commands.d.ts +51 -0
  16. package/lib/delegation.d.ts +92 -0
  17. package/lib/enforcement.d.ts +53 -0
  18. package/lib/events.d.ts +173 -0
  19. package/lib/handoff.d.ts +51 -0
  20. package/lib/index.d.ts +40 -0
  21. package/lib/lifecycle.d.ts +107 -0
  22. package/lib/lock.d.ts +92 -0
  23. package/lib/policy.d.ts +12 -0
  24. package/lib/projection.d.ts +29 -0
  25. package/lib/recursive_closeout.tool.d.ts +8 -0
  26. package/lib/recursive_init.tool.d.ts +2 -0
  27. package/lib/recursive_lint.tool.d.ts +2 -0
  28. package/lib/recursive_lock.tool.d.ts +2 -0
  29. package/lib/recursive_scratch.tool.d.ts +7 -0
  30. package/lib/recursive_status.tool.d.ts +2 -0
  31. package/lib/review.d.ts +39 -0
  32. package/lib/router.d.ts +77 -0
  33. package/lib/run.d.ts +29 -0
  34. package/lib/runtime.d.ts +241 -0
  35. package/lib/scratch.d.ts +18 -0
  36. package/lib/status.d.ts +19 -0
  37. package/lib/types.d.ts +104 -0
  38. package/lib/workspace.d.ts +50 -0
  39. package/package.json +119 -0
  40. package/preset/recursive/agent.cordis.yml +282 -0
  41. package/preset/recursive/preset.yml +3 -0
  42. package/scripts/install-recursive-mode.ps1 +956 -0
  43. package/scripts/install-recursive-mode.py +750 -0
  44. package/scripts/lint-recursive-run.py +2868 -0
  45. package/scripts/recursive-closeout.py +541 -0
  46. package/scripts/recursive-init.py +356 -0
  47. package/scripts/recursive-lock.py +302 -0
  48. package/scripts/recursive-status.py +2124 -0
  49. package/scripts/recursive_phase_rules.py +367 -0
  50. package/scripts/recursive_router_lib.py +2282 -0
  51. package/scripts/test-recursive-mode-smoke.ts +204 -0
  52. package/scripts/verify-locks.py +353 -0
  53. package/src/bootstrap.ts +118 -0
  54. package/src/client/board.tsx +61 -0
  55. package/src/client/contract.ts +58 -0
  56. package/src/client/derive.ts +241 -0
  57. package/src/client/index.ts +28 -0
  58. package/src/client/inspector.tsx +49 -0
  59. package/src/client/node.ts +156 -0
  60. package/src/client/settings.tsx +18 -0
  61. package/src/client/slots.ts +67 -0
  62. package/src/client/strip.tsx +28 -0
  63. package/src/client.ts +11 -0
  64. package/src/closeout.ts +183 -0
  65. package/src/commands.ts +142 -0
  66. package/src/delegation.ts +306 -0
  67. package/src/enforcement.ts +180 -0
  68. package/src/events.ts +173 -0
  69. package/src/handoff.ts +165 -0
  70. package/src/index.ts +283 -0
  71. package/src/lifecycle.ts +235 -0
  72. package/src/lock.ts +369 -0
  73. package/src/policy.ts +56 -0
  74. package/src/projection.ts +237 -0
  75. package/src/recursive_closeout.tool.ts +35 -0
  76. package/src/recursive_init.tool.ts +28 -0
  77. package/src/recursive_lint.tool.ts +29 -0
  78. package/src/recursive_lock.tool.ts +33 -0
  79. package/src/recursive_scratch.tool.ts +42 -0
  80. package/src/recursive_status.tool.ts +24 -0
  81. package/src/review.ts +178 -0
  82. package/src/router.ts +197 -0
  83. package/src/run.ts +85 -0
  84. package/src/runtime.ts +564 -0
  85. package/src/scratch.ts +85 -0
  86. package/src/status.ts +194 -0
  87. package/src/types.ts +112 -0
  88. package/src/workspace.ts +67 -0
package/src/status.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFileSync, statSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import type { ArtifactState, PhaseDef, PhaseState, RecursiveStatusResult } from './types.ts'
5
+
6
+ export const RUN_ARTIFACT_SEQUENCE = [
7
+ '00-requirements.md',
8
+ '00-worktree.md',
9
+ '01-as-is.md',
10
+ '01.5-root-cause.md',
11
+ '02-to-be-plan.md',
12
+ '03-implementation-summary.md',
13
+ '03.5-code-review.md',
14
+ '04-test-summary.md',
15
+ '05-manual-qa.md',
16
+ '06-decisions-update.md',
17
+ '07-state-update.md',
18
+ '08-memory-impact.md',
19
+ ]
20
+
21
+ export const PHASES: PhaseDef[] = [
22
+ { key: '00R', label: 'Phase 0 (Requirements)', file: '00-requirements.md', optional: false, phaseName: '0 (Requirements)' },
23
+ { key: '00W', label: 'Phase 0 (Worktree)', file: '00-worktree.md', optional: false, phaseName: '0 (Worktree)' },
24
+ { key: '01', label: 'Phase 1 (AS-IS)', file: '01-as-is.md', optional: false, phaseName: '1 (AS-IS)' },
25
+ { key: '01.5', label: 'Phase 1.5 (Root Cause)', file: '01.5-root-cause.md', optional: true, phaseName: '1.5 (Root Cause)' },
26
+ { key: '02', label: 'Phase 2 (TO-BE Plan)', file: '02-to-be-plan.md', optional: false, phaseName: '2 (TO-BE Plan)' },
27
+ { key: '03', label: 'Phase 3 (Implementation)', file: '03-implementation-summary.md', optional: false, phaseName: '3 (Implementation)' },
28
+ { key: '03.5', label: 'Phase 3.5 (Code Review)', file: '03.5-code-review.md', optional: true, phaseName: '3.5 (Code Review)' },
29
+ { key: '04', label: 'Phase 4 (Test Summary)', file: '04-test-summary.md', optional: false, phaseName: '4 (Test Summary)' },
30
+ { key: '05', label: 'Phase 5 (Manual QA)', file: '05-manual-qa.md', optional: false, phaseName: '5 (Manual QA)' },
31
+ { key: '06', label: 'Phase 6 (Decisions)', file: '06-decisions-update.md', optional: false, phaseName: '6 (Decisions Update)' },
32
+ { key: '07', label: 'Phase 7 (State)', file: '07-state-update.md', optional: false, phaseName: '7 (State Update)' },
33
+ { key: '08', label: 'Phase 8 (Memory)', file: '08-memory-impact.md', optional: false, phaseName: '8 (Memory Impact)' },
34
+ ]
35
+
36
+ const LOCK_HASH_LINE_RE = /^[ \t]*LockHash:.*(?:\n|$)/gm
37
+ const STRICT_WORKFLOW_PROFILES = new Set(['recursive-mode-audit-v1', 'recursive-mode-audit-v2'])
38
+ const AUDITED_PHASE_FILES = new Set([
39
+ '01-as-is.md', '01.5-root-cause.md', '02-to-be-plan.md', '03-implementation-summary.md',
40
+ '03.5-code-review.md', '04-test-summary.md', '06-decisions-update.md', '07-state-update.md', '08-memory-impact.md',
41
+ ])
42
+ const CURRENT_WORKFLOW_PROFILE = 'recursive-mode-audit-v2'
43
+ const STRICT_WORKFLOW_PROFILE = 'recursive-mode-audit-v1'
44
+ const COMPAT_WORKFLOW_PROFILE = 'memory-phase8'
45
+ const LATE_PHASE_FILES = ['06-decisions-update.md', '07-state-update.md', '08-memory-impact.md']
46
+ const LATE_PHASE_KEYS = new Set(['06', '07', '08'])
47
+
48
+ export function escapeRegExp(value: string): string {
49
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
50
+ }
51
+
52
+ export function trimMdValue(value: string): string {
53
+ let trimmed = value.trim()
54
+ for (const quote of ['`', '"', "'"]) {
55
+ if (trimmed.startsWith(quote) && trimmed.endsWith(quote) && trimmed.length >= 2) {
56
+ const inner = trimmed.slice(1, -1)
57
+ if (!inner.includes(quote)) return inner.trim()
58
+ }
59
+ }
60
+ return trimmed
61
+ }
62
+
63
+ export function getMdFieldValue(content: string, fieldName: string): string | null {
64
+ const pattern = new RegExp(`^[ \\t]*(?:[-*][ \\t]+)?${escapeRegExp(fieldName)}:[ \\t]*(.+?)\\s*$`, 'm')
65
+ const match = pattern.exec(content)
66
+ if (!match) return null
67
+ return trimMdValue(match[1])
68
+ }
69
+
70
+ export function getGateStatus(content: string, gateName: string): string {
71
+ const pattern = new RegExp(`^[ \\t]*${escapeRegExp(gateName)}:\\s*(PASS|FAIL)\\s*$`, 'm')
72
+ const match = pattern.exec(content)
73
+ return match ? match[1].toUpperCase() : 'MISSING'
74
+ }
75
+
76
+ export function getTodoStats(content: string): { hasTodo: boolean; total: number; checked: number; unchecked: number } {
77
+ const lines = content.split(/\r?\n/)
78
+ let inTodo = false
79
+ let hasTodo = false
80
+ let checked = 0
81
+ let unchecked = 0
82
+ let total = 0
83
+ for (const line of lines) {
84
+ if (!inTodo) {
85
+ if (/^\s*##\s+TODO\s*$/.test(line)) { inTodo = true; hasTodo = true }
86
+ continue
87
+ }
88
+ if (/^\s*##\s+/.test(line) || /^\s*#\s+/.test(line)) break
89
+ const item = /^\s*[-*]\s+\[([ xX])\]\s+/.exec(line)
90
+ if (item) {
91
+ total += 1
92
+ if (item[1].toLowerCase() === 'x') checked += 1
93
+ else unchecked += 1
94
+ }
95
+ }
96
+ return { hasTodo, total, checked, unchecked }
97
+ }
98
+
99
+ export function lockHashFromContent(content: string): string {
100
+ const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
101
+ const stripped = normalized.replace(LOCK_HASH_LINE_RE, '')
102
+ return createHash('sha256').update(stripped, 'utf8').digest('hex')
103
+ }
104
+
105
+ export function getWorkflowProfile(runDir: string): string {
106
+ const requirementsPath = join(runDir, '00-requirements.md')
107
+ try {
108
+ const content = readFileSync(requirementsPath, 'utf8')
109
+ const workflowVersion = getMdFieldValue(content, 'Workflow version')
110
+ if (workflowVersion === CURRENT_WORKFLOW_PROFILE) return CURRENT_WORKFLOW_PROFILE
111
+ if (workflowVersion === STRICT_WORKFLOW_PROFILE) return STRICT_WORKFLOW_PROFILE
112
+ if (workflowVersion === COMPAT_WORKFLOW_PROFILE) return COMPAT_WORKFLOW_PROFILE
113
+ } catch { /* fall through */ }
114
+ if (LATE_PHASE_FILES.some(file => { try { statSync(join(runDir, file)); return true } catch { return false } })) return COMPAT_WORKFLOW_PROFILE
115
+ return 'legacy'
116
+ }
117
+
118
+ export { getLatestRunDirectory, discoverRuns, resolveRunDir } from './run.ts'
119
+ export type { RunDiscoveryResult } from './run.ts'
120
+
121
+ export function getArtifactState(artifactPath: string, workflowProfile: string): ArtifactState {
122
+ let content: string | null = null
123
+ try { content = readFileSync(artifactPath, 'utf8') } catch { /* missing */ }
124
+ if (content === null) {
125
+ return { exists: false, status: 'PENDING', lockValid: false, lockProblems: ['File missing'], blockers: ['File missing'], lockedAt: null, storedHash: null, actualHash: null, coverage: 'MISSING', approval: 'MISSING', audit: 'MISSING', todoHasSection: false, todoUnchecked: 0 }
126
+ }
127
+ const status = getMdFieldValue(content, 'Status') ?? 'UNKNOWN'
128
+ const todo = getTodoStats(content)
129
+ const coverage = getGateStatus(content, 'Coverage')
130
+ const approval = getGateStatus(content, 'Approval')
131
+ const audit = getGateStatus(content, 'Audit')
132
+ const tddCompliance = getGateStatus(content, 'TDD Compliance')
133
+ const lockedAt = getMdFieldValue(content, 'LockedAt')
134
+ const storedHash = getMdFieldValue(content, 'LockHash')
135
+ let actualHash: string | null = null
136
+ const lockProblems: string[] = []
137
+ const blockers: string[] = []
138
+ let lockValid = false
139
+ const fileName = artifactPath.split(/[\\/]/).pop() ?? ''
140
+
141
+ if (status !== 'LOCKED') {
142
+ lockProblems.push(`Status is '${status}' (expected LOCKED for lock-valid)`)
143
+ } else {
144
+ if (!lockedAt) lockProblems.push('Missing LockedAt')
145
+ if (!storedHash) lockProblems.push('Missing LockHash')
146
+ if (storedHash) {
147
+ actualHash = lockHashFromContent(content)
148
+ if (storedHash.toLowerCase() !== actualHash.toLowerCase()) lockProblems.push('LockHash mismatch')
149
+ }
150
+ if (coverage !== 'PASS') lockProblems.push(`Coverage gate is ${coverage}`)
151
+ if (approval !== 'PASS') lockProblems.push(`Approval gate is ${approval}`)
152
+ if (STRICT_WORKFLOW_PROFILES.has(workflowProfile) && AUDITED_PHASE_FILES.has(fileName) && audit !== 'PASS') lockProblems.push(`Audit gate is ${audit}`)
153
+ if (fileName === '03-implementation-summary.md' && tddCompliance !== 'PASS') lockProblems.push(`TDD Compliance gate is ${tddCompliance}`)
154
+ if (!todo.hasTodo) lockProblems.push('Missing ## TODO section')
155
+ else if (todo.unchecked > 0) lockProblems.push(`Unchecked TODO items: ${todo.unchecked}`)
156
+ if (lockProblems.length === 0) lockValid = true
157
+ }
158
+
159
+ if (!todo.hasTodo) blockers.push('Missing ## TODO section')
160
+ else if (todo.unchecked > 0) blockers.push(`Unchecked TODO items: ${todo.unchecked}`)
161
+ if (coverage !== 'PASS') blockers.push(`Coverage gate is ${coverage}`)
162
+ if (approval !== 'PASS') blockers.push(`Approval gate is ${approval}`)
163
+ if (fileName === '03-implementation-summary.md' && tddCompliance !== 'PASS') blockers.push(`TDD Compliance gate is ${tddCompliance}`)
164
+
165
+ const dedupedBlockers: string[] = []
166
+ for (const blocker of blockers) if (!dedupedBlockers.includes(blocker)) dedupedBlockers.push(blocker)
167
+
168
+ return { exists: true, status, lockValid, lockProblems, blockers: dedupedBlockers, lockedAt, storedHash, actualHash, coverage, approval, audit, todoHasSection: todo.hasTodo, todoUnchecked: todo.unchecked }
169
+ }
170
+
171
+ export function foldRun(runDir: string, runId: string): RecursiveStatusResult {
172
+ const workflowProfile = getWorkflowProfile(runDir)
173
+ const states = new Map<string, ArtifactState>()
174
+ for (const phase of PHASES) {
175
+ const state = getArtifactState(join(runDir, phase.file), workflowProfile)
176
+ if (phase.optional && !state.exists) state.status = 'SKIPPED'
177
+ else if (workflowProfile === 'legacy' && LATE_PHASE_KEYS.has(phase.key) && !state.exists) state.status = 'SKIPPED'
178
+ states.set(phase.key, state)
179
+ }
180
+ let currentPhase: RecursiveStatusResult['currentPhase'] = null
181
+ for (const phase of PHASES) {
182
+ const state = states.get(phase.key)!
183
+ if (state.status === 'SKIPPED') continue
184
+ if (!state.exists || !state.lockValid) {
185
+ currentPhase = { key: phase.key, label: phase.label, phaseName: phase.phaseName, status: state.status }
186
+ break
187
+ }
188
+ }
189
+ const phases: PhaseState[] = PHASES.map(phase => {
190
+ const state = states.get(phase.key)!
191
+ return { key: phase.key, label: phase.label, file: phase.file, optional: phase.optional, exists: state.exists, status: state.status, lockValid: state.lockValid, lockProblems: state.lockProblems, blockers: state.blockers }
192
+ })
193
+ return { runId, currentPhase, phases, workflowProfile }
194
+ }
package/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ export interface PhaseDef {
2
+ key: string
3
+ label: string
4
+ file: string
5
+ optional: boolean
6
+ phaseName: string
7
+ }
8
+
9
+ export interface ArtifactState {
10
+ exists: boolean
11
+ status: string
12
+ lockValid: boolean
13
+ lockProblems: string[]
14
+ blockers: string[]
15
+ lockedAt: string | null
16
+ storedHash: string | null
17
+ actualHash: string | null
18
+ coverage: string
19
+ approval: string
20
+ audit: string
21
+ todoHasSection: boolean
22
+ todoUnchecked: number
23
+ }
24
+
25
+ export interface PhaseState {
26
+ key: string
27
+ label: string
28
+ file: string
29
+ optional: boolean
30
+ exists: boolean
31
+ status: string
32
+ lockValid: boolean
33
+ lockProblems: string[]
34
+ blockers: string[]
35
+ }
36
+
37
+ export interface RecursiveStatusResult {
38
+ runId: string
39
+ currentPhase: { key: string; label: string; phaseName: string; status: string } | null
40
+ phases: PhaseState[]
41
+ workflowProfile: string
42
+ }
43
+
44
+ /* ================= Phase D wire vocabulary (run 06 R3, PROPOSAL 11.4-11.7) === */
45
+
46
+ /** Run-level durable state (mirrors lifecycle.ts RUN_STATES; single pure-type home for the wire). */
47
+ export type RecursiveRunState = 'new' | 'active' | 'paused' | 'blocked' | 'complete'
48
+
49
+ /** One locked-phase fact carried by a recursive/phase-locked event. */
50
+ export interface RecursivePhaseLock {
51
+ lockedAt: string
52
+ lockHash: string
53
+ }
54
+
55
+ /** One gate failure fact carried by a recursive/gate-blocked event. */
56
+ export interface RecursiveGateBlock {
57
+ failures: string[]
58
+ kind: string
59
+ }
60
+
61
+ /** One tamper fact carried by a recursive/tamper event. */
62
+ export interface RecursiveTamper {
63
+ path: string
64
+ reason: string
65
+ }
66
+
67
+ /** One subagent activity fact (start or end). */
68
+ export interface RecursiveSubagent {
69
+ childId: string
70
+ role: string
71
+ provider: string
72
+ status?: 'running' | 'done' | 'failed'
73
+ }
74
+
75
+ /** One phase row in a run card's folded phase chain. */
76
+ export interface RecursivePhaseRow {
77
+ phase: string
78
+ status: string
79
+ lockedAt?: string
80
+ lockHash?: string
81
+ }
82
+
83
+ /**
84
+ * The per-run wire card the recursive projection folds: the board, inspector,
85
+ * node, and strip all render from this whole value — never from the file tree.
86
+ */
87
+ export interface RecursiveRunCard {
88
+ runId: string
89
+ worktreeRoot: string
90
+ repo?: string
91
+ template?: string
92
+ /** Folded phase chain: last-wins per phase key. */
93
+ phases: Record<string, RecursivePhaseRow>
94
+ /** Folded run-level state: last recursive/run-state wins (default 'active'). */
95
+ state: RecursiveRunState
96
+ stateReason?: string
97
+ /** Latest gate block (last-wins); absent when no block is in force. */
98
+ gateBlocked?: RecursiveGateBlock
99
+ /** Tamper facts (accumulated, latest wins per path). */
100
+ tampers: Record<string, RecursiveTamper>
101
+ /** Subagent activity (latest status wins per childId). */
102
+ subagents: Record<string, RecursiveSubagent>
103
+ /** Set on recursive/run-merged; the run re-keys to this root. */
104
+ mergedToRepoRoot?: string
105
+ }
106
+
107
+ /**
108
+ * The recursive projection whole value: runs grouped by worktree root first,
109
+ * then by runId. The board renders one lane per run; the client enumerates by
110
+ * worktree root so it never crosses the workspace boundary.
111
+ */
112
+ export type RecursiveProjection = Record<string, Record<string, RecursiveRunCard>>
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Workspace-scoped control-plane root resolution (R1, binding invariant).
3
+ *
4
+ * The plugin NEVER scans `workspaceRegistry.list()` to find "the" workspace and
5
+ * NEVER reads another open workspace's `.recursive/` tree. The ONLY root-resolution
6
+ * path is: session canonical cwd (`agent.session.header.cwd`) ->
7
+ * `workspaceRegistry.resolveByPath(cwd)` -> the owning workspace's canonical `path`,
8
+ * which IS the control-plane root.
9
+ */
10
+
11
+ export interface WorkspaceLike {
12
+ readonly path: string
13
+ readonly id: string
14
+ }
15
+
16
+ export interface WorkspaceRegistryLike {
17
+ /** Resolve by canonical directory path without creating or mutating. */
18
+ resolveByPath(path: string): Promise<WorkspaceLike | undefined> | WorkspaceLike | undefined
19
+ /** MUST NOT be used by this plugin (workspace-scoping invariant). */
20
+ list?(): unknown
21
+ }
22
+
23
+ export interface WorkspaceResolver {
24
+ (cwd: string): Promise<string | null>
25
+ }
26
+
27
+ /**
28
+ * Build a resolver bound to a workspace registry. Returns the canonical workspace
29
+ * path for the given cwd, or `null` when the cwd is not a registered workspace
30
+ * (no repo -> defer, never fail hard).
31
+ */
32
+ export function makeWorkspaceResolver(registry: WorkspaceRegistryLike): WorkspaceResolver {
33
+ return async (cwd: string): Promise<string | null> => {
34
+ if (!cwd) return null
35
+ const workspace = await registry.resolveByPath(cwd)
36
+ return workspace?.path ?? null
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Resolve the control-plane root for an agent-like object with a session header
42
+ * carrying a canonical `cwd`. Degrades to `null` when the registry or cwd is
43
+ * unavailable (callers defer, never fail hard).
44
+ */
45
+ export async function resolveControlPlaneRoot(
46
+ agent: { session?: { header?: { cwd?: string } } } | null | undefined,
47
+ registry?: WorkspaceRegistryLike | null,
48
+ cwdFallback?: string,
49
+ ): Promise<string | null> {
50
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
51
+ if (!cwd) return null
52
+ // B4: the session header cwd IS the workspace/control-plane root when the
53
+ // registry is absent (headless/code-mode assemblies do not compose the
54
+ // workspace service). Registry, when present, canonicalizes; otherwise the
55
+ // session cwd is authoritative — never null just because the registry is
56
+ // missing, or scratch/closeout (and every other per-call read) would be
57
+ // permanently dead in headless hosts.
58
+ if (!registry) return cwd
59
+ return makeWorkspaceResolver(registry)(cwd)
60
+ }
61
+
62
+ /**
63
+ * Extract the canonical session cwd (for callers that pass it explicitly).
64
+ */
65
+ export function sessionCwd(agent: { session?: { header?: { cwd?: string } } } | null | undefined): string | null {
66
+ return agent?.session?.header?.cwd ?? null
67
+ }