@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.
Files changed (123) 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/index.d.ts +11 -11
  14. package/lib/index.js +972 -4504
  15. package/lib/init-templates.d.ts +41 -0
  16. package/lib/live-route.d.ts +69 -0
  17. package/lib/phase-rules.d.ts +34 -0
  18. package/lib/runtime.d.ts +24 -17
  19. package/lib/snapshot.d.ts +11 -0
  20. package/package.json +4 -3
  21. package/preset/recursive/agent.cordis.yml +7 -4
  22. package/preset/recursive/preset.yml +1 -1
  23. package/references/agents-block.md +95 -0
  24. package/references/artifact-template.md +2574 -0
  25. package/references/bodies/claude.md +7 -0
  26. package/references/bodies/codex-agents.md +95 -0
  27. package/references/bodies/copilot.md +7 -0
  28. package/references/bodies/cursorrules.md +7 -0
  29. package/references/bodies/decisions.md +5 -0
  30. package/references/bodies/delegated-verification.md +49 -0
  31. package/references/bodies/memory-router.md +45 -0
  32. package/references/bodies/phase8-skill-memory.md +48 -0
  33. package/references/bodies/plans-bridge.md +43 -0
  34. package/references/bodies/recursive-agents-router.md +64 -0
  35. package/references/bodies/skill-discovery.md +51 -0
  36. package/references/bodies/skill-memory-router.md +42 -0
  37. package/references/bodies/state.md +5 -0
  38. package/references/bootstrap/RECURSIVE.md +2362 -0
  39. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  40. package/references/scripts/lint-recursive-run.ps1 +25 -0
  41. package/references/scripts/lint-recursive-run.py +2870 -0
  42. package/references/scripts/recursive-closeout.ps1 +38 -0
  43. package/references/scripts/recursive-closeout.py +541 -0
  44. package/references/scripts/recursive-init.ps1 +515 -0
  45. package/references/scripts/recursive-init.py +356 -0
  46. package/references/scripts/recursive-lock.ps1 +26 -0
  47. package/references/scripts/recursive-lock.py +302 -0
  48. package/references/scripts/recursive-review-bundle.ps1 +58 -0
  49. package/references/scripts/recursive-review-bundle.py +503 -0
  50. package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
  51. package/references/scripts/recursive-router-cli-configure.py +9 -0
  52. package/references/scripts/recursive-router-cli-init.ps1 +2 -0
  53. package/references/scripts/recursive-router-cli-init.py +9 -0
  54. package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
  55. package/references/scripts/recursive-router-cli-invoke.py +9 -0
  56. package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
  57. package/references/scripts/recursive-router-cli-probe.py +9 -0
  58. package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
  59. package/references/scripts/recursive-router-cli-resolve.py +9 -0
  60. package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
  61. package/references/scripts/recursive-router-cli-validate.py +9 -0
  62. package/references/scripts/recursive-router-configure.ps1 +27 -0
  63. package/references/scripts/recursive-router-configure.py +74 -0
  64. package/references/scripts/recursive-router-init.ps1 +17 -0
  65. package/references/scripts/recursive-router-init.py +29 -0
  66. package/references/scripts/recursive-router-invoke.ps1 +47 -0
  67. package/references/scripts/recursive-router-invoke.py +103 -0
  68. package/references/scripts/recursive-router-probe.ps1 +25 -0
  69. package/references/scripts/recursive-router-probe.py +44 -0
  70. package/references/scripts/recursive-router-resolve.ps1 +26 -0
  71. package/references/scripts/recursive-router-resolve.py +46 -0
  72. package/references/scripts/recursive-router-validate.ps1 +17 -0
  73. package/references/scripts/recursive-router-validate.py +27 -0
  74. package/references/scripts/recursive-status.ps1 +23 -0
  75. package/references/scripts/recursive-status.py +2124 -0
  76. package/references/scripts/recursive-subagent-action.ps1 +98 -0
  77. package/references/scripts/recursive-subagent-action.py +197 -0
  78. package/references/scripts/recursive-training-extract.ps1 +23 -0
  79. package/references/scripts/recursive-training-extract.py +99 -0
  80. package/references/scripts/recursive-training-grpo.ps1 +52 -0
  81. package/references/scripts/recursive-training-grpo.py +1341 -0
  82. package/references/scripts/recursive-training-loader.ps1 +78 -0
  83. package/references/scripts/recursive-training-loader.py +558 -0
  84. package/references/scripts/recursive-training-mcp.ps1 +29 -0
  85. package/references/scripts/recursive-training-mcp.py +261 -0
  86. package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
  87. package/references/scripts/recursive-training-phase8-trigger.py +137 -0
  88. package/references/scripts/recursive-training-sync.ps1 +29 -0
  89. package/references/scripts/recursive-training-sync.py +183 -0
  90. package/references/scripts/recursive_phase_rules.py +367 -0
  91. package/references/scripts/recursive_router_cli_lib.py +2 -0
  92. package/references/scripts/recursive_router_lib.py +2282 -0
  93. package/references/scripts/verify-locks.ps1 +25 -0
  94. package/references/scripts/verify-locks.py +353 -0
  95. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  96. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  97. package/scripts/install-preset.cmd +7 -0
  98. package/scripts/install-preset.js +101 -0
  99. package/scripts/lint-recursive-run.py +7 -5
  100. package/scripts/test-recursive-mode-smoke.ts +29 -28
  101. package/src/bootstrap.ts +253 -39
  102. package/src/client/apply-guard.ts +23 -0
  103. package/src/client/board.tsx +0 -0
  104. package/src/client/contract.ts +72 -24
  105. package/src/client/host-api.ts +90 -0
  106. package/src/client/index.ts +22 -11
  107. package/src/client/inspector.tsx +7 -9
  108. package/src/client/slots.ts +70 -26
  109. package/src/client/strip.tsx +9 -9
  110. package/src/client/use-live.ts +52 -0
  111. package/src/index.ts +89 -146
  112. package/src/init-templates.ts +276 -0
  113. package/src/live-route.ts +163 -0
  114. package/src/phase-rules.ts +262 -0
  115. package/src/policy.ts +28 -4
  116. package/src/runtime.ts +129 -77
  117. package/src/snapshot.ts +104 -0
  118. package/lib/client/node.d.ts +0 -71
  119. package/lib/events.d.ts +0 -173
  120. package/lib/projection.d.ts +0 -29
  121. package/src/client/node.ts +0 -156
  122. package/src/events.ts +0 -173
  123. package/src/projection.ts +0 -237
