@try-works/dsh-recursive-mode 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.d.ts +11 -11
- package/lib/index.js +972 -4504
- 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/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/index.ts +89 -146
- 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 +28 -4
- 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/bootstrap.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idempotent scaffold installer
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
3
|
+
* core: bootstrap the FULL canonical /.recursive/ control plane + cross-tool
|
|
4
|
+
* bridges byte-identically (RECURSIVE.md marker-wrapped, AGENTS.md, STATE/
|
|
5
|
+
* DECISIONS, memory routers + shards, config/recursive-router.json, .gitignore,
|
|
6
|
+
* vendored runtime scripts copied into .recursive/scripts/), plus the
|
|
7
|
+
* agent/session-start Stage B (new vs resume) workspace-scoped to the session's
|
|
8
|
+
* control-plane root (R1).
|
|
9
|
+
*
|
|
10
|
+
* Templates + bodies + runtime scripts are SHIPPED package files under
|
|
11
|
+
* references/ (never inlined TS string literals) and resolved relative to this
|
|
12
|
+
* module (package install location), never process.cwd().
|
|
7
13
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
9
|
-
import { join } from 'node:path'
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync, copyFileSync } from 'node:fs'
|
|
15
|
+
import { join, dirname } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
/** Package root: <package>/src/.. — references/ sits next to src/. */
|
|
20
|
+
export const PACKAGE_ROOT = join(MODULE_DIR, '..')
|
|
21
|
+
|
|
22
|
+
/** references/ root (shipped package files). */
|
|
23
|
+
export function referencesRoot(): string {
|
|
24
|
+
return join(PACKAGE_ROOT, 'references')
|
|
25
|
+
}
|
|
10
26
|
|
|
11
27
|
export interface BootstrapResult {
|
|
12
28
|
root: string
|
|
@@ -23,56 +39,254 @@ export interface StageBResult {
|
|
|
23
39
|
activeRunId?: string
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
// Marker strings (byte-identical to the canonical installer).
|
|
43
|
+
const M = {
|
|
44
|
+
recursiveStart: '<!-- RECURSIVE-MODE-CANONICAL:START -->',
|
|
45
|
+
recursiveEnd: '<!-- RECURSIVE-MODE-CANONICAL:END -->',
|
|
46
|
+
memoryStart: '<!-- RECURSIVE-MODE-MEMORY:START -->',
|
|
47
|
+
memoryEnd: '<!-- RECURSIVE-MODE-MEMORY:END -->',
|
|
48
|
+
agentsStart: '<!-- RECURSIVE-MODE-AGENTS:START -->',
|
|
49
|
+
agentsEnd: '<!-- RECURSIVE-MODE-AGENTS:END -->',
|
|
50
|
+
plansStart: '<!-- RECURSIVE-MODE-PLANS-BRIDGE:START -->',
|
|
51
|
+
plansEnd: '<!-- RECURSIVE-MODE-PLANS-BRIDGE:END -->',
|
|
52
|
+
cursorrulesStart: '# RECURSIVE-MODE-MEMORY-POINTERS:START',
|
|
53
|
+
cursorrulesEnd: '# RECURSIVE-MODE-MEMORY-POINTERS:END',
|
|
54
|
+
repoMdStart: '<!-- RECURSIVE-MODE-MEMORY-POINTERS:START -->',
|
|
55
|
+
repoMdEnd: '<!-- RECURSIVE-MODE-MEMORY-POINTERS:END -->',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** write_utf8_no_bom: LF newlines, no BOM. */
|
|
59
|
+
export function writeUtf8NoBom(path: string, content: string): void {
|
|
60
|
+
writeFileSync(path, content, { encoding: 'utf8' })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** ensure_gitignore_line: add the line when absent (preserving existing). */
|
|
64
|
+
function ensureGitignoreLine(repoRoot: string, line: string): void {
|
|
65
|
+
const gitignorePath = join(repoRoot, '.gitignore')
|
|
66
|
+
const normalized = line.trim()
|
|
67
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : ''
|
|
68
|
+
const existingLines = existing.split(/\r?\n/)
|
|
69
|
+
if (existingLines.some(c => c.trim() === normalized)) return
|
|
70
|
+
let updated = existing
|
|
71
|
+
if (updated && !updated.endsWith('\n')) updated += '\n'
|
|
72
|
+
updated += normalized + '\n'
|
|
73
|
+
writeUtf8NoBom(gitignorePath, updated)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** upsert_marked_block: replace the START..END block, else append after existing. */
|
|
77
|
+
function upsertMarkedBlock(filePath: string, startMarker: string, endMarker: string, blockBody: string): void {
|
|
78
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''
|
|
79
|
+
const block = startMarker + '\n' + blockBody + '\n' + endMarker
|
|
80
|
+
const pattern = new RegExp(escapeRegExp(startMarker) + '[\\s\\S]*?' + escapeRegExp(endMarker))
|
|
81
|
+
let updated: string
|
|
82
|
+
if (pattern.test(existing)) {
|
|
83
|
+
updated = existing.replace(pattern, block)
|
|
84
|
+
} else if (existing.trim() !== '') {
|
|
85
|
+
updated = existing.replace(/\s+$/, '') + '\n\n' + block + '\n'
|
|
86
|
+
} else {
|
|
87
|
+
updated = block + '\n'
|
|
88
|
+
}
|
|
89
|
+
if (updated !== existing) writeUtf8NoBom(filePath, updated)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeRegExp(s: string): string {
|
|
93
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** normalize_plain_or_wrapped_content: strip markers; return plain body when wrapped. */
|
|
97
|
+
function normalizePlainOrWrappedContent(content: string, startMarker: string, endMarker: string): string {
|
|
98
|
+
const startIndex = content.indexOf(startMarker)
|
|
99
|
+
const endIndex = content.lastIndexOf(endMarker)
|
|
100
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
101
|
+
return content.replace(/\r?\n+$/, '')
|
|
102
|
+
}
|
|
103
|
+
const prefix = content.slice(0, startIndex).replace(/\r?\n+$/, '')
|
|
104
|
+
if (prefix.trim() !== '') return prefix
|
|
105
|
+
const bodyStart = startIndex + startMarker.length
|
|
106
|
+
return content.slice(bodyStart, endIndex).replace(/^\r?\n+|\r?\n+$/g, '')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** upsert_or_migrate_canonical: canonical block upsert with plain-file migration. */
|
|
110
|
+
function upsertOrMigrateCanonical(filePath: string, startMarker: string, endMarker: string, canonicalBody: string): void {
|
|
111
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''
|
|
112
|
+
const block = startMarker + '\n' + canonicalBody.replace(/\r?\n+$/, '') + '\n' + endMarker
|
|
113
|
+
const pattern = new RegExp(escapeRegExp(startMarker) + '[\\s\\S]*?' + escapeRegExp(endMarker))
|
|
114
|
+
let updated: string
|
|
115
|
+
if (pattern.test(existing)) {
|
|
116
|
+
updated = existing.replace(pattern, block)
|
|
117
|
+
} else if (existing.trim() === '') {
|
|
118
|
+
updated = block + '\n'
|
|
119
|
+
} else if (existing.replace(/\r?\n+$/, '') === canonicalBody.replace(/\r?\n+$/, '')) {
|
|
120
|
+
updated = block + '\n'
|
|
121
|
+
} else {
|
|
122
|
+
updated = existing.replace(/\s+$/, '') + '\n\n' + block + '\n'
|
|
123
|
+
}
|
|
124
|
+
if (updated !== existing) writeUtf8NoBom(filePath, updated)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read a shipped body file (references/bodies/<name>) — CRLF-normalized. */
|
|
128
|
+
function body(name: string): string {
|
|
129
|
+
return readFileSync(join(referencesRoot(), 'bodies', name), 'utf8').replace(/\r\n/g, '\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read the canonical RECURSIVE.md template from shipped references/bootstrap/ — CRLF-normalized. */
|
|
133
|
+
function canonicalWorkflowContent(): string {
|
|
134
|
+
return readFileSync(join(referencesRoot(), 'bootstrap', 'RECURSIVE.md'), 'utf8').replace(/\r\n/g, '\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read the agents bridge block from shipped references/ — CRLF-normalized. */
|
|
138
|
+
function agentsBlockContent(): string {
|
|
139
|
+
return readFileSync(join(referencesRoot(), 'agents-block.md'), 'utf8').replace(/\r\n/g, '\n').replace(/\r?\n+$/, '')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** default_router_policy(): byte-identical to recursive_router_lib.default_router_policy + pretty_json. */
|
|
143
|
+
function defaultRouterPolicy(): string {
|
|
144
|
+
const roleRoutes: Record<string, Record<string, unknown>> = {
|
|
145
|
+
orchestrator: { enabled: true, mode: 'local-only', cli: null, model: null, fallback: 'local-controller' },
|
|
146
|
+
analyst: { enabled: true, mode: 'external-cli', cli: null, model: null, fallback: 'self-audit' },
|
|
147
|
+
planner: { enabled: true, mode: 'external-cli', cli: null, model: null, fallback: 'self-audit' },
|
|
148
|
+
implementer: { enabled: false, mode: 'external-cli', cli: null, model: null, fallback: 'local-controller' },
|
|
149
|
+
'code-reviewer': { enabled: true, mode: 'external-cli', cli: null, model: null, fallback: 'self-audit' },
|
|
150
|
+
tester: { enabled: true, mode: 'external-cli', cli: null, model: null, fallback: 'self-audit' },
|
|
151
|
+
'memory-auditor': { enabled: true, mode: 'external-cli', cli: null, model: null, fallback: 'self-audit' },
|
|
152
|
+
}
|
|
153
|
+
const policy = {
|
|
154
|
+
version: 1,
|
|
155
|
+
defaults: {
|
|
156
|
+
when_role_unconfigured: 'ask',
|
|
157
|
+
when_cli_unavailable: 'fallback-local',
|
|
158
|
+
when_model_unknown: 'ask',
|
|
159
|
+
allow_auto_assign_if_single_cli: false,
|
|
160
|
+
probe_timeout_ms: 50000,
|
|
161
|
+
invoke_timeout_ms: 180000,
|
|
162
|
+
},
|
|
163
|
+
role_routes: roleRoutes,
|
|
164
|
+
cli_overrides: {},
|
|
165
|
+
custom_clis: [],
|
|
166
|
+
}
|
|
167
|
+
return JSON.stringify(policy, null, 2) + '\n'
|
|
30
168
|
}
|
|
31
169
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
170
|
+
/** ensure_router_scaffold: write config/recursive-router.json when missing. */
|
|
171
|
+
function ensureRouterScaffold(repoRoot: string): void {
|
|
172
|
+
const configDir = join(repoRoot, '.recursive', 'config')
|
|
173
|
+
const policyPath = join(configDir, 'recursive-router.json')
|
|
174
|
+
if (!existsSync(policyPath)) {
|
|
175
|
+
mkdirSync(configDir, { recursive: true })
|
|
176
|
+
writeUtf8NoBom(policyPath, defaultRouterPolicy())
|
|
177
|
+
}
|
|
35
178
|
}
|
|
36
179
|
|
|
37
180
|
/**
|
|
38
|
-
* Idempotent scaffold install:
|
|
39
|
-
*
|
|
181
|
+
* Idempotent scaffold install: reproduce the canonical installer's 82-file
|
|
182
|
+
* tree byte-for-byte. Never overwrites existing user content; marked blocks
|
|
183
|
+
* are re-upserted in place.
|
|
40
184
|
*/
|
|
41
185
|
export function bootstrapScaffold(root: string): BootstrapResult {
|
|
42
|
-
const recursiveRoot = join(root, '.recursive')
|
|
43
186
|
const created: string[] = []
|
|
44
187
|
const existing: string[] = []
|
|
45
188
|
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (existsSync(
|
|
49
|
-
|
|
50
|
-
|
|
189
|
+
const noteDir = (rel: string) => {
|
|
190
|
+
const p = join(root, rel)
|
|
191
|
+
if (existsSync(p)) existing.push(rel + '/')
|
|
192
|
+
else { mkdirSync(p, { recursive: true }); created.push(rel + '/') }
|
|
193
|
+
}
|
|
194
|
+
const noteFile = (rel: string, content?: string) => {
|
|
195
|
+
const p = join(root, rel)
|
|
196
|
+
if (existsSync(p)) { existing.push(rel); return }
|
|
197
|
+
if (content !== undefined) {
|
|
198
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
199
|
+
writeUtf8NoBom(p, content)
|
|
200
|
+
} else {
|
|
201
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
202
|
+
writeFileSync(p, '')
|
|
51
203
|
}
|
|
52
|
-
mkdirSync(join(path, '..'), { recursive: true })
|
|
53
|
-
writeFileSync(path, content, 'utf8')
|
|
54
204
|
created.push(rel)
|
|
55
205
|
}
|
|
56
206
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
207
|
+
const recursiveRoot = join(root, '.recursive')
|
|
208
|
+
const memoryRoot = join(recursiveRoot, 'memory')
|
|
209
|
+
const skillMemoryRoot = join(memoryRoot, 'skills')
|
|
210
|
+
|
|
211
|
+
// Directories.
|
|
212
|
+
for (const rel of [
|
|
213
|
+
'.recursive', '.codex', '.agent', '.github',
|
|
214
|
+
'.recursive/memory', '.recursive/memory/training', '.recursive/memory/skills', '.recursive/run', '.recursive/config',
|
|
215
|
+
]) noteDir(rel)
|
|
216
|
+
for (const sub of ['domains', 'patterns', 'incidents', 'episodes', 'archive']) noteDir(join('.recursive/memory', sub))
|
|
217
|
+
for (const sub of ['availability', 'usage', 'issues', 'patterns']) noteDir(join('.recursive/memory/skills', sub))
|
|
218
|
+
|
|
219
|
+
// .gitkeeps.
|
|
220
|
+
for (const rel of [
|
|
221
|
+
'.recursive/memory/domains/.gitkeep', '.recursive/memory/patterns/.gitkeep', '.recursive/memory/incidents/.gitkeep',
|
|
222
|
+
'.recursive/memory/episodes/.gitkeep', '.recursive/memory/archive/.gitkeep', '.recursive/memory/training/.gitkeep',
|
|
223
|
+
'.recursive/memory/skills/availability/.gitkeep', '.recursive/memory/skills/usage/.gitkeep',
|
|
224
|
+
'.recursive/memory/skills/issues/.gitkeep', '.recursive/memory/skills/patterns/.gitkeep', '.recursive/run/.gitkeep',
|
|
225
|
+
]) noteFile(rel, '')
|
|
226
|
+
|
|
227
|
+
// Runtime scripts: copy vendored references/scripts/* into .recursive/scripts/.
|
|
228
|
+
const scriptsSrc = join(referencesRoot(), 'scripts')
|
|
229
|
+
const scriptsDst = join(recursiveRoot, 'scripts')
|
|
230
|
+
mkdirSync(scriptsDst, { recursive: true })
|
|
231
|
+
for (const name of readdirSync(scriptsSrc)) {
|
|
232
|
+
const src = join(scriptsSrc, name)
|
|
233
|
+
if (!statSync(src).isFile()) continue
|
|
234
|
+
const dst = join(scriptsDst, name)
|
|
235
|
+
if (!existsSync(dst) || !buffersEqual(src, dst)) {
|
|
236
|
+
copyFileSync(src, dst)
|
|
237
|
+
created.push('.recursive/scripts/' + name)
|
|
65
238
|
} else {
|
|
66
|
-
existing.push(
|
|
239
|
+
existing.push('.recursive/scripts/' + name)
|
|
67
240
|
}
|
|
68
241
|
}
|
|
69
242
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
243
|
+
// Plain control-plane files (trailing newline).
|
|
244
|
+
noteFile('.recursive/RECURSIVE.md', '# RECURSIVE.md\n')
|
|
245
|
+
noteFile('.recursive/AGENTS.md', '# AGENTS.md\n')
|
|
246
|
+
noteFile('.recursive/STATE.md', body('state.md') + '\n')
|
|
247
|
+
noteFile('.recursive/DECISIONS.md', body('decisions.md') + '\n')
|
|
248
|
+
noteFile('.recursive/memory/MEMORY.md', '# MEMORY.md\n')
|
|
249
|
+
noteFile('.recursive/memory/skills/SKILLS.md', '# SKILLS.md\n')
|
|
250
|
+
noteFile('.recursive/memory/skills/usage/skill-discovery-and-evaluation.md', body('skill-discovery.md') + '\n')
|
|
251
|
+
noteFile('.recursive/memory/skills/patterns/delegated-verification-and-refresh.md', body('delegated-verification.md') + '\n')
|
|
252
|
+
noteFile('.recursive/memory/skills/patterns/phase8-skill-memory-promotion.md', body('phase8-skill-memory.md') + '\n')
|
|
253
|
+
noteFile('.codex/AGENTS.md', '# AGENTS.md\n')
|
|
254
|
+
noteFile('.agent/PLANS.md', '# PLANS.md\n')
|
|
255
|
+
noteFile('.cursorrules', '')
|
|
256
|
+
noteFile('CLAUDE.md', '')
|
|
257
|
+
noteFile('.github/copilot-instructions.md', '')
|
|
258
|
+
|
|
259
|
+
// .gitignore.
|
|
260
|
+
ensureGitignoreLine(root, '/.recursive/config/recursive-router-discovered.json')
|
|
261
|
+
|
|
262
|
+
// Router config.
|
|
263
|
+
ensureRouterScaffold(root)
|
|
264
|
+
|
|
265
|
+
// Marked blocks.
|
|
266
|
+
upsertMarkedBlock(join(recursiveRoot, 'AGENTS.md'), M.agentsStart, M.agentsEnd, body('recursive-agents-router.md'))
|
|
267
|
+
upsertMarkedBlock(join(memoryRoot, 'MEMORY.md'), M.memoryStart, M.memoryEnd, body('memory-router.md'))
|
|
268
|
+
upsertMarkedBlock(join(skillMemoryRoot, 'SKILLS.md'), M.memoryStart, M.memoryEnd, body('skill-memory-router.md'))
|
|
269
|
+
upsertMarkedBlock(join(root, '.cursorrules'), M.cursorrulesStart, M.cursorrulesEnd, body('cursorrules.md'))
|
|
270
|
+
upsertMarkedBlock(join(root, 'CLAUDE.md'), M.repoMdStart, M.repoMdEnd, body('claude.md'))
|
|
271
|
+
upsertMarkedBlock(join(root, '.github', 'copilot-instructions.md'), M.repoMdStart, M.repoMdEnd, body('copilot.md'))
|
|
272
|
+
upsertMarkedBlock(join(root, '.codex', 'AGENTS.md'), M.agentsStart, M.agentsEnd, body('codex-agents.md'))
|
|
273
|
+
const rootAgents = join(root, 'AGENTS.md')
|
|
274
|
+
if (existsSync(rootAgents)) {
|
|
275
|
+
upsertMarkedBlock(rootAgents, M.agentsStart, M.agentsEnd, agentsBlockContent())
|
|
75
276
|
}
|
|
277
|
+
upsertMarkedBlock(join(root, '.agent', 'PLANS.md'), M.plansStart, M.plansEnd, body('plans-bridge.md'))
|
|
278
|
+
|
|
279
|
+
// Canonical RECURSIVE.md (marker-wrapped, migrate plain installs).
|
|
280
|
+
const canonicalBody = normalizePlainOrWrappedContent(canonicalWorkflowContent(), M.recursiveStart, M.recursiveEnd)
|
|
281
|
+
upsertOrMigrateCanonical(join(recursiveRoot, 'RECURSIVE.md'), M.recursiveStart, M.recursiveEnd, canonicalBody)
|
|
282
|
+
|
|
283
|
+
return { root, bootstrapped: created.length > 0, created, existing }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buffersEqual(a: string, b: string): boolean {
|
|
287
|
+
const ab = readFileSync(a)
|
|
288
|
+
const bb = readFileSync(b)
|
|
289
|
+
return ab.length === bb.length && ab.equals(bb)
|
|
76
290
|
}
|
|
77
291
|
|
|
78
292
|
/**
|
|
@@ -115,4 +329,4 @@ export function stageBWorkflowInit(input: StageBInput): StageBResult {
|
|
|
115
329
|
runs,
|
|
116
330
|
activeRunId,
|
|
117
331
|
}
|
|
118
|
-
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-module-instance apply guard for the recursive client bundle (mirrors
|
|
3
|
+
* the dsh-task-board apply-guard). The client factory can run more than once in
|
|
4
|
+
* a single page lifetime (a stale bundle mixed with a rebuilt one); without a
|
|
5
|
+
* guard every factory run mounts its own board/strip. First claim wins; later
|
|
6
|
+
* claims no-op until the claim is released (fiber unload / hot-reload).
|
|
7
|
+
*/
|
|
8
|
+
declare global {
|
|
9
|
+
// eslint-disable-next-line no-var
|
|
10
|
+
var __dshRecursiveModeClientApplied: boolean | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Claims the client apply slot. Returns true when this call won the slot. */
|
|
14
|
+
export function claimClientApply(): boolean {
|
|
15
|
+
if (globalThis.__dshRecursiveModeClientApplied === true) return false
|
|
16
|
+
globalThis.__dshRecursiveModeClientApplied = true
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Releases the claim (called from the client fiber cleanup on unload/hot-reload). */
|
|
21
|
+
export function releaseClientApply(): void {
|
|
22
|
+
globalThis.__dshRecursiveModeClientApplied = undefined
|
|
23
|
+
}
|
package/src/client/board.tsx
CHANGED
|
Binary file
|
package/src/client/contract.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural client seams (self-contained). The real @deepseek-ai/dsh-client-*
|
|
3
3
|
* packages are HOST-INJECTED at bundle time via dsh.client.inject (never
|
|
4
|
-
* installed in this package's dependency tree
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* isolation; the DSH loader links them to the real runtime identities at
|
|
8
|
-
* compose time.
|
|
4
|
+
* installed in this package's dependency tree). We declare the minimal shapes
|
|
5
|
+
* we consume so the pure client modules type-check and bundle in isolation;
|
|
6
|
+
* the DSH loader links them to the real runtime identities at compose time.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* SP2 R1 (live route): the client now CALLS the host route (read-only GET
|
|
9
|
+
* state + SSE events) instead of reading a session projection. The host serves
|
|
10
|
+
* the per-workspace filesystem fold; this module never mutates the host and
|
|
11
|
+
* never touches the filesystem (R9).
|
|
12
12
|
*/
|
|
13
|
+
import type { RecursiveProjection } from '../types.ts'
|
|
13
14
|
|
|
14
|
-
/** SessionEvent-like carrier (the
|
|
15
|
+
/** SessionEvent-like carrier (kept for the pure derive surface; the live route replaces the fold). */
|
|
15
16
|
export interface ClientSessionEvent {
|
|
16
17
|
type: string
|
|
17
18
|
seq?: number
|
|
@@ -19,24 +20,44 @@ export interface ClientSessionEvent {
|
|
|
19
20
|
data?: Record<string, unknown>
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
/** A session header (cwd is the
|
|
23
|
+
/** A session header (cwd is the session's canonical cwd). */
|
|
23
24
|
export interface ClientSessionHeader {
|
|
24
25
|
cwd?: string
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
export interface
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/** A session-list row (mirrors SessionSummary — cwd + agentPreset are the gate inputs). */
|
|
29
|
+
export interface SessionSummaryRow {
|
|
30
|
+
id: string
|
|
31
|
+
cwd?: string
|
|
32
|
+
/** Agent preset this session was composed from; absent when the deployment has none. */
|
|
33
|
+
agentPreset?: string
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
/** The
|
|
34
|
-
export interface
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
/** The session-list snapshot (mirrors SessionListState). */
|
|
37
|
+
export interface SessionListStateLike {
|
|
38
|
+
ids: string[]
|
|
39
|
+
byId: Record<string, SessionSummaryRow>
|
|
40
|
+
current: string | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A snapshot store the client runtime hands out (mirrors SnapshotStore<T>). */
|
|
44
|
+
export interface SnapshotStoreLike<T> {
|
|
45
|
+
getSnapshot(): T
|
|
46
|
+
subscribe(fn: () => void): () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The sessions service surface (mirrors ISessions.list). */
|
|
50
|
+
export interface ISessionsLike {
|
|
51
|
+
list: SnapshotStoreLike<SessionListStateLike>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The slots registry surface the client injects into. */
|
|
55
|
+
export interface ClientSlots {
|
|
56
|
+
inject(seat: string, factory: (ctx: unknown) => () => void): () => void
|
|
57
|
+
register(options: SlotOptions, component: unknown): () => void
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
/** A registered slot's options
|
|
60
|
+
/** A registered slot's options. */
|
|
40
61
|
export interface SlotOptions {
|
|
41
62
|
name: string
|
|
42
63
|
id?: string
|
|
@@ -48,11 +69,38 @@ export interface SlotOptions {
|
|
|
48
69
|
registrant?: unknown
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
/** The
|
|
52
|
-
export interface
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
/** The client root context the bundle receives from the module loader. */
|
|
73
|
+
export interface ClientContext {
|
|
74
|
+
slots: ClientSlots
|
|
75
|
+
get(name: string): unknown
|
|
76
|
+
/** Cordis effect: registers a disposer; returns a disposer. */
|
|
77
|
+
effect(fn: () => () => void, label?: string): () => void
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
/**
|
|
58
|
-
export
|
|
80
|
+
/** The live state payload the host route serves (mirrors host-api.ts). */
|
|
81
|
+
export interface LiveProjectionValue {
|
|
82
|
+
root: string | null
|
|
83
|
+
projection: RecursiveProjection
|
|
84
|
+
revision: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The client-side preset gate (acceptance #3): the board/strip render ONLY when
|
|
89
|
+
* the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
|
|
90
|
+
* session (or a session whose preset is undefined) shows NOTHING — not an empty
|
|
91
|
+
* board, not a spinner.
|
|
92
|
+
*/
|
|
93
|
+
export function isRecursivePreset(sessions: ISessionsLike): boolean {
|
|
94
|
+
const snapshot = sessions.list.getSnapshot()
|
|
95
|
+
const current = snapshot.current
|
|
96
|
+
if (current === undefined) return false
|
|
97
|
+
return snapshot.byId[current]?.agentPreset === 'recursive'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
|
|
101
|
+
export function currentSessionCwd(sessions: ISessionsLike): string {
|
|
102
|
+
const snapshot = sessions.list.getSnapshot()
|
|
103
|
+
const current = snapshot.current
|
|
104
|
+
if (current === undefined) return ''
|
|
105
|
+
return snapshot.byId[current]?.cwd ?? ''
|
|
106
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live recursive board/strip feed (SP2 R1): the browser half of the host
|
|
3
|
+
* HTTP/SSE route. Replaces useProjection('recursive') — the board reads the
|
|
4
|
+
* FILESYSTEM FOLD served by the host, keyed per-WORKSPACE. The client passes
|
|
5
|
+
* its sessionId (PRIMARY — the host resolves the cwd from the attached
|
|
6
|
+
* session header) plus cwd as a fallback hint while hydrating; the host
|
|
7
|
+
* resolves the recursive root server-side, never trusting a client path.
|
|
8
|
+
*
|
|
9
|
+
* READ-ONLY (R9): GET state + GET events only; this module never mutates the
|
|
10
|
+
* host and never touches the filesystem.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the dsh-web-ui task-board + DSH-better-sidebar host-api pattern:
|
|
13
|
+
* fetch /state with no-store, EventSource /events with a visibilitychange
|
|
14
|
+
* re-listener, and an AbortController timeout on the initial fetch.
|
|
15
|
+
*/
|
|
16
|
+
import { RECURSIVE_API_PREFIX } from '../live-route.ts'
|
|
17
|
+
import type { RecursiveProjection } from '../types.ts'
|
|
18
|
+
|
|
19
|
+
/** The state payload the host serves. */
|
|
20
|
+
export interface LiveRecursiveState {
|
|
21
|
+
root: string | null
|
|
22
|
+
projection: RecursiveProjection
|
|
23
|
+
revision: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A single SSE frame. */
|
|
27
|
+
export interface LiveRecursiveFrame extends LiveRecursiveState {}
|
|
28
|
+
|
|
29
|
+
/** The client scope the host keys on: sessionId PRIMARY, cwd fallback hint. */
|
|
30
|
+
export interface LiveScope {
|
|
31
|
+
sessionId?: string
|
|
32
|
+
cwd?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FETCH_TIMEOUT_MS = 15_000
|
|
36
|
+
|
|
37
|
+
/** Query-string encoder for the two params. */
|
|
38
|
+
function scopeQuery(scope: LiveScope): string {
|
|
39
|
+
const parts: string[] = []
|
|
40
|
+
if (scope.sessionId) parts.push('sessionId=' + encodeURIComponent(scope.sessionId))
|
|
41
|
+
if (scope.cwd) parts.push('cwd=' + encodeURIComponent(scope.cwd))
|
|
42
|
+
return parts.length === 0 ? '' : '?' + parts.join('&')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Fetch the current state for one session scope (host resolves the workspace root). */
|
|
46
|
+
export async function fetchLiveState(scope: LiveScope, signal?: AbortSignal): Promise<LiveRecursiveState> {
|
|
47
|
+
const controller = new AbortController()
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(RECURSIVE_API_PREFIX + '/state' + scopeQuery(scope), { signal: controller.signal, cache: 'no-store' })
|
|
51
|
+
if (!res.ok) throw new Error('recursive live state: HTTP ' + res.status)
|
|
52
|
+
return (await res.json()) as LiveRecursiveState
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to the SSE events feed. Returns a disposer. The host pushes a full
|
|
60
|
+
* {revision, root, projection} frame on every fs change + a 15s heartbeat.
|
|
61
|
+
*
|
|
62
|
+
* @param scope - sessionId primary + cwd fallback (host resolves the root).
|
|
63
|
+
* @param onFrame - called per data frame.
|
|
64
|
+
* @param onError - called when the stream fails/ends (the caller re-subscribes).
|
|
65
|
+
*/
|
|
66
|
+
export function subscribeLiveEvents(scope: LiveScope, onFrame: (frame: LiveRecursiveFrame) => void, onError?: () => void): () => void {
|
|
67
|
+
const url = RECURSIVE_API_PREFIX + '/events' + scopeQuery(scope)
|
|
68
|
+
const es = new EventSource(url)
|
|
69
|
+
const onMessage = (event: MessageEvent) => {
|
|
70
|
+
try {
|
|
71
|
+
const frame = JSON.parse(event.data as string) as LiveRecursiveFrame
|
|
72
|
+
onFrame(frame)
|
|
73
|
+
} catch { /* malformed frame — ignore */ }
|
|
74
|
+
}
|
|
75
|
+
es.addEventListener('message', onMessage)
|
|
76
|
+
es.onerror = () => { es.close(); onError?.() }
|
|
77
|
+
const onVisible = () => {
|
|
78
|
+
if (document.visibilityState === 'visible') {
|
|
79
|
+
// Re-open the stream after the tab slept (the host heartbeat is the only
|
|
80
|
+
// liveness signal; a sleeping tab drops it).
|
|
81
|
+
es.close()
|
|
82
|
+
onError?.()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
88
|
+
es.close()
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client entry (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Client entry (SP2 R1): the browser half of dsh-recursive-mode. Receives the
|
|
3
|
+
* client root context from the DSH loader and registers the board, inspector,
|
|
4
|
+
* status strip, and settings page. The board/strip read the LIVE host route
|
|
5
|
+
* (per-workspace filesystem fold) — not a session projection — and render ONLY
|
|
6
|
+
* for the recursive preset (acceptance #3). READ-ONLY (R9): the client never
|
|
7
|
+
* mutates the host.
|
|
7
8
|
*
|
|
8
9
|
* The apply(ctx) surface mirrors the shipped dsh-client-ui-* packages; the
|
|
9
10
|
* dsh.client.inject list in package.json names the host packages the loader
|
|
10
11
|
* must provide (they are injected at compose time, never installed here).
|
|
12
|
+
* The apply-guard prevents a stale+rebuilt bundle from double-mounting.
|
|
11
13
|
*/
|
|
12
14
|
import type { ClientContext } from './contract.ts'
|
|
13
15
|
import { registerSlots } from './slots.ts'
|
|
16
|
+
import { claimClientApply, releaseClientApply } from './apply-guard.ts'
|
|
14
17
|
|
|
15
|
-
export { recursiveNodeDefinition, RECURSIVE_NODE_KIND, matchRecursiveNode, foldRecursiveNode } from './node.ts'
|
|
16
|
-
export type { RecursiveNodeData, RecursiveNodeState } from './node.ts'
|
|
17
18
|
export { Board, listRuns } from './board.tsx'
|
|
18
19
|
export { Inspector } from './inspector.tsx'
|
|
19
20
|
export { StatusStrip } from './strip.tsx'
|
|
20
21
|
export { RecursiveSettings } from './settings.tsx'
|
|
21
|
-
export
|
|
22
|
+
export { useLiveProjection } from './use-live.ts'
|
|
23
|
+
export type { LiveProjectionSnapshot } from './use-live.ts'
|
|
24
|
+
export { fetchLiveState, subscribeLiveEvents } from './host-api.ts'
|
|
25
|
+
export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts'
|
|
26
|
+
export { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
27
|
+
export type { SessionSummaryRow, SessionListStateLike, ISessionsLike, ClientContext } from './contract.ts'
|
|
22
28
|
|
|
23
|
-
/**
|
|
29
|
+
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
30
|
+
export const inject = ['slots', 'sessions', 'workspaces', 'connection']
|
|
31
|
+
|
|
32
|
+
/** Browser-half plugin entry (R4 + SP2 R1). */
|
|
24
33
|
export function apply(ctx: ClientContext): void {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
34
|
+
// First application wins; a duplicated client injection would mount a second
|
|
35
|
+
// board/strip. Release on unload so a hot-reloaded bundle can claim again.
|
|
36
|
+
if (!claimClientApply()) return
|
|
37
|
+
ctx.effect(() => releaseClientApply, 'recursive: client apply claim')
|
|
27
38
|
registerSlots(ctx)
|
|
28
39
|
}
|