@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.
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
@@ -0,0 +1,104 @@
1
+ /**
2
+ * snapshotWorkspace(root): fold the filesystem into the RecursiveProjection whole value.
3
+ *
4
+ * SP2 R1 (live-route) — the ONLY projection path. The board/strip read this through
5
+ * the host HTTP/SSE route; cold resume and every GET are a FRESH fs read (the route
6
+ * never serves a cached/stale snapshot). Per-workspace: root is the resolved
7
+ * control-plane root (cwd == workspace root, cwd == subdir, or cwd == repo root).
8
+ *
9
+ * Phase-lock facts (lockedAt/lockHash) are enriched from lock receipts because
10
+ * foldRun PhaseState carries no lock fields; state comes from getArtifactState.
11
+ */
12
+ import { readFileSync, existsSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+ import { discoverRuns } from './run.ts'
15
+ import { foldRun } from './status.ts'
16
+ import { getLockStatus, receiptPath, readReceipt } from './lock.ts'
17
+ import type { RecursiveProjection, RecursiveRunCard, RecursivePhaseRow, RecursiveRunState } from './types.ts'
18
+
19
+ /** One locked-phase fact from a lock receipt (artifacts/<file>.receipt.json). */
20
+ interface LockFacts {
21
+ lockedAt?: string
22
+ lockHash?: string
23
+ }
24
+
25
+ /** Resolve a run's lock facts for one artifact file (receipt first, status fallback). */
26
+ function lockFactsOf(runDir: string, artifact: string): LockFacts {
27
+ const receipt = readReceipt(runDir, artifact)
28
+ if (receipt && typeof receipt.locked_at === 'string' && typeof receipt.artifact_hash === 'string') {
29
+ return {
30
+ lockedAt: receipt.locked_at,
31
+ lockHash: receipt.artifact_hash,
32
+ }
33
+ }
34
+ // No usable receipt: fall back to the artifact's own LockedAt/LockHash fields.
35
+ const status = getLockStatus(join(runDir, artifact))
36
+ if (status !== 'LOCKED') return {}
37
+ return {
38
+ lockedAt: new Date(0).toISOString().replace(/\.\d{3}Z$/, 'Z'),
39
+ lockHash: '0'.repeat(64),
40
+ }
41
+ }
42
+
43
+ /** One phase row from a run's folded status (locked facts enriched). */
44
+ function phaseRowOf(runDir: string, key: string, file: string, status: string): RecursivePhaseRow {
45
+ const row: RecursivePhaseRow = { phase: key, status }
46
+ if (status === 'LOCKED') {
47
+ const facts = lockFactsOf(runDir, file)
48
+ if (facts.lockedAt !== undefined) row.lockedAt = facts.lockedAt
49
+ if (facts.lockHash !== undefined) row.lockHash = facts.lockHash
50
+ }
51
+ return row
52
+ }
53
+
54
+ /**
55
+ * Fold one run directory into a RecursiveRunCard (the wire shape the board renders).
56
+ * Runs that no longer exist on disk are omitted (the fs is the source of truth).
57
+ */
58
+ export function foldRunCard(runDir: string, runId: string, worktreeRoot: string): RecursiveRunCard {
59
+ const status = foldRun(runDir, runId)
60
+ // Wire contract parity (deleted projection.ts): phases are keyed by FILENAME
61
+ // (the recursive/phase event carried phase: '03-implementation-summary.md'),
62
+ // which is what derive.ts phaseGroupOf parses. phase.key ('03', '00R', '00W')
63
+ // would never match the filename regex and would break columnForRun/cardFacts.
64
+ const phases: Record<string, RecursivePhaseRow> = {}
65
+ for (const phase of status.phases) {
66
+ if (phase.exists) phases[phase.file] = phaseRowOf(runDir, phase.file, phase.file, phase.status)
67
+ }
68
+ const state = runStateOf(status)
69
+ return {
70
+ runId,
71
+ worktreeRoot,
72
+ phases,
73
+ state,
74
+ tampers: {},
75
+ subagents: {},
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Derive the run-level state from the folded status. The fs stores no run-level
81
+ * state file, so the honest mapping is: 'complete' when every non-optional phase
82
+ * exists and is lock-valid, else 'active'. Transient facts (gateBlocked, tamper,
83
+ * subagent activity) are NOT fs-recoverable and read as absent (recorded
84
+ * limitation; future enhancement).
85
+ */
86
+ function runStateOf(status: ReturnType<typeof foldRun>): RecursiveRunState {
87
+ const mandatory = status.phases.filter(p => !p.optional)
88
+ const complete = mandatory.every(p => p.exists && p.lockValid)
89
+ return complete ? 'complete' : 'active'
90
+ }
91
+
92
+ /**
93
+ * Fold every run under <root>/.recursive/run/ into the projection, grouped by
94
+ * worktree root then runId. A fresh fs read on every call (cold-resume live).
95
+ */
96
+ export function snapshotWorkspace(root: string): RecursiveProjection {
97
+ const projection: RecursiveProjection = {}
98
+ for (const runId of discoverRuns(root)) {
99
+ const runDir = join(root, '.recursive', 'run', runId)
100
+ projection[root] ??= {}
101
+ projection[root][runId] = foldRunCard(runDir, runId, root)
102
+ }
103
+ return projection
104
+ }
@@ -1,71 +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
- export declare const RECURSIVE_NODE_KIND = "recursive/run";
15
- /** The chat-node payload the renderer receives (pure derived state). */
16
- export interface RecursiveNodeData {
17
- readonly runId: string;
18
- readonly worktreeRoot: string;
19
- readonly state: string;
20
- readonly currentPhase: string | null;
21
- readonly lockedCount: number;
22
- readonly totalPhases: number;
23
- readonly tampered: boolean;
24
- readonly gateBlocked: boolean;
25
- readonly mergedToRepoRoot?: string;
26
- }
27
- /** Pure fold state for one run node. */
28
- export interface RecursiveNodeState {
29
- runId: string;
30
- worktreeRoot: string;
31
- state: string;
32
- phases: Record<string, {
33
- phase: string;
34
- status: string;
35
- }>;
36
- tampered: boolean;
37
- gateBlocked: boolean;
38
- mergedToRepoRoot?: string;
39
- }
40
- /** Extract a stable (runId, worktreeRoot) business id + lifecycle role from one event. */
41
- export declare function matchRecursiveNode(event: ClientSessionEvent): {
42
- id: string;
43
- role: 'start' | 'update';
44
- } | null;
45
- /** Fold one recursive/* event into the node state. */
46
- export declare function foldRecursiveNode(s: RecursiveNodeState | undefined, event: ClientSessionEvent): RecursiveNodeState;
47
- /** The structural ConversationNodeDefinition (host links to the real contract). */
48
- export declare const recursiveNodeDefinition: {
49
- kind: string;
50
- target: string;
51
- match: typeof matchRecursiveNode;
52
- start: (_ctx: unknown, match: {
53
- event: ClientSessionEvent;
54
- }) => RecursiveNodeState;
55
- update: (ctx: {
56
- state: RecursiveNodeState;
57
- }, match: {
58
- event: ClientSessionEvent;
59
- }) => RecursiveNodeState;
60
- buildViewNode: (ctx: {
61
- key: string;
62
- id: string;
63
- state?: RecursiveNodeState;
64
- }) => {
65
- key: string;
66
- kind: string;
67
- id: string;
68
- target: string;
69
- data: RecursiveNodeData;
70
- } | null;
71
- };
package/lib/events.d.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
- /** Declare the recursive/* event family on the session event map (merge-extensible). */
19
- declare module '@deepseek-ai/dsh-session/types' {
20
- interface SessionEventMap {
21
- /**
22
- * A transition the lifecycle driver is ABOUT to validate/commit (logged
23
- * before any file write). The pre-step gate reads this as the caller of the
24
- * transition set — never a projection event (log-only internal signal).
25
- */
26
- 'recursive/phase-intent': {
27
- runId: string;
28
- worktreeRoot: string;
29
- targetArtifact: string;
30
- kind: 'lock' | 'reopen' | 'advance';
31
- evidence?: Record<string, unknown>;
32
- };
33
- /** A run was created/bootstrapped in this workspace. */
34
- 'recursive/run-created': {
35
- runId: string;
36
- worktreeRoot: string;
37
- template?: string;
38
- repo?: string;
39
- };
40
- /** A phase transition committed (validate -> commit -> flush -> emit). */
41
- 'recursive/phase': {
42
- runId: string;
43
- worktreeRoot: string;
44
- phase: string;
45
- status: string;
46
- };
47
- /** A phase artifact was locked (lock hash + timestamp recorded). */
48
- 'recursive/phase-locked': {
49
- runId: string;
50
- worktreeRoot: string;
51
- phase: string;
52
- lockedAt: string;
53
- lockHash: string;
54
- };
55
- /** A transition's gate predicate failed (rejected in strict, warned in advisory). */
56
- 'recursive/gate-blocked': {
57
- runId: string;
58
- worktreeRoot: string;
59
- phase: string;
60
- failures: string[];
61
- kind: string;
62
- };
63
- /** fs/observed detected a change to a locked artifact outside a tool call. */
64
- 'recursive/tamper': {
65
- runId: string;
66
- worktreeRoot: string;
67
- path: string;
68
- reason: string;
69
- };
70
- /** A run was merged back to the repo root (conversation history re-keys). */
71
- 'recursive/run-merged': {
72
- runId: string;
73
- worktreeRoot: string;
74
- repoRoot: string;
75
- };
76
- /** Run-level lifecycle flag (active/paused/blocked/complete + reason). */
77
- 'recursive/run-state': {
78
- runId: string;
79
- worktreeRoot: string;
80
- state: RecursiveRunState;
81
- reason?: string;
82
- };
83
- /** A subagent started for this run. */
84
- 'recursive/subagent-start': {
85
- runId: string;
86
- worktreeRoot: string;
87
- childId: string;
88
- role: string;
89
- provider: string;
90
- };
91
- /** A subagent finished for this run. */
92
- 'recursive/subagent-end': {
93
- runId: string;
94
- worktreeRoot: string;
95
- childId: string;
96
- role: string;
97
- provider: string;
98
- status: 'running' | 'done' | 'failed';
99
- };
100
- }
101
- }
102
- /** Enforce the runId + non-empty worktreeRoot invariant at emit time. */
103
- export declare function requireRunKey(runId: string, worktreeRoot: string): void;
104
- export interface PhaseIntentInput {
105
- runId: string;
106
- worktreeRoot: string;
107
- targetArtifact: string;
108
- kind: 'lock' | 'reopen' | 'advance';
109
- evidence?: Record<string, unknown>;
110
- }
111
- export interface RunCreatedInput {
112
- runId: string;
113
- worktreeRoot: string;
114
- template?: string;
115
- repo?: string;
116
- }
117
- export interface PhaseInput {
118
- runId: string;
119
- worktreeRoot: string;
120
- phase: string;
121
- status: string;
122
- }
123
- export interface PhaseLockedInput {
124
- runId: string;
125
- worktreeRoot: string;
126
- phase: string;
127
- lockedAt: string;
128
- lockHash: string;
129
- }
130
- export interface GateBlockedInput {
131
- runId: string;
132
- worktreeRoot: string;
133
- phase: string;
134
- failures: string[];
135
- kind: string;
136
- }
137
- export interface TamperInput {
138
- runId: string;
139
- worktreeRoot: string;
140
- path: string;
141
- reason: string;
142
- }
143
- export interface RunMergedInput {
144
- runId: string;
145
- worktreeRoot: string;
146
- repoRoot: string;
147
- }
148
- export interface RunStateInput {
149
- runId: string;
150
- worktreeRoot: string;
151
- state: RecursiveRunState;
152
- reason?: string;
153
- }
154
- export interface SubagentInput {
155
- runId: string;
156
- worktreeRoot: string;
157
- childId: string;
158
- role: string;
159
- provider: string;
160
- }
161
- export interface SubagentEndInput extends SubagentInput {
162
- status: 'running' | 'done' | 'failed';
163
- }
164
- export declare function phaseIntent(data: PhaseIntentInput): PhaseIntentInput;
165
- export declare function runCreated(data: RunCreatedInput): RunCreatedInput;
166
- export declare function phase(data: PhaseInput): PhaseInput;
167
- export declare function phaseLocked(data: PhaseLockedInput): PhaseLockedInput;
168
- export declare function gateBlocked(data: GateBlockedInput): GateBlockedInput;
169
- export declare function tamper(data: TamperInput): TamperInput;
170
- export declare function runMerged(data: RunMergedInput): RunMergedInput;
171
- export declare function runState(data: RunStateInput): RunStateInput;
172
- export declare function subagentStart(data: SubagentInput): SubagentInput;
173
- export declare function subagentEnd(data: SubagentEndInput): SubagentEndInput;
@@ -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
- }