@try-works/dsh-recursive-mode 0.1.5 → 0.1.6

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 (85) hide show
  1. package/lib/bootstrap.d.ts +6 -3
  2. package/lib/client/board.d.ts +4 -1
  3. package/lib/client/open-state.d.ts +20 -0
  4. package/lib/client/slots.d.ts +7 -1
  5. package/lib/client.js +135 -47
  6. package/lib/index.js +3391 -906
  7. package/lib/runtime.d.ts +12 -5
  8. package/lib/ts-lint.d.ts +205 -0
  9. package/package.json +1 -1
  10. package/src/bootstrap.ts +338 -331
  11. package/src/client/board.tsx +0 -0
  12. package/src/client/open-state.ts +46 -0
  13. package/src/client/slots.ts +41 -20
  14. package/src/commands.ts +11 -0
  15. package/src/index.ts +238 -228
  16. package/src/runtime.ts +19 -23
  17. package/src/ts-lint.ts +2110 -0
  18. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  19. package/references/scripts/lint-recursive-run.ps1 +0 -25
  20. package/references/scripts/lint-recursive-run.py +0 -2870
  21. package/references/scripts/recursive-closeout.ps1 +0 -38
  22. package/references/scripts/recursive-closeout.py +0 -541
  23. package/references/scripts/recursive-init.ps1 +0 -515
  24. package/references/scripts/recursive-init.py +0 -356
  25. package/references/scripts/recursive-lock.ps1 +0 -26
  26. package/references/scripts/recursive-lock.py +0 -302
  27. package/references/scripts/recursive-review-bundle.ps1 +0 -58
  28. package/references/scripts/recursive-review-bundle.py +0 -503
  29. package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
  30. package/references/scripts/recursive-router-cli-configure.py +0 -9
  31. package/references/scripts/recursive-router-cli-init.ps1 +0 -2
  32. package/references/scripts/recursive-router-cli-init.py +0 -9
  33. package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
  34. package/references/scripts/recursive-router-cli-invoke.py +0 -9
  35. package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
  36. package/references/scripts/recursive-router-cli-probe.py +0 -9
  37. package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
  38. package/references/scripts/recursive-router-cli-resolve.py +0 -9
  39. package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
  40. package/references/scripts/recursive-router-cli-validate.py +0 -9
  41. package/references/scripts/recursive-router-configure.ps1 +0 -27
  42. package/references/scripts/recursive-router-configure.py +0 -74
  43. package/references/scripts/recursive-router-init.ps1 +0 -17
  44. package/references/scripts/recursive-router-init.py +0 -29
  45. package/references/scripts/recursive-router-invoke.ps1 +0 -47
  46. package/references/scripts/recursive-router-invoke.py +0 -103
  47. package/references/scripts/recursive-router-probe.ps1 +0 -25
  48. package/references/scripts/recursive-router-probe.py +0 -44
  49. package/references/scripts/recursive-router-resolve.ps1 +0 -26
  50. package/references/scripts/recursive-router-resolve.py +0 -46
  51. package/references/scripts/recursive-router-validate.ps1 +0 -17
  52. package/references/scripts/recursive-router-validate.py +0 -27
  53. package/references/scripts/recursive-status.ps1 +0 -23
  54. package/references/scripts/recursive-status.py +0 -2124
  55. package/references/scripts/recursive-subagent-action.ps1 +0 -98
  56. package/references/scripts/recursive-subagent-action.py +0 -197
  57. package/references/scripts/recursive-training-extract.ps1 +0 -23
  58. package/references/scripts/recursive-training-extract.py +0 -99
  59. package/references/scripts/recursive-training-grpo.ps1 +0 -52
  60. package/references/scripts/recursive-training-grpo.py +0 -1341
  61. package/references/scripts/recursive-training-loader.ps1 +0 -78
  62. package/references/scripts/recursive-training-loader.py +0 -558
  63. package/references/scripts/recursive-training-mcp.ps1 +0 -29
  64. package/references/scripts/recursive-training-mcp.py +0 -261
  65. package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
  66. package/references/scripts/recursive-training-phase8-trigger.py +0 -137
  67. package/references/scripts/recursive-training-sync.ps1 +0 -29
  68. package/references/scripts/recursive-training-sync.py +0 -183
  69. package/references/scripts/recursive_phase_rules.py +0 -367
  70. package/references/scripts/recursive_router_cli_lib.py +0 -2
  71. package/references/scripts/recursive_router_lib.py +0 -2282
  72. package/references/scripts/verify-locks.ps1 +0 -25
  73. package/references/scripts/verify-locks.py +0 -353
  74. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  75. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  76. package/scripts/install-recursive-mode.ps1 +0 -956
  77. package/scripts/install-recursive-mode.py +0 -750
  78. package/scripts/lint-recursive-run.py +0 -2870
  79. package/scripts/recursive-closeout.py +0 -541
  80. package/scripts/recursive-init.py +0 -356
  81. package/scripts/recursive-lock.py +0 -302
  82. package/scripts/recursive-status.py +0 -2124
  83. package/scripts/recursive_phase_rules.py +0 -367
  84. package/scripts/recursive_router_lib.py +0 -2282
  85. package/scripts/verify-locks.py +0 -353
