@try-works/dsh-recursive-mode 0.1.4 → 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 (90) 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/fs-intent.d.ts +34 -0
  7. package/lib/index.d.ts +1 -0
  8. package/lib/index.js +3429 -911
  9. package/lib/policy.d.ts +1 -1
  10. package/lib/runtime.d.ts +12 -5
  11. package/lib/ts-lint.d.ts +205 -0
  12. package/package.json +1 -1
  13. package/src/bootstrap.ts +338 -331
  14. package/src/client/board.tsx +0 -0
  15. package/src/client/open-state.ts +46 -0
  16. package/src/client/slots.ts +41 -20
  17. package/src/commands.ts +11 -0
  18. package/src/fs-intent.ts +84 -0
  19. package/src/index.ts +238 -226
  20. package/src/policy.ts +1 -1
  21. package/src/runtime.ts +19 -23
  22. package/src/ts-lint.ts +2110 -0
  23. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  24. package/references/scripts/lint-recursive-run.ps1 +0 -25
  25. package/references/scripts/lint-recursive-run.py +0 -2870
  26. package/references/scripts/recursive-closeout.ps1 +0 -38
  27. package/references/scripts/recursive-closeout.py +0 -541
  28. package/references/scripts/recursive-init.ps1 +0 -515
  29. package/references/scripts/recursive-init.py +0 -356
  30. package/references/scripts/recursive-lock.ps1 +0 -26
  31. package/references/scripts/recursive-lock.py +0 -302
  32. package/references/scripts/recursive-review-bundle.ps1 +0 -58
  33. package/references/scripts/recursive-review-bundle.py +0 -503
  34. package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
  35. package/references/scripts/recursive-router-cli-configure.py +0 -9
  36. package/references/scripts/recursive-router-cli-init.ps1 +0 -2
  37. package/references/scripts/recursive-router-cli-init.py +0 -9
  38. package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
  39. package/references/scripts/recursive-router-cli-invoke.py +0 -9
  40. package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
  41. package/references/scripts/recursive-router-cli-probe.py +0 -9
  42. package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
  43. package/references/scripts/recursive-router-cli-resolve.py +0 -9
  44. package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
  45. package/references/scripts/recursive-router-cli-validate.py +0 -9
  46. package/references/scripts/recursive-router-configure.ps1 +0 -27
  47. package/references/scripts/recursive-router-configure.py +0 -74
  48. package/references/scripts/recursive-router-init.ps1 +0 -17
  49. package/references/scripts/recursive-router-init.py +0 -29
  50. package/references/scripts/recursive-router-invoke.ps1 +0 -47
  51. package/references/scripts/recursive-router-invoke.py +0 -103
  52. package/references/scripts/recursive-router-probe.ps1 +0 -25
  53. package/references/scripts/recursive-router-probe.py +0 -44
  54. package/references/scripts/recursive-router-resolve.ps1 +0 -26
  55. package/references/scripts/recursive-router-resolve.py +0 -46
  56. package/references/scripts/recursive-router-validate.ps1 +0 -17
  57. package/references/scripts/recursive-router-validate.py +0 -27
  58. package/references/scripts/recursive-status.ps1 +0 -23
  59. package/references/scripts/recursive-status.py +0 -2124
  60. package/references/scripts/recursive-subagent-action.ps1 +0 -98
  61. package/references/scripts/recursive-subagent-action.py +0 -197
  62. package/references/scripts/recursive-training-extract.ps1 +0 -23
  63. package/references/scripts/recursive-training-extract.py +0 -99
  64. package/references/scripts/recursive-training-grpo.ps1 +0 -52
  65. package/references/scripts/recursive-training-grpo.py +0 -1341
  66. package/references/scripts/recursive-training-loader.ps1 +0 -78
  67. package/references/scripts/recursive-training-loader.py +0 -558
  68. package/references/scripts/recursive-training-mcp.ps1 +0 -29
  69. package/references/scripts/recursive-training-mcp.py +0 -261
  70. package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
  71. package/references/scripts/recursive-training-phase8-trigger.py +0 -137
  72. package/references/scripts/recursive-training-sync.ps1 +0 -29
  73. package/references/scripts/recursive-training-sync.py +0 -183
  74. package/references/scripts/recursive_phase_rules.py +0 -367
  75. package/references/scripts/recursive_router_cli_lib.py +0 -2
  76. package/references/scripts/recursive_router_lib.py +0 -2282
  77. package/references/scripts/verify-locks.ps1 +0 -25
  78. package/references/scripts/verify-locks.py +0 -353
  79. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  80. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  81. package/scripts/install-recursive-mode.ps1 +0 -956
  82. package/scripts/install-recursive-mode.py +0 -750
  83. package/scripts/lint-recursive-run.py +0 -2870
  84. package/scripts/recursive-closeout.py +0 -541
  85. package/scripts/recursive-init.py +0 -356
  86. package/scripts/recursive-lock.py +0 -302
  87. package/scripts/recursive-status.py +0 -2124
  88. package/scripts/recursive_phase_rules.py +0 -367
  89. package/scripts/recursive_router_lib.py +0 -2282
  90. package/scripts/verify-locks.py +0 -353