package/src/events.ts DELETED
@@ -1,173 +0,0 @@
1
- /**
2
- * recursive/* session events (Phase D R1, PROPOSAL 11.2/11.6): the complete
3
- * durable event vocabulary the projection folds, the board renders, and the
4
- * conversation node correlates. Every payload carries a non-empty
5
- * { runId, worktreeRoot } so the whole plane is worktree-aware and the
6
- * projection can group runs by their control-plane root (binding
7
- * workspace-scoping invariant, run 03 R1).
8
- *
9
- * Events are LOG-ONLY (no live mirror): the projection unit (projection.ts)
10
- * is the only derived view. Payloads are plain JSON and declared on the
11
- * SessionEventMap merge via @deepseek-ai/dsh-session/types.
12
- *
13
- * The pure constructor helpers below enforce the runId + non-empty
14
- * worktreeRoot invariant at emit time, so a malformed event fails loud at
15
- * the emit site instead of silently landing cross-workspace.
16
- */
17
- import type { RecursiveRunState } from './types.ts'
18
-
19
- /** Declare the recursive/* event family on the session event map (merge-extensible). */
20
- declare module '@deepseek-ai/dsh-session/types' {
21
- interface SessionEventMap {
22
- /**
23
- * A transition the lifecycle driver is ABOUT to validate/commit (logged
24
- * before any file write). The pre-step gate reads this as the caller of the
25
- * transition set — never a projection event (log-only internal signal).
26
- */
27
- 'recursive/phase-intent': {
28
- runId: string
29
- worktreeRoot: string
30
- targetArtifact: string
31
- kind: 'lock' | 'reopen' | 'advance'
32
- evidence?: Record<string, unknown>
33
- }
34
- /** A run was created/bootstrapped in this workspace. */
35
- 'recursive/run-created': {
36
- runId: string
37
- worktreeRoot: string
38
- template?: string
39
- repo?: string
40
- }
41
- /** A phase transition committed (validate -> commit -> flush -> emit). */
42
- 'recursive/phase': {
43
- runId: string
44
- worktreeRoot: string
45
- phase: string
46
- status: string
47
- }
48
- /** A phase artifact was locked (lock hash + timestamp recorded). */
49
- 'recursive/phase-locked': {
50
- runId: string
51
- worktreeRoot: string
52
- phase: string
53
- lockedAt: string
54
- lockHash: string
55
- }
56
- /** A transition's gate predicate failed (rejected in strict, warned in advisory). */
57
- 'recursive/gate-blocked': {
58
- runId: string
59
- worktreeRoot: string
60
- phase: string
61
- failures: string[]
62
- kind: string
63
- }
64
- /** fs/observed detected a change to a locked artifact outside a tool call. */
65
- 'recursive/tamper': {
66
- runId: string
67
- worktreeRoot: string
68
- path: string
69
- reason: string
70
- }
71
- /** A run was merged back to the repo root (conversation history re-keys). */
72
- 'recursive/run-merged': {
73
- runId: string
74
- worktreeRoot: string
75
- repoRoot: string
76
- }
77
- /** Run-level lifecycle flag (active/paused/blocked/complete + reason). */
78
- 'recursive/run-state': {
79
- runId: string
80
- worktreeRoot: string
81
- state: RecursiveRunState
82
- reason?: string
83
- }
84
- /** A subagent started for this run. */
85
- 'recursive/subagent-start': {
86
- runId: string
87
- worktreeRoot: string
88
- childId: string
89
- role: string
90
- provider: string
91
- }
92
- /** A subagent finished for this run. */
93
- 'recursive/subagent-end': {
94
- runId: string
95
- worktreeRoot: string
96
- childId: string
97
- role: string
98
- provider: string
99
- status: 'running' | 'done' | 'failed'
100
- }
101
- }
102
- }
103
-
104
- /** Enforce the runId + non-empty worktreeRoot invariant at emit time. */
105
- export function requireRunKey(runId: string, worktreeRoot: string): void {
106
- if (typeof runId !== 'string' || runId.trim() === '') {
107
- throw new Error('recursive event requires a non-empty runId')
108
- }
109
- if (typeof worktreeRoot !== 'string' || worktreeRoot.trim() === '') {
110
- throw new Error('recursive event requires a non-empty worktreeRoot')
111
- }
112
- }
113
-
114
- export interface PhaseIntentInput { runId: string; worktreeRoot: string; targetArtifact: string; kind: 'lock' | 'reopen' | 'advance'; evidence?: Record<string, unknown> }
115
- export interface RunCreatedInput { runId: string; worktreeRoot: string; template?: string; repo?: string }
116
- export interface PhaseInput { runId: string; worktreeRoot: string; phase: string; status: string }
117
- export interface PhaseLockedInput { runId: string; worktreeRoot: string; phase: string; lockedAt: string; lockHash: string }
118
- export interface GateBlockedInput { runId: string; worktreeRoot: string; phase: string; failures: string[]; kind: string }
119
- export interface TamperInput { runId: string; worktreeRoot: string; path: string; reason: string }
120
- export interface RunMergedInput { runId: string; worktreeRoot: string; repoRoot: string }
121
- export interface RunStateInput { runId: string; worktreeRoot: string; state: RecursiveRunState; reason?: string }
122
- export interface SubagentInput { runId: string; worktreeRoot: string; childId: string; role: string; provider: string }
123
- export interface SubagentEndInput extends SubagentInput { status: 'running' | 'done' | 'failed' }
124
-
125
- export function phaseIntent(data: PhaseIntentInput): PhaseIntentInput {
126
- requireRunKey(data.runId, data.worktreeRoot)
127
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, targetArtifact: data.targetArtifact, kind: data.kind, ...(data.evidence === undefined ? {} : { evidence: data.evidence }) }
128
- }
129
-
130
- export function runCreated(data: RunCreatedInput): RunCreatedInput {
131
- requireRunKey(data.runId, data.worktreeRoot)
132
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, ...(data.template === undefined ? {} : { template: data.template }), ...(data.repo === undefined ? {} : { repo: data.repo }) }
133
- }
134
-
135
- export function phase(data: PhaseInput): PhaseInput {
136
- requireRunKey(data.runId, data.worktreeRoot)
137
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, phase: data.phase, status: data.status }
138
- }
139
-
140
- export function phaseLocked(data: PhaseLockedInput): PhaseLockedInput {
141
- requireRunKey(data.runId, data.worktreeRoot)
142
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, phase: data.phase, lockedAt: data.lockedAt, lockHash: data.lockHash }
143
- }
144
-
145
- export function gateBlocked(data: GateBlockedInput): GateBlockedInput {
146
- requireRunKey(data.runId, data.worktreeRoot)
147
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, phase: data.phase, failures: data.failures, kind: data.kind }
148
- }
149
-
150
- export function tamper(data: TamperInput): TamperInput {
151
- requireRunKey(data.runId, data.worktreeRoot)
152
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, path: data.path, reason: data.reason }
153
- }
154
-
155
- export function runMerged(data: RunMergedInput): RunMergedInput {
156
- requireRunKey(data.runId, data.worktreeRoot)
157
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, repoRoot: data.repoRoot }
158
- }
159
-
160
- export function runState(data: RunStateInput): RunStateInput {
161
- requireRunKey(data.runId, data.worktreeRoot)
162
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, state: data.state, ...(data.reason === undefined ? {} : { reason: data.reason }) }
163
- }
164
-
165
- export function subagentStart(data: SubagentInput): SubagentInput {
166
- requireRunKey(data.runId, data.worktreeRoot)
167
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, childId: data.childId, role: data.role, provider: data.provider }
168
- }
169
-
170
- export function subagentEnd(data: SubagentEndInput): SubagentEndInput {
171
- requireRunKey(data.runId, data.worktreeRoot)
172
- return { runId: data.runId, worktreeRoot: data.worktreeRoot, childId: data.childId, role: data.role, provider: data.provider, status: data.status }
173
- }
package/src/projection.ts DELETED
@@ -1,237 +0,0 @@
1
- /**
2
- * recursive session projection unit (Phase D R2/R9, PROPOSAL 11.2/11.4): the
3
- * pure fold that maps SessionEvent[] -> per-run wire-JSON, grouped by
4
- * worktree root first then runId. The board, inspector, node, and strip all
5
- * render from this whole value; the client NEVER scrapes /.recursive/run/.
6
- *
7
- * The unit is three pure synchronous functions (init/apply/view) plus a zod
8
- * schema + stateVersion (the session-projection ProjectionDefinition
9
- * contract). apply returns the same state reference for unrelated events
10
- * (Object.is zero-work rule), so non-recursive sessions cost nothing.
11
- *
12
- * Worktree scoping (binding invariant, run 03 R1): the fold only commits a
13
- * recursive/* event whose worktreeRoot is INSIDE the workspace control-plane
14
- * root captured at registration; out-of-workspace events are dropped.
15
- *
16
- * The core fold is structural (RecursiveEventLike) so it is unit-testable
17
- * without a live session; the ProjectionDefinition adapts the framework's
18
- * SessionEvent to it.
19
- */
20
- import type { SessionEvent } from '@deepseek-ai/dsh-session'
21
- import { z as zod } from 'zod'
22
- import type { ZodType } from 'zod'
23
- import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
24
- import type { RecursiveProjection, RecursiveRunCard, RecursiveRunState, RecursivePhaseRow, RecursiveGateBlock, RecursiveTamper, RecursiveSubagent } from './types.ts'
25
-
26
- /** Declare the 'recursive' projection key on the shared type table. */
27
- declare module '@deepseek-ai/dsh-session-projection/types' {
28
- interface SessionProjectionMap {
29
- /** Runs grouped by worktree root, then runId. */
30
- recursive: RecursiveProjection
31
- }
32
- }
33
-
34
- /** Structural event shape the pure fold reads (unit-testable without a session). */
35
- export interface RecursiveEventLike {
36
- type: string
37
- data?: Record<string, unknown>
38
- }
39
-
40
- /** Mutable fold accumulator (private to the pure fold; never leaks). */
41
- export interface RecursiveFoldState {
42
- /** The workspace control-plane root this unit is scoped to ('' = accept all). */
43
- workspaceRoot: string
44
- /** Runs grouped by worktree root then runId. */
45
- byWorktree: RecursiveProjection
46
- }
47
-
48
- export function emptyRecursiveFoldState(workspaceRoot = ''): RecursiveFoldState {
49
- // B6: the unit itself has no session/agent to learn the workspace root from —
50
- // init() is synchronous and receives no context. The EMITTERS are the scope
51
- // authority: they resolve the control-plane root per-call and never append a
52
- // cross-workspace event. workspaceRoot = '' (accept-all) is therefore a
53
- // per-session safety default, not a no-op: (a) each session's projection cell
54
- // is its own workspace (the log belongs to one agent), and (b) the re-key
55
- // guard still refuses a merge whose repoRoot falls outside the captured
56
- // root. A host that registers a session-scoped root may still pass it; the
57
- // default is the per-session invariant, not a cross-workspace hole.
58
- return { workspaceRoot, byWorktree: {} }
59
- }
60
-
61
- /** Whether a worktreeRoot is inside (or equal to) the workspace root. */
62
- export function isInsideWorkspace(worktreeRoot: string, workspaceRoot: string): boolean {
63
- if (workspaceRoot === '') return true
64
- const w = worktreeRoot.replace(/\\/g, '/').replace(/\/$/, '')
65
- const r = workspaceRoot.replace(/\\/g, '/').replace(/\/$/, '')
66
- return w === r || w.startsWith(r + '/')
67
- }
68
-
69
- function ensureCard(state: RecursiveFoldState, runId: string, worktreeRoot: string): RecursiveRunCard {
70
- let tree = state.byWorktree[worktreeRoot]
71
- if (tree === undefined) {
72
- tree = {}
73
- state.byWorktree[worktreeRoot] = tree
74
- }
75
- let card = tree[runId]
76
- if (card === undefined) {
77
- card = { runId, worktreeRoot, phases: {}, state: 'active', tampers: {}, subagents: {} }
78
- tree[runId] = card
79
- }
80
- return card
81
- }
82
-
83
- function asRunState(value: unknown): RecursiveRunState {
84
- return value === 'new' || value === 'active' || value === 'paused' || value === 'blocked' || value === 'complete'
85
- ? value as RecursiveRunState
86
- : 'active'
87
- }
88
-
89
- /** Pure transition: previous state + one committed event -> next state. */
90
- /** Internal transition signals (never commit into the projection). */
91
- const INTERNAL_RECURSIVE_EVENTS = new Set(['recursive/phase-intent'])
92
-
93
- export function foldRecursiveProjection(state: RecursiveFoldState, event: RecursiveEventLike): RecursiveFoldState {
94
- const type = event.type
95
- if (typeof type !== 'string' || !type.startsWith('recursive/')) return state
96
- if (INTERNAL_RECURSIVE_EVENTS.has(type)) return state
97
- const data = event.data ?? {}
98
- const runId = typeof data.runId === 'string' ? data.runId : ''
99
- const worktreeRoot = typeof data.worktreeRoot === 'string' ? data.worktreeRoot : ''
100
- if (runId === '' || worktreeRoot === '') return state
101
- // Binding workspace-scoping invariant: drop out-of-workspace events.
102
- if (!isInsideWorkspace(worktreeRoot, state.workspaceRoot)) return state
103
-
104
- const card = ensureCard(state, runId, worktreeRoot)
105
-
106
- switch (type) {
107
- case 'recursive/run-created': {
108
- if (typeof data.template === 'string') card.template = data.template
109
- if (typeof data.repo === 'string') card.repo = data.repo
110
- return state
111
- }
112
- case 'recursive/phase': {
113
- const phase = String(data.phase ?? '')
114
- if (phase === '') return state
115
- const row: RecursivePhaseRow = card.phases[phase] ?? { phase, status: String(data.status ?? '') }
116
- row.status = String(data.status ?? '')
117
- card.phases[phase] = row
118
- return state
119
- }
120
- case 'recursive/phase-locked': {
121
- const phase = String(data.phase ?? '')
122
- if (phase === '') return state
123
- const row: RecursivePhaseRow = card.phases[phase] ?? { phase, status: 'LOCKED' }
124
- row.status = 'LOCKED'
125
- row.lockedAt = String(data.lockedAt ?? '')
126
- row.lockHash = String(data.lockHash ?? '')
127
- card.phases[phase] = row
128
- return state
129
- }
130
- case 'recursive/gate-blocked': {
131
- const gate: RecursiveGateBlock = {
132
- failures: Array.isArray(data.failures) ? data.failures.map(String) : [],
133
- kind: String(data.kind ?? ''),
134
- }
135
- card.gateBlocked = gate
136
- return state
137
- }
138
- case 'recursive/tamper': {
139
- const path = String(data.path ?? '')
140
- if (path === '') return state
141
- const t: RecursiveTamper = { path, reason: String(data.reason ?? '') }
142
- card.tampers[path] = t
143
- return state
144
- }
145
- case 'recursive/run-merged': {
146
- const repoRoot = typeof data.repoRoot === 'string' ? data.repoRoot : ''
147
- if (repoRoot === '') return state
148
- // R9: a merged run is listed under the repo root, not the stale worktree
149
- // path. Re-key by moving the card across the outer group — but only when
150
- // the repo root itself is inside the workspace (binding invariant);
151
- // otherwise the merge note is recorded without crossing scope.
152
- card.mergedToRepoRoot = repoRoot
153
- if (isInsideWorkspace(repoRoot, state.workspaceRoot)) {
154
- const tree = state.byWorktree[worktreeRoot]
155
- if (tree !== undefined && tree[runId] === card) {
156
- delete tree[runId]
157
- if (Object.keys(tree).length === 0) delete state.byWorktree[worktreeRoot]
158
- }
159
- const target = state.byWorktree[repoRoot] ?? (state.byWorktree[repoRoot] = {})
160
- target[runId] = card
161
- // The card's own worktreeRoot now reflects the merge target.
162
- card.worktreeRoot = repoRoot
163
- }
164
- return state
165
- }
166
- case 'recursive/run-state': {
167
- card.state = asRunState(data.state)
168
- if (typeof data.reason === 'string') card.stateReason = data.reason
169
- return state
170
- }
171
- case 'recursive/subagent-start':
172
- case 'recursive/subagent-end': {
173
- const childId = String(data.childId ?? '')
174
- if (childId === '') return state
175
- const sub: RecursiveSubagent = card.subagents[childId] ?? {
176
- childId,
177
- role: String(data.role ?? ''),
178
- provider: String(data.provider ?? ''),
179
- }
180
- sub.role = String(data.role ?? sub.role)
181
- sub.provider = String(data.provider ?? sub.provider)
182
- if (type === 'recursive/subagent-end') {
183
- sub.status = (data.status === 'running' || data.status === 'done' || data.status === 'failed')
184
- ? data.status as 'running' | 'done' | 'failed'
185
- : 'done'
186
- }
187
- card.subagents[childId] = sub
188
- return state
189
- }
190
- default:
191
- return state
192
- }
193
- }
194
-
195
- /** Wire-payload schema (view output) — plain-JSON validation before it leaves the host. */
196
- const recursivePhaseRowSchema = zod.object({
197
- phase: zod.string(),
198
- status: zod.string(),
199
- lockedAt: zod.string().optional(),
200
- lockHash: zod.string().optional(),
201
- })
202
-
203
- const recursiveSubagentSchema = zod.object({
204
- childId: zod.string(),
205
- role: zod.string(),
206
- provider: zod.string(),
207
- status: zod.enum(['running', 'done', 'failed']).optional(),
208
- })
209
-
210
- const recursiveRunCardSchema = zod.object({
211
- runId: zod.string(),
212
- worktreeRoot: zod.string(),
213
- repo: zod.string().optional(),
214
- template: zod.string().optional(),
215
- phases: zod.record(zod.string(), recursivePhaseRowSchema),
216
- state: zod.enum(['new', 'active', 'paused', 'blocked', 'complete']),
217
- stateReason: zod.string().optional(),
218
- gateBlocked: zod.object({ failures: zod.array(zod.string()), kind: zod.string() }).optional(),
219
- tampers: zod.record(zod.string(), zod.object({ path: zod.string(), reason: zod.string() })),
220
- subagents: zod.record(zod.string(), recursiveSubagentSchema),
221
- mergedToRepoRoot: zod.string().optional(),
222
- })
223
-
224
- export const recursiveProjectionSchema: ZodType<RecursiveProjection> = zod.record(
225
- zod.string(),
226
- zod.record(zod.string(), recursiveRunCardSchema),
227
- )
228
-
229
- /** The registered projection unit (key 'recursive'). */
230
- export const recursiveProjectionUnit: ProjectionDefinition<'recursive', RecursiveFoldState> = {
231
- key: 'recursive',
232
- schema: recursiveProjectionSchema,
233
- init: () => emptyRecursiveFoldState(),
234
- apply: (state, event: SessionEvent) => foldRecursiveProjection(state, event as RecursiveEventLike),
235
- view: (state) => state.byWorktree,
236
- stateVersion: 1,
237
- }