package/src/index.ts CHANGED
@@ -1,228 +1,238 @@
1
- import { type Context } from '@deepseek-ai/cordis'
2
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
3
- import { existsSync } from 'node:fs'
4
- import { join } from 'node:path'
5
- import { RecursiveRuntime } from './runtime.ts'
6
- import { createRecursiveStatusTool } from './recursive_status.tool.ts'
7
- import { createRecursiveInitTool } from './recursive_init.tool.ts'
8
- import { createRecursiveLockTool } from './recursive_lock.tool.ts'
9
- import { createRecursiveLintTool } from './recursive_lint.tool.ts'
10
- import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
11
- import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
12
- import { registerRecursiveCommand } from './commands.ts'
13
- import { evaluateToolGuard } from './enforcement.ts'
14
- import { renderRecursivePolicy } from './policy.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'
21
-
22
- export const name = '@try-works/dsh-recursive-mode'
23
-
24
- export { RecursiveRuntime } from './runtime.ts'
25
- export { createRecursiveStatusTool } from './recursive_status.tool.ts'
26
- export { createRecursiveInitTool } from './recursive_init.tool.ts'
27
- export { createRecursiveLockTool } from './recursive_lock.tool.ts'
28
- export { createRecursiveLintTool } from './recursive_lint.tool.ts'
29
- export { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
30
- export { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
31
- export * from './status.ts'
32
- export {
33
- PHASE_SEQUENCE,
34
- OPTIONAL_PHASES,
35
- normalizeForLockHash,
36
- lockHashFromContent,
37
- phaseIndex,
38
- isCoreArtifact,
39
- getPrerequisites,
40
- getLockStatus,
41
- getPrerequisiteBlockers,
42
- receiptPath,
43
- readReceipt,
44
- writeReceipt,
45
- invalidateReceipt,
46
- getStaleDownstreamPhases,
47
- getNextLegalPhase,
48
- getAllStaleReceipts,
49
- validateChain,
50
- } from './lock.ts'
51
- export type {
52
- LockReceipt,
53
- LockStatus,
54
- PrerequisiteBlocker,
55
- StaleDownstream,
56
- ChainPhaseResult,
57
- LockChainResult,
58
- } from './lock.ts'
59
- export * from './run.ts'
60
- export * from './review.ts'
61
- export * from './handoff.ts'
62
- export * from './router.ts'
63
- export * from './delegation.ts'
64
- export * from './lifecycle.ts'
65
- export * from './enforcement.ts'
66
- export * from './policy.ts'
67
- export * from './snapshot.ts'
68
- export * from './live-route.ts'
69
-
70
- /**
71
- * Bundle plugin entry. The Loader activates this row once `tools` is available
72
- * (`inject` below); the RecursiveRuntime service is constructed directly so it
73
- * is provided on `ctx.recursive` for the lifetime of this fiber, and the
74
- * read-path tools (status/init/lock/lint) are registered through it (R2/R4).
75
- */
76
- export const inject = ['tools']
77
-
78
- /**
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.
85
- */
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
95
- ctx.effect(function* () {
96
- // Workspace registry: optional host service (durable). Access via ctx.get —
97
- // property access requires inject and would fail boot when undeclared.
98
- // Resolve the control-plane root strictly from the session agent's cwd.
99
- const workspaceRegistry = ctx.get('workspaceRegistry') as never
100
- const recursive = new RecursiveRuntime(ctx, { repoRoot: config?.repoRoot ?? process.cwd(), workspaceRegistry })
101
-
102
- const disposers = [
103
- ctx.tools.register(createRecursiveStatusTool(recursive)),
104
- ctx.tools.register(createRecursiveInitTool(recursive)),
105
- ctx.tools.register(createRecursiveLockTool(recursive)),
106
- ctx.tools.register(createRecursiveLintTool(recursive)),
107
- ctx.tools.register(createRecursiveCloseoutTool(recursive)),
108
- ctx.tools.register(createRecursiveScratchTool(recursive)),
109
- ]
110
-
111
- // /recursive command (R4): preset-scoped registration, workspace-scoped dispatch.
112
- const commands = ctx.get('commands') as { register: (def: unknown) => () => void } | undefined
113
- if (commands) {
114
- disposers.push(registerRecursiveCommand({ commands } as never, recursive))
115
- }
116
-
117
- // recursive:policy prompt section (Phase C R5): workspace-scoped behavior +
118
- // current-phase contract rendered from folded state + enforcement config.
119
- const systemPrompt = ctx.get('systemPrompt') as { section: (def: unknown) => () => void } | undefined
120
- if (systemPrompt) {
121
- disposers.push(systemPrompt.section({
122
- name: 'recursive:policy',
123
- order: 55,
124
- text: (context: unknown) => {
125
- const agent = (context as { agent?: { session?: { header?: { cwd?: string } } } } | undefined)?.agent
126
- if (!agent) return ''
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 })
135
- },
136
- }))
137
- }
138
-
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.
147
-
148
- // Phase C R4: tools/pre-execute surgical guards (Layer 2, caller of the
149
- // transition set). Scope-filtered to the active run's worktree.
150
- const toolRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
151
- if (toolRuntime.on) {
152
- disposers.push(toolRuntime.on('tools/pre-execute', (payload, next) => {
153
- const exec = payload as { name?: string; arguments?: unknown; agent?: { session?: { header?: { cwd?: string } } } | null } | null
154
- if (!exec?.name) return typeof next === 'function' ? next() : { kind: 'allow' }
155
- // B3: per-call root is the session cwd (authoritative when the registry is
156
- // absent), never process.cwd().
157
- const root = exec?.agent?.session?.header?.cwd ?? ''
158
- const decision = evaluateToolGuard(exec as never, root, '', recursive.enforcementConfig.toolGuards)
159
- if (decision.kind === 'allow') return typeof next === 'function' ? next() : { kind: 'allow' }
160
- if (decision.kind === 'deny') return decision
161
- // ask -> in advisory we let it through with a warn (the approval seam is Phase D)
162
- }))
163
- }
164
-
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
202
- }))
203
- }
204
-
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,
222
- }
223
- disposers.push(mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes(host), webServer))
224
- }
225
-
226
- yield () => { for (const d of disposers) d() }
227
- })
228
- }
1
+ import { type Context } from '@deepseek-ai/cordis'
2
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
3
+ import { existsSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { RecursiveRuntime } from './runtime.ts'
6
+ import { createRecursiveStatusTool } from './recursive_status.tool.ts'
7
+ import { createRecursiveInitTool } from './recursive_init.tool.ts'
8
+ import { createRecursiveLockTool } from './recursive_lock.tool.ts'
9
+ import { createRecursiveLintTool } from './recursive_lint.tool.ts'
10
+ import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
11
+ import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
12
+ import { registerRecursiveCommand } from './commands.ts'
13
+ import { evaluateToolGuard } from './enforcement.ts'
14
+ import { renderRecursivePolicy } from './policy.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, stageBWorkflowInit } from './bootstrap.ts'
19
+ import { getNextLegalPhase, getLockStatus } from './lock.ts'
20
+ import { phaseLintRulesMessage } from './phase-rules.ts'
21
+
22
+ export const name = '@try-works/dsh-recursive-mode'
23
+
24
+ export { RecursiveRuntime } from './runtime.ts'
25
+ export { createRecursiveStatusTool } from './recursive_status.tool.ts'
26
+ export { createRecursiveInitTool } from './recursive_init.tool.ts'
27
+ export { createRecursiveLockTool } from './recursive_lock.tool.ts'
28
+ export { createRecursiveLintTool } from './recursive_lint.tool.ts'
29
+ export { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
30
+ export { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
31
+ export * from './status.ts'
32
+ export {
33
+ PHASE_SEQUENCE,
34
+ OPTIONAL_PHASES,
35
+ normalizeForLockHash,
36
+ lockHashFromContent,
37
+ phaseIndex,
38
+ isCoreArtifact,
39
+ getPrerequisites,
40
+ getLockStatus,
41
+ getPrerequisiteBlockers,
42
+ receiptPath,
43
+ readReceipt,
44
+ writeReceipt,
45
+ invalidateReceipt,
46
+ getStaleDownstreamPhases,
47
+ getNextLegalPhase,
48
+ getAllStaleReceipts,
49
+ validateChain,
50
+ } from './lock.ts'
51
+ export type {
52
+ LockReceipt,
53
+ LockStatus,
54
+ PrerequisiteBlocker,
55
+ StaleDownstream,
56
+ ChainPhaseResult,
57
+ LockChainResult,
58
+ } from './lock.ts'
59
+ export * from './run.ts'
60
+ export * from './review.ts'
61
+ export * from './handoff.ts'
62
+ export * from './router.ts'
63
+ export * from './delegation.ts'
64
+ export * from './lifecycle.ts'
65
+ export * from './enforcement.ts'
66
+ export * from './policy.ts'
67
+ export * from './snapshot.ts'
68
+ export * from './live-route.ts'
69
+
70
+ /**
71
+ * Bundle plugin entry. The Loader activates this row once `tools` is available
72
+ * (`inject` below); the RecursiveRuntime service is constructed directly so it
73
+ * is provided on `ctx.recursive` for the lifetime of this fiber, and the
74
+ * read-path tools (status/init/lock/lint) are registered through it (R2/R4).
75
+ */
76
+ export const inject = ['tools']
77
+
78
+ /**
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.
85
+ */
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
95
+ ctx.effect(function* () {
96
+ // Workspace registry: optional host service (durable). Access via ctx.get —
97
+ // property access requires inject and would fail boot when undeclared.
98
+ // Resolve the control-plane root strictly from the session agent's cwd.
99
+ const workspaceRegistry = ctx.get('workspaceRegistry') as never
100
+ const recursive = new RecursiveRuntime(ctx, { repoRoot: config?.repoRoot ?? process.cwd(), workspaceRegistry })
101
+
102
+ const repairedRoots = new Set<string>()
103
+ const disposers = [
104
+ ctx.tools.register(createRecursiveStatusTool(recursive)),
105
+ ctx.tools.register(createRecursiveInitTool(recursive)),
106
+ ctx.tools.register(createRecursiveLockTool(recursive)),
107
+ ctx.tools.register(createRecursiveLintTool(recursive)),
108
+ ctx.tools.register(createRecursiveCloseoutTool(recursive)),
109
+ ctx.tools.register(createRecursiveScratchTool(recursive)),
110
+ ]
111
+
112
+ // /recursive command (R4): preset-scoped registration, workspace-scoped dispatch.
113
+ const commands = ctx.get('commands') as { register: (def: unknown) => () => void } | undefined
114
+ if (commands) {
115
+ disposers.push(registerRecursiveCommand({ commands } as never, recursive))
116
+ }
117
+
118
+ // recursive:policy prompt section (Phase C R5): workspace-scoped behavior +
119
+ // current-phase contract rendered from folded state + enforcement config.
120
+ const systemPrompt = ctx.get('systemPrompt') as { section: (def: unknown) => () => void } | undefined
121
+ if (systemPrompt) {
122
+ disposers.push(systemPrompt.section({
123
+ name: 'recursive:policy',
124
+ order: 55,
125
+ text: (context: unknown) => {
126
+ const agent = (context as { agent?: { session?: { header?: { cwd?: string } } } } | undefined)?.agent
127
+ if (!agent) return ''
128
+ // SP3 R5 policy-render fix: derive intent from the FILESYSTEM, not the
129
+ // retired recursive/phase-intent session event (zero-emission removed
130
+ // the emitter, so detectTransitionIntent was ALWAYS null and this
131
+ // section rendered ''). Pure read-only fs folding; no recursive/*
132
+ // events are appended.
133
+ const intent = fsPolicyIntent(agent, workspaceRegistry as never)
134
+ if (!intent) return ''
135
+ return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, config: recursive.enforcementConfig })
136
+ },
137
+ }))
138
+ }
139
+
140
+ // Phase C R3 (Layer 1, agent/pre-step proactive intent gate) is RETIRED
141
+ // under zero-emission (SP2 R1): its only signal was the recursive/phase-intent
142
+ // session event, whose emitter is now deleted, and it was ADVISORY by default
143
+ // (it never rejected; its sole side effect was the now-removed emission).
144
+ // Enforcement is preserved where it actually bites: Layer 2 (tools/pre-execute
145
+ // inspects the REAL tool call args below) plus the recursive_lock tool's own
146
+ // prerequisite validation in lockArtifact — strictly more reliable than the
147
+ // heuristic intent scan. See 03-implementation-summary.addendum-r1-*.md.
148
+
149
+ // Phase C R4: tools/pre-execute surgical guards (Layer 2, caller of the
150
+ // transition set). Scope-filtered to the active run's worktree.
151
+ const toolRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
152
+ if (toolRuntime.on) {
153
+ disposers.push(toolRuntime.on('tools/pre-execute', (payload, next) => {
154
+ const exec = payload as { name?: string; arguments?: unknown; agent?: { session?: { header?: { cwd?: string } } } | null } | null
155
+ if (!exec?.name) return typeof next === 'function' ? next() : { kind: 'allow' }
156
+ // B3: per-call root is the session cwd (authoritative when the registry is
157
+ // absent), never process.cwd().
158
+ const root = exec?.agent?.session?.header?.cwd ?? ''
159
+ const decision = evaluateToolGuard(exec as never, root, '', recursive.enforcementConfig.toolGuards)
160
+ if (decision.kind === 'allow') return typeof next === 'function' ? next() : { kind: 'allow' }
161
+ if (decision.kind === 'deny') return decision
162
+ // ask -> in advisory we let it through with a warn (the approval seam is Phase D)
163
+ }))
164
+ }
165
+
166
+ // SP3 R5: agent/pre-step lint-rules injection (pre-step contract verified: the
167
+ // listener returns { kind: 'enter', messages: [...messages, injected] } and the
168
+ // returned array REPLACES the default [...claimed, context]). When the session's
169
+ // control-plane root has an active recursive run whose current phase doc is
170
+ // DRAFT, prepend a compact system-reminder with THAT phase's required sections +
171
+ // gates. Pure fs read (zero-emission): never appends recursive/* events.
172
+ const agentRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
173
+ if (agentRuntime.on) {
174
+ disposers.push(agentRuntime.on('agent/pre-step', async (payload, next) => {
175
+ const p = payload as {
176
+ messages?: Array<{ content: Array<{ type: string; text?: string }> }>,
177
+ agent?: { session?: { header?: { cwd?: string } } } | null,
178
+ } | null
179
+ const messages = p?.messages ?? []
180
+ const agent = p?.agent ?? null
181
+ const cwd = agent?.session?.header?.cwd ?? ''
182
+ // delegate first so later listeners keep veto power, then fold ours on
183
+ if (typeof next === 'function') await next()
184
+ if (!cwd) return { kind: 'enter', messages } as const
185
+ const root = await recursive.resolveRootForRoute(undefined, cwd, undefined)
186
+ if (!root) return { kind: 'enter', messages } as const
187
+ // R3/R6 (run 09): idempotent scaffold REPAIR on session-start (new AND
188
+ // resume). bootstrapScaffold is upsert-only: it adds missing control-plane
189
+ // files/dirs + re-upserts marked blocks, NEVER overwriting user content or
190
+ // touching run/ artifacts (in-flight 02-to-be-plan etc. are preserved).
191
+ // Guarded once per root so repeated pre-steps are cheap no-ops.
192
+ if (!repairedRoots.has(root)) {
193
+ repairedRoots.add(root)
194
+ stageBWorkflowInit({ root, source: 'resume' })
195
+ }
196
+ const runs = enumerateRuns(root)
197
+ if (runs.length === 0) return { kind: 'enter', messages } as const
198
+ const runId = runs[runs.length - 1]
199
+ const runDir = join(root, '.recursive', 'run', runId)
200
+ if (!existsSync(runDir)) return { kind: 'enter', messages } as const
201
+ const phase = getNextLegalPhase(runDir)
202
+ if (!phase) return { kind: 'enter', messages } as const
203
+ // only inject when the phase doc is DRAFT (not locked/missing).
204
+ const phasePath = join(runDir, phase)
205
+ const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
206
+ if (status !== 'DRAFT') return { kind: 'enter', messages } as const
207
+ const reminder = phaseLintRulesMessage(phase)
208
+ return {
209
+ kind: 'enter',
210
+ messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
211
+ } as const
212
+ }))
213
+ }
214
+
215
+ // Phase C R8: fs/observed lock-tamper WARNINGS are served to the board via the
216
+ // mountOnce-global: apply() runs per-session, but the route must register
217
+ // exactly once (WebServer.register throws on duplicate kind+path) and serve
218
+ // PER-WORKSPACE state. No-op when the host composes no webServer (headless).
219
+ const webServer = ctx.get('webServer') as
220
+ | { register: (route: { kind: string; path: string; handler: unknown }) => () => void }
221
+ | undefined
222
+ const sessionsStore = ctx.get('sessions') as
223
+ | { get?: (id: string) => { header?: { cwd?: string } } | undefined }
224
+ | undefined
225
+ if (webServer) {
226
+ const host: RecursiveRouteHost = {
227
+ // sessionId PRIMARY: the host resolves cwd from the attached session header;
228
+ // the client-passed cwd is a fallback hint (hydration / headless).
229
+ resolveRoot: async (sessionId: string | undefined, cwd: string) => recursive.resolveRootForRoute(sessionId, cwd, sessionsStore),
230
+ snapshot: async (root: string) => snapshotWorkspace(root),
231
+ revision: () => 1,
232
+ }
233
+ disposers.push(mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes(host), webServer))
234
+ }
235
+
236
+ yield () => { for (const d of disposers) d() }
237
+ })
238
+ }
package/src/runtime.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Service, type Context } from '@deepseek-ai/cordis'
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
- import { execFileSync } from 'node:child_process'
4
3
  import { join } from 'node:path'