package/src/index.ts CHANGED
@@ -1,226 +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 { detectTransitionIntent } from './lifecycle.ts'
15
- import { renderRecursivePolicy } from './policy.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 }) {
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: 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 }; events?: { type: string; data?: Record<string, unknown> }[] } } } | undefined)?.agent
126
- if (!agent) return ''
127
- const cwd = agent.session?.header?.cwd ?? ''
128
- if (!cwd) return ''
129
- const events = agent.session?.events ?? []
130
- const intent = detectTransitionIntent(events as never)
131
- if (!intent || intent.worktreeRoot === '') return ''
132
- return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, folded: recursive.foldPhase(events as never), config: recursive.enforcementConfig })
133
- },
134
- }))
135
- }
136
-
137
- // Phase C R3 (Layer 1, agent/pre-step proactive intent gate) is RETIRED
138
- // under zero-emission (SP2 R1): its only signal was the recursive/phase-intent
139
- // session event, whose emitter is now deleted, and it was ADVISORY by default
140
- // (it never rejected; its sole side effect was the now-removed emission).
141
- // Enforcement is preserved where it actually bites: Layer 2 (tools/pre-execute
142
- // inspects the REAL tool call args below) plus the recursive_lock tool's own
143
- // prerequisite validation in lockArtifact strictly more reliable than the
144
- // heuristic intent scan. See 03-implementation-summary.addendum-r1-*.md.
145
-
146
- // Phase C R4: tools/pre-execute surgical guards (Layer 2, caller of the
147
- // transition set). Scope-filtered to the active run's worktree.
148
- const toolRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
149
- if (toolRuntime.on) {
150
- disposers.push(toolRuntime.on('tools/pre-execute', (payload, next) => {
151
- const exec = payload as { name?: string; arguments?: unknown; agent?: { session?: { header?: { cwd?: string } } } | null } | null
152
- if (!exec?.name) return typeof next === 'function' ? next() : { kind: 'allow' }
153
- // B3: per-call root is the session cwd (authoritative when the registry is
154
- // absent), never process.cwd().
155
- const root = exec?.agent?.session?.header?.cwd ?? ''
156
- const decision = evaluateToolGuard(exec as never, root, '', recursive.enforcementConfig.toolGuards)
157
- if (decision.kind === 'allow') return typeof next === 'function' ? next() : { kind: 'allow' }
158
- if (decision.kind === 'deny') return decision
159
- // ask -> in advisory we let it through with a warn (the approval seam is Phase D)
160
- }))
161
- }
162
-
163
- // SP3 R5: agent/pre-step lint-rules injection (pre-step contract verified: the
164
- // listener returns { kind: 'enter', messages: [...messages, injected] } and the
165
- // returned array REPLACES the default [...claimed, context]). When the session's
166
- // control-plane root has an active recursive run whose current phase doc is
167
- // DRAFT, prepend a compact system-reminder with THAT phase's required sections +
168
- // gates. Pure fs read (zero-emission): never appends recursive/* events.
169
- const agentRuntime = ctx as unknown as { on?: (event: string, listener: (payload: unknown, next?: unknown) => unknown) => () => void }
170
- if (agentRuntime.on) {
171
- disposers.push(agentRuntime.on('agent/pre-step', async (payload, next) => {
172
- const p = payload as {
173
- messages?: Array<{ content: Array<{ type: string; text?: string }> }>,
174
- agent?: { session?: { header?: { cwd?: string } } } | null,
175
- } | null
176
- const messages = p?.messages ?? []
177
- const agent = p?.agent ?? null
178
- const cwd = agent?.session?.header?.cwd ?? ''
179
- // delegate first so later listeners keep veto power, then fold ours on
180
- if (typeof next === 'function') await next()
181
- if (!cwd) return { kind: 'enter', messages } as const
182
- const root = await recursive.resolveRootForRoute(undefined, cwd, undefined)
183
- if (!root) return { kind: 'enter', messages } as const
184
- const runs = enumerateRuns(root)
185
- if (runs.length === 0) return { kind: 'enter', messages } as const
186
- const runId = runs[runs.length - 1]
187
- const runDir = join(root, '.recursive', 'run', runId)
188
- if (!existsSync(runDir)) return { kind: 'enter', messages } as const
189
- const phase = getNextLegalPhase(runDir)
190
- if (!phase) return { kind: 'enter', messages } as const
191
- // only inject when the phase doc is DRAFT (not locked/missing).
192
- const phasePath = join(runDir, phase)
193
- const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
194
- if (status !== 'DRAFT') return { kind: 'enter', messages } as const
195
- const reminder = phaseLintRulesMessage(phase)
196
- return {
197
- kind: 'enter',
198
- messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
199
- } as const
200
- }))
201
- }
202
-
203
- // Phase C R8: fs/observed lock-tamper WARNINGS are served to the board via the
204
- // mountOnce-global: apply() runs per-session, but the route must register
205
- // exactly once (WebServer.register throws on duplicate kind+path) and serve
206
- // PER-WORKSPACE state. No-op when the host composes no webServer (headless).
207
- const webServer = ctx.get('webServer') as
208
- | { register: (route: { kind: string; path: string; handler: unknown }) => () => void }
209
- | undefined
210
- const sessionsStore = ctx.get('sessions') as
211
- | { get?: (id: string) => { header?: { cwd?: string } } | undefined }
212
- | undefined
213
- if (webServer) {
214
- const host: RecursiveRouteHost = {
215
- // sessionId PRIMARY: the host resolves cwd from the attached session header;
216
- // the client-passed cwd is a fallback hint (hydration / headless).
217
- resolveRoot: async (sessionId: string | undefined, cwd: string) => recursive.resolveRootForRoute(sessionId, cwd, sessionsStore),
218
- snapshot: async (root: string) => snapshotWorkspace(root),
219
- revision: () => 1,
220
- }
221
- disposers.push(mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes(host), webServer))
222
- }
223
-
224
- yield () => { for (const d of disposers) d() }
225
- })
226
- }
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/policy.ts CHANGED
@@ -18,7 +18,7 @@ import { getArtifactRequiredSections, AUDITED_PHASE_FILES, CURRENT_WORKFLOW_PROF
18
18
  export interface PolicyContext {
19
19
  worktreeRoot: string
20
20
  runId: string
21
- folded: RecursivePhaseState | null
21
+ folded?: RecursivePhaseState | null
22
22
  config?: EnforcementConfig
23
23
  }
24
24
 
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')