@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
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type Context } from '@deepseek-ai/cordis'
2
- import type { Session } from '@deepseek-ai/dsh-session'
2
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
3
+ import { existsSync } from 'node:fs'
4
+ import { join } from 'node:path'
3
5
  import { RecursiveRuntime } from './runtime.ts'
4
6
  import { createRecursiveStatusTool } from './recursive_status.tool.ts'
5
7
  import { createRecursiveInitTool } from './recursive_init.tool.ts'
@@ -8,23 +10,14 @@ import { createRecursiveLintTool } from './recursive_lint.tool.ts'
8
10
  import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
9
11
  import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
10
12
  import { registerRecursiveCommand } from './commands.ts'
11
- import { stageBWorkflowInit } from './bootstrap.ts'
12
- import { evaluatePreStepGate, evaluateToolGuard, detectTamper, resolveEnforcementConfig, DEFAULT_ENFORCEMENT, type EnforcementConfig } from './enforcement.ts'
13
- import { detectTransitionIntent } from './lifecycle.ts'
13
+ import { evaluateToolGuard } from './enforcement.ts'
14
14
  import { renderRecursivePolicy } from './policy.ts'
15
- import {
16
- gateBlocked as gateBlockedEvent,
17
- tamper as tamperEvent,
18
- phaseIntent as phaseIntentEvent,
19
- runCreated as runCreatedEvent,
20
- phase as phaseEvent,
21
- phaseLocked as phaseLockedEvent,
22
- runState as runStateEvent,
23
- runMerged as runMergedEvent,
24
- subagentStart as subagentStartEvent,
25
- subagentEnd as subagentEndEvent,
26
- } from './events.ts'
27
- import { recursiveProjectionUnit } from './projection.ts'
15
+ import { fsPolicyIntent } from './fs-intent.ts'
16
+ import { snapshotWorkspace } from './snapshot.ts'
17
+ import { mountRecursiveRoutesOnce, makeRecursiveRoutes, type RecursiveRouteHost } from './live-route.ts'
18
+ import { enumerateRuns } from './bootstrap.ts'
19
+ import { getNextLegalPhase, getLockStatus } from './lock.ts'
20
+ import { phaseLintRulesMessage } from './phase-rules.ts'
28
21
 
29
22
  export const name = '@try-works/dsh-recursive-mode'
30
23
 
@@ -71,8 +64,8 @@ export * from './delegation.ts'
71
64
  export * from './lifecycle.ts'
72
65
  export * from './enforcement.ts'
73
66
  export * from './policy.ts'
74
- export * from './events.ts'
75
- export * from './projection.ts'
67
+ export * from './snapshot.ts'
68
+ export * from './live-route.ts'
76
69
 
77
70
  /**
78
71
  * Bundle plugin entry. The Loader activates this row once `tools` is available
@@ -83,44 +76,29 @@ export * from './projection.ts'
83
76
  export const inject = ['tools']
84
77
 
85
78
  /**
86
- * Two-stage init (PROPOSAL §5.10):
87
- * - Stage A (mount-time, this apply): register the isolated ctx.recursive
88
- * service + the recursive_* tools + the /recursive command + the
89
- * recursive:policy prompt section. No repo/run work here.
90
- * - Stage B (agent/session-start): resolve the session's workspace root (R1),
91
- * bootstrap the scaffold if missing, enumerate runs as dir names only, and
92
- * inject a workspace-scoped notice. See stageBWorkflowInit + the
93
- * agent/session-start listener below.
79
+ * Plugin entry (SP2 R1). Stage A (mount-time, this apply): register the
80
+ * isolated ctx.recursive service + the recursive_* tools + the /recursive
81
+ * command + the recursive:policy prompt section + the LIVE board/strip route
82
+ * (HTTP state + SSE), served per-workspace from the filesystem fold. No
83
+ * repo/run work and NO session-event emission here: zero recursive/* events
84
+ * are ever appended (resume-crash fix), and the board reads the live fs route.
94
85
  */
