@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.
Files changed (123) hide show
  1. package/cordis.patch.yml +15 -10
  2. package/lib/bootstrap.d.ts +9 -2
  3. package/lib/client/apply-guard.d.ts +14 -0
  4. package/lib/client/board.d.ts +5 -4
  5. package/lib/client/contract.d.ts +57 -24
  6. package/lib/client/host-api.d.ts +26 -0
  7. package/lib/client/index.d.ts +16 -9
  8. package/lib/client/inspector.d.ts +3 -3
  9. package/lib/client/slots.d.ts +4 -6
  10. package/lib/client/strip.d.ts +4 -3
  11. package/lib/client/use-live.d.ts +9 -0
  12. package/lib/client.js +252 -174
  13. package/lib/index.d.ts +11 -11
  14. package/lib/index.js +972 -4504
  15. package/lib/init-templates.d.ts +41 -0
  16. package/lib/live-route.d.ts +69 -0
  17. package/lib/phase-rules.d.ts +34 -0
  18. package/lib/runtime.d.ts +24 -17
  19. package/lib/snapshot.d.ts +11 -0
  20. package/package.json +4 -3
  21. package/preset/recursive/agent.cordis.yml +7 -4
  22. package/preset/recursive/preset.yml +1 -1
  23. package/references/agents-block.md +95 -0
  24. package/references/artifact-template.md +2574 -0
  25. package/references/bodies/claude.md +7 -0
  26. package/references/bodies/codex-agents.md +95 -0
  27. package/references/bodies/copilot.md +7 -0
  28. package/references/bodies/cursorrules.md +7 -0
  29. package/references/bodies/decisions.md +5 -0
  30. package/references/bodies/delegated-verification.md +49 -0
  31. package/references/bodies/memory-router.md +45 -0
  32. package/references/bodies/phase8-skill-memory.md +48 -0
  33. package/references/bodies/plans-bridge.md +43 -0
  34. package/references/bodies/recursive-agents-router.md +64 -0
  35. package/references/bodies/skill-discovery.md +51 -0
  36. package/references/bodies/skill-memory-router.md +42 -0
  37. package/references/bodies/state.md +5 -0
  38. package/references/bootstrap/RECURSIVE.md +2362 -0
  39. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  40. package/references/scripts/lint-recursive-run.ps1 +25 -0
  41. package/references/scripts/lint-recursive-run.py +2870 -0
  42. package/references/scripts/recursive-closeout.ps1 +38 -0
  43. package/references/scripts/recursive-closeout.py +541 -0
  44. package/references/scripts/recursive-init.ps1 +515 -0
  45. package/references/scripts/recursive-init.py +356 -0
  46. package/references/scripts/recursive-lock.ps1 +26 -0
  47. package/references/scripts/recursive-lock.py +302 -0
  48. package/references/scripts/recursive-review-bundle.ps1 +58 -0
  49. package/references/scripts/recursive-review-bundle.py +503 -0
  50. package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
  51. package/references/scripts/recursive-router-cli-configure.py +9 -0
  52. package/references/scripts/recursive-router-cli-init.ps1 +2 -0
  53. package/references/scripts/recursive-router-cli-init.py +9 -0
  54. package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
  55. package/references/scripts/recursive-router-cli-invoke.py +9 -0
  56. package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
  57. package/references/scripts/recursive-router-cli-probe.py +9 -0
  58. package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
  59. package/references/scripts/recursive-router-cli-resolve.py +9 -0
  60. package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
  61. package/references/scripts/recursive-router-cli-validate.py +9 -0
  62. package/references/scripts/recursive-router-configure.ps1 +27 -0
  63. package/references/scripts/recursive-router-configure.py +74 -0
  64. package/references/scripts/recursive-router-init.ps1 +17 -0
  65. package/references/scripts/recursive-router-init.py +29 -0
  66. package/references/scripts/recursive-router-invoke.ps1 +47 -0
  67. package/references/scripts/recursive-router-invoke.py +103 -0
  68. package/references/scripts/recursive-router-probe.ps1 +25 -0
  69. package/references/scripts/recursive-router-probe.py +44 -0
  70. package/references/scripts/recursive-router-resolve.ps1 +26 -0
  71. package/references/scripts/recursive-router-resolve.py +46 -0
  72. package/references/scripts/recursive-router-validate.ps1 +17 -0
  73. package/references/scripts/recursive-router-validate.py +27 -0
  74. package/references/scripts/recursive-status.ps1 +23 -0
  75. package/references/scripts/recursive-status.py +2124 -0
  76. package/references/scripts/recursive-subagent-action.ps1 +98 -0
  77. package/references/scripts/recursive-subagent-action.py +197 -0
  78. package/references/scripts/recursive-training-extract.ps1 +23 -0
  79. package/references/scripts/recursive-training-extract.py +99 -0
  80. package/references/scripts/recursive-training-grpo.ps1 +52 -0
  81. package/references/scripts/recursive-training-grpo.py +1341 -0
  82. package/references/scripts/recursive-training-loader.ps1 +78 -0
  83. package/references/scripts/recursive-training-loader.py +558 -0
  84. package/references/scripts/recursive-training-mcp.ps1 +29 -0
  85. package/references/scripts/recursive-training-mcp.py +261 -0
  86. package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
  87. package/references/scripts/recursive-training-phase8-trigger.py +137 -0
  88. package/references/scripts/recursive-training-sync.ps1 +29 -0
  89. package/references/scripts/recursive-training-sync.py +183 -0
  90. package/references/scripts/recursive_phase_rules.py +367 -0
  91. package/references/scripts/recursive_router_cli_lib.py +2 -0
  92. package/references/scripts/recursive_router_lib.py +2282 -0
  93. package/references/scripts/verify-locks.ps1 +25 -0
  94. package/references/scripts/verify-locks.py +353 -0
  95. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  96. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  97. package/scripts/install-preset.cmd +7 -0
  98. package/scripts/install-preset.js +101 -0
  99. package/scripts/lint-recursive-run.py +7 -5
  100. package/scripts/test-recursive-mode-smoke.ts +29 -28
  101. package/src/bootstrap.ts +253 -39
  102. package/src/client/apply-guard.ts +23 -0
  103. package/src/client/board.tsx +0 -0
  104. package/src/client/contract.ts +72 -24
  105. package/src/client/host-api.ts +90 -0
  106. package/src/client/index.ts +22 -11
  107. package/src/client/inspector.tsx +7 -9
  108. package/src/client/slots.ts +70 -26
  109. package/src/client/strip.tsx +9 -9
  110. package/src/client/use-live.ts +52 -0
  111. package/src/index.ts +89 -146
  112. package/src/init-templates.ts +276 -0
  113. package/src/live-route.ts +163 -0
  114. package/src/phase-rules.ts +262 -0
  115. package/src/policy.ts +28 -4
  116. package/src/runtime.ts +129 -77
  117. package/src/snapshot.ts +104 -0
  118. package/lib/client/node.d.ts +0 -71
  119. package/lib/events.d.ts +0 -173
  120. package/lib/projection.d.ts +0 -29
  121. package/src/client/node.ts +0 -156
  122. package/src/events.ts +0 -173
  123. package/src/projection.ts +0 -237
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Phase rules (R5): canonical parity port of lint-recursive-run.py's
3
+ * get_artifact_required_sections + workflow/audit constants. Single source of
4
+ * truth for per-phase required section headings and audit extras; consumed by
5
+ * initRun templates, renderRecursivePolicy, and the pre-step lint-rules
6
+ * injection. Values are byte-identical to the canonical linter (recursive-
7
+ * mode-audit-v2).
8
+ */
9
+
10
+ export const CURRENT_WORKFLOW_PROFILE = 'recursive-mode-audit-v2'
11
+ export const STRICT_WORKFLOW_PROFILE = 'recursive-mode-audit-v1'
12
+ export const COMPAT_WORKFLOW_PROFILE = 'memory-phase8'
13
+ export const STRICT_WORKFLOW_PROFILES = new Set([CURRENT_WORKFLOW_PROFILE, STRICT_WORKFLOW_PROFILE])
14
+
15
+ export const LATE_PHASE_ARTIFACTS = ['06-decisions-update.md', '07-state-update.md', '08-memory-impact.md']
16
+
17
+ export const AUDITED_PHASE_FILES = new Set([
18
+ '01-as-is.md',
19
+ '01.5-root-cause.md',
20
+ '02-to-be-plan.md',
21
+ '03-implementation-summary.md',
22
+ '03.5-code-review.md',
23
+ '04-test-summary.md',
24
+ '06-decisions-update.md',
25
+ '07-state-update.md',
26
+ '08-memory-impact.md',
27
+ ])
28
+
29
+ export const PRIOR_RECURSIVE_EVIDENCE_FILES = new Set([
30
+ '01-as-is.md',
31
+ '02-to-be-plan.md',
32
+ '04-test-summary.md',
33
+ '07-state-update.md',
34
+ '08-memory-impact.md',
35
+ ])
36
+
37
+ export const DIFF_AUDITED_FILES = new Set([
38
+ '02-to-be-plan.md',
39
+ '03-implementation-summary.md',
40
+ '03.5-code-review.md',
41
+ '04-test-summary.md',
42
+ '06-decisions-update.md',
43
+ '07-state-update.md',
44
+ '08-memory-impact.md',
45
+ ])
46
+
47
+ export const TRACEABILITY_REQUIRED_FILES = new Set([
48
+ '01-as-is.md',
49
+ '01.5-root-cause.md',
50
+ '02-to-be-plan.md',
51
+ '03-implementation-summary.md',
52
+ '03.5-code-review.md',
53
+ '04-test-summary.md',
54
+ '05-manual-qa.md',
55
+ '06-decisions-update.md',
56
+ '07-state-update.md',
57
+ '08-memory-impact.md',
58
+ ])
59
+
60
+ export const AUDIT_REQUIRED_HEADINGS = [
61
+ 'Audit Context',
62
+ 'Effective Inputs Re-read',
63
+ 'Earlier Phase Reconciliation',
64
+ 'Subagent Contribution Verification',
65
+ 'Worktree Diff Audit',
66
+ 'Gaps Found',
67
+ 'Repair Work Performed',
68
+ 'Requirement Completion Status',
69
+ 'Audit Verdict',
70
+ ]
71
+
72
+ export const DIFF_BASIS_FIELDS = [
73
+ 'Baseline type',
74
+ 'Baseline reference',
75
+ 'Comparison reference',
76
+ 'Normalized baseline',
77
+ 'Normalized comparison',
78
+ 'Normalized diff command',
79
+ ]
80
+
81
+ const SECTION_MAP: Record<string, string[]> = {
82
+ '00-worktree.md': [
83
+ 'TODO',
84
+ 'Directory Selection',
85
+ 'Safety Verification',
86
+ 'Worktree Creation',
87
+ 'Main Branch Protection',
88
+ 'Project Setup',
89
+ 'Test Baseline Verification',
90
+ 'Worktree Context',
91
+ 'Diff Basis For Later Audits',
92
+ 'Traceability',
93
+ 'Coverage Gate',
94
+ 'Approval Gate',
95
+ ],
96
+ '00-requirements.md': [
97
+ 'TODO',
98
+ 'Requirements',
99
+ 'Out of Scope',
100
+ 'Constraints',
101
+ 'Coverage Gate',
102
+ 'Approval Gate',
103
+ ],
104
+ '01-as-is.md': [
105
+ 'TODO',
106
+ 'Reproduction Steps (Novice-Runnable)',
107
+ 'Current Behavior by Requirement',
108
+ 'Source Requirement Inventory',
109
+ 'Relevant Code Pointers',
110
+ 'Known Unknowns',
111
+ 'Evidence',
112
+ 'Traceability',
113
+ 'Coverage Gate',
114
+ 'Approval Gate',
115
+ ],
116
+ '01.5-root-cause.md': [
117
+ 'TODO',
118
+ 'Error Analysis',
119
+ 'Reproduction Verification',
120
+ 'Recent Changes Analysis',
121
+ 'Evidence Gathering (Multi-Layer if applicable)',
122
+ 'Data Flow Trace',
123
+ 'Pattern Analysis',
124
+ 'Hypothesis Testing',
125
+ 'Root Cause Summary',
126
+ 'Traceability',
127
+ 'Coverage Gate',
128
+ 'Approval Gate',
129
+ ],
130
+ '02-to-be-plan.md': [
131
+ 'TODO',
132
+ 'Planned Changes by File',
133
+ 'Requirement Mapping',
134
+ 'Implementation Steps',
135
+ 'Testing Strategy',
136
+ 'Playwright Plan (if applicable)',
137
+ 'Manual QA Scenarios',
138
+ 'Idempotence and Recovery',
139
+ 'Implementation Sub-phases',
140
+ 'Plan Drift Check',
141
+ 'Traceability',
142
+ 'Coverage Gate',
143
+ 'Approval Gate',
144
+ ],
145
+ '03-implementation-summary.md': [
146
+ 'TODO',
147
+ 'Changes Applied',
148
+ 'TDD Compliance Log',
149
+ 'Plan Deviations',
150
+ 'Implementation Evidence',
151
+ 'Traceability',
152
+ 'Coverage Gate',
153
+ 'Approval Gate',
154
+ ],
155
+ '03.5-code-review.md': [
156
+ 'TODO',
157
+ 'Review Scope',
158
+ 'Plan Alignment Assessment',
159
+ 'Code Quality Assessment',
160
+ 'Issues Found',
161
+ 'Verdict',
162
+ 'Review Metadata',
163
+ 'Traceability',
164
+ 'Coverage Gate',
165
+ 'Approval Gate',
166
+ ],
167
+ '04-test-summary.md': [
168
+ 'TODO',
169
+ 'Pre-Test Implementation Audit',
170
+ 'Environment',
171
+ 'Execution Mode',
172
+ 'Commands Executed (Exact)',
173
+ 'Results Summary',
174
+ 'Evidence and Artifacts',
175
+ 'Failures and Diagnostics (if any)',
176
+ 'Flake/Rerun Notes',
177
+ 'Traceability',
178
+ 'Coverage Gate',
179
+ 'Approval Gate',
180
+ ],
181
+ '05-manual-qa.md': [
182
+ 'TODO',
183
+ 'QA Execution Record',
184
+ 'QA Scenarios and Results',
185
+ 'Evidence and Artifacts',
186
+ 'User Sign-Off',
187
+ 'Traceability',
188
+ 'Coverage Gate',
189
+ 'Approval Gate',
190
+ ],
191
+ '06-decisions-update.md': [
192
+ 'TODO',
193
+ 'Decisions Changes Applied',
194
+ 'Rationale',
195
+ 'Resulting Decision Entry',
196
+ 'Traceability',
197
+ 'Coverage Gate',
198
+ 'Approval Gate',
199
+ ],
200
+ '07-state-update.md': [
201
+ 'TODO',
202
+ 'State Changes Applied',
203
+ 'Rationale',
204
+ 'Resulting State Summary',
205
+ 'Traceability',
206
+ 'Coverage Gate',
207
+ 'Approval Gate',
208
+ ],
209
+ '08-memory-impact.md': [
210
+ 'TODO',
211
+ 'Diff Basis',
212
+ 'Changed Paths Review',
213
+ 'Affected Memory Docs',
214
+ 'Run-Local Skill Usage Capture',
215
+ 'Skill Memory Promotion Review',
216
+ 'Uncovered Paths',
217
+ 'Router and Parent Refresh',
218
+ 'Final Status Summary',
219
+ 'Traceability',
220
+ 'Coverage Gate',
221
+ 'Approval Gate',
222
+ ],
223
+ }
224
+
225
+ /**
226
+ * get_artifact_required_sections(file_name, workflow_profile): canonical-parity
227
+ * required section headings for a phase artifact. Defaults to TODO + Coverage
228
+ * Gate + Approval Gate for unknown files. Audited phases in strict profiles get
229
+ * the audit headings appended (plus Prior Recursive Evidence Reviewed for the
230
+ * prior-evidence file set).
231
+ */
232
+ export function getArtifactRequiredSections(fileName: string, workflowProfile: string = CURRENT_WORKFLOW_PROFILE): string[] {
233
+ const headings = [...(SECTION_MAP[fileName] ?? ['TODO', 'Coverage Gate', 'Approval Gate'])]
234
+ if (STRICT_WORKFLOW_PROFILES.has(workflowProfile) && AUDITED_PHASE_FILES.has(fileName)) {
235
+ headings.push(...AUDIT_REQUIRED_HEADINGS)
236
+ if (PRIOR_RECURSIVE_EVIDENCE_FILES.has(fileName)) {
237
+ headings.push('Prior Recursive Evidence Reviewed')
238
+ }
239
+ }
240
+ return headings
241
+ }
242
+
243
+ /**
244
+ * Compact lint-rules message for the pre-step injection (R5): this phase's
245
+ * required sections + phase-specific gate notes, derived from the canonical
246
+ * artifact-template sections. Kept short enough to prepend as a system-style
247
+ * reminder without bloating the step context.
248
+ */
249
+ export function phaseLintRulesMessage(fileName: string, workflowProfile: string = CURRENT_WORKFLOW_PROFILE): string {
250
+ const sections = getArtifactRequiredSections(fileName, workflowProfile)
251
+ const lines = [
252
+ '<system-reminder>',
253
+ 'Recursive-mode phase lint rules for THIS phase (' + fileName + '):',
254
+ 'Required sections: ' + sections.join(' | '),
255
+ 'Gates: Coverage: FAIL until all checkboxes pass; Approval: FAIL until user sign-off; lock only via recursive_lock (monotonic).',
256
+ 'Audited phases: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context and Audit Verdict.',
257
+ 'TDD (phase 3): declare TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.',
258
+ 'QA (phase 5): declare QA Execution Mode: human|agent-operated|hybrid; human/hybrid need user sign-off.',
259
+ '</system-reminder>',
260
+ ]
261
+ return lines.join('\n')
262
+ }
package/src/policy.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * recursive:policy prompt-section renderer (Phase C R5, PROPOSAL 8.4 Layer 3).
3
3
  * Renders the CURRENT phase's contract from folded state + enforcement config:
4
4
  * what must exist before advancing, which tools are denied/asked this phase,
5
- * the strict/pragmatic TDD mode, the QA mode, and the lock chain - the same
6
- * rules the machine gates enforce (no prompt/gate contradiction).
5
+ * the strict/pragmatic TDD mode, the QA mode, the lock chain, and (R5) the
6
+ * current phase's required sections + gate checklist — the same rules the
7
+ * machine gates enforce (no prompt/gate contradiction).
7
8
  */
8
9
  import { existsSync } from 'node:fs'
9
10
  import { join } from 'node:path'
@@ -12,6 +13,7 @@ import { foldRun } from './status.ts'
12
13
  import type { RecursivePhaseState } from './lifecycle.ts'
13
14
  import type { EnforcementConfig } from './enforcement.ts'
14
15
  import { DEFAULT_ENFORCEMENT } from './enforcement.ts'
16
+ import { getArtifactRequiredSections, AUDITED_PHASE_FILES, CURRENT_WORKFLOW_PROFILE } from './phase-rules.ts'
15
17
 
16
18
  export interface PolicyContext {
17
19
  worktreeRoot: string
@@ -32,10 +34,12 @@ export function renderRecursivePolicy(context: PolicyContext | null): string {
32
34
  // Derive the current phase from files (never trust an unchecked fold).
33
35
  let currentPhase = folded?.phase ?? ''
34
36
  let nextRequired = ''
37
+ let currentFile = ''
35
38
  const status = existsSync(runDir) ? foldRun(runDir, context.runId) : null
36
39
  if (status?.currentPhase) {
37
40
  currentPhase = status.currentPhase.label + ' (' + status.currentPhase.status + ')'
38
41
  nextRequired = status.currentPhase.phaseName + ' (' + status.currentPhase.key + ')'
42
+ currentFile = status.currentPhase.phaseName
39
43
  }
40
44
 
41
45
  const strictness = (gate: EnforcementConfig[keyof EnforcementConfig]) => gate
@@ -51,6 +55,26 @@ export function renderRecursivePolicy(context: PolicyContext | null): string {
51
55
  '- The control-plane root is resolved STRICTLY from this session workspace (never scan other workspaces).',
52
56
  '- Scratch is disposable, git-ignored, and never citable as an Input.',
53
57
  '- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing.',
54
- ]
58
+ ];
59
+
60
+ // R5: surface the current phase's required sections + gate checklist.
61
+ if (currentFile) {
62
+ const sections = getArtifactRequiredSections(currentFile, CURRENT_WORKFLOW_PROFILE)
63
+ lines.push('');
64
+ lines.push('## This phase (' + currentFile + ') required sections (canonical lint):');
65
+ lines.push('- ' + sections.join(' | '));
66
+ lines.push('## This phase gate checklist:');
67
+ lines.push('- Coverage: FAIL until every checklist item is done; Approval: FAIL until sign-off; lock only via recursive_lock (monotonic).');
68
+ if (AUDITED_PHASE_FILES.has(currentFile)) {
69
+ lines.push('- Audited phase: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context + Audit Verdict.');
70
+ }
71
+ if (currentFile === '03-implementation-summary.md') {
72
+ lines.push('- TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.');
73
+ }
74
+ if (currentFile === '05-manual-qa.md') {
75
+ lines.push('- QA Execution Mode: human|agent-operated|hybrid; human/hybrid require user sign-off.');
76
+ }
77
+ }
78
+
55
79
  return lines.join('\n')
56
- }
80
+ }
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 { foldRecursiveProjection as foldProjectionEvent, emptyRecursiveFoldState, recursiveProjectionUnit, isInsideWorkspace, type RecursiveEventLike, type RecursiveFoldState } from './projection.ts'
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
- * Emit a recursive/* event into the session whose control-plane root matches
307
- * `root` (additive log-only; the projection drives from these). Falls back to
308
- * the agent's session when passed, else the session store list.
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
- private emitToSession(root: string, type: string, data: unknown, agent?: { session?: { header?: { cwd?: string } } | null } | null) {
311
- const sessions = this.ctx.get('sessions') as { list?: () => Session[] } | undefined
312
- const candidates: Session[] = []
313
- if (agent?.session) candidates.push(agent.session as unknown as Session)
314
- const listed = sessions?.list?.() ?? []
315
- for (const s of listed) candidates.push(s)
316
- const norm = (x: string) => x.split(String.fromCharCode(92)).join("/")
317
- const target = candidates.find((s) => {
318
- const cwd = norm(s.header?.cwd ?? "")
319
- return cwd === "" || cwd === norm(root) || cwd.startsWith(norm(root) + "/")
320
- })
321
- if (!target) return
322
- try { (target.append as unknown as (t: string, d: unknown) => void)(type, data) } catch { /* log-only */ }
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 stub artifact headers (no-op if exists).
335
- * Returns the run dir + created artifacts.
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
- for (const [file, fields] of Object.entries(ARTIFACT_STUB)) {
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
- existing.push(file)
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
- // B2: a run's files first exist here — the run-created event feeds the
355
- // projection's worktree-grouped card. Emit only when the scaffold is fresh
356
- // (created.length > 0) so resume/ensure does not double-log.
357
- if (created.length > 0) {
358
- this.emitToSession(root, 'recursive/run-created', runCreatedEvent({ runId, worktreeRoot: root }), agent)
359
- this.emitToSession(root, 'recursive/run-state', runStateEvent({ runId, worktreeRoot: root, state: 'new' }), agent)
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: log the phase-intent BEFORE validation so the pre-step gate reads it
380
- // (the caller of the transition set). Never a projection event.
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 — emit phase + phase-locked (the projection's
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 — log phase-intent + emit the transition so
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 for phase-specific issues. Minimal structural checks:
439
- * gates present, TODO section, Status field, LockHash consistency.
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('^[ \\t]*' + gate + ':\\s*(PASS|FAIL)\\s*$', 'm').test(content)) warnings.push('Missing ' + gate + ' gate')
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
+