@try-works/dsh-recursive-mode 0.1.0

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 (88) hide show
  1. package/cordis.patch.yml +12 -0
  2. package/lib/bootstrap.d.ts +35 -0
  3. package/lib/client/board.d.ts +10 -0
  4. package/lib/client/contract.d.ts +51 -0
  5. package/lib/client/derive.d.ts +92 -0
  6. package/lib/client/index.d.ts +21 -0
  7. package/lib/client/inspector.d.ts +10 -0
  8. package/lib/client/node.d.ts +71 -0
  9. package/lib/client/settings.d.ts +6 -0
  10. package/lib/client/slots.d.ts +7 -0
  11. package/lib/client/strip.d.ts +7 -0
  12. package/lib/client.d.ts +10 -0
  13. package/lib/client.js +490 -0
  14. package/lib/closeout.d.ts +23 -0
  15. package/lib/commands.d.ts +51 -0
  16. package/lib/delegation.d.ts +92 -0
  17. package/lib/enforcement.d.ts +53 -0
  18. package/lib/events.d.ts +173 -0
  19. package/lib/handoff.d.ts +51 -0
  20. package/lib/index.d.ts +40 -0
  21. package/lib/lifecycle.d.ts +107 -0
  22. package/lib/lock.d.ts +92 -0
  23. package/lib/policy.d.ts +12 -0
  24. package/lib/projection.d.ts +29 -0
  25. package/lib/recursive_closeout.tool.d.ts +8 -0
  26. package/lib/recursive_init.tool.d.ts +2 -0
  27. package/lib/recursive_lint.tool.d.ts +2 -0
  28. package/lib/recursive_lock.tool.d.ts +2 -0
  29. package/lib/recursive_scratch.tool.d.ts +7 -0
  30. package/lib/recursive_status.tool.d.ts +2 -0
  31. package/lib/review.d.ts +39 -0
  32. package/lib/router.d.ts +77 -0
  33. package/lib/run.d.ts +29 -0
  34. package/lib/runtime.d.ts +241 -0
  35. package/lib/scratch.d.ts +18 -0
  36. package/lib/status.d.ts +19 -0
  37. package/lib/types.d.ts +104 -0
  38. package/lib/workspace.d.ts +50 -0
  39. package/package.json +119 -0
  40. package/preset/recursive/agent.cordis.yml +282 -0
  41. package/preset/recursive/preset.yml +3 -0
  42. package/scripts/install-recursive-mode.ps1 +956 -0
  43. package/scripts/install-recursive-mode.py +750 -0
  44. package/scripts/lint-recursive-run.py +2868 -0
  45. package/scripts/recursive-closeout.py +541 -0
  46. package/scripts/recursive-init.py +356 -0
  47. package/scripts/recursive-lock.py +302 -0
  48. package/scripts/recursive-status.py +2124 -0
  49. package/scripts/recursive_phase_rules.py +367 -0
  50. package/scripts/recursive_router_lib.py +2282 -0
  51. package/scripts/test-recursive-mode-smoke.ts +204 -0
  52. package/scripts/verify-locks.py +353 -0
  53. package/src/bootstrap.ts +118 -0
  54. package/src/client/board.tsx +61 -0
  55. package/src/client/contract.ts +58 -0
  56. package/src/client/derive.ts +241 -0
  57. package/src/client/index.ts +28 -0
  58. package/src/client/inspector.tsx +49 -0
  59. package/src/client/node.ts +156 -0
  60. package/src/client/settings.tsx +18 -0
  61. package/src/client/slots.ts +67 -0
  62. package/src/client/strip.tsx +28 -0
  63. package/src/client.ts +11 -0
  64. package/src/closeout.ts +183 -0
  65. package/src/commands.ts +142 -0
  66. package/src/delegation.ts +306 -0
  67. package/src/enforcement.ts +180 -0
  68. package/src/events.ts +173 -0
  69. package/src/handoff.ts +165 -0
  70. package/src/index.ts +283 -0
  71. package/src/lifecycle.ts +235 -0
  72. package/src/lock.ts +369 -0
  73. package/src/policy.ts +56 -0
  74. package/src/projection.ts +237 -0
  75. package/src/recursive_closeout.tool.ts +35 -0
  76. package/src/recursive_init.tool.ts +28 -0
  77. package/src/recursive_lint.tool.ts +29 -0
  78. package/src/recursive_lock.tool.ts +33 -0
  79. package/src/recursive_scratch.tool.ts +42 -0
  80. package/src/recursive_status.tool.ts +24 -0
  81. package/src/review.ts +178 -0
  82. package/src/router.ts +197 -0
  83. package/src/run.ts +85 -0
  84. package/src/runtime.ts +564 -0
  85. package/src/scratch.ts +85 -0
  86. package/src/status.ts +194 -0
  87. package/src/types.ts +112 -0
  88. package/src/workspace.ts +67 -0
