@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.
- package/cordis.patch.yml +15 -10
- package/lib/bootstrap.d.ts +9 -2
- package/lib/client/apply-guard.d.ts +14 -0
- package/lib/client/board.d.ts +5 -4
- package/lib/client/contract.d.ts +57 -24
- package/lib/client/host-api.d.ts +26 -0
- package/lib/client/index.d.ts +16 -9
- package/lib/client/inspector.d.ts +3 -3
- package/lib/client/slots.d.ts +4 -6
- package/lib/client/strip.d.ts +4 -3
- package/lib/client/use-live.d.ts +9 -0
- package/lib/client.js +252 -174
- package/lib/fs-intent.d.ts +34 -0
- package/lib/index.d.ts +12 -11
- package/lib/index.js +1008 -4507
- package/lib/init-templates.d.ts +41 -0
- package/lib/live-route.d.ts +69 -0
- package/lib/phase-rules.d.ts +34 -0
- package/lib/policy.d.ts +1 -1
- package/lib/runtime.d.ts +24 -17
- package/lib/snapshot.d.ts +11 -0
- package/package.json +4 -3
- package/preset/recursive/agent.cordis.yml +7 -4
- package/preset/recursive/preset.yml +1 -1
- package/references/agents-block.md +95 -0
- package/references/artifact-template.md +2574 -0
- package/references/bodies/claude.md +7 -0
- package/references/bodies/codex-agents.md +95 -0
- package/references/bodies/copilot.md +7 -0
- package/references/bodies/cursorrules.md +7 -0
- package/references/bodies/decisions.md +5 -0
- package/references/bodies/delegated-verification.md +49 -0
- package/references/bodies/memory-router.md +45 -0
- package/references/bodies/phase8-skill-memory.md +48 -0
- package/references/bodies/plans-bridge.md +43 -0
- package/references/bodies/recursive-agents-router.md +64 -0
- package/references/bodies/skill-discovery.md +51 -0
- package/references/bodies/skill-memory-router.md +42 -0
- package/references/bodies/state.md +5 -0
- package/references/bootstrap/RECURSIVE.md +2362 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +25 -0
- package/references/scripts/lint-recursive-run.py +2870 -0
- package/references/scripts/recursive-closeout.ps1 +38 -0
- package/references/scripts/recursive-closeout.py +541 -0
- package/references/scripts/recursive-init.ps1 +515 -0
- package/references/scripts/recursive-init.py +356 -0
- package/references/scripts/recursive-lock.ps1 +26 -0
- package/references/scripts/recursive-lock.py +302 -0
- package/references/scripts/recursive-review-bundle.ps1 +58 -0
- package/references/scripts/recursive-review-bundle.py +503 -0
- package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
- package/references/scripts/recursive-router-cli-configure.py +9 -0
- package/references/scripts/recursive-router-cli-init.ps1 +2 -0
- package/references/scripts/recursive-router-cli-init.py +9 -0
- package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
- package/references/scripts/recursive-router-cli-invoke.py +9 -0
- package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
- package/references/scripts/recursive-router-cli-probe.py +9 -0
- package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
- package/references/scripts/recursive-router-cli-resolve.py +9 -0
- package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
- package/references/scripts/recursive-router-cli-validate.py +9 -0
- package/references/scripts/recursive-router-configure.ps1 +27 -0
- package/references/scripts/recursive-router-configure.py +74 -0
- package/references/scripts/recursive-router-init.ps1 +17 -0
- package/references/scripts/recursive-router-init.py +29 -0
- package/references/scripts/recursive-router-invoke.ps1 +47 -0
- package/references/scripts/recursive-router-invoke.py +103 -0
- package/references/scripts/recursive-router-probe.ps1 +25 -0
- package/references/scripts/recursive-router-probe.py +44 -0
- package/references/scripts/recursive-router-resolve.ps1 +26 -0
- package/references/scripts/recursive-router-resolve.py +46 -0
- package/references/scripts/recursive-router-validate.ps1 +17 -0
- package/references/scripts/recursive-router-validate.py +27 -0
- package/references/scripts/recursive-status.ps1 +23 -0
- package/references/scripts/recursive-status.py +2124 -0
- package/references/scripts/recursive-subagent-action.ps1 +98 -0
- package/references/scripts/recursive-subagent-action.py +197 -0
- package/references/scripts/recursive-training-extract.ps1 +23 -0
- package/references/scripts/recursive-training-extract.py +99 -0
- package/references/scripts/recursive-training-grpo.ps1 +52 -0
- package/references/scripts/recursive-training-grpo.py +1341 -0
- package/references/scripts/recursive-training-loader.ps1 +78 -0
- package/references/scripts/recursive-training-loader.py +558 -0
- package/references/scripts/recursive-training-mcp.ps1 +29 -0
- package/references/scripts/recursive-training-mcp.py +261 -0
- package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
- package/references/scripts/recursive-training-phase8-trigger.py +137 -0
- package/references/scripts/recursive-training-sync.ps1 +29 -0
- package/references/scripts/recursive-training-sync.py +183 -0
- package/references/scripts/recursive_phase_rules.py +367 -0
- package/references/scripts/recursive_router_cli_lib.py +2 -0
- package/references/scripts/recursive_router_lib.py +2282 -0
- package/references/scripts/verify-locks.ps1 +25 -0
- package/references/scripts/verify-locks.py +353 -0
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-preset.cmd +7 -0
- package/scripts/install-preset.js +101 -0
- package/scripts/lint-recursive-run.py +7 -5
- package/scripts/test-recursive-mode-smoke.ts +29 -28
- package/src/bootstrap.ts +253 -39
- package/src/client/apply-guard.ts +23 -0
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +72 -24
- package/src/client/host-api.ts +90 -0
- package/src/client/index.ts +22 -11
- package/src/client/inspector.tsx +7 -9
- package/src/client/slots.ts +70 -26
- package/src/client/strip.tsx +9 -9
- package/src/client/use-live.ts +52 -0
- package/src/fs-intent.ts +84 -0
- package/src/index.ts +100 -155
- package/src/init-templates.ts +276 -0
- package/src/live-route.ts +163 -0
- package/src/phase-rules.ts +262 -0
- package/src/policy.ts +29 -5
- package/src/runtime.ts +129 -77
- package/src/snapshot.ts +104 -0
- package/lib/client/node.d.ts +0 -71
- package/lib/events.d.ts +0 -173
- package/lib/projection.d.ts +0 -29
- package/src/client/node.ts +0 -156
- package/src/events.ts +0 -173
- 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
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 './
|
|
75
|
-
export * from './
|
|
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
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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 }
|
|
125
|
+
const agent = (context as { agent?: { session?: { header?: { cwd?: string } } } } | undefined)?.agent
|
|
148
126
|
if (!agent) return ''
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
ctx.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
]
|