5
- import { referencesRoot } from './bootstrap.ts'
4
+ import { lintRun } from './ts-lint.ts'
6
5
  import { requirementsContent, worktreeContent, laterPhaseContent, detectGitContext, RUN_SCAFFOLD_DIRS, type GitContext } from './init-templates.ts'
7
6
  import { foldRun, resolveRunDir } from './status.ts'
8
7
  import {
@@ -464,12 +463,11 @@ export class RecursiveRuntime extends Service {
464
463
  }
465
464
 
466
465
  /**
467
- * Lint an artifact with FULL canonical parity: shells out to the vendored
468
- * lint-recursive-run.py (byte-identical to canonical) and parses its
469
- * [FAIL]/[WARN] lines + Summary counts. `errors` = FAIL lines, `warnings` =
466
+ * Lint an artifact with FULL canonical parity: runs the in-process ts-lint
467
+ * port (ts-lint.ts `lintRun`) against the whole run and filters the verdict
468
+ * lines to the requested artifact. `errors` = FAIL lines, `warnings` =
470
469
  * WARN lines, `passed` = no FAIL (WARN-only passes, matching the canonical
471
- * non-strict verdict). Falls back to the structural check only if the vendored
472
- * script is unavailable (e.g. bundled lib without references/).
470
+ * non-strict verdict).
473
471
  */
474
472
  async lintArtifact(runId: string, artifact?: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<LintArtifactResult> {
475
473
  const root = await this.resolveRootFor(agent)
@@ -480,22 +478,20 @@ export class RecursiveRuntime extends Service {
480
478
  if (!existsSync(artifactPath)) {
481
479
  return { artifact: target, runId, errors: ['Artifact not found'], warnings: [], passed: false }
482
480
  }
483
- const lintScript = join(referencesRoot(), 'scripts', 'lint-recursive-run.py')
484
- if (existsSync(lintScript)) {
485
- try {
486
- const stdout = execFileSync('python', [lintScript, '--repo-root', root, '--run-id', runId], {
487
- encoding: 'utf8',
488
- stdio: ['ignore', 'pipe', 'ignore'],
489
- timeout: 60000,
490
- })
491
- return parseLintOutput(stdout, target, runId)
492
- } catch (err) {
493
- const stdout = (err as { stdout?: string })?.stdout ?? ''
494
- if (stdout.includes('Summary')) {
495
- return parseLintOutput(stdout, target, runId)
496
- }
497
- // script failed to run entirely — fall through to structural check
498
- }
481
+ const result = lintRun(root, runId)
482
+ const stdout = [...result.errors, ...result.warnings].join('\n')
483
+ return parseLintOutput(stdout, target, runId)
484
+ }
485
+
486
+ /** Legacy structural fallback (kept for type reference; lintArtifact uses lintRun). */
487
+ async lintArtifactStructuralFallback(runId: string, artifact: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<LintArtifactResult> {
488
+ const root = await this.resolveRootFor(agent)
489
+ if (!root) throw new Error('cannot resolve workspace control-plane root for this session')
490
+ const runDir = join(root, '.recursive', 'run', runId)
491
+ const target = artifact ?? getNextLegalPhase(runDir) ?? '00-requirements.md'
492
+ const artifactPath = join(runDir, target)
493
+ if (!existsSync(artifactPath)) {
494
+ return { artifact: target, runId, errors: ['Artifact not found'], warnings: [], passed: false }
499
495
  }
500
496
  // Structural fallback (no vendored script available).
501
497
  const content = readFileSync(artifactPath, 'utf8')