@@ -0,0 +1,77 @@
1
+ export interface RouterDefaults {
2
+ when_role_unconfigured: string;
3
+ when_cli_unavailable: string;
4
+ when_model_unknown: string;
5
+ allow_auto_assign_if_single_cli: boolean;
6
+ probe_timeout_ms: number;
7
+ invoke_timeout_ms: number;
8
+ }
9
+ export interface RoleRoute {
10
+ enabled: boolean;
11
+ mode: string;
12
+ cli: string | null;
13
+ model: string | null;
14
+ fallback: string;
15
+ }
16
+ export interface RouterPolicy {
17
+ version: number;
18
+ defaults: RouterDefaults;
19
+ role_routes: Record<string, RoleRoute>;
20
+ cli_overrides: Record<string, unknown>;
21
+ custom_clis: unknown[];
22
+ }
23
+ export type RouteTier = 'native' | 'external-cli' | 'self-audit' | 'local-controller';
24
+ export interface RouteDecision {
25
+ tier: RouteTier;
26
+ provider?: string;
27
+ reason: string;
28
+ }
29
+ export interface SubagentProviderLike {
30
+ name: string;
31
+ capabilities?: {
32
+ outputSchema?: boolean;
33
+ depthLimit?: boolean;
34
+ toolFilter?: boolean;
35
+ persona?: boolean;
36
+ };
37
+ }
38
+ export interface CapabilityProbe {
39
+ available: boolean;
40
+ provider?: string;
41
+ capabilities?: {
42
+ outputSchema: boolean;
43
+ depthLimit: boolean;
44
+ toolFilter: boolean;
45
+ persona: boolean;
46
+ };
47
+ reason: string;
48
+ }
49
+ /** Parse recursive-router.json. A missing/invalid file yields a default self-audit policy (never throws). */
50
+ export declare function loadRouterPolicy(path?: string): RouterPolicy;
51
+ /** Default router policy path inside a workspace root. */
52
+ export declare function routerPolicyPath(root: string): string;
53
+ /**
54
+ * Resolve a role to a tier, preferring a native provider whose name maps to
55
+ * the role (e.g. role 'code-reviewer' -> provider 'code-reviewer' or the
56
+ * generic spawn/fork provider). External CLIs ride their provider rows; else
57
+ * the policy fallback.
58
+ */
59
+ export declare function resolveRole(role: string, policy: RouterPolicy, providers: Record<string, SubagentProviderLike>): RouteDecision;
60
+ /** Probe a single provider and return its advertised capabilities. */
61
+ export declare function probeCapabilities(provider: SubagentProviderLike | undefined): CapabilityProbe;
62
+ /**
63
+ * Probe the router-relevant capability for a role. No provider -> available
64
+ * false with a concrete reason (the self-audit fallback trigger).
65
+ */
66
+ export declare function capabilityProbe(input: {
67
+ providers: Record<string, SubagentProviderLike>;
68
+ role: string;
69
+ policy?: RouterPolicy;
70
+ }): CapabilityProbe;
71
+ /** Render the Delegation Decision Basis prose the phase doc records. */
72
+ export declare function delegationDecisionBasis(input: {
73
+ role: string;
74
+ available: boolean;
75
+ provider?: string;
76
+ fallback: string;
77
+ }): string;
package/lib/run.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Run discovery for recursive-mode runs.
3
+ *
4
+ * Ports the discovery subset of recursive-status.py:
5
+ * - discoverRuns: list run directories under <repoRoot>/.recursive/run/
6
+ * - resolveRunDir: explicit run id, or the latest run by mtime (stable sort)
7
+ *
8
+ * R1 (run 02): dedicated module so tools and status share one discovery path.
9
+ */
10
+ export interface RunDiscoveryResult {
11
+ runDir: string;
12
+ runId: string;
13
+ }
14
+ /**
15
+ * List run ids under <repoRoot>/.recursive/run/, excluding non-directories.
16
+ * Mirrors: runs = [p for p in run_root.iterdir() if p.is_dir()]
17
+ */
18
+ export declare function discoverRuns(repoRoot: string): string[];
19
+ /**
20
+ * Resolve the run directory for an explicit run id, or the latest run by mtime.
21
+ * Mirrors get_latest_run_directory: sort by st_mtime descending, stable sort
22
+ * (ties preserve discovery order — no name-based tie-break).
23
+ */
24
+ export declare function resolveRunDir(repoRoot: string, runId?: string): RunDiscoveryResult | null;
25
+ /**
26
+ * Latest run directory by mtime (descending), stable sort preserving
27
+ * discovery order for exact ties. Returns null when no runs exist.
28
+ */
29
+ export declare function getLatestRunDirectory(runRoot: string): string | null;
@@ -0,0 +1,241 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ import type { RecursiveStatusResult } from './types.ts';
3
+ import { type WorkspaceRegistryLike } from './workspace.ts';
4
+ import { type ScratchTarget } from './scratch.ts';
5
+ import { type ReviewBundleInput } from './review.ts';
6
+ import { type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
7
+ import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference } from './delegation.ts';
8
+ import { type PhaseTransitionIntent, type SessionEventLike, type RecursivePhaseState, type GateCheckResult } from './lifecycle.ts';
9
+ import { type EnforcementConfig, type PreStepGateDecision, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
10
+ import { type RecursiveEventLike, type RecursiveFoldState } from './projection.ts';
11
+ declare module '@deepseek-ai/cordis' {
12
+ interface Context {
13
+ recursive: RecursiveRuntime;
14
+ }
15
+ }
16
+ export interface LockArtifactResult {
17
+ artifact: string;
18
+ runId: string;
19
+ status: string;
20
+ lockedAt: string | null;
21
+ lockHash: string | null;
22
+ receipt?: unknown;
23
+ blockers: string[];
24
+ }
25
+ export interface LintArtifactResult {
26
+ artifact: string;
27
+ runId: string;
28
+ errors: string[];
29
+ warnings: string[];
30
+ passed: boolean;
31
+ }
32
+ export declare class RecursiveRuntime extends Service {
33
+ /** Recursive-mode runtime service. Owns run-state reads + lock/init/lint operations. */
34
+ constructor(ctx: Context, config?: {
35
+ repoRoot?: string;
36
+ workspaceRegistry?: WorkspaceRegistryLike;
37
+ });
38
+ private readonly repoRoot;
39
+ private readonly workspaceRegistry;
40
+ private _enforcementConfig;
41
+ /**
42
+ * Workspace-scoped control-plane root (R1 binding invariant).
43
+ * Resolves the session agent's canonical cwd -> workspace path via the
44
+ * registry; NEVER scans list(). Returns null when unavailable (defer).
45
+ */
46
+ resolveWorkspaceRoot(agent?: {
47
+ session?: {
48
+ header?: {
49
+ cwd?: string;
50
+ };
51
+ };
52
+ } | null): Promise<string | null>;
53
+ /**
54
+ * Run-scoped closeout receipt scaffold (R2), rooted under the given
55
+ * workspace root. Refuses runIds outside the root (never crosses workspaces).
56
+ */
57
+ closeoutRun(root: string, runId: string, phase: string): {
58
+ error: string;
59
+ } | {
60
+ phase: string;
61
+ file: string;
62
+ created: string[];
63
+ existing: string[];
64
+ closeoutPhase: string;
65
+ runId: string;
66
+ error?: undefined;
67
+ };
68
+ /**
69
+ * Run-scoped scratchpad access (R5), rooted under the given workspace root.
70
+ */
71
+ scratchRun(root: string, runId: string, action: string, target: ScratchTarget, content?: string): {
72
+ error: string;
73
+ runId?: undefined;
74
+ target?: undefined;
75
+ action?: undefined;
76
+ content?: undefined;
77
+ path?: undefined;
78
+ } | {
79
+ runId: string;
80
+ target: ScratchTarget;
81
+ action: string;
82
+ content: string;
83
+ path: string;
84
+ error?: undefined;
85
+ } | {
86
+ runId: string;
87
+ target: ScratchTarget;
88
+ action: string;
89
+ path: string;
90
+ error?: undefined;
91
+ content?: undefined;
92
+ };
93
+ /**
94
+ * Phase B (native delegation): build a review bundle (R1) + file-backed
95
+ * handoff docs (R2), resolve the role via the router policy (R3), and call
96
+ * ctx.subagents.start() with the full request (R4). Workspace-scoped: every
97
+ * path resolves under the session's control-plane root.
98
+ */
99
+ delegateReview(input: {
100
+ root: string;
101
+ runId: string;
102
+ phase: string;
103
+ role: string;
104
+ delegationId: string;
105
+ childId: string;
106
+ artifactPath: string;
107
+ upstreamArtifacts: string[];
108
+ auditQuestions: string[];
109
+ requiredOutput: string;
110
+ codeRefs?: string[];
111
+ changedFiles?: string[];
112
+ diffBasis?: ReviewBundleInput['diffBasis'];
113
+ policyPath?: string;
114
+ providers?: Record<string, SubagentProviderLike>;
115
+ subagents?: SubagentsRuntimeLike;
116
+ maxDepth?: number;
117
+ toolFilter?: unknown;
118
+ }): Promise<{
119
+ decision: RouteDecision;
120
+ probe: CapabilityProbe;
121
+ bundle: import("./review.ts").ReviewBundleResult;
122
+ handoffPath: string;
123
+ briefPath: string;
124
+ replyPath: string;
125
+ childScratchPath: string;
126
+ prompt: string;
127
+ request: SubagentStartRequestLike;
128
+ result: SubagentResultLike | null;
129
+ evaluation: {
130
+ accepted: boolean;
131
+ reason: string;
132
+ };
133
+ actionRecordPath: string;
134
+ error: string | null;
135
+ }>;
136
+ /** R6: validate a child's claimed references against actual files. */
137
+ validateReferences(root: string, references: Reference[]): import("./delegation.ts").ReferenceCheck;
138
+ /** R7: probe availability for a role and render the decision basis prose. */
139
+ probeDelegation(root: string, role: string, providers?: Record<string, SubagentProviderLike>): {
140
+ decision: RouteDecision;
141
+ probe: CapabilityProbe;
142
+ basis: string;
143
+ };
144
+ /**
145
+ * B3: per-call workspace root resolution. The control-plane root is the
146
+ * session's cwd (or registry-canonicalized), NEVER process.cwd() — the host
147
+ * checkout is not the run's workspace. Reads resolve under that root only.
148
+ */
149
+ resolveRootFor(agent?: {
150
+ session?: {
151
+ header?: {
152
+ cwd?: string;
153
+ };
154
+ };
155
+ } | null): Promise<string | null>;
156
+ /**
157
+ * Emit a recursive/* event into the session whose control-plane root matches
158
+ * `root` (additive log-only; the projection drives from these). Falls back to
159
+ * the agent's session when passed, else the session store list.
160
+ */
161
+ private emitToSession;
162
+ status(runId?: string, agent?: {
163
+ session?: {
164
+ header?: {
165
+ cwd?: string;
166
+ };
167
+ };
168
+ } | null): Promise<RecursiveStatusResult | null>;
169
+ /**
170
+ * Scaffold a run directory with stub artifact headers (no-op if exists).
171
+ * Returns the run dir + created artifacts.
172
+ */
173
+ initRun(runId: string, agent?: {
174
+ session?: {
175
+ header?: {
176
+ cwd?: string;
177
+ };
178
+ };
179
+ } | null): Promise<{
180
+ runDir: string;
181
+ runId: string;
182
+ created: string[];
183
+ existing: string[];
184
+ }>;
185
+ /**
186
+ * Lock a DRAFT artifact (or reopen a LOCKED one). Validates prerequisites;
187
+ * writes Status/LockedAt/LockHash + receipt. Returns the lock result.
188
+ */
189
+ lockArtifact(runId: string, artifact: string, reopen?: boolean, agent?: {
190
+ session?: {
191
+ header?: {
192
+ cwd?: string;
193
+ };
194
+ };
195
+ } | null): Promise<LockArtifactResult>;
196
+ /** Reopen a LOCKED artifact to DRAFT (delete LockedAt/LockHash, invalidate downstream receipts). */
197
+ private reopenArtifact;
198
+ /**
199
+ * Lint an artifact for phase-specific issues. Minimal structural checks:
200
+ * gates present, TODO section, Status field, LockHash consistency.
201
+ */
202
+ lintArtifact(runId: string, artifact?: string, agent?: {
203
+ session?: {
204
+ header?: {
205
+ cwd?: string;
206
+ };
207
+ };
208
+ } | null): Promise<LintArtifactResult>;
209
+ /** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
210
+ foldPhase(events: readonly SessionEventLike[]): RecursivePhaseState | null;
211
+ validateTransition(intent: PhaseTransitionIntent): GateCheckResult;
212
+ detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null;
213
+ /** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
214
+ gatePreStep(events: readonly SessionEventLike[], config?: EnforcementConfig): PreStepGateDecision;
215
+ /** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
216
+ guardTool(exec: ToolExecLike, root: string, runId: string, config?: EnforcementConfig): ToolGuardDecision;
217
+ /** Phase C R8: fs/observed tamper detection. */
218
+ detectTamper(targetPath: string, root: string, runId: string): {
219
+ runId: string;
220
+ path: string;
221
+ reason: string;
222
+ } | null;
223
+ /** Phase C R5: render the current-phase policy contract. */
224
+ renderPolicy(root: string, runId: string, folded: RecursivePhaseState | null): string;
225
+ /** Phase C R6: couple a gate-block to the goal service (graceful no-op). */
226
+ coupleGateBlockToGoal(goalService: unknown, agent: unknown, ref: unknown, reason: {
227
+ code: string;
228
+ message: string;
229
+ }): boolean;
230
+ /** Phase D R1: append a recursive/* event through the host session adapter (worktree-keyed). */
231
+ emitRecursiveEvent(session: unknown, type: string, data: Record<string, unknown>): void;
232
+ /** Phase D R2/R9: fold a session log into the worktree-grouped projection. */
233
+ foldRecursiveProjection(events: readonly RecursiveEventLike[], workspaceRoot?: string): RecursiveFoldState;
234
+ /** Phase D R2: the registered projection unit (key 'recursive'). */
235
+ get projectionUnit(): import("@deepseek-ai/dsh-session-projection").ProjectionDefinition<"recursive", RecursiveFoldState>;
236
+ /** Phase D R9: whether a worktreeRoot is inside the workspace control-plane root. */
237
+ isInsideWorkspace(worktreeRoot: string, workspaceRoot: string): boolean;
238
+ /** Phase C R7: resolve the enforcement config (strict|advisory, default advisory). */
239
+ get enforcementConfig(): EnforcementConfig;
240
+ setEnforcementConfig(config: unknown): EnforcementConfig;
241
+ }
@@ -0,0 +1,18 @@
1
+ export type ScratchTarget = 'md' | 'ts';
2
+ export declare function scratchPathFor(runDir: string, target: ScratchTarget): string;
3
+ export declare function readScratch(runDir: string, target: ScratchTarget): string;
4
+ export declare function writeScratch(runDir: string, target: ScratchTarget, content: string): string;
5
+ export declare function appendScratch(runDir: string, target: ScratchTarget, content: string): string;
6
+ /**
7
+ * Child-scoped scratch path (Phase B R5): <run-dir>/scratch/<child-id>.md.
8
+ * Resolved under the run dir; a child id escaping the scratch dir is rejected.
9
+ */
10
+ export declare function childScratchPath(runDir: string, childId: string): string;
11
+ /** Read-only access to the PARENT's scratch (the main agent's working memory). */
12
+ export declare function readParentScratch(runDir: string, target?: ScratchTarget): string;
13
+ /**
14
+ * Write ONLY the child's own scratch file. Refuses any target outside
15
+ * <run-dir>/scratch/<child-id>.md (a child cannot overwrite the parent's
16
+ * scratch.md through this writer).
17
+ */
18
+ export declare function writeChildScratch(runDir: string, childId: string, content: string): string;
@@ -0,0 +1,19 @@
1
+ import type { ArtifactState, PhaseDef, RecursiveStatusResult } from './types.ts';
2
+ export declare const RUN_ARTIFACT_SEQUENCE: string[];
3
+ export declare const PHASES: PhaseDef[];
4
+ export declare function escapeRegExp(value: string): string;
5
+ export declare function trimMdValue(value: string): string;
6
+ export declare function getMdFieldValue(content: string, fieldName: string): string | null;
7
+ export declare function getGateStatus(content: string, gateName: string): string;
8
+ export declare function getTodoStats(content: string): {
9
+ hasTodo: boolean;
10
+ total: number;
11
+ checked: number;
12
+ unchecked: number;
13
+ };
14
+ export declare function lockHashFromContent(content: string): string;
15
+ export declare function getWorkflowProfile(runDir: string): string;
16
+ export { getLatestRunDirectory, discoverRuns, resolveRunDir } from './run.ts';
17
+ export type { RunDiscoveryResult } from './run.ts';
18
+ export declare function getArtifactState(artifactPath: string, workflowProfile: string): ArtifactState;
19
+ export declare function foldRun(runDir: string, runId: string): RecursiveStatusResult;
package/lib/types.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ export interface PhaseDef {
2
+ key: string;
3
+ label: string;
4
+ file: string;
5
+ optional: boolean;
6
+ phaseName: string;
7
+ }
8
+ export interface ArtifactState {
9
+ exists: boolean;
10
+ status: string;
11
+ lockValid: boolean;
12
+ lockProblems: string[];
13
+ blockers: string[];
14
+ lockedAt: string | null;
15
+ storedHash: string | null;
16
+ actualHash: string | null;
17
+ coverage: string;
18
+ approval: string;
19
+ audit: string;
20
+ todoHasSection: boolean;
21
+ todoUnchecked: number;
22
+ }
23
+ export interface PhaseState {
24
+ key: string;
25
+ label: string;
26
+ file: string;
27
+ optional: boolean;
28
+ exists: boolean;
29
+ status: string;
30
+ lockValid: boolean;
31
+ lockProblems: string[];
32
+ blockers: string[];
33
+ }
34
+ export interface RecursiveStatusResult {
35
+ runId: string;
36
+ currentPhase: {
37
+ key: string;
38
+ label: string;
39
+ phaseName: string;
40
+ status: string;
41
+ } | null;
42
+ phases: PhaseState[];
43
+ workflowProfile: string;
44
+ }
45
+ /** Run-level durable state (mirrors lifecycle.ts RUN_STATES; single pure-type home for the wire). */
46
+ export type RecursiveRunState = 'new' | 'active' | 'paused' | 'blocked' | 'complete';
47
+ /** One locked-phase fact carried by a recursive/phase-locked event. */
48
+ export interface RecursivePhaseLock {
49
+ lockedAt: string;
50
+ lockHash: string;
51
+ }
52
+ /** One gate failure fact carried by a recursive/gate-blocked event. */
53
+ export interface RecursiveGateBlock {
54
+ failures: string[];
55
+ kind: string;
56
+ }
57
+ /** One tamper fact carried by a recursive/tamper event. */
58
+ export interface RecursiveTamper {
59
+ path: string;
60
+ reason: string;
61
+ }
62
+ /** One subagent activity fact (start or end). */
63
+ export interface RecursiveSubagent {
64
+ childId: string;
65
+ role: string;
66
+ provider: string;
67
+ status?: 'running' | 'done' | 'failed';
68
+ }
69
+ /** One phase row in a run card's folded phase chain. */
70
+ export interface RecursivePhaseRow {
71
+ phase: string;
72
+ status: string;
73
+ lockedAt?: string;
74
+ lockHash?: string;
75
+ }
76
+ /**
77
+ * The per-run wire card the recursive projection folds: the board, inspector,
78
+ * node, and strip all render from this whole value — never from the file tree.
79
+ */
80
+ export interface RecursiveRunCard {
81
+ runId: string;
82
+ worktreeRoot: string;
83
+ repo?: string;
84
+ template?: string;
85
+ /** Folded phase chain: last-wins per phase key. */
86
+ phases: Record<string, RecursivePhaseRow>;
87
+ /** Folded run-level state: last recursive/run-state wins (default 'active'). */
88
+ state: RecursiveRunState;
89
+ stateReason?: string;
90
+ /** Latest gate block (last-wins); absent when no block is in force. */
91
+ gateBlocked?: RecursiveGateBlock;
92
+ /** Tamper facts (accumulated, latest wins per path). */
93
+ tampers: Record<string, RecursiveTamper>;
94
+ /** Subagent activity (latest status wins per childId). */
95
+ subagents: Record<string, RecursiveSubagent>;
96
+ /** Set on recursive/run-merged; the run re-keys to this root. */
97
+ mergedToRepoRoot?: string;
98
+ }
99
+ /**
100
+ * The recursive projection whole value: runs grouped by worktree root first,
101
+ * then by runId. The board renders one lane per run; the client enumerates by
102
+ * worktree root so it never crosses the workspace boundary.
103
+ */
104
+ export type RecursiveProjection = Record<string, Record<string, RecursiveRunCard>>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Workspace-scoped control-plane root resolution (R1, binding invariant).
3
+ *
4
+ * The plugin NEVER scans `workspaceRegistry.list()` to find "the" workspace and
5
+ * NEVER reads another open workspace's `.recursive/` tree. The ONLY root-resolution
6
+ * path is: session canonical cwd (`agent.session.header.cwd`) ->
7
+ * `workspaceRegistry.resolveByPath(cwd)` -> the owning workspace's canonical `path`,
8
+ * which IS the control-plane root.
9
+ */
10
+ export interface WorkspaceLike {
11
+ readonly path: string;
12
+ readonly id: string;
13
+ }
14
+ export interface WorkspaceRegistryLike {
15
+ /** Resolve by canonical directory path without creating or mutating. */
16
+ resolveByPath(path: string): Promise<WorkspaceLike | undefined> | WorkspaceLike | undefined;
17
+ /** MUST NOT be used by this plugin (workspace-scoping invariant). */
18
+ list?(): unknown;
19
+ }
20
+ export interface WorkspaceResolver {
21
+ (cwd: string): Promise<string | null>;
22
+ }
23
+ /**
24
+ * Build a resolver bound to a workspace registry. Returns the canonical workspace
25
+ * path for the given cwd, or `null` when the cwd is not a registered workspace
26
+ * (no repo -> defer, never fail hard).
27
+ */
28
+ export declare function makeWorkspaceResolver(registry: WorkspaceRegistryLike): WorkspaceResolver;
29
+ /**
30
+ * Resolve the control-plane root for an agent-like object with a session header
31
+ * carrying a canonical `cwd`. Degrades to `null` when the registry or cwd is
32
+ * unavailable (callers defer, never fail hard).
33
+ */
34
+ export declare function resolveControlPlaneRoot(agent: {
35
+ session?: {
36
+ header?: {
37
+ cwd?: string;
38
+ };
39
+ };
40
+ } | null | undefined, registry?: WorkspaceRegistryLike | null, cwdFallback?: string): Promise<string | null>;
41
+ /**
42
+ * Extract the canonical session cwd (for callers that pass it explicitly).
43
+ */
44
+ export declare function sessionCwd(agent: {
45
+ session?: {
46
+ header?: {
47
+ cwd?: string;
48
+ };
49
+ };
50
+ } | null | undefined): string | null;
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "@try-works/dsh-recursive-mode",
3
+ "description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + recursive_status tool",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./types": {
14
+ "types": "./lib/types/types.d.ts",
15
+ "default": "./lib/types/types.js"
16
+ },
17
+ "./client": {
18
+ "types": "./lib/client.d.ts",
19
+ "default": "./lib/client.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./cordis.patch.yml": "./cordis.patch.yml",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "lib",
27
+ "cordis.patch.yml",
28
+ "src",
29
+ "preset",
30
+ "scripts"
31
+ ],
32
+ "license": "MIT",
33
+ "dsh": {
34
+ "bundle": {
35
+ "patch": "./cordis.patch.yml"
36
+ },
37
+ "client": {
38
+ "platform": "web",
39
+ "inject": [
40
+ "@deepseek-ai/dsh-client-runtime",
41
+ "@deepseek-ai/dsh-client-ui-slots",
42
+ "@deepseek-ai/dsh-client-ui-conversation",
43
+ "@deepseek-ai/dsh-client-ui-sidebar",
44
+ "@deepseek-ai/dsh-client-ui-settings",
45
+ "@deepseek-ai/dsh-api-remotes"
46
+ ]
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "@deepseek-ai/cordis": "^4.0.1",
51
+ "@deepseek-ai/dsh-tools": "0.1.0-rc.5",
52
+ "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
53
+ "@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
54
+ "@deepseek-ai/dsh-session": "0.1.0-rc.5",
55
+ "react": "^18.2.0"
56
+ },
57
+ "devDependencies": {
58
+ "@deepseek-ai/cordis": "file:D:/deepseek-harness/vendor/cordis",
59
+ "@deepseek-ai/cosmokit": "file:D:/deepseek-harness/vendor/cosmokit",
60
+ "@deepseek-ai/dsh-agent": "file:D:/deepseek-harness/packages/core/agent",
61
+ "@deepseek-ai/dsh-attachment": "file:D:/deepseek-harness/packages/attachment/attachment",
62
+ "@deepseek-ai/dsh-brand": "file:D:/deepseek-harness/packages/util/brand",
63
+ "@deepseek-ai/dsh-code-runtime": "file:D:/deepseek-harness/packages/code-runtime/code-runtime",
64
+ "@deepseek-ai/dsh-invariants": "file:D:/deepseek-harness/packages/runtime-diagnostics/invariants",
65
+ "@deepseek-ai/dsh-llm": "file:D:/deepseek-harness/packages/llm/llm",
66
+ "@deepseek-ai/dsh-scope": "file:D:/deepseek-harness/packages/core/scope",
67
+ "@deepseek-ai/dsh-session": "file:D:/deepseek-harness/packages/core/session",
68
+ "@deepseek-ai/dsh-session-projection": "file:D:/deepseek-harness/packages/session/session-projection",
69
+ "@deepseek-ai/dsh-system-prompt": "file:D:/deepseek-harness/packages/core/system-prompt",
70
+ "@deepseek-ai/dsh-timeout": "file:D:/deepseek-harness/packages/util/timeout",
71
+ "@deepseek-ai/dsh-tools": "file:D:/deepseek-harness/packages/core/tools",
72
+ "@deepseek-ai/dsh-user-approval": "file:D:/deepseek-harness/packages/interaction/user-approval",
73
+ "@deepseek-ai/schemastery": "file:D:/deepseek-harness/vendor/schemastery",
74
+ "@types/node": "^20.0.0",
75
+ "@types/react": "^18.2.0",
76
+ "@types/react-dom": "^18.2.0",
77
+ "react": "^18.2.0",
78
+ "tsdown": "^0.22.2",
79
+ "tsx": "^4.19.0",
80
+ "typescript": "^5.6.0",
81
+ "vitest": "^2.1.0",
82
+ "zod": "^4.4.3"
83
+ },
84
+ "scripts": {
85
+ "build": "tsc -p tsconfig.build.json && tsdown",
86
+ "bundle": "tsdown",
87
+ "test": "vitest run",
88
+ "typecheck": "tsc --noEmit"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "@deepseek-ai/dsh-session-projection": {
92
+ "optional": true
93
+ },
94
+ "@deepseek-ai/dsh-session": {
95
+ "optional": true
96
+ },
97
+ "react": {
98
+ "optional": true
99
+ },
100
+ "@deepseek-ai/dsh-client-runtime": {
101
+ "optional": true
102
+ },
103
+ "@deepseek-ai/dsh-client-ui-slots": {
104
+ "optional": true
105
+ },
106
+ "@deepseek-ai/dsh-client-ui-conversation": {
107
+ "optional": true
108
+ },
109
+ "@deepseek-ai/dsh-client-ui-sidebar": {
110
+ "optional": true
111
+ },
112
+ "@deepseek-ai/dsh-client-ui-settings": {
113
+ "optional": true
114
+ },
115
+ "@deepseek-ai/dsh-api-remotes": {
116
+ "optional": true
117
+ }
118
+ }
119
+ }