@try-works/dsh-recursive-mode 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bootstrap.d.ts +6 -3
- package/lib/client/board.d.ts +4 -1
- package/lib/client/open-state.d.ts +20 -0
- package/lib/client/slots.d.ts +7 -1
- package/lib/client.js +135 -47
- package/lib/index.js +3391 -906
- package/lib/runtime.d.ts +12 -5
- package/lib/ts-lint.d.ts +205 -0
- package/package.json +1 -1
- package/src/bootstrap.ts +338 -331
- package/src/client/board.tsx +0 -0
- package/src/client/open-state.ts +46 -0
- package/src/client/slots.ts +41 -20
- package/src/commands.ts +11 -0
- package/src/index.ts +238 -228
- package/src/runtime.ts +19 -23
- package/src/ts-lint.ts +2110 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +0 -25
- package/references/scripts/lint-recursive-run.py +0 -2870
- package/references/scripts/recursive-closeout.ps1 +0 -38
- package/references/scripts/recursive-closeout.py +0 -541
- package/references/scripts/recursive-init.ps1 +0 -515
- package/references/scripts/recursive-init.py +0 -356
- package/references/scripts/recursive-lock.ps1 +0 -26
- package/references/scripts/recursive-lock.py +0 -302
- package/references/scripts/recursive-review-bundle.ps1 +0 -58
- package/references/scripts/recursive-review-bundle.py +0 -503
- package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
- package/references/scripts/recursive-router-cli-configure.py +0 -9
- package/references/scripts/recursive-router-cli-init.ps1 +0 -2
- package/references/scripts/recursive-router-cli-init.py +0 -9
- package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
- package/references/scripts/recursive-router-cli-invoke.py +0 -9
- package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
- package/references/scripts/recursive-router-cli-probe.py +0 -9
- package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
- package/references/scripts/recursive-router-cli-resolve.py +0 -9
- package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
- package/references/scripts/recursive-router-cli-validate.py +0 -9
- package/references/scripts/recursive-router-configure.ps1 +0 -27
- package/references/scripts/recursive-router-configure.py +0 -74
- package/references/scripts/recursive-router-init.ps1 +0 -17
- package/references/scripts/recursive-router-init.py +0 -29
- package/references/scripts/recursive-router-invoke.ps1 +0 -47
- package/references/scripts/recursive-router-invoke.py +0 -103
- package/references/scripts/recursive-router-probe.ps1 +0 -25
- package/references/scripts/recursive-router-probe.py +0 -44
- package/references/scripts/recursive-router-resolve.ps1 +0 -26
- package/references/scripts/recursive-router-resolve.py +0 -46
- package/references/scripts/recursive-router-validate.ps1 +0 -17
- package/references/scripts/recursive-router-validate.py +0 -27
- package/references/scripts/recursive-status.ps1 +0 -23
- package/references/scripts/recursive-status.py +0 -2124
- package/references/scripts/recursive-subagent-action.ps1 +0 -98
- package/references/scripts/recursive-subagent-action.py +0 -197
- package/references/scripts/recursive-training-extract.ps1 +0 -23
- package/references/scripts/recursive-training-extract.py +0 -99
- package/references/scripts/recursive-training-grpo.ps1 +0 -52
- package/references/scripts/recursive-training-grpo.py +0 -1341
- package/references/scripts/recursive-training-loader.ps1 +0 -78
- package/references/scripts/recursive-training-loader.py +0 -558
- package/references/scripts/recursive-training-mcp.ps1 +0 -29
- package/references/scripts/recursive-training-mcp.py +0 -261
- package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
- package/references/scripts/recursive-training-phase8-trigger.py +0 -137
- package/references/scripts/recursive-training-sync.ps1 +0 -29
- package/references/scripts/recursive-training-sync.py +0 -183
- package/references/scripts/recursive_phase_rules.py +0 -367
- package/references/scripts/recursive_router_cli_lib.py +0 -2
- package/references/scripts/recursive_router_lib.py +0 -2282
- package/references/scripts/verify-locks.ps1 +0 -25
- package/references/scripts/verify-locks.py +0 -353
- 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-recursive-mode.ps1 +0 -956
- package/scripts/install-recursive-mode.py +0 -750
- package/scripts/lint-recursive-run.py +0 -2870
- package/scripts/recursive-closeout.py +0 -541
- package/scripts/recursive-init.py +0 -356
- package/scripts/recursive-lock.py +0 -302
- package/scripts/recursive-status.py +0 -2124
- package/scripts/recursive_phase_rules.py +0 -367
- package/scripts/recursive_router_lib.py +0 -2282
- package/scripts/verify-locks.py +0 -353
package/src/bootstrap.ts
CHANGED
|
@@ -1,332 +1,339 @@
|
|
|
1
|
-
/**
|
|
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().
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync,
|
|
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
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface BootstrapResult {
|
|
28
|
-
root: string
|
|
29
|
-
bootstrapped: boolean
|
|
30
|
-
created: string[]
|
|
31
|
-
existing: string[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface StageBResult {
|
|
35
|
-
root: string
|
|
36
|
-
source: 'new' | 'resume'
|
|
37
|
-
bootstrapped: boolean
|
|
38
|
-
runs: string[]
|
|
39
|
-
activeRunId?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
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'
|
|
168
|
-
}
|
|
169
|
-
|
|
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
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
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.
|
|
184
|
-
*/
|
|
185
|
-
export function bootstrapScaffold(root: string): BootstrapResult {
|
|
186
|
-
const created: string[] = []
|
|
187
|
-
const existing: string[] = []
|
|
188
|
-
|
|
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, '')
|
|
203
|
-
}
|
|
204
|
-
created.push(rel)
|
|
205
|
-
}
|
|
206
|
-
|
|
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:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
noteFile('.recursive/
|
|
249
|
-
noteFile('.recursive/
|
|
250
|
-
noteFile('.recursive/
|
|
251
|
-
noteFile('.recursive/
|
|
252
|
-
noteFile('.recursive/memory/
|
|
253
|
-
noteFile('.
|
|
254
|
-
noteFile('.
|
|
255
|
-
noteFile('.
|
|
256
|
-
noteFile('
|
|
257
|
-
noteFile('.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
upsertMarkedBlock(join(
|
|
271
|
-
upsertMarkedBlock(join(
|
|
272
|
-
upsertMarkedBlock(join(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
1
|
+
/**
|
|
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().
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync, rmSync } 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
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BootstrapResult {
|
|
28
|
+
root: string
|
|
29
|
+
bootstrapped: boolean
|
|
30
|
+
created: string[]
|
|
31
|
+
existing: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StageBResult {
|
|
35
|
+
root: string
|
|
36
|
+
source: 'new' | 'resume'
|
|
37
|
+
bootstrapped: boolean
|
|
38
|
+
runs: string[]
|
|
39
|
+
activeRunId?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
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'
|
|
168
|
+
}
|
|
169
|
+
|
|
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
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
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.
|
|
184
|
+
*/
|
|
185
|
+
export function bootstrapScaffold(root: string): BootstrapResult {
|
|
186
|
+
const created: string[] = []
|
|
187
|
+
const existing: string[] = []
|
|
188
|
+
|
|
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, '')
|
|
203
|
+
}
|
|
204
|
+
created.push(rel)
|
|
205
|
+
}
|
|
206
|
+
|
|
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: TS-only scaffold (R2). No .py/.ps1 are vendored; the
|
|
228
|
+
// plugin's lint/lock/status/init run in-process via TS tools, so the
|
|
229
|
+
// scaffold carries an empty .recursive/scripts/ dir (kept for canonical
|
|
230
|
+
// tree-shape parity with the golden fixture).
|
|
231
|
+
noteDir('.recursive/scripts')
|
|
232
|
+
// R3 (run 09): TS-only repair — drop python-era artifacts from an EXISTING
|
|
233
|
+
// workspace's .recursive/scripts/ (the pre-0.1.6 scaffold vendored 29 .py +
|
|
234
|
+
// .ps1 wrappers; those are no longer shipped or needed). Deletes only
|
|
235
|
+
// .py/.ps1 under scripts/; never touches user content elsewhere.
|
|
236
|
+
{
|
|
237
|
+
const scriptsDir = join(recursiveRoot, 'scripts')
|
|
238
|
+
if (existsSync(scriptsDir)) {
|
|
239
|
+
for (const name of readdirSync(scriptsDir)) {
|
|
240
|
+
if (name.endsWith('.py') || name.endsWith('.ps1')) {
|
|
241
|
+
rmSync(join(scriptsDir, name), { force: true })
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Plain control-plane files (trailing newline).
|
|
248
|
+
noteFile('.recursive/RECURSIVE.md', '# RECURSIVE.md\n')
|
|
249
|
+
noteFile('.recursive/AGENTS.md', '# AGENTS.md\n')
|
|
250
|
+
noteFile('.recursive/STATE.md', body('state.md') + '\n')
|
|
251
|
+
noteFile('.recursive/DECISIONS.md', body('decisions.md') + '\n')
|
|
252
|
+
noteFile('.recursive/memory/MEMORY.md', '# MEMORY.md\n')
|
|
253
|
+
noteFile('.recursive/memory/skills/SKILLS.md', '# SKILLS.md\n')
|
|
254
|
+
noteFile('.recursive/memory/skills/usage/skill-discovery-and-evaluation.md', body('skill-discovery.md') + '\n')
|
|
255
|
+
noteFile('.recursive/memory/skills/patterns/delegated-verification-and-refresh.md', body('delegated-verification.md') + '\n')
|
|
256
|
+
noteFile('.recursive/memory/skills/patterns/phase8-skill-memory-promotion.md', body('phase8-skill-memory.md') + '\n')
|
|
257
|
+
noteFile('.codex/AGENTS.md', '# AGENTS.md\n')
|
|
258
|
+
noteFile('.agent/PLANS.md', '# PLANS.md\n')
|
|
259
|
+
noteFile('.cursorrules', '')
|
|
260
|
+
noteFile('CLAUDE.md', '')
|
|
261
|
+
noteFile('.github/copilot-instructions.md', '')
|
|
262
|
+
|
|
263
|
+
// .gitignore.
|
|
264
|
+
ensureGitignoreLine(root, '/.recursive/config/recursive-router-discovered.json')
|
|
265
|
+
|
|
266
|
+
// Router config.
|
|
267
|
+
ensureRouterScaffold(root)
|
|
268
|
+
|
|
269
|
+
// Marked blocks.
|
|
270
|
+
upsertMarkedBlock(join(recursiveRoot, 'AGENTS.md'), M.agentsStart, M.agentsEnd, body('recursive-agents-router.md'))
|
|
271
|
+
upsertMarkedBlock(join(memoryRoot, 'MEMORY.md'), M.memoryStart, M.memoryEnd, body('memory-router.md'))
|
|
272
|
+
upsertMarkedBlock(join(skillMemoryRoot, 'SKILLS.md'), M.memoryStart, M.memoryEnd, body('skill-memory-router.md'))
|
|
273
|
+
upsertMarkedBlock(join(root, '.cursorrules'), M.cursorrulesStart, M.cursorrulesEnd, body('cursorrules.md'))
|
|
274
|
+
upsertMarkedBlock(join(root, 'CLAUDE.md'), M.repoMdStart, M.repoMdEnd, body('claude.md'))
|
|
275
|
+
upsertMarkedBlock(join(root, '.github', 'copilot-instructions.md'), M.repoMdStart, M.repoMdEnd, body('copilot.md'))
|
|
276
|
+
upsertMarkedBlock(join(root, '.codex', 'AGENTS.md'), M.agentsStart, M.agentsEnd, body('codex-agents.md'))
|
|
277
|
+
const rootAgents = join(root, 'AGENTS.md')
|
|
278
|
+
if (existsSync(rootAgents)) {
|
|
279
|
+
upsertMarkedBlock(rootAgents, M.agentsStart, M.agentsEnd, agentsBlockContent())
|
|
280
|
+
}
|
|
281
|
+
upsertMarkedBlock(join(root, '.agent', 'PLANS.md'), M.plansStart, M.plansEnd, body('plans-bridge.md'))
|
|
282
|
+
|
|
283
|
+
// Canonical RECURSIVE.md (marker-wrapped, migrate plain installs).
|
|
284
|
+
const canonicalBody = normalizePlainOrWrappedContent(canonicalWorkflowContent(), M.recursiveStart, M.recursiveEnd)
|
|
285
|
+
upsertOrMigrateCanonical(join(recursiveRoot, 'RECURSIVE.md'), M.recursiveStart, M.recursiveEnd, canonicalBody)
|
|
286
|
+
|
|
287
|
+
return { root, bootstrapped: created.length > 0, created, existing }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buffersEqual(a: string, b: string): boolean {
|
|
291
|
+
const ab = readFileSync(a)
|
|
292
|
+
const bb = readFileSync(b)
|
|
293
|
+
return ab.length === bb.length && ab.equals(bb)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Enumerate runs as directory names only (bounded O(#dirs)). Never reads
|
|
298
|
+
* run docs; never returns file paths.
|
|
299
|
+
*/
|
|
300
|
+
export function enumerateRuns(root: string): string[] {
|
|
301
|
+
const runRoot = join(root, '.recursive', 'run')
|
|
302
|
+
if (!existsSync(runRoot)) return []
|
|
303
|
+
return readdirSync(runRoot, { withFileTypes: true })
|
|
304
|
+
.filter(d => {
|
|
305
|
+
if (d.isDirectory()) return true
|
|
306
|
+
if (!d.isSymbolicLink()) return false
|
|
307
|
+
try { return statSync(join(runRoot, d.name)).isDirectory() } catch { return false }
|
|
308
|
+
})
|
|
309
|
+
.map(d => d.name)
|
|
310
|
+
.sort()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface StageBInput {
|
|
314
|
+
root: string
|
|
315
|
+
source: 'new' | 'resume'
|
|
316
|
+
activeRunId?: string
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Stage B workflow init at agent/session-start. New: bootstrap if missing +
|
|
321
|
+
* enumerate runs. Resume: idempotent REPAIR (not a full re-bootstrap) — the
|
|
322
|
+
* upsert-only bootstrapScaffold adds missing control-plane files/dirs and
|
|
323
|
+
* re-upserts marked blocks WITHOUT overwriting user content or touching run/
|
|
324
|
+
* artifacts, then enumerates + notes the active run. The repair is safe on
|
|
325
|
+
* fresh, partial, AND complete scaffolds (run 09 R3/R6). Always scoped to the
|
|
326
|
+
* given control-plane root.
|
|
327
|
+
*/
|
|
328
|
+
export function stageBWorkflowInit(input: StageBInput): StageBResult {
|
|
329
|
+
const { root, source, activeRunId } = input
|
|
330
|
+
const bootstrap = bootstrapScaffold(root)
|
|
331
|
+
const runs = enumerateRuns(root)
|
|
332
|
+
return {
|
|
333
|
+
root,
|
|
334
|
+
source,
|
|
335
|
+
bootstrapped: source === 'new' ? bootstrap.bootstrapped : false,
|
|
336
|
+
runs,
|
|
337
|
+
activeRunId,
|
|
338
|
+
}
|
|
332
339
|
}
|