@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,29 +0,0 @@
1
- import type { ZodType } from 'zod';
2
- import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
3
- import type { RecursiveProjection } from './types.ts';
4
- /** Declare the 'recursive' projection key on the shared type table. */
5
- declare module '@deepseek-ai/dsh-session-projection/types' {
6
- interface SessionProjectionMap {
7
- /** Runs grouped by worktree root, then runId. */
8
- recursive: RecursiveProjection;
9
- }
10
- }
11
- /** Structural event shape the pure fold reads (unit-testable without a session). */
12
- export interface RecursiveEventLike {
13
- type: string;
14
- data?: Record<string, unknown>;
15
- }
16
- /** Mutable fold accumulator (private to the pure fold; never leaks). */
17
- export interface RecursiveFoldState {
18
- /** The workspace control-plane root this unit is scoped to ('' = accept all). */
19
- workspaceRoot: string;
20
- /** Runs grouped by worktree root then runId. */
21
- byWorktree: RecursiveProjection;
22
- }
23
- export declare function emptyRecursiveFoldState(workspaceRoot?: string): RecursiveFoldState;
24
- /** Whether a worktreeRoot is inside (or equal to) the workspace root. */
25
- export declare function isInsideWorkspace(worktreeRoot: string, workspaceRoot: string): boolean;
26
- export declare function foldRecursiveProjection(state: RecursiveFoldState, event: RecursiveEventLike): RecursiveFoldState;
27
- export declare const recursiveProjectionSchema: ZodType<RecursiveProjection>;
28
- /** The registered projection unit (key 'recursive'). */
29
- export declare const recursiveProjectionUnit: ProjectionDefinition<'recursive', RecursiveFoldState>;
@@ -1,156 +0,0 @@
1
- /**
2
- * Conversation run node (Phase D R7, §11.6): a structural ConversationNodeDefinition
3
- * over the recursive/* event family, keyed by runId + worktreeRoot. One compact,
4
- * live node per run in the Chat view. PURE fold (no React) — the renderer lives
5
- * in slots.ts.
6
- *
7
- * The real ConversationNodeDefinition contract arrives from
8
- * @deepseek-ai/dsh-client-runtime/client at compose time; the structural shape
9
- * here is type-compatible with the cookbook surface (match/start/update/
10
- * buildViewNode + target/kind). The host links the bundle's exports to the real
11
- * runtime identities.
12
- */
13
- import type { ClientSessionEvent } from './contract.ts'
14
- import type { RecursiveRunCard } from '../types.ts'
15
-
16
- export const RECURSIVE_NODE_KIND = 'recursive/run'
17
-
18
- /** The chat-node payload the renderer receives (pure derived state). */
19
- export interface RecursiveNodeData {
20
- readonly runId: string
21
- readonly worktreeRoot: string
22
- readonly state: string
23
- readonly currentPhase: string | null
24
- readonly lockedCount: number
25
- readonly totalPhases: number
26
- readonly tampered: boolean
27
- readonly gateBlocked: boolean
28
- readonly mergedToRepoRoot?: string
29
- }
30
-
31
- /** Pure fold state for one run node. */
32
- export interface RecursiveNodeState {
33
- runId: string
34
- worktreeRoot: string
35
- state: string
36
- phases: Record<string, { phase: string; status: string }>
37
- tampered: boolean
38
- gateBlocked: boolean
39
- mergedToRepoRoot?: string
40
- }
41
-
42
- const GROUP_ORDER: readonly string[] = ['00', '01', '01.5', '02', '03', '03.5', '04', '05', '06', '07', '08']
43
- const GROUP_INDEX = new Map(GROUP_ORDER.map((g, i) => [g, i]))
44
-
45
- function phaseGroupOf(phase: string): string | null {
46
- const m = /^(\d{2}(?:\.\d)?)(?:-|$)/.exec(phase)
47
- return m ? m[1] : null
48
- }
49
-
50
- function currentPhaseOf(phases: Record<string, { phase: string; status: string }>): string | null {
51
- let best: string | null = null
52
- let bestIdx = -1
53
- for (const key of Object.keys(phases)) {
54
- const g = phaseGroupOf(key)
55
- if (g === null) continue
56
- const idx = GROUP_INDEX.get(g) ?? -1
57
- if (idx > bestIdx) { bestIdx = idx; best = key }
58
- }
59
- return best
60
- }
61
-
62
- function nodeDataOf(s: RecursiveNodeState): RecursiveNodeData {
63
- const lockedCount = Object.values(s.phases).filter(p => p.status === 'LOCKED').length
64
- return {
65
- runId: s.runId,
66
- worktreeRoot: s.worktreeRoot,
67
- state: s.state,
68
- currentPhase: currentPhaseOf(s.phases),
69
- lockedCount,
70
- totalPhases: Object.keys(s.phases).length,
71
- tampered: s.tampered,
72
- gateBlocked: s.gateBlocked,
73
- ...(s.mergedToRepoRoot === undefined ? {} : { mergedToRepoRoot: s.mergedToRepoRoot }),
74
- }
75
- }
76
-
77
- /** Extract a stable (runId, worktreeRoot) business id + lifecycle role from one event. */
78
- export function matchRecursiveNode(event: ClientSessionEvent): { id: string; role: 'start' | 'update' } | null {
79
- if (typeof event.type !== 'string' || !event.type.startsWith('recursive/')) return null
80
- const data = event.data ?? {}
81
- const runId = typeof data.runId === 'string' ? data.runId : ''
82
- const worktreeRoot = typeof data.worktreeRoot === 'string' ? data.worktreeRoot : ''
83
- if (runId === '' || worktreeRoot === '') return null
84
- return { id: worktreeRoot + '\u0000' + runId, role: event.type === 'recursive/run-created' ? 'start' : 'update' }
85
- }
86
-
87
- /** Fold one recursive/* event into the node state. */
88
- export function foldRecursiveNode(s: RecursiveNodeState | undefined, event: ClientSessionEvent): RecursiveNodeState {
89
- const data = event.data ?? {}
90
- const runId = typeof data.runId === 'string' ? data.runId : ''
91
- const worktreeRoot = typeof data.worktreeRoot === 'string' ? data.worktreeRoot : ''
92
- const base: RecursiveNodeState = s ?? {
93
- runId,
94
- worktreeRoot,
95
- state: 'active',
96
- phases: {},
97
- tampered: false,
98
- gateBlocked: false,
99
- }
100
-
101
- switch (event.type) {
102
- case 'recursive/run-created':
103
- base.runId = runId
104
- base.worktreeRoot = worktreeRoot
105
- return base
106
- case 'recursive/phase': {
107
- const phase = String(data.phase ?? '')
108
- if (phase !== '') base.phases[phase] = { phase, status: String(data.status ?? '') }
109
- return base
110
- }
111
- case 'recursive/phase-locked': {
112
- const phase = String(data.phase ?? '')
113
- if (phase !== '') base.phases[phase] = { phase, status: 'LOCKED' }
114
- return base
115
- }
116
- case 'recursive/gate-blocked':
117
- base.gateBlocked = true
118
- return base
119
- case 'recursive/tamper':
120
- base.tampered = true
121
- return base
122
- case 'recursive/run-state':
123
- base.state = typeof data.state === 'string' ? data.state : base.state
124
- return base
125
- case 'recursive/run-merged': {
126
- const repoRoot = typeof data.repoRoot === 'string' ? data.repoRoot : ''
127
- if (repoRoot !== '') {
128
- base.mergedToRepoRoot = repoRoot
129
- base.worktreeRoot = repoRoot
130
- }
131
- return base
132
- }
133
- default:
134
- return base
135
- }
136
- }
137
-
138
- /** The structural ConversationNodeDefinition (host links to the real contract). */
139
- export const recursiveNodeDefinition = {
140
- kind: RECURSIVE_NODE_KIND,
141
- target: 'chat',
142
- match: matchRecursiveNode,
143
- start: (_ctx: unknown, match: { event: ClientSessionEvent }): RecursiveNodeState => foldRecursiveNode(undefined, match.event),
144
- update: (ctx: { state: RecursiveNodeState }, match: { event: ClientSessionEvent }): RecursiveNodeState => foldRecursiveNode(ctx.state, match.event),
145
- buildViewNode: (ctx: { key: string; id: string; state?: RecursiveNodeState }) => {
146
- const state = ctx.state
147
- if (state === undefined) return null
148
- return {
149
- key: ctx.key,
150
- kind: RECURSIVE_NODE_KIND,
151
- id: ctx.id,
152
- target: 'chat',
153
- data: nodeDataOf(state),
154
- }
155
- },
156
- }
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
- }