@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/runtime.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
3
4
|
import { join } from 'node:path'
|
|
5
|
+
import { referencesRoot } from './bootstrap.ts'
|
|
6
|
+
import { requirementsContent, worktreeContent, laterPhaseContent, detectGitContext, RUN_SCAFFOLD_DIRS, type GitContext } from './init-templates.ts'
|
|
4
7
|
import { foldRun, resolveRunDir } from './status.ts'
|
|
5
8
|
import {
|
|
6
9
|
getLockStatus,
|
|
@@ -22,9 +25,8 @@ import { delegate, validateReferences, writeActionRecord, evaluateDelegationResu
|
|
|
22
25
|
import { foldRecursivePhase, validateTransition, detectTransitionIntent, LifecycleDriver, coupleGateBlockToGoal, type PhaseTransitionIntent, type SessionEventLike, type RecursivePhaseState, type GateCheckResult } from './lifecycle.ts'
|
|
23
26
|
import { resolveEnforcementConfig, DEFAULT_ENFORCEMENT, evaluatePreStepGate, evaluateToolGuard, detectTamper, type EnforcementConfig, type PreStepGateDecision, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts'
|
|
24
27
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
25
|
-
import { runCreated as runCreatedEvent, phase as phaseEvent, phaseLocked as phaseLockedEvent, runState as runStateEvent } from './events.ts'
|
|
26
28
|
import { renderRecursivePolicy, type PolicyContext } from './policy.ts'
|
|
27
|
-
import {
|
|
29
|
+
import { snapshotWorkspace } from './snapshot.ts'
|
|
28
30
|
|
|
29
31
|
declare module '@deepseek-ai/cordis' {
|
|
30
32
|
interface Context {
|
|
@@ -303,23 +305,28 @@ export class RecursiveRuntime extends Service {
|
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* the
|
|
308
|
+
* SP2 R1 route adapter: resolve the control-plane root for the live route.
|
|
309
|
+
* sessionId PRIMARY — the host looks up the attached session header cwd and
|
|
310
|
+
* resolves the root from THAT; the client-passed cwd is a fallback hint only
|
|
311
|
+
* (hydration / headless callers). Two sessions in one workspace collapse to
|
|
312
|
+
* one root; a subdir cwd resolves up to the workspace root.
|
|
309
313
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
314
|
+
async resolveRootForRoute(sessionId: string | undefined, cwd: string, sessionsStore?: { get?: (id: string) => { header?: { cwd?: string } } | undefined } | null): Promise<string | null> {
|
|
315
|
+
// 1) Attached session header (authoritative when present).
|
|
316
|
+
if (sessionId && sessionsStore?.get) {
|
|
317
|
+
const attached = sessionsStore.get(sessionId)
|
|
318
|
+
const headerCwd = attached?.header?.cwd
|
|
319
|
+
if (headerCwd) {
|
|
320
|
+
const root = await resolveControlPlaneRoot({ session: { header: { cwd: headerCwd } } }, this.workspaceRegistry, this.repoRoot)
|
|
321
|
+
if (root) return root
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// 2) Client cwd fallback (hydration / headless) — still registry-canonicalized.
|
|
325
|
+
if (cwd) {
|
|
326
|
+
const root = await resolveControlPlaneRoot({ session: { header: { cwd } } }, this.workspaceRegistry, this.repoRoot)
|
|
327
|
+
if (root) return root
|
|
328
|
+
}
|
|
329
|
+
return null
|
|
323
330
|
}
|
|
324
331
|
|
|
325
332
|
async status(runId?: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<RecursiveStatusResult | null> {
|
|
@@ -331,8 +338,12 @@ export class RecursiveRuntime extends Service {
|
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
/**
|
|
334
|
-
* Scaffold a run directory with
|
|
335
|
-
*
|
|
341
|
+
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
342
|
+
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
343
|
+
* recursive-init.py (incl. git-context prefill); later phases carry every
|
|
344
|
+
* required section (get_artifact_required_sections) + TODO + FAIL gates.
|
|
345
|
+
* Also scaffolds addenda/subagents/router-prompts/evidence dirs. Returns the
|
|
346
|
+
* run dir + created artifacts.
|
|
336
347
|
*/
|
|
337
348
|
async initRun(runId: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<{ runDir: string; runId: string; created: string[]; existing: string[] }> {
|
|
338
349
|
const root = await this.resolveRootFor(agent)
|
|
@@ -341,23 +352,50 @@ export class RecursiveRuntime extends Service {
|
|
|
341
352
|
mkdirSync(runDir, { recursive: true })
|
|
342
353
|
const created: string[] = []
|
|
343
354
|
const existing: string[] = []
|
|
344
|
-
|
|
355
|
+
|
|
356
|
+
// Scaffold dirs (canonical recursive-init sequence).
|
|
357
|
+
for (const dir of RUN_SCAFFOLD_DIRS) {
|
|
358
|
+
const p = join(runDir, dir)
|
|
359
|
+
if (existsSync(p)) { existing.push(dir + '/'); continue }
|
|
360
|
+
mkdirSync(p, { recursive: true })
|
|
361
|
+
created.push(dir + '/')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Git context for the Phase 0 diff-basis prefill (canonical parity).
|
|
365
|
+
const { context: gitContext, error: prefillError } = detectGitContext(root)
|
|
366
|
+
|
|
367
|
+
// Phase 0 templates: byte-identical to canonical recursive-init.py.
|
|
368
|
+
const phase0: Array<[string, string]> = [
|
|
369
|
+
['00-requirements.md', requirementsContent(runId, 'feature', '')],
|
|
370
|
+
['00-worktree.md', worktreeContent(runId, root, gitContext, prefillError)],
|
|
371
|
+
]
|
|
372
|
+
for (const [file, content] of phase0) {
|
|
345
373
|
const path = join(runDir, file)
|
|
346
|
-
if (existsSync(path)) {
|
|
347
|
-
|
|
348
|
-
continue
|
|
349
|
-
}
|
|
350
|
-
const header = fields.map((f, i) => (i === 0 ? f + runId : f)).join('\n')
|
|
351
|
-
writeFileSync(path, '# ' + file.replace(/\.md$/, '') + ' — ' + file + '\n\n' + header + '\n', 'utf8')
|
|
374
|
+
if (existsSync(path)) { existing.push(file); continue }
|
|
375
|
+
writeFileSync(path, content, 'utf8')
|
|
352
376
|
created.push(file)
|
|
353
377
|
}
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
|
|
379
|
+
// Later phases: full required-sections scaffold.
|
|
380
|
+
const laterPhases = [
|
|
381
|
+
'01-as-is.md',
|
|
382
|
+
'01.5-root-cause.md',
|
|
383
|
+
'02-to-be-plan.md',
|
|
384
|
+
'03-implementation-summary.md',
|
|
385
|
+
'03.5-code-review.md',
|
|
386
|
+
'04-test-summary.md',
|
|
387
|
+
'05-manual-qa.md',
|
|
388
|
+
'06-decisions-update.md',
|
|
389
|
+
'07-state-update.md',
|
|
390
|
+
'08-memory-impact.md',
|
|
391
|
+
]
|
|
392
|
+
for (const file of laterPhases) {
|
|
393
|
+
const path = join(runDir, file)
|
|
394
|
+
if (existsSync(path)) { existing.push(file); continue }
|
|
395
|
+
writeFileSync(path, laterPhaseContent(runId, file), 'utf8')
|
|
396
|
+
created.push(file)
|
|
360
397
|
}
|
|
398
|
+
|
|
361
399
|
return { runDir, runId, created, existing }
|
|
362
400
|
}
|
|
363
401
|
|
|
@@ -376,9 +414,8 @@ export class RecursiveRuntime extends Service {
|
|
|
376
414
|
if (!existsSync(artifactPath)) throw new Error('Artifact not found: ' + artifact)
|
|
377
415
|
const status = getLockStatus(artifactPath)
|
|
378
416
|
if (status === 'LOCKED') throw new Error('Artifact already LOCKED: ' + artifact)
|
|
379
|
-
// B2:
|
|
380
|
-
//
|
|
381
|
-
this.emitToSession(root, 'recursive/phase-intent', { runId, worktreeRoot: root, targetArtifact: artifact, kind: 'lock' }, agent)
|
|
417
|
+
// B2: the pre-step gate reads transition intent from the session log; the
|
|
418
|
+
// durable commit below is what the live fs route folds. No phase-intent event.
|
|
382
419
|
const blockers = getPrerequisiteBlockers(runDir, artifact)
|
|
383
420
|
if (blockers.length > 0) {
|
|
384
421
|
throw new Error('Prerequisite blockers: ' + blockers.map(b => b.artifact + ' (' + b.status + ')').join(', '))
|
|
@@ -392,11 +429,7 @@ export class RecursiveRuntime extends Service {
|
|
|
392
429
|
content = setOrInsertField(content, 'LockHash', lockHash, ['LockedAt', 'Status'])
|
|
393
430
|
writeFileSync(artifactPath, content, 'utf8')
|
|
394
431
|
const receipt = writeReceipt(runDir, artifact, artifactPath)
|
|
395
|
-
// B2: the commit is durable —
|
|
396
|
-
// last-wins phase chain + lock badge + truncated hash).
|
|
397
|
-
this.emitToSession(root, 'recursive/phase', phaseEvent({ runId, worktreeRoot: root, phase: artifact, status: 'LOCKED' }), agent)
|
|
398
|
-
this.emitToSession(root, 'recursive/phase-locked', phaseLockedEvent({ runId, worktreeRoot: root, phase: artifact, lockedAt, lockHash }), agent)
|
|
399
|
-
this.emitToSession(root, 'recursive/run-state', runStateEvent({ runId, worktreeRoot: root, state: 'active' }), agent)
|
|
432
|
+
// B2: the commit is durable — the live fs route folds it on the next GET.
|
|
400
433
|
return {
|
|
401
434
|
artifact,
|
|
402
435
|
runId,
|
|
@@ -419,11 +452,7 @@ export class RecursiveRuntime extends Service {
|
|
|
419
452
|
const hadReceipt = invalidateReceipt(runDir, artifact)
|
|
420
453
|
const stale = getStaleDownstreamPhases(runDir, artifact)
|
|
421
454
|
for (const entry of stale) invalidateReceipt(runDir, entry.artifact)
|
|
422
|
-
// B2: reopen reverts to DRAFT —
|
|
423
|
-
// the pre-step gate and projection both see the reverted state.
|
|
424
|
-
this.emitToSession(root, 'recursive/phase-intent', { runId, worktreeRoot: root, targetArtifact: artifact, kind: 'reopen' }, agent)
|
|
425
|
-
this.emitToSession(root, 'recursive/phase', phaseEvent({ runId, worktreeRoot: root, phase: artifact, status: 'DRAFT' }), agent)
|
|
426
|
-
this.emitToSession(root, 'recursive/run-state', runStateEvent({ runId, worktreeRoot: root, state: 'active', reason: 'reopened ' + artifact }), agent)
|
|
455
|
+
// B2: reopen reverts to DRAFT — the live fs route folds the reverted state.
|
|
427
456
|
return {
|
|
428
457
|
artifact,
|
|
429
458
|
runId,
|
|
@@ -435,8 +464,12 @@ export class RecursiveRuntime extends Service {
|
|
|
435
464
|
}
|
|
436
465
|
|
|
437
466
|
/**
|
|
438
|
-
* Lint an artifact
|
|
439
|
-
*
|
|
467
|
+
* Lint an artifact with FULL canonical parity: shells out to the vendored
|
|
468
|
+
* lint-recursive-run.py (byte-identical to canonical) and parses its
|
|
469
|
+
* [FAIL]/[WARN] lines + Summary counts. `errors` = FAIL lines, `warnings` =
|
|
470
|
+
* WARN lines, `passed` = no FAIL (WARN-only passes, matching the canonical
|
|
471
|
+
* non-strict verdict). Falls back to the structural check only if the vendored
|
|
472
|
+
* script is unavailable (e.g. bundled lib without references/).
|
|
440
473
|
*/
|
|
441
474
|
async lintArtifact(runId: string, artifact?: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<LintArtifactResult> {
|
|
442
475
|
const root = await this.resolveRootFor(agent)
|
|
@@ -447,6 +480,24 @@ export class RecursiveRuntime extends Service {
|
|
|
447
480
|
if (!existsSync(artifactPath)) {
|
|
448
481
|
return { artifact: target, runId, errors: ['Artifact not found'], warnings: [], passed: false }
|
|
449
482
|
}
|
|
483
|
+
const lintScript = join(referencesRoot(), 'scripts', 'lint-recursive-run.py')
|
|
484
|
+
if (existsSync(lintScript)) {
|
|
485
|
+
try {
|
|
486
|
+
const stdout = execFileSync('python', [lintScript, '--repo-root', root, '--run-id', runId], {
|
|
487
|
+
encoding: 'utf8',
|
|
488
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
489
|
+
timeout: 60000,
|
|
490
|
+
})
|
|
491
|
+
return parseLintOutput(stdout, target, runId)
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const stdout = (err as { stdout?: string })?.stdout ?? ''
|
|
494
|
+
if (stdout.includes('Summary')) {
|
|
495
|
+
return parseLintOutput(stdout, target, runId)
|
|
496
|
+
}
|
|
497
|
+
// script failed to run entirely — fall through to structural check
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Structural fallback (no vendored script available).
|
|
450
501
|
const content = readFileSync(artifactPath, 'utf8')
|
|
451
502
|
const errors: string[] = []
|
|
452
503
|
const warnings: string[] = []
|
|
@@ -456,10 +507,7 @@ export class RecursiveRuntime extends Service {
|
|
|
456
507
|
else if (status === 'STALE_LOCK') errors.push('LockHash mismatch or missing lock fields')
|
|
457
508
|
if (!/^[ \t]*## TODO[ \t]*$/m.test(content)) errors.push('Missing ## TODO section')
|
|
458
509
|
for (const gate of ['Coverage', 'Approval']) {
|
|
459
|
-
if (!new RegExp('^[
|
|
460
|
-
}
|
|
461
|
-
if (target === '03-implementation-summary.md' && !new RegExp('^[ \\t]*TDD Compliance:\\s*(PASS|FAIL)\\s*$', 'm').test(content)) {
|
|
462
|
-
warnings.push('Missing TDD Compliance gate')
|
|
510
|
+
if (!new RegExp('^[ \t]*' + gate + ':\s*(PASS|FAIL)\s*$', 'm').test(content)) warnings.push('Missing ' + gate + ' gate')
|
|
463
511
|
}
|
|
464
512
|
return { artifact: target, runId, errors, warnings, passed: errors.length === 0 }
|
|
465
513
|
}
|
|
@@ -504,31 +552,6 @@ export class RecursiveRuntime extends Service {
|
|
|
504
552
|
return coupleGateBlockToGoal(goalService as never, agent, ref, reason)
|
|
505
553
|
}
|
|
506
554
|
|
|
507
|
-
/** Phase D R1: append a recursive/* event through the host session adapter (worktree-keyed). */
|
|
508
|
-
emitRecursiveEvent(session: unknown, type: string, data: Record<string, unknown>): void {
|
|
509
|
-
const s = session as { append?: (type: string, data: unknown) => void } | null | undefined
|
|
510
|
-
if (s && typeof s.append === 'function') {
|
|
511
|
-
s.append(type, data)
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/** Phase D R2/R9: fold a session log into the worktree-grouped projection. */
|
|
516
|
-
foldRecursiveProjection(events: readonly RecursiveEventLike[], workspaceRoot = ''): RecursiveFoldState {
|
|
517
|
-
let state = emptyRecursiveFoldState(workspaceRoot)
|
|
518
|
-
for (const event of events) state = foldProjectionEvent(state, event)
|
|
519
|
-
return state
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Phase D R2: the registered projection unit (key 'recursive'). */
|
|
523
|
-
get projectionUnit() {
|
|
524
|
-
return recursiveProjectionUnit
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** Phase D R9: whether a worktreeRoot is inside the workspace control-plane root. */
|
|
528
|
-
isInsideWorkspace(worktreeRoot: string, workspaceRoot: string): boolean {
|
|
529
|
-
return isInsideWorkspace(worktreeRoot, workspaceRoot)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
555
|
/** Phase C R7: resolve the enforcement config (strict|advisory, default advisory). */
|
|
533
556
|
get enforcementConfig(): EnforcementConfig {
|
|
534
557
|
return this._enforcementConfig ?? DEFAULT_ENFORCEMENT
|
|
@@ -562,3 +585,32 @@ function setOrInsertField(content: string, fieldName: string, value: string, aft
|
|
|
562
585
|
function escapeRegExp(s: string): string {
|
|
563
586
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
564
587
|
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Parse the vendored lint-recursive-run.py stdout into a LintArtifactResult.
|
|
591
|
+
* [FAIL] lines -> errors, [WARN] lines -> warnings. When the run directory has
|
|
592
|
+
* multiple artifacts, FAIL/WARN lines carry the artifact path; we keep only the
|
|
593
|
+
* lines for the requested artifact (or all when the path is ambiguous).
|
|
594
|
+
*/
|
|
595
|
+
function parseLintOutput(stdout: string, target: string, runId: string): LintArtifactResult {
|
|
596
|
+
const errors: string[] = []
|
|
597
|
+
const warnings: string[] = []
|
|
598
|
+
const failRe = /^\[FAIL\]\s+(.*)$/gm
|
|
599
|
+
const warnRe = /^\[WARN\]\s+(.*)$/gm
|
|
600
|
+
const targetSuffix = target.replace(/^.*[\\/]/, '')
|
|
601
|
+
const isForTarget = (line: string) => {
|
|
602
|
+
const pathMatch = line.match(/[A-Za-z0-9_.-]+\.md/)
|
|
603
|
+
return !pathMatch || pathMatch[0] === targetSuffix
|
|
604
|
+
}
|
|
605
|
+
let m: RegExpExecArray | null
|
|
606
|
+
while ((m = failRe.exec(stdout)) !== null) {
|
|
607
|
+
if (isForTarget(m[1])) errors.push(m[1].trim())
|
|
608
|
+
}
|
|
609
|
+
while ((m = warnRe.exec(stdout)) !== null) {
|
|
610
|
+
if (isForTarget(m[1])) warnings.push(m[1].trim())
|
|
611
|
+
}
|
|
612
|
+
// If the target artifact appears nowhere in the output but the run was linted,
|
|
613
|
+
// keep the parsed errors/warnings (canonical script lints the whole run).
|
|
614
|
+
return { artifact: target, runId, errors, warnings, passed: errors.length === 0 }
|
|
615
|
+
}
|
|
616
|
+
|
package/src/snapshot.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshotWorkspace(root): fold the filesystem into the RecursiveProjection whole value.
|
|
3
|
+
*
|
|
4
|
+
* SP2 R1 (live-route) — the ONLY projection path. The board/strip read this through
|
|
5
|
+
* the host HTTP/SSE route; cold resume and every GET are a FRESH fs read (the route
|
|
6
|
+
* never serves a cached/stale snapshot). Per-workspace: root is the resolved
|
|
7
|
+
* control-plane root (cwd == workspace root, cwd == subdir, or cwd == repo root).
|
|
8
|
+
*
|
|
9
|
+
* Phase-lock facts (lockedAt/lockHash) are enriched from lock receipts because
|
|
10
|
+
* foldRun PhaseState carries no lock fields; state comes from getArtifactState.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { discoverRuns } from './run.ts'
|
|
15
|
+
import { foldRun } from './status.ts'
|
|
16
|
+
import { getLockStatus, receiptPath, readReceipt } from './lock.ts'
|
|
17
|
+
import type { RecursiveProjection, RecursiveRunCard, RecursivePhaseRow, RecursiveRunState } from './types.ts'
|
|
18
|
+
|
|
19
|
+
/** One locked-phase fact from a lock receipt (artifacts/<file>.receipt.json). */
|
|
20
|
+
interface LockFacts {
|
|
21
|
+
lockedAt?: string
|
|
22
|
+
lockHash?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve a run's lock facts for one artifact file (receipt first, status fallback). */
|
|
26
|
+
function lockFactsOf(runDir: string, artifact: string): LockFacts {
|
|
27
|
+
const receipt = readReceipt(runDir, artifact)
|
|
28
|
+
if (receipt && typeof receipt.locked_at === 'string' && typeof receipt.artifact_hash === 'string') {
|
|
29
|
+
return {
|
|
30
|
+
lockedAt: receipt.locked_at,
|
|
31
|
+
lockHash: receipt.artifact_hash,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// No usable receipt: fall back to the artifact's own LockedAt/LockHash fields.
|
|
35
|
+
const status = getLockStatus(join(runDir, artifact))
|
|
36
|
+
if (status !== 'LOCKED') return {}
|
|
37
|
+
return {
|
|
38
|
+
lockedAt: new Date(0).toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
39
|
+
lockHash: '0'.repeat(64),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One phase row from a run's folded status (locked facts enriched). */
|
|
44
|
+
function phaseRowOf(runDir: string, key: string, file: string, status: string): RecursivePhaseRow {
|
|
45
|
+
const row: RecursivePhaseRow = { phase: key, status }
|
|
46
|
+
if (status === 'LOCKED') {
|
|
47
|
+
const facts = lockFactsOf(runDir, file)
|
|
48
|
+
if (facts.lockedAt !== undefined) row.lockedAt = facts.lockedAt
|
|
49
|
+
if (facts.lockHash !== undefined) row.lockHash = facts.lockHash
|
|
50
|
+
}
|
|
51
|
+
return row
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fold one run directory into a RecursiveRunCard (the wire shape the board renders).
|
|
56
|
+
* Runs that no longer exist on disk are omitted (the fs is the source of truth).
|
|
57
|
+
*/
|
|
58
|
+
export function foldRunCard(runDir: string, runId: string, worktreeRoot: string): RecursiveRunCard {
|
|
59
|
+
const status = foldRun(runDir, runId)
|
|
60
|
+
// Wire contract parity (deleted projection.ts): phases are keyed by FILENAME
|
|
61
|
+
// (the recursive/phase event carried phase: '03-implementation-summary.md'),
|
|
62
|
+
// which is what derive.ts phaseGroupOf parses. phase.key ('03', '00R', '00W')
|
|
63
|
+
// would never match the filename regex and would break columnForRun/cardFacts.
|
|
64
|
+
const phases: Record<string, RecursivePhaseRow> = {}
|
|
65
|
+
for (const phase of status.phases) {
|
|
66
|
+
if (phase.exists) phases[phase.file] = phaseRowOf(runDir, phase.file, phase.file, phase.status)
|
|
67
|
+
}
|
|
68
|
+
const state = runStateOf(status)
|
|
69
|
+
return {
|
|
70
|
+
runId,
|
|
71
|
+
worktreeRoot,
|
|
72
|
+
phases,
|
|
73
|
+
state,
|
|
74
|
+
tampers: {},
|
|
75
|
+
subagents: {},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Derive the run-level state from the folded status. The fs stores no run-level
|
|
81
|
+
* state file, so the honest mapping is: 'complete' when every non-optional phase
|
|
82
|
+
* exists and is lock-valid, else 'active'. Transient facts (gateBlocked, tamper,
|
|
83
|
+
* subagent activity) are NOT fs-recoverable and read as absent (recorded
|
|
84
|
+
* limitation; future enhancement).
|
|
85
|
+
*/
|
|
86
|
+
function runStateOf(status: ReturnType<typeof foldRun>): RecursiveRunState {
|
|
87
|
+
const mandatory = status.phases.filter(p => !p.optional)
|
|
88
|
+
const complete = mandatory.every(p => p.exists && p.lockValid)
|
|
89
|
+
return complete ? 'complete' : 'active'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fold every run under <root>/.recursive/run/ into the projection, grouped by
|
|
94
|
+
* worktree root then runId. A fresh fs read on every call (cold-resume live).
|
|
95
|
+
*/
|
|
96
|
+
export function snapshotWorkspace(root: string): RecursiveProjection {
|
|
97
|
+
const projection: RecursiveProjection = {}
|
|
98
|
+
for (const runId of discoverRuns(root)) {
|
|
99
|
+
const runDir = join(root, '.recursive', 'run', runId)
|
|
100
|
+
projection[root] ??= {}
|
|
101
|
+
projection[root][runId] = foldRunCard(runDir, runId, root)
|
|
102
|
+
}
|
|
103
|
+
return projection
|
|
104
|
+
}
|
package/lib/client/node.d.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conversation run node (Phase D R7, §11.6): a structural ConversationNodeDefinition
|
|
3
|
-
* over the recursive/* event family, keyed by runId + worktreeRoot. One compact,
|
|
4
|
-
* live node per run in the Chat view. PURE fold (no React) — the renderer lives
|
|
5
|
-
* in slots.ts.
|
|
6
|
-
*
|
|
7
|
-
* The real ConversationNodeDefinition contract arrives from
|
|
8
|
-
* @deepseek-ai/dsh-client-runtime/client at compose time; the structural shape
|
|
9
|
-
* here is type-compatible with the cookbook surface (match/start/update/
|
|
10
|
-
* buildViewNode + target/kind). The host links the bundle's exports to the real
|
|
11
|
-
* runtime identities.
|
|
12
|
-
*/
|
|
13
|
-
import type { ClientSessionEvent } from './contract.ts';
|
|
14
|
-
export declare const RECURSIVE_NODE_KIND = "recursive/run";
|
|
15
|
-
/** The chat-node payload the renderer receives (pure derived state). */
|
|
16
|
-
export interface RecursiveNodeData {
|
|
17
|
-
readonly runId: string;
|
|
18
|
-
readonly worktreeRoot: string;
|
|
19
|
-
readonly state: string;
|
|
20
|
-
readonly currentPhase: string | null;
|
|
21
|
-
readonly lockedCount: number;
|
|
22
|
-
readonly totalPhases: number;
|
|
23
|
-
readonly tampered: boolean;
|
|
24
|
-
readonly gateBlocked: boolean;
|
|
25
|
-
readonly mergedToRepoRoot?: string;
|
|
26
|
-
}
|
|
27
|
-
/** Pure fold state for one run node. */
|
|
28
|
-
export interface RecursiveNodeState {
|
|
29
|
-
runId: string;
|
|
30
|
-
worktreeRoot: string;
|
|
31
|
-
state: string;
|
|
32
|
-
phases: Record<string, {
|
|
33
|
-
phase: string;
|
|
34
|
-
status: string;
|
|
35
|
-
}>;
|
|
36
|
-
tampered: boolean;
|
|
37
|
-
gateBlocked: boolean;
|
|
38
|
-
mergedToRepoRoot?: string;
|
|
39
|
-
}
|
|
40
|
-
/** Extract a stable (runId, worktreeRoot) business id + lifecycle role from one event. */
|
|
41
|
-
export declare function matchRecursiveNode(event: ClientSessionEvent): {
|
|
42
|
-
id: string;
|
|
43
|
-
role: 'start' | 'update';
|
|
44
|
-
} | null;
|
|
45
|
-
/** Fold one recursive/* event into the node state. */
|
|
46
|
-
export declare function foldRecursiveNode(s: RecursiveNodeState | undefined, event: ClientSessionEvent): RecursiveNodeState;
|
|
47
|
-
/** The structural ConversationNodeDefinition (host links to the real contract). */
|
|
48
|
-
export declare const recursiveNodeDefinition: {
|
|
49
|
-
kind: string;
|
|
50
|
-
target: string;
|
|
51
|
-
match: typeof matchRecursiveNode;
|
|
52
|
-
start: (_ctx: unknown, match: {
|
|
53
|
-
event: ClientSessionEvent;
|
|
54
|
-
}) => RecursiveNodeState;
|
|
55
|
-
update: (ctx: {
|
|
56
|
-
state: RecursiveNodeState;
|
|
57
|
-
}, match: {
|
|
58
|
-
event: ClientSessionEvent;
|
|
59
|
-
}) => RecursiveNodeState;
|
|
60
|
-
buildViewNode: (ctx: {
|
|
61
|
-
key: string;
|
|
62
|
-
id: string;
|
|
63
|
-
state?: RecursiveNodeState;
|
|
64
|
-
}) => {
|
|
65
|
-
key: string;
|
|
66
|
-
kind: string;
|
|
67
|
-
id: string;
|
|
68
|
-
target: string;
|
|
69
|
-
data: RecursiveNodeData;
|
|
70
|
-
} | null;
|
|
71
|
-
};
|
package/lib/events.d.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* recursive/* session events (Phase D R1, PROPOSAL 11.2/11.6): the complete
|
|
3
|
-
* durable event vocabulary the projection folds, the board renders, and the
|
|
4
|
-
* conversation node correlates. Every payload carries a non-empty
|
|
5
|
-
* { runId, worktreeRoot } so the whole plane is worktree-aware and the
|
|
6
|
-
* projection can group runs by their control-plane root (binding
|
|
7
|
-
* workspace-scoping invariant, run 03 R1).
|
|
8
|
-
*
|
|
9
|
-
* Events are LOG-ONLY (no live mirror): the projection unit (projection.ts)
|
|
10
|
-
* is the only derived view. Payloads are plain JSON and declared on the
|
|
11
|
-
* SessionEventMap merge via @deepseek-ai/dsh-session/types.
|
|
12
|
-
*
|
|
13
|
-
* The pure constructor helpers below enforce the runId + non-empty
|
|
14
|
-
* worktreeRoot invariant at emit time, so a malformed event fails loud at
|
|
15
|
-
* the emit site instead of silently landing cross-workspace.
|
|
16
|
-
*/
|
|
17
|
-
import type { RecursiveRunState } from './types.ts';
|
|
18
|
-
/** Declare the recursive/* event family on the session event map (merge-extensible). */
|
|
19
|
-
declare module '@deepseek-ai/dsh-session/types' {
|
|
20
|
-
interface SessionEventMap {
|
|
21
|
-
/**
|
|
22
|
-
* A transition the lifecycle driver is ABOUT to validate/commit (logged
|
|
23
|
-
* before any file write). The pre-step gate reads this as the caller of the
|
|
24
|
-
* transition set — never a projection event (log-only internal signal).
|
|
25
|
-
*/
|
|
26
|
-
'recursive/phase-intent': {
|
|
27
|
-
runId: string;
|
|
28
|
-
worktreeRoot: string;
|
|
29
|
-
targetArtifact: string;
|
|
30
|
-
kind: 'lock' | 'reopen' | 'advance';
|
|
31
|
-
evidence?: Record<string, unknown>;
|
|
32
|
-
};
|
|
33
|
-
/** A run was created/bootstrapped in this workspace. */
|
|
34
|
-
'recursive/run-created': {
|
|
35
|
-
runId: string;
|
|
36
|
-
worktreeRoot: string;
|
|
37
|
-
template?: string;
|
|
38
|
-
repo?: string;
|
|
39
|
-
};
|
|
40
|
-
/** A phase transition committed (validate -> commit -> flush -> emit). */
|
|
41
|
-
'recursive/phase': {
|
|
42
|
-
runId: string;
|
|
43
|
-
worktreeRoot: string;
|
|
44
|
-
phase: string;
|
|
45
|
-
status: string;
|
|
46
|
-
};
|
|
47
|
-
/** A phase artifact was locked (lock hash + timestamp recorded). */
|
|
48
|
-
'recursive/phase-locked': {
|
|
49
|
-
runId: string;
|
|
50
|
-
worktreeRoot: string;
|
|
51
|
-
phase: string;
|
|
52
|
-
lockedAt: string;
|
|
53
|
-
lockHash: string;
|
|
54
|
-
};
|
|
55
|
-
/** A transition's gate predicate failed (rejected in strict, warned in advisory). */
|
|
56
|
-
'recursive/gate-blocked': {
|
|
57
|
-
runId: string;
|
|
58
|
-
worktreeRoot: string;
|
|
59
|
-
phase: string;
|
|
60
|
-
failures: string[];
|
|
61
|
-
kind: string;
|
|
62
|
-
};
|
|
63
|
-
/** fs/observed detected a change to a locked artifact outside a tool call. */
|
|
64
|
-
'recursive/tamper': {
|
|
65
|
-
runId: string;
|
|
66
|
-
worktreeRoot: string;
|
|
67
|
-
path: string;
|
|
68
|
-
reason: string;
|
|
69
|
-
};
|
|
70
|
-
/** A run was merged back to the repo root (conversation history re-keys). */
|
|
71
|
-
'recursive/run-merged': {
|
|
72
|
-
runId: string;
|
|
73
|
-
worktreeRoot: string;
|
|
74
|
-
repoRoot: string;
|
|
75
|
-
};
|
|
76
|
-
/** Run-level lifecycle flag (active/paused/blocked/complete + reason). */
|
|
77
|
-
'recursive/run-state': {
|
|
78
|
-
runId: string;
|
|
79
|
-
worktreeRoot: string;
|
|
80
|
-
state: RecursiveRunState;
|
|
81
|
-
reason?: string;
|
|
82
|
-
};
|
|
83
|
-
/** A subagent started for this run. */
|
|
84
|
-
'recursive/subagent-start': {
|
|
85
|
-
runId: string;
|
|
86
|
-
worktreeRoot: string;
|
|
87
|
-
childId: string;
|
|
88
|
-
role: string;
|
|
89
|
-
provider: string;
|
|
90
|
-
};
|
|
91
|
-
/** A subagent finished for this run. */
|
|
92
|
-
'recursive/subagent-end': {
|
|
93
|
-
runId: string;
|
|
94
|
-
worktreeRoot: string;
|
|
95
|
-
childId: string;
|
|
96
|
-
role: string;
|
|
97
|
-
provider: string;
|
|
98
|
-
status: 'running' | 'done' | 'failed';
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Enforce the runId + non-empty worktreeRoot invariant at emit time. */
|
|
103
|
-
export declare function requireRunKey(runId: string, worktreeRoot: string): void;
|
|
104
|
-
export interface PhaseIntentInput {
|
|
105
|
-
runId: string;
|
|
106
|
-
worktreeRoot: string;
|
|
107
|
-
targetArtifact: string;
|
|
108
|
-
kind: 'lock' | 'reopen' | 'advance';
|
|
109
|
-
evidence?: Record<string, unknown>;
|
|
110
|
-
}
|
|
111
|
-
export interface RunCreatedInput {
|
|
112
|
-
runId: string;
|
|
113
|
-
worktreeRoot: string;
|
|
114
|
-
template?: string;
|
|
115
|
-
repo?: string;
|
|
116
|
-
}
|
|
117
|
-
export interface PhaseInput {
|
|
118
|
-
runId: string;
|
|
119
|
-
worktreeRoot: string;
|
|
120
|
-
phase: string;
|
|
121
|
-
status: string;
|
|
122
|
-
}
|
|
123
|
-
export interface PhaseLockedInput {
|
|
124
|
-
runId: string;
|
|
125
|
-
worktreeRoot: string;
|
|
126
|
-
phase: string;
|
|
127
|
-
lockedAt: string;
|
|
128
|
-
lockHash: string;
|
|
129
|
-
}
|
|
130
|
-
export interface GateBlockedInput {
|
|
131
|
-
runId: string;
|
|
132
|
-
worktreeRoot: string;
|
|
133
|
-
phase: string;
|
|
134
|
-
failures: string[];
|
|
135
|
-
kind: string;
|
|
136
|
-
}
|
|
137
|
-
export interface TamperInput {
|
|
138
|
-
runId: string;
|
|
139
|
-
worktreeRoot: string;
|
|
140
|
-
path: string;
|
|
141
|
-
reason: string;
|
|
142
|
-
}
|
|
143
|
-
export interface RunMergedInput {
|
|
144
|
-
runId: string;
|
|
145
|
-
worktreeRoot: string;
|
|
146
|
-
repoRoot: string;
|
|
147
|
-
}
|
|
148
|
-
export interface RunStateInput {
|
|
149
|
-
runId: string;
|
|
150
|
-
worktreeRoot: string;
|
|
151
|
-
state: RecursiveRunState;
|
|
152
|
-
reason?: string;
|
|
153
|
-
}
|
|
154
|
-
export interface SubagentInput {
|
|
155
|
-
runId: string;
|
|
156
|
-
worktreeRoot: string;
|
|
157
|
-
childId: string;
|
|
158
|
-
role: string;
|
|
159
|
-
provider: string;
|
|
160
|
-
}
|
|
161
|
-
export interface SubagentEndInput extends SubagentInput {
|
|
162
|
-
status: 'running' | 'done' | 'failed';
|
|
163
|
-
}
|
|
164
|
-
export declare function phaseIntent(data: PhaseIntentInput): PhaseIntentInput;
|
|
165
|
-
export declare function runCreated(data: RunCreatedInput): RunCreatedInput;
|
|
166
|
-
export declare function phase(data: PhaseInput): PhaseInput;
|
|
167
|
-
export declare function phaseLocked(data: PhaseLockedInput): PhaseLockedInput;
|
|
168
|
-
export declare function gateBlocked(data: GateBlockedInput): GateBlockedInput;
|
|
169
|
-
export declare function tamper(data: TamperInput): TamperInput;
|
|
170
|
-
export declare function runMerged(data: RunMergedInput): RunMergedInput;
|
|
171
|
-
export declare function runState(data: RunStateInput): RunStateInput;
|
|
172
|
-
export declare function subagentStart(data: SubagentInput): SubagentInput;
|
|
173
|
-
export declare function subagentEnd(data: SubagentEndInput): SubagentEndInput;
|