@try-works/dsh-recursive-mode 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +15 -10
- package/lib/bootstrap.d.ts +9 -2
- package/lib/client/apply-guard.d.ts +14 -0
- package/lib/client/board.d.ts +5 -4
- package/lib/client/contract.d.ts +57 -24
- package/lib/client/host-api.d.ts +26 -0
- package/lib/client/index.d.ts +16 -9
- package/lib/client/inspector.d.ts +3 -3
- package/lib/client/slots.d.ts +4 -6
- package/lib/client/strip.d.ts +4 -3
- package/lib/client/use-live.d.ts +9 -0
- package/lib/client.js +252 -174
- package/lib/index.d.ts +11 -11
- package/lib/index.js +972 -4504
- package/lib/init-templates.d.ts +41 -0
- package/lib/live-route.d.ts +69 -0
- package/lib/phase-rules.d.ts +34 -0
- package/lib/runtime.d.ts +24 -17
- package/lib/snapshot.d.ts +11 -0
- package/package.json +4 -3
- package/preset/recursive/agent.cordis.yml +7 -4
- package/preset/recursive/preset.yml +1 -1
- package/references/agents-block.md +95 -0
- package/references/artifact-template.md +2574 -0
- package/references/bodies/claude.md +7 -0
- package/references/bodies/codex-agents.md +95 -0
- package/references/bodies/copilot.md +7 -0
- package/references/bodies/cursorrules.md +7 -0
- package/references/bodies/decisions.md +5 -0
- package/references/bodies/delegated-verification.md +49 -0
- package/references/bodies/memory-router.md +45 -0
- package/references/bodies/phase8-skill-memory.md +48 -0
- package/references/bodies/plans-bridge.md +43 -0
- package/references/bodies/recursive-agents-router.md +64 -0
- package/references/bodies/skill-discovery.md +51 -0
- package/references/bodies/skill-memory-router.md +42 -0
- package/references/bodies/state.md +5 -0
- package/references/bootstrap/RECURSIVE.md +2362 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +25 -0
- package/references/scripts/lint-recursive-run.py +2870 -0
- package/references/scripts/recursive-closeout.ps1 +38 -0
- package/references/scripts/recursive-closeout.py +541 -0
- package/references/scripts/recursive-init.ps1 +515 -0
- package/references/scripts/recursive-init.py +356 -0
- package/references/scripts/recursive-lock.ps1 +26 -0
- package/references/scripts/recursive-lock.py +302 -0
- package/references/scripts/recursive-review-bundle.ps1 +58 -0
- package/references/scripts/recursive-review-bundle.py +503 -0
- package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
- package/references/scripts/recursive-router-cli-configure.py +9 -0
- package/references/scripts/recursive-router-cli-init.ps1 +2 -0
- package/references/scripts/recursive-router-cli-init.py +9 -0
- package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
- package/references/scripts/recursive-router-cli-invoke.py +9 -0
- package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
- package/references/scripts/recursive-router-cli-probe.py +9 -0
- package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
- package/references/scripts/recursive-router-cli-resolve.py +9 -0
- package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
- package/references/scripts/recursive-router-cli-validate.py +9 -0
- package/references/scripts/recursive-router-configure.ps1 +27 -0
- package/references/scripts/recursive-router-configure.py +74 -0
- package/references/scripts/recursive-router-init.ps1 +17 -0
- package/references/scripts/recursive-router-init.py +29 -0
- package/references/scripts/recursive-router-invoke.ps1 +47 -0
- package/references/scripts/recursive-router-invoke.py +103 -0
- package/references/scripts/recursive-router-probe.ps1 +25 -0
- package/references/scripts/recursive-router-probe.py +44 -0
- package/references/scripts/recursive-router-resolve.ps1 +26 -0
- package/references/scripts/recursive-router-resolve.py +46 -0
- package/references/scripts/recursive-router-validate.ps1 +17 -0
- package/references/scripts/recursive-router-validate.py +27 -0
- package/references/scripts/recursive-status.ps1 +23 -0
- package/references/scripts/recursive-status.py +2124 -0
- package/references/scripts/recursive-subagent-action.ps1 +98 -0
- package/references/scripts/recursive-subagent-action.py +197 -0
- package/references/scripts/recursive-training-extract.ps1 +23 -0
- package/references/scripts/recursive-training-extract.py +99 -0
- package/references/scripts/recursive-training-grpo.ps1 +52 -0
- package/references/scripts/recursive-training-grpo.py +1341 -0
- package/references/scripts/recursive-training-loader.ps1 +78 -0
- package/references/scripts/recursive-training-loader.py +558 -0
- package/references/scripts/recursive-training-mcp.ps1 +29 -0
- package/references/scripts/recursive-training-mcp.py +261 -0
- package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
- package/references/scripts/recursive-training-phase8-trigger.py +137 -0
- package/references/scripts/recursive-training-sync.ps1 +29 -0
- package/references/scripts/recursive-training-sync.py +183 -0
- package/references/scripts/recursive_phase_rules.py +367 -0
- package/references/scripts/recursive_router_cli_lib.py +2 -0
- package/references/scripts/recursive_router_lib.py +2282 -0
- package/references/scripts/verify-locks.ps1 +25 -0
- package/references/scripts/verify-locks.py +353 -0
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-preset.cmd +7 -0
- package/scripts/install-preset.js +101 -0
- package/scripts/lint-recursive-run.py +7 -5
- package/scripts/test-recursive-mode-smoke.ts +29 -28
- package/src/bootstrap.ts +253 -39
- package/src/client/apply-guard.ts +23 -0
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +72 -24
- package/src/client/host-api.ts +90 -0
- package/src/client/index.ts +22 -11
- package/src/client/inspector.tsx +7 -9
- package/src/client/slots.ts +70 -26
- package/src/client/strip.tsx +9 -9
- package/src/client/use-live.ts +52 -0
- package/src/index.ts +89 -146
- package/src/init-templates.ts +276 -0
- package/src/live-route.ts +163 -0
- package/src/phase-rules.ts +262 -0
- package/src/policy.ts +28 -4
- package/src/runtime.ts +129 -77
- package/src/snapshot.ts +104 -0
- package/lib/client/node.d.ts +0 -71
- package/lib/events.d.ts +0 -173
- package/lib/projection.d.ts +0 -29
- package/src/client/node.ts +0 -156
- package/src/events.ts +0 -173
- package/src/projection.ts +0 -237
|
@@ -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,
|
|
6
|
-
*
|
|
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 {
|
|
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
|
+
|