95
- export function apply(ctx: Context) {
86
+ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: string }) {
87
+ // R4 shell split (02-to-be-plan.addendum-r4-r2-mount-resolution.md): the
88
+ // global bare-name row in cordis.patch.yml mounts with config.shellOnly=true
89
+ // to expose ONLY the client bundle for client discovery. It must register
90
+ // NOTHING on the server root — no tools, no /recursive command, no
91
+ // recursive:policy, no projection (BUG 4 always-on leak). The full server
92
+ // surface is mounted ONLY by the recursive preset's isolated recursive-realm,
93
+ // whose rows carry no config (shellOnly undefined).
94
+ if (config?.shellOnly) return
96
95
  ctx.effect(function* () {
97
96
  // Workspace registry: optional host service (durable). Access via ctx.get —
98
97
  // property access requires inject and would fail boot when undeclared.
99
98
  // Resolve the control-plane root strictly from the session agent's cwd.
100
99
  const workspaceRegistry = ctx.get('workspaceRegistry') as never
101
- const recursive = new RecursiveRuntime(ctx, { repoRoot: process.cwd(), workspaceRegistry })
100
+ const recursive = new RecursiveRuntime(ctx, { repoRoot: config?.repoRoot ?? process.cwd(), workspaceRegistry })
102
101
 
103
- /** Typed agent payload shape (session is a full Session). */
104
- type AgentLike = { session?: Session | null }
105
-
106
- /** Extract the session from a listener payload. */
107
- const sessionOf = (p: unknown): Session | null => {
108
- const a = (p as { agent?: AgentLike | null } | null)?.agent
109
- return a?.session ?? null
110
- }
111
-
112
- /** Resolve the session's control-plane root (registry first, cwd fallback). */
113
- const rootOf = async (p: unknown): Promise<string> => {
114
- const a = (p as { agent?: AgentLike | null } | null)?.agent
115
- const root = await recursive.resolveRootFor(a as { session?: { header?: { cwd?: string } } } | null)
116
- return root ?? a?.session?.header.cwd ?? process.cwd()
117
- }
118
-
119
- /** Emit one recursive/* event into the session log (log-only). */
120
- const emit = (session: Session | null, type: string, data: unknown) => {
121
- if (!session) return
122
- try { (session.append as unknown as (t: string, d: unknown) => void)(type, data) } catch { /* log-only; never fail the gate on an emit error */ }
123
- }
124
102
  const disposers = [
125
103
  ctx.tools.register(createRecursiveStatusTool(recursive)),
126
104
  ctx.tools.register(createRecursiveInitTool(recursive)),
@@ -144,42 +122,28 @@ export function apply(ctx: Context) {
144
122
  name: 'recursive:policy',
145
123
  order: 55,
146
124
  text: (context: unknown) => {
147
- const agent = (context as { agent?: { session?: { header?: { cwd?: string }; events?: { type: string; data?: Record<string, unknown> }[] } } } | undefined)?.agent
125
+ const agent = (context as { agent?: { session?: { header?: { cwd?: string } } } } | undefined)?.agent
148
126
  if (!agent) return ''
149
- const cwd = agent.session?.header?.cwd ?? ''
150
- if (!cwd) return ''
151
- const events = agent.session?.events ?? []
152
- const intent = detectTransitionIntent(events as never)
153
- if (!intent || intent.worktreeRoot === '') return ''
154
- return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, folded: recursive.foldPhase(events as never), config: recursive.enforcementConfig })
127
+ // SP3 R5 policy-render fix: derive intent from the FILESYSTEM, not the
128
+ // retired recursive/phase-intent session event (zero-emission removed
129
+ // the emitter, so detectTransitionIntent was ALWAYS null and this
130
+ // section rendered ''). Pure read-only fs folding; no recursive/*
131
+ // events are appended.
132
+ const intent = fsPolicyIntent(agent, workspaceRegistry as never)
133
+ if (!intent) return ''
134
+ return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, config: recursive.enforcementConfig })
155
135
  },
156
136
  }))
157
137
  }
158
138
 
159
- // Phase C R3: agent/pre-step phase-transition gate (Layer 1, caller of the
160
- // lifecycle transition set). Reads on transition intent only; rejects (strict)
161
- // or warns (advisory) when the target gates fail.
162
- const agentEvents = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
163
- if (agentEvents.on) {
164
- disposers.push(agentEvents.on('agent/pre-step', (payload, next) => {
165
- const p = payload as { agent?: { session?: { events?: { type: string; data?: Record<string, unknown> }[] } } } | null
166
- const events = p?.agent?.session?.events ?? []
167
- const session = sessionOf(p)
168
- const intent = recursive.detectTransitionIntent(events as never)
169
- const decision = evaluatePreStepGate(events as never, recursive.enforcementConfig.preStep)
170
- void (async () => {
171
- // Emit a committed run-state (blocked) when a gate rejects the intent —
172
- // the board then shows the run blocked, not merely a log message.
173
- if (decision.gateBlocked && intent?.runId) {
174
- const worktreeRoot = intent?.worktreeRoot ?? (await rootOf(p))
175
- emit(session, 'recursive/gate-blocked', gateBlockedEvent({ runId: intent.runId, worktreeRoot, phase: intent?.targetArtifact ?? '', failures: decision.failures, kind: 'pre-step' }))
176
- emit(session, 'recursive/run-state', runStateEvent({ runId: intent.runId, worktreeRoot, state: 'blocked', reason: 'gate-blocked: ' + decision.failures.join('; ') }))
177
- }
178
- })()
179
- if (decision.kind === 'reject') return { kind: 'reject' }
180
- return typeof next === 'function' ? next() : { kind: 'enter', messages: [] }
181
- }))
182
- }
139
+ // Phase C R3 (Layer 1, agent/pre-step proactive intent gate) is RETIRED
140
+ // under zero-emission (SP2 R1): its only signal was the recursive/phase-intent
141
+ // session event, whose emitter is now deleted, and it was ADVISORY by default
142
+ // (it never rejected; its sole side effect was the now-removed emission).
143
+ // Enforcement is preserved where it actually bites: Layer 2 (tools/pre-execute
144
+ // inspects the REAL tool call args below) plus the recursive_lock tool's own
145
+ // prerequisite validation in lockArtifact strictly more reliable than the
146
+ // heuristic intent scan. See 03-implementation-summary.addendum-r1-*.md.
183
147
 
184
148
  // Phase C R4: tools/pre-execute surgical guards (Layer 2, caller of the
185
149
  // transition set). Scope-filtered to the active run's worktree.
@@ -195,87 +159,68 @@ export function apply(ctx: Context) {
195
159
  if (decision.kind === 'allow') return typeof next === 'function' ? next() : { kind: 'allow' }
196
160
  if (decision.kind === 'deny') return decision
197
161
  // ask -> in advisory we let it through with a warn (the approval seam is Phase D)
198
- return typeof next === 'function' ? next() : { kind: 'allow' }
199
162
  }))
200
163
  }
201
164
 
202
- // Phase C R8 + Phase D R1: fs/observed lock-tamper warnings -> recursive/tamper event.
203
- const fsRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown) => void) => () => void }
204
- if (fsRuntime.on) {
205
- disposers.push(fsRuntime.on('fs/observed', (payload) => {
206
- const p = payload as { target?: { displayPath?: string }; agent?: AgentLike | null } | null
207
- const path = p?.target?.displayPath ?? ''
208
- if (!path) return
209
- // B3: tamper detection is scoped to the SESSION's control-plane root,
210
- // not the host checkout.
211
- const sessionCwd = p?.agent?.session?.header.cwd ?? ''
212
- const tamper = detectTamper(path, sessionCwd, '')
213
- if (!tamper) return
214
- // Resolve the worktree root from the tampered path (strip /.recursive/run/<id>/).
215
- const normalized = path.replace(/\\/g, '/')
216
- const marker = '/.recursive/run/'
217
- const idx = normalized.indexOf(marker)
218
- const worktreeRoot = idx >= 0 ? normalized.slice(0, idx) : sessionCwd
219
- // Prefer the observing session's own log (additive); fall back to the
220
- // session store list only when the payload carries no session.
221
- const session = sessionOf(p) ?? null
222
- if (session) {
223
- emit(session, 'recursive/tamper', tamperEvent({ runId: tamper.runId, worktreeRoot, path: tamper.path, reason: tamper.reason }))
224
- return
225
- }
226
- const sessions = ctx.get('sessions') as { list?: () => Session[] } | undefined
227
- const match = sessions?.list?.().find((s) => {
228
- const cwd = (s.header?.cwd ?? '').replace(/\\/g, '/')
229
- return cwd === '' || cwd === worktreeRoot || cwd.startsWith(worktreeRoot + '/')
230
- })
231
- if (match) emit(match, 'recursive/tamper', tamperEvent({ runId: tamper.runId, worktreeRoot, path: tamper.path, reason: tamper.reason }))
165
+ // SP3 R5: agent/pre-step lint-rules injection (pre-step contract verified: the
166
+ // listener returns { kind: 'enter', messages: [...messages, injected] } and the
167
+ // returned array REPLACES the default [...claimed, context]). When the session's
168
+ // control-plane root has an active recursive run whose current phase doc is
169
+ // DRAFT, prepend a compact system-reminder with THAT phase's required sections +
170
+ // gates. Pure fs read (zero-emission): never appends recursive/* events.
171
+ const agentRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
172
+ if (agentRuntime.on) {
173
+ disposers.push(agentRuntime.on('agent/pre-step', async (payload, next) => {
174
+ const p = payload as {
175
+ messages?: Array<{ content: Array<{ type: string; text?: string }> }>,
176
+ agent?: { session?: { header?: { cwd?: string } } } | null,
177
+ } | null
178
+ const messages = p?.messages ?? []
179
+ const agent = p?.agent ?? null
180
+ const cwd = agent?.session?.header?.cwd ?? ''
181
+ // delegate first so later listeners keep veto power, then fold ours on
182
+ if (typeof next === 'function') await next()
183
+ if (!cwd) return { kind: 'enter', messages } as const
184
+ const root = await recursive.resolveRootForRoute(undefined, cwd, undefined)
185
+ if (!root) return { kind: 'enter', messages } as const
186
+ const runs = enumerateRuns(root)
187
+ if (runs.length === 0) return { kind: 'enter', messages } as const
188
+ const runId = runs[runs.length - 1]
189
+ const runDir = join(root, '.recursive', 'run', runId)
190
+ if (!existsSync(runDir)) return { kind: 'enter', messages } as const
191
+ const phase = getNextLegalPhase(runDir)
192
+ if (!phase) return { kind: 'enter', messages } as const
193
+ // only inject when the phase doc is DRAFT (not locked/missing).
194
+ const phasePath = join(runDir, phase)
195
+ const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
196
+ if (status !== 'DRAFT') return { kind: 'enter', messages } as const
197
+ const reminder = phaseLintRulesMessage(phase)
198
+ return {
199
+ kind: 'enter',
200
+ messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
201
+ } as const
232
202
  }))
233
203
  }
234
204
 
235
- // Phase D R2: register the recursive projection unit (activates only when a
236
- // projection registry is composed headless assemblies stay unaffected).
237
- // The unit's cell is per-session: a session's own log is its workspace, so
238
- // an accept-all init is scoped by the session boundary (see projection.ts
239
- // emptyRecursiveFoldState). The registry (if present) is cached here so the
240
- // session-start boundary can re-read run state after the projection drove.
241
- let sessionProjections: { snapshot?: (session: Session) => { values: Partial<Record<string, unknown>> } } | null = null
242
- ctx.inject(['sessionProjections'], (projectionCtx) => {
243
- const registry = (projectionCtx as { sessionProjections?: { register: (def: unknown) => () => void; snapshot?: (session: Session) => unknown } }).sessionProjections
244
- if (registry?.register) {
245
- disposers.push(registry.register(recursiveProjectionUnit))
246
- sessionProjections = registry as { snapshot?: (session: Session) => { values: Partial<Record<string, unknown>> } }
205
+ // Phase C R8: fs/observed lock-tamper WARNINGS are served to the board via the
206
+ // mountOnce-global: apply() runs per-session, but the route must register
207
+ // exactly once (WebServer.register throws on duplicate kind+path) and serve
208
+ // PER-WORKSPACE state. No-op when the host composes no webServer (headless).
209
+ const webServer = ctx.get('webServer') as
210
+ | { register: (route: { kind: string; path: string; handler: unknown }) => () => void }
211
+ | undefined
212
+ const sessionsStore = ctx.get('sessions') as
213
+ | { get?: (id: string) => { header?: { cwd?: string } } | undefined }
214
+ | undefined
215
+ if (webServer) {
216
+ const host: RecursiveRouteHost = {
217
+ // sessionId PRIMARY: the host resolves cwd from the attached session header;
218
+ // the client-passed cwd is a fallback hint (hydration / headless).
219
+ resolveRoot: async (sessionId: string | undefined, cwd: string) => recursive.resolveRootForRoute(sessionId, cwd, sessionsStore),
220
+ snapshot: async (root: string) => snapshotWorkspace(root),
221
+ revision: () => 1,
247
222
  }
248
- })
249
-
250
- // Stage B (R6): agent/session-start workflow init, workspace-scoped.
251
- // NEW sessions emit recursive/run-state('active') after init; resumed
252
- // sessions replay the projection (already folded from the persisted log) —
253
- // no duplicate run-created. run-created is emitted by the lifecycle at the
254
- // init/lock boundary (the only place a run's files first exist).
255
- const events = ctx as unknown as { on?: (event: string, listener: (payload: unknown) => void) => () => void }
256
- if (events.on) {
257
- disposers.push(events.on('agent/session-start', (payload) => {
258
- const p = payload as { agent?: AgentLike | null; source?: 'new' | 'resume' }
259
- const session = sessionOf(p)
260
- const cwd = p?.agent?.session?.header?.cwd
261
- if (!cwd) return
262
- void (async () => {
263
- const root = await recursive.resolveRootFor(p?.agent as { session?: { header?: { cwd?: string } } } | null)
264
- if (!root) return
265
- const result = stageBWorkflowInit({ root, source: p.source === 'resume' ? 'resume' : 'new' })
266
- // Emit run-state for the active run (from the projected wire value, the
267
- // one same-source view) when the projection registry is present.
268
- if (session) {
269
- const runId = result.activeRunId ?? result.runs[result.runs.length - 1] ?? ''
270
- if (runId) {
271
- const snap = sessionProjections?.snapshot?.(session)
272
- const proj = (snap?.values as { recursive?: Record<string, Record<string, { state?: string }>> } | undefined)?.recursive
273
- const state = proj?.[root]?.[runId]?.state ?? 'active'
274
- emit(session, 'recursive/run-state', runStateEvent({ runId, worktreeRoot: root, state: state as 'new' | 'active' | 'paused' | 'blocked' | 'complete' }))
275
- }
276
- }
277
- })()
278
- }))
223
+ disposers.push(mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes(host), webServer))
279
224
  }
280
225
 
281
226
  yield () => { for (const d of disposers) d() }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Init templates (R5): canonical-parity port of recursive-init.py's
3
+ * requirements_content / worktree_content / detect_git_context, plus full
4
+ * per-phase templates derived from the canonical artifact-template sections
5
+ * (get_artifact_required_sections) + TODO + Coverage/Approval FAIL gates.
6
+ *
7
+ * 00-requirements.md + 00-worktree.md are byte-identical to canonical
8
+ * recursive-init.py output (LF, no BOM). Later phases are structural: every
9
+ * required section heading + the two FAIL gates, so lint fails honestly until
10
+ * the phase is actually worked.
11
+ */
12
+ import { execFileSync } from 'node:child_process'
13
+ import { join } from 'node:path'
14
+ import { getArtifactRequiredSections } from './phase-rules.ts'
15
+
16
+ export interface GitContext {
17
+ baselineType: string
18
+ baselineReference: string
19
+ comparisonReference: string
20
+ normalizedBaseline: string
21
+ normalizedComparison: string
22
+ normalizedDiffCommand: string
23
+ baseBranch: string
24
+ worktreeBranch: string
25
+ baseCommit: string
26
+ notes: string
27
+ }
28
+
29
+ /**
30
+ * detect_git_context(): canonical parity. Returns {context, error} — never
31
+ * throws on missing git; error is surfaced in the worktree template note.
32
+ */
33
+ export function detectGitContext(repoRoot: string): { context: Partial<GitContext>; error: string | null } {
34
+ const run = (args: string[]): string | null => {
35
+ try {
36
+ return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+ const headSha = run(['rev-parse', '--verify', 'HEAD^{commit}'])
42
+ if (!headSha) {
43
+ return { context: {}, error: 'Unable to resolve HEAD commit for Phase 0 diff basis prefill: git rev-parse returned no output' }
44
+ }
45
+ const branch = run(['symbolic-ref', '--quiet', '--short', 'HEAD']) || '(detached HEAD)'
46
+ const diffCommand = 'git diff --name-only ' + headSha
47
+ return {
48
+ context: {
49
+ baselineType: 'local commit',
50
+ baselineReference: headSha,
51
+ comparisonReference: 'working-tree',
52
+ normalizedBaseline: headSha,
53
+ normalizedComparison: 'working-tree',
54
+ normalizedDiffCommand: diffCommand,
55
+ baseBranch: branch,
56
+ worktreeBranch: branch,
57
+ baseCommit: headSha,
58
+ notes: 'recursive-init prefilled this executable diff basis from the current HEAD commit. If Phase 0 later changes the chosen baseline, update every diff-basis field and rerun lint before locking.',
59
+ },
60
+ error: null,
61
+ }
62
+ }
63
+
64
+ /**
65
+ * requirements_content(run_id, template, from_issue): byte-identical to the
66
+ * canonical recursive-init.py function (LF, template flavor in Scope note).
67
+ */
68
+ export function requirementsContent(runId: string, template = 'feature', fromIssue = ''): string {
69
+ const inputs = ['- [chat summary or source notes if captured in repo]']
70
+ if (fromIssue.trim()) inputs.push('- Source: ' + fromIssue.trim())
71
+ const inputsBlock = inputs.join('\n')
72
+ return [
73
+ 'Run: `/.recursive/run/' + runId + '/`',
74
+ 'Phase: `00 Requirements`',
75
+ 'Status: `DRAFT`',
76
+ 'Workflow version: `recursive-mode-audit-v2`',
77
+ 'Inputs:',
78
+ inputsBlock,
79
+ 'Outputs:',
80
+ '- `/.recursive/run/' + runId + '/00-requirements.md`',
81
+ 'Scope note: This document defines stable requirement identifiers and acceptance criteria. (Template: ' + template + ')',
82
+ '',
83
+ '## TODO',
84
+ '',
85
+ '- [ ] Elicit requirements from user/context',
86
+ '- [ ] Define requirement identifiers (R1, R2, ...)',
87
+ '- [ ] Write acceptance criteria for each requirement',
88
+ '- [ ] Document out of scope items (OOS1, OOS2, ...)',
89
+ '- [ ] List constraints and assumptions',
90
+ '- [ ] Complete Coverage Gate checklist',
91
+ '- [ ] Complete Approval Gate checklist',
92
+ '',
93
+ '## Requirements',
94
+ '',
95
+ '### `R1` <short title>',
96
+ '',
97
+ 'Description:',
98
+ 'Acceptance criteria:',
99
+ '- [observable condition 1]',
100
+ '- [observable condition 2]',
101
+ '',
102
+ '## Out of Scope',
103
+ '',
104
+ '- `OOS1`: ...',
105
+ '',
106
+ '## Constraints',
107
+ '',
108
+ '- ...',
109
+ '',
110
+ '## Coverage Gate',
111
+ '...',
112
+ 'Coverage: FAIL',
113
+ '',
114
+ '## Approval Gate',
115
+ '...',
116
+ 'Approval: FAIL',
117
+ ].join('\n') + '\n'
118
+ }
119
+
120
+ /**
121
+ * worktree_content(run_id, repo_root, git_context, prefill_error): byte-identical
122
+ * to the canonical recursive-init.py function.
123
+ */
124
+ export function worktreeContent(runId: string, repoRoot: string, git: Partial<GitContext>, prefillError: string | null): string {
125
+ const baseBranch = git.baseBranch ?? '(resolve during Phase 0)'
126
+ const worktreeBranch = git.worktreeBranch ?? '(resolve during Phase 0)'
127
+ const baseCommit = git.baseCommit ?? '<resolve-before-locking>'
128
+ const baselineType = git.baselineType ?? 'local commit'
129
+ const baselineReference = git.baselineReference ?? '<resolve-before-locking>'
130
+ const comparisonReference = git.comparisonReference ?? 'working-tree'
131
+ const normalizedBaseline = git.normalizedBaseline ?? '<resolve-before-locking>'
132
+ const normalizedComparison = git.normalizedComparison ?? 'working-tree'
133
+ const normalizedDiffCommand = git.normalizedDiffCommand ?? 'git diff --name-only <resolve-before-locking>'
134
+ const notes = git.notes ?? 'Populate an executable diff basis before locking Phase 0. Lint and lock will fail until the normalized basis matches live git state.'
135
+ const setupNote = prefillError
136
+ ? 'recursive-init could not prefill the Phase 0 diff basis automatically: ' + prefillError
137
+ : 'recursive-init detected the current repository context and prefilled the Phase 0 diff basis.'
138
+ return [
139
+ 'Run: `/.recursive/run/' + runId + '/`',
140
+ 'Phase: `00 Worktree`',
141
+ 'Status: `DRAFT`',
142
+ 'Inputs:',
143
+ '- `/.recursive/run/' + runId + '/00-requirements.md`',
144
+ '- Current git repository state',
145
+ 'Outputs:',
146
+ '- `/.recursive/run/' + runId + '/00-worktree.md`',
147
+ 'Scope note: This document records the Phase 0 worktree context and the executable diff basis that all later audited phases must reuse.',
148
+ '',
149
+ '## TODO',
150
+ '',
151
+ '- [ ] Confirm the selected worktree location and isolation approach',
152
+ '- [ ] Confirm the base branch and worktree branch values',
153
+ '- [ ] Run setup and verify the clean test baseline',
154
+ '- [ ] Confirm the diff basis fields still match live git state',
155
+ '- [ ] Complete Coverage Gate checklist',
156
+ '- [ ] Complete Approval Gate checklist',
157
+ '',
158
+ '## Directory Selection',
159
+ '',
160
+ '- Repository root: `' + repoRoot + '`',
161
+ '- Preferred worktree location: `.worktrees/' + runId + '/`',
162
+ '- Update this section with the actual selected location before locking Phase 0.',
163
+ '',
164
+ '## Safety Verification',
165
+ '',
166
+ '- Original branch / repo state observed at init time: `' + baseBranch + '`',
167
+ '- Isolation still must be confirmed after the actual worktree is created.',
168
+ '',
169
+ '## Worktree Creation',
170
+ '',
171
+ '- Intended worktree branch: `' + worktreeBranch + '`',
172
+ '- Record the actual worktree creation command and output before locking.',
173
+ '',
174
+ '## Main Branch Protection',
175
+ '',
176
+ '- Base branch source of truth at init time: `' + baseBranch + '`',
177
+ '- Explicitly document any deviation from isolated worktree execution before locking.',
178
+ '',
179
+ '## Project Setup',
180
+ '',
181
+ '- Init-time note: ' + setupNote,
182
+ '- Replace this section with the actual setup commands and results during Phase 0.',
183
+ '',
184
+ '## Test Baseline Verification',
185
+ '',
186
+ '- Record the baseline commands and results after setup completes.',
187
+ '',
188
+ '## Worktree Context',
189
+ '',
190
+ '- Base branch: `' + baseBranch + '`',
191
+ '- Worktree branch: `' + worktreeBranch + '`',
192
+ '- Base commit: `' + baseCommit + '`',
193
+ '',
194
+ '## Diff Basis For Later Audits',
195
+ '',
196
+ '- Baseline type: `' + baselineType + '`',
197
+ '- Baseline reference: `' + baselineReference + '`',
198
+ '- Comparison reference: `' + comparisonReference + '`',
199
+ '- Normalized baseline: `' + normalizedBaseline + '`',
200
+ '- Normalized comparison: `' + normalizedComparison + '`',
201
+ '- Normalized diff command: `' + normalizedDiffCommand + '`',
202
+ '- Base branch: `' + baseBranch + '`',
203
+ '- Worktree branch: `' + worktreeBranch + '`',
204
+ '- Diff basis notes: `' + notes + '`',
205
+ '',
206
+ '## Traceability',
207
+ '',
208
+ '- Recursive workflow safety -> Phase 0 records a reusable executable diff basis before audited phases begin.',
209
+ '',
210
+ '## Coverage Gate',
211
+ '',
212
+ '- [ ] Worktree location and branch context are recorded',
213
+ '- [ ] Setup and clean baseline verification are recorded',
214
+ '- [ ] Diff basis fields are executable against live git state',
215
+ '',
216
+ 'Coverage: FAIL',
217
+ '',
218
+ '## Approval Gate',
219
+ '',
220
+ '- [ ] Phase 0 context is ready for downstream audited phases',
221
+ '- [ ] No unresolved setup or diff-basis inconsistencies remain',
222
+ '',
223
+ 'Approval: FAIL',
224
+ ].join('\n') + '\n'
225
+ }
226
+
227
+ /**
228
+ * Later-phase template: required sections (canonical get_artifact_required_
229
+ * sections) as ## headings + TODO + FAIL gates. Structural scaffold: the phase
230
+ * author fills each section; lint stays FAIL until Coverage/Approval are set.
231
+ */
232
+ export function laterPhaseContent(runId: string, fileName: string, workflowProfile = 'recursive-mode-audit-v2'): string {
233
+ const sections = getArtifactRequiredSections(fileName, workflowProfile)
234
+ const lines = [
235
+ 'Run: `/.recursive/run/' + runId + '/`',
236
+ 'Phase: `' + fileName.replace(/\.md$/, '').replace(/^0?/, '') + '`',
237
+ 'Status: `DRAFT`',
238
+ 'Workflow version: `' + workflowProfile + '`',
239
+ 'Inputs:',
240
+ '- (list upstream artifacts re-read for this phase)',
241
+ 'Outputs:',
242
+ '- `/.recursive/run/' + runId + '/' + fileName + '`',
243
+ 'Scope note: Scaffold generated by the recursive-mode plugin (R5). Fill every required section before lint.',
244
+ '',
245
+ '## TODO',
246
+ '',
247
+ '- [ ] Work through every required section below',
248
+ '- [ ] Record evidence and traceability',
249
+ '- [ ] Complete Coverage Gate checklist',
250
+ '- [ ] Complete Approval Gate checklist',
251
+ ];
252
+ for (const section of sections) {
253
+ if (section === 'TODO') continue
254
+ lines.push('', '## ' + section, '', '- (fill)')
255
+ }
256
+ lines.push('', '## Coverage Gate', '', 'Coverage: FAIL', '', '## Approval Gate', '', 'Approval: FAIL', '')
257
+ return lines.join('\n')
258
+ }
259
+
260
+ /**
261
+ * Scaffold dirs under a run (addenda/subagents/router-prompts/evidence + subs).
262
+ * Mirrors canonical recursive-init.py's ensure_directory sequence.
263
+ */
264
+ export const RUN_SCAFFOLD_DIRS = [
265
+ 'addenda',
266
+ 'subagents',
267
+ 'router-prompts',
268
+ 'evidence',
269
+ 'evidence/screenshots',
270
+ 'evidence/logs',
271
+ 'evidence/perf',
272
+ 'evidence/traces',
273
+ 'evidence/review-bundles',
274
+ 'evidence/router',
275
+ 'evidence/other',
276
+ ]