@try-works/dsh-recursive-mode 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ import type { WorkspaceRegistryLike } from './workspace.ts';
2
+ export interface FsPolicyIntent {
3
+ worktreeRoot: string;
4
+ runId: string;
5
+ /** The current phase doc file name (e.g. 03-implementation-summary.md), when determinable. */
6
+ currentPhaseFile: string | null;
7
+ }
8
+ /**
9
+ * Resolve the recursive policy intent for one agent from the FILESYSTEM.
10
+ * Returns null when no control-plane root or run is present (callers defer,
11
+ * never fail hard). The registry path is async-only in the harness, so when a
12
+ * registry is present this resolves the session cwd directly (B4: the session
13
+ * cwd IS the workspace root when unregistered; a registered workspace resolves
14
+ * to the same cwd via resolveControlPlaneRoot's sync fallback).
15
+ */
16
+ export declare function fsPolicyIntent(agent: {
17
+ session?: {
18
+ header?: {
19
+ cwd?: string;
20
+ };
21
+ };
22
+ } | null | undefined, registry?: WorkspaceRegistryLike | null, cwdFallback?: string): FsPolicyIntent | null;
23
+ /**
24
+ * Async variant for callers that CAN await (pre-step listener, routes): uses
25
+ * resolveControlPlaneRoot for registry canonicalization when a registry is
26
+ * present, falling back to the session cwd (B4).
27
+ */
28
+ export declare function fsPolicyIntentAsync(agent: {
29
+ session?: {
30
+ header?: {
31
+ cwd?: string;
32
+ };
33
+ };
34
+ } | null | undefined, registry?: WorkspaceRegistryLike | null, cwdFallback?: string): Promise<FsPolicyIntent | null>;
package/lib/index.d.ts CHANGED
@@ -37,4 +37,5 @@ export declare const inject: string[];
37
37
  */
38
38
  export declare function apply(ctx: Context, config?: {
39
39
  shellOnly?: boolean;
40
+ repoRoot?: string;
40
41
  }): void;
package/lib/index.js CHANGED
@@ -3444,6 +3444,42 @@ function registerRecursiveCommand(ctx, recursive) {
3444
3444
  });
3445
3445
  }
3446
3446
  //#endregion
3447
+ //#region src/fs-intent.ts
3448
+ /**
3449
+ * fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
3450
+ *
3451
+ * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
3452
+ * so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
3453
+ * prompt section rendered ''. This module derives the SAME intent from the
3454
+ * filesystem instead: session cwd -> control-plane root -> enumerate runs ->
3455
+ * latest run -> current phase (foldRun). Pure read-only fs folding, zero
3456
+ * emission — consistent with the per-workspace design + pre-step listener.
3457
+ */
3458
+ /**
3459
+ * Resolve the recursive policy intent for one agent from the FILESYSTEM.
3460
+ * Returns null when no control-plane root or run is present (callers defer,
3461
+ * never fail hard). The registry path is async-only in the harness, so when a
3462
+ * registry is present this resolves the session cwd directly (B4: the session
3463
+ * cwd IS the workspace root when unregistered; a registered workspace resolves
3464
+ * to the same cwd via resolveControlPlaneRoot's sync fallback).
3465
+ */
3466
+ function fsPolicyIntent(agent, registry, cwdFallback) {
3467
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null;
3468
+ if (!cwd) return null;
3469
+ const root = registry ? cwd : cwd;
3470
+ const runs = enumerateRuns(root);
3471
+ if (runs.length === 0) return null;
3472
+ const runId = runs[runs.length - 1];
3473
+ const runDir = join(root, ".recursive", "run", runId);
3474
+ if (!existsSync(runDir)) return null;
3475
+ const folded = foldRun(runDir, runId);
3476
+ return {
3477
+ worktreeRoot: root,
3478
+ runId,
3479
+ currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null
3480
+ };
3481
+ }
3482
+ //#endregion
3447
3483
  //#region src/snapshot.ts
3448
3484
  /** Resolve a run's lock facts for one artifact file (receipt first, status fallback). */
3449
3485
  function lockFactsOf(runDir, artifact) {
@@ -3682,7 +3718,7 @@ function apply(ctx, config) {
3682
3718
  ctx.effect(function* () {
3683
3719
  const workspaceRegistry = ctx.get("workspaceRegistry");
3684
3720
  const recursive = new RecursiveRuntime(ctx, {
3685
- repoRoot: process.cwd(),
3721
+ repoRoot: config?.repoRoot ?? process.cwd(),
3686
3722
  workspaceRegistry
3687
3723
  });
3688
3724
  const disposers = [
@@ -3702,14 +3738,11 @@ function apply(ctx, config) {
3702
3738
  text: (context) => {
3703
3739
  const agent = context?.agent;
3704
3740
  if (!agent) return "";
3705
- if (!(agent.session?.header?.cwd ?? "")) return "";
3706
- const events = agent.session?.events ?? [];
3707
- const intent = detectTransitionIntent(events);
3708
- if (!intent || intent.worktreeRoot === "") return "";
3741
+ const intent = fsPolicyIntent(agent, workspaceRegistry);
3742
+ if (!intent) return "";
3709
3743
  return renderRecursivePolicy({
3710
3744
  worktreeRoot: intent.worktreeRoot,
3711
3745
  runId: intent.runId,
3712
- folded: recursive.foldPhase(events),
3713
3746
  config: recursive.enforcementConfig
3714
3747
  });
3715
3748
  }
package/lib/policy.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { EnforcementConfig } from './enforcement.ts';
3
3
  export interface PolicyContext {
4
4
  worktreeRoot: string;
5
5
  runId: string;
6
- folded: RecursivePhaseState | null;
6
+ folded?: RecursivePhaseState | null;
7
7
  config?: EnforcementConfig;
8
8
  }
9
9
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@try-works/dsh-recursive-mode",
3
3
  "description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + recursive_status tool",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -0,0 +1,84 @@
1
+ /**
2
+ * fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
3
+ *
4
+ * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
5
+ * so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
6
+ * prompt section rendered ''. This module derives the SAME intent from the
7
+ * filesystem instead: session cwd -> control-plane root -> enumerate runs ->
8
+ * latest run -> current phase (foldRun). Pure read-only fs folding, zero
9
+ * emission — consistent with the per-workspace design + pre-step listener.
10
+ */
11
+ import { existsSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+ import { enumerateRuns } from './bootstrap.ts'
14
+ import { foldRun } from './status.ts'
15
+ import type { WorkspaceRegistryLike } from './workspace.ts'
16
+ import { resolveControlPlaneRoot } from './workspace.ts'
17
+
18
+ export interface FsPolicyIntent {
19
+ worktreeRoot: string
20
+ runId: string
21
+ /** The current phase doc file name (e.g. 03-implementation-summary.md), when determinable. */
22
+ currentPhaseFile: string | null
23
+ }
24
+
25
+ /**
26
+ * Resolve the recursive policy intent for one agent from the FILESYSTEM.
27
+ * Returns null when no control-plane root or run is present (callers defer,
28
+ * never fail hard). The registry path is async-only in the harness, so when a
29
+ * registry is present this resolves the session cwd directly (B4: the session
30
+ * cwd IS the workspace root when unregistered; a registered workspace resolves
31
+ * to the same cwd via resolveControlPlaneRoot's sync fallback).
32
+ */
33
+ export function fsPolicyIntent(
34
+ agent: { session?: { header?: { cwd?: string } } } | null | undefined,
35
+ registry?: WorkspaceRegistryLike | null,
36
+ cwdFallback?: string,
37
+ ): FsPolicyIntent | null {
38
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
39
+ if (!cwd) return null
40
+ // B4 sync path: when the registry is absent the session cwd IS the root;
41
+ // when present, resolveControlPlaneRoot canonicalizes through the registry
42
+ // (async). For the sync prompt-section callback we take the B4 shortcut: the
43
+ // session cwd is authoritative per the workspace-scoping invariant. This is
44
+ // exactly what the pre-step listener's resolveRootForRoute falls back to.
45
+ const root = registry ? cwd : cwd
46
+ const runs = enumerateRuns(root)
47
+ if (runs.length === 0) return null
48
+ const runId = runs[runs.length - 1]
49
+ const runDir = join(root, '.recursive', 'run', runId)
50
+ if (!existsSync(runDir)) return null
51
+ const folded = foldRun(runDir, runId)
52
+ return {
53
+ worktreeRoot: root,
54
+ runId,
55
+ currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Async variant for callers that CAN await (pre-step listener, routes): uses
61
+ * resolveControlPlaneRoot for registry canonicalization when a registry is
62
+ * present, falling back to the session cwd (B4).
63
+ */
64
+ export async function fsPolicyIntentAsync(
65
+ agent: { session?: { header?: { cwd?: string } } } | null | undefined,
66
+ registry?: WorkspaceRegistryLike | null,
67
+ cwdFallback?: string,
68
+ ): Promise<FsPolicyIntent | null> {
69
+ const cwd = agent?.session?.header?.cwd ?? cwdFallback ?? null
70
+ if (!cwd) return null
71
+ const root = await resolveControlPlaneRoot(agent, registry, cwdFallback)
72
+ if (!root) return null
73
+ const runs = enumerateRuns(root)
74
+ if (runs.length === 0) return null
75
+ const runId = runs[runs.length - 1]
76
+ const runDir = join(root, '.recursive', 'run', runId)
77
+ if (!existsSync(runDir)) return null
78
+ const folded = foldRun(runDir, runId)
79
+ return {
80
+ worktreeRoot: root,
81
+ runId,
82
+ currentPhaseFile: folded.currentPhase ? folded.currentPhase.phaseName : null,
83
+ }
84
+ }
package/src/index.ts CHANGED
@@ -11,8 +11,8 @@ import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
11
11
  import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
12
12
  import { registerRecursiveCommand } from './commands.ts'
13
13
  import { evaluateToolGuard } from './enforcement.ts'
14
- import { detectTransitionIntent } from './lifecycle.ts'
15
14
  import { renderRecursivePolicy } from './policy.ts'
15
+ import { fsPolicyIntent } from './fs-intent.ts'
16
16
  import { snapshotWorkspace } from './snapshot.ts'
17
17
  import { mountRecursiveRoutesOnce, makeRecursiveRoutes, type RecursiveRouteHost } from './live-route.ts'
18
18
  import { enumerateRuns } from './bootstrap.ts'
@@ -83,7 +83,7 @@ export const inject = ['tools']
83
83
  * repo/run work and NO session-event emission here: zero recursive/* events
84
84
  * are ever appended (resume-crash fix), and the board reads the live fs route.
85
85
  */
86
- export function apply(ctx: Context, config?: { shellOnly?: boolean }) {
86
+ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: string }) {
87
87
  // R4 shell split (02-to-be-plan.addendum-r4-r2-mount-resolution.md): the
88
88
  // global bare-name row in cordis.patch.yml mounts with config.shellOnly=true
89
89
  // to expose ONLY the client bundle for client discovery. It must register
@@ -97,7 +97,7 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean }) {
97
97
  // property access requires inject and would fail boot when undeclared.
98
98
  // Resolve the control-plane root strictly from the session agent's cwd.
99
99
  const workspaceRegistry = ctx.get('workspaceRegistry') as never
100
- const recursive = new RecursiveRuntime(ctx, { repoRoot: process.cwd(), workspaceRegistry })
100
+ const recursive = new RecursiveRuntime(ctx, { repoRoot: config?.repoRoot ?? process.cwd(), workspaceRegistry })
101
101
 
102
102
  const disposers = [
103
103
  ctx.tools.register(createRecursiveStatusTool(recursive)),
@@ -122,14 +122,16 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean }) {
122
122
  name: 'recursive:policy',
123
123
  order: 55,
124
124
  text: (context: unknown) => {
125
- const agent = (context as { agent?: { session?: { header?: { cwd?: string }; events?: { type: string; data?: Record<string, unknown> }[] } } } | undefined)?.agent
125
+ const agent = (context as { agent?: { session?: { header?: { cwd?: string } } } } | undefined)?.agent
126
126
  if (!agent) return ''
127
- const cwd = agent.session?.header?.cwd ?? ''
128
- if (!cwd) return ''
129
- const events = agent.session?.events ?? []
130
- const intent = detectTransitionIntent(events as never)
131
- if (!intent || intent.worktreeRoot === '') return ''
132
- return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, folded: recursive.foldPhase(events as never), config: recursive.enforcementConfig })
127
+ // SP3 R5 policy-render fix: derive intent from the FILESYSTEM, not the
128
+ // retired recursive/phase-intent session event (zero-emission removed
129
+ // the emitter, so detectTransitionIntent was ALWAYS null and this
130
+ // section rendered ''). Pure read-only fs folding; no recursive/*
131
+ // events are appended.
132
+ const intent = fsPolicyIntent(agent, workspaceRegistry as never)
133
+ if (!intent) return ''
134
+ return renderRecursivePolicy({ worktreeRoot: intent.worktreeRoot, runId: intent.runId, config: recursive.enforcementConfig })
133
135
  },
134
136
  }))
135
137
  }
package/src/policy.ts CHANGED
@@ -18,7 +18,7 @@ import { getArtifactRequiredSections, AUDITED_PHASE_FILES, CURRENT_WORKFLOW_PROF
18
18
  export interface PolicyContext {
19
19
  worktreeRoot: string
20
20
  runId: string
21
- folded: RecursivePhaseState | null
21
+ folded?: RecursivePhaseState | null
22
22
  config?: EnforcementConfig
23
23
  }
24
24