@try-works/dsh-recursive-mode 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +15 -10
- package/lib/bootstrap.d.ts +9 -2
- package/lib/client/apply-guard.d.ts +14 -0
- package/lib/client/board.d.ts +5 -4
- package/lib/client/contract.d.ts +57 -24
- package/lib/client/host-api.d.ts +26 -0
- package/lib/client/index.d.ts +16 -9
- package/lib/client/inspector.d.ts +3 -3
- package/lib/client/slots.d.ts +4 -6
- package/lib/client/strip.d.ts +4 -3
- package/lib/client/use-live.d.ts +9 -0
- package/lib/client.js +252 -174
- package/lib/index.d.ts +11 -11
- package/lib/index.js +972 -4504
- package/lib/init-templates.d.ts +41 -0
- package/lib/live-route.d.ts +69 -0
- package/lib/phase-rules.d.ts +34 -0
- package/lib/runtime.d.ts +24 -17
- package/lib/snapshot.d.ts +11 -0
- package/package.json +4 -3
- package/preset/recursive/agent.cordis.yml +7 -4
- package/preset/recursive/preset.yml +1 -1
- package/references/agents-block.md +95 -0
- package/references/artifact-template.md +2574 -0
- package/references/bodies/claude.md +7 -0
- package/references/bodies/codex-agents.md +95 -0
- package/references/bodies/copilot.md +7 -0
- package/references/bodies/cursorrules.md +7 -0
- package/references/bodies/decisions.md +5 -0
- package/references/bodies/delegated-verification.md +49 -0
- package/references/bodies/memory-router.md +45 -0
- package/references/bodies/phase8-skill-memory.md +48 -0
- package/references/bodies/plans-bridge.md +43 -0
- package/references/bodies/recursive-agents-router.md +64 -0
- package/references/bodies/skill-discovery.md +51 -0
- package/references/bodies/skill-memory-router.md +42 -0
- package/references/bodies/state.md +5 -0
- package/references/bootstrap/RECURSIVE.md +2362 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +25 -0
- package/references/scripts/lint-recursive-run.py +2870 -0
- package/references/scripts/recursive-closeout.ps1 +38 -0
- package/references/scripts/recursive-closeout.py +541 -0
- package/references/scripts/recursive-init.ps1 +515 -0
- package/references/scripts/recursive-init.py +356 -0
- package/references/scripts/recursive-lock.ps1 +26 -0
- package/references/scripts/recursive-lock.py +302 -0
- package/references/scripts/recursive-review-bundle.ps1 +58 -0
- package/references/scripts/recursive-review-bundle.py +503 -0
- package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
- package/references/scripts/recursive-router-cli-configure.py +9 -0
- package/references/scripts/recursive-router-cli-init.ps1 +2 -0
- package/references/scripts/recursive-router-cli-init.py +9 -0
- package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
- package/references/scripts/recursive-router-cli-invoke.py +9 -0
- package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
- package/references/scripts/recursive-router-cli-probe.py +9 -0
- package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
- package/references/scripts/recursive-router-cli-resolve.py +9 -0
- package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
- package/references/scripts/recursive-router-cli-validate.py +9 -0
- package/references/scripts/recursive-router-configure.ps1 +27 -0
- package/references/scripts/recursive-router-configure.py +74 -0
- package/references/scripts/recursive-router-init.ps1 +17 -0
- package/references/scripts/recursive-router-init.py +29 -0
- package/references/scripts/recursive-router-invoke.ps1 +47 -0
- package/references/scripts/recursive-router-invoke.py +103 -0
- package/references/scripts/recursive-router-probe.ps1 +25 -0
- package/references/scripts/recursive-router-probe.py +44 -0
- package/references/scripts/recursive-router-resolve.ps1 +26 -0
- package/references/scripts/recursive-router-resolve.py +46 -0
- package/references/scripts/recursive-router-validate.ps1 +17 -0
- package/references/scripts/recursive-router-validate.py +27 -0
- package/references/scripts/recursive-status.ps1 +23 -0
- package/references/scripts/recursive-status.py +2124 -0
- package/references/scripts/recursive-subagent-action.ps1 +98 -0
- package/references/scripts/recursive-subagent-action.py +197 -0
- package/references/scripts/recursive-training-extract.ps1 +23 -0
- package/references/scripts/recursive-training-extract.py +99 -0
- package/references/scripts/recursive-training-grpo.ps1 +52 -0
- package/references/scripts/recursive-training-grpo.py +1341 -0
- package/references/scripts/recursive-training-loader.ps1 +78 -0
- package/references/scripts/recursive-training-loader.py +558 -0
- package/references/scripts/recursive-training-mcp.ps1 +29 -0
- package/references/scripts/recursive-training-mcp.py +261 -0
- package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
- package/references/scripts/recursive-training-phase8-trigger.py +137 -0
- package/references/scripts/recursive-training-sync.ps1 +29 -0
- package/references/scripts/recursive-training-sync.py +183 -0
- package/references/scripts/recursive_phase_rules.py +367 -0
- package/references/scripts/recursive_router_cli_lib.py +2 -0
- package/references/scripts/recursive_router_lib.py +2282 -0
- package/references/scripts/verify-locks.ps1 +25 -0
- package/references/scripts/verify-locks.py +353 -0
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-preset.cmd +7 -0
- package/scripts/install-preset.js +101 -0
- package/scripts/lint-recursive-run.py +7 -5
- package/scripts/test-recursive-mode-smoke.ts +29 -28
- package/src/bootstrap.ts +253 -39
- package/src/client/apply-guard.ts +23 -0
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +72 -24
- package/src/client/host-api.ts +90 -0
- package/src/client/index.ts +22 -11
- package/src/client/inspector.tsx +7 -9
- package/src/client/slots.ts +70 -26
- package/src/client/strip.tsx +9 -9
- package/src/client/use-live.ts +52 -0
- package/src/index.ts +89 -146
- package/src/init-templates.ts +276 -0
- package/src/live-route.ts +163 -0
- package/src/phase-rules.ts +262 -0
- package/src/policy.ts +28 -4
- package/src/runtime.ts +129 -77
- package/src/snapshot.ts +104 -0
- package/lib/client/node.d.ts +0 -71
- package/lib/events.d.ts +0 -173
- package/lib/projection.d.ts +0 -29
- package/src/client/node.ts +0 -156
- package/src/events.ts +0 -173
- package/src/projection.ts +0 -237
package/src/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
|
-
}
|