@try-works/dsh-recursive-mode 0.2.1 → 0.2.3

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.
@@ -9,8 +9,14 @@ export interface BoardProps {
9
9
  workspacePath?: string;
10
10
  onOpenInspector?: (selection: BoardSelection) => void;
11
11
  onClose?: () => void;
12
+ /**
13
+ * 'overlay' (default) renders the board as a full-viewport fixed overlay
14
+ * (shell.overlay / launcher). 'view' renders it inline, filling its parent
15
+ * (a conversation.view tab), with no close button and no fixed positioning.
16
+ */
17
+ variant?: 'overlay' | 'view';
12
18
  }
13
- export declare function Board({ snapshot, workspacePath, onOpenInspector, onClose }: BoardProps): import("react").DetailedReactHTMLElement<{
19
+ export declare function Board({ snapshot, workspacePath, onOpenInspector, onClose, variant }: BoardProps): import("react").DetailedReactHTMLElement<{
14
20
  className: string;
15
21
  'data-theme': import("./theme.ts").BoardTheme;
16
22
  }, HTMLElement> | null;
@@ -14,6 +14,7 @@
14
14
  import type { ClientContext } from './contract.ts';
15
15
  export { Board, listRuns } from './board.tsx';
16
16
  export { Inspector } from './inspector.tsx';
17
+ export { RecursiveView } from './slots.ts';
17
18
  export { RecursiveSettings } from './settings.tsx';
18
19
  export { useLiveProjection } from './use-live.ts';
19
20
  export type { LiveProjectionSnapshot } from './use-live.ts';
@@ -41,6 +41,16 @@ export declare function useRecursiveSessions(useSessions: SnapshotSelectorHook<S
41
41
  export declare function RecursiveLauncherGate({ useSessions }: {
42
42
  useSessions: SnapshotSelectorHook<SessionListStateLike>;
43
43
  }): ReactNode;
44
+ /**
45
+ * RecursiveView: the conversation.view tab body. Renders the run board INLINE
46
+ * (fills the view area) keyed on the CURRENT workspace, and swaps to the
47
+ * inspector modal when a run is opened. Read-only (R9): uses the live host
48
+ * route. No preset gate — the tab is discoverable in any session.
49
+ */
50
+ export declare function RecursiveView({ useSessions, useWorkspaces }: {
51
+ useSessions: SnapshotSelectorHook<SessionListStateLike>;
52
+ useWorkspaces: SnapshotSelectorHook<WorkspaceListStateLike>;
53
+ }): ReactNode;
44
54
  export declare function registerSlots(ctx: ClientContext): () => void;
45
55
  /**
46
56
  * Board overlay: subscribes to the shared board store, gates on open + recursive
package/lib/client.js CHANGED
@@ -335,8 +335,8 @@ window.__ModuleLoader__.load({
335
335
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
336
336
  return runs;
337
337
  }
338
- function closeButton$1(onClose) {
339
- if (onClose === void 0) return null;
338
+ function closeButton$1(onClose, variant) {
339
+ if (onClose === void 0 || variant === "view") return null;
340
340
  return (0, react.createElement)("button", {
341
341
  type: "button",
342
342
  className: "rec-close",
@@ -356,7 +356,7 @@ window.__ModuleLoader__.load({
356
356
  "data-pill": kind
357
357
  }, PILL_LABELS[kind]);
358
358
  }
359
- function Board({ snapshot, workspacePath, onOpenInspector, onClose }) {
359
+ function Board({ snapshot, workspacePath, onOpenInspector, onClose, variant = "overlay" }) {
360
360
  const { theme, toggle } = useBoardTheme();
361
361
  if (snapshot === null || snapshot.root === null) return null;
362
362
  const runs = workspacePath !== void 0 && workspacePath !== "" && !sameWorkspacePath(workspacePath, snapshot.root) ? [] : listRuns(snapshot.projection);
@@ -365,21 +365,22 @@ window.__ModuleLoader__.load({
365
365
  worktreeRoot: run.worktreeRoot,
366
366
  runId: run.runId
367
367
  });
368
+ const rootCls = variant === "view" ? "rec-board rec-board-view" : "rec-board";
368
369
  if (runs.length === 0) return (0, react.createElement)("div", {
369
- className: "rec-board",
370
+ className: rootCls,
370
371
  "data-theme": theme,
371
372
  "data-empty": true
372
373
  }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, "0 runs"), (0, react.createElement)(ThemeToggle, {
373
374
  theme,
374
375
  toggle
375
- }), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
376
+ }), closeButton$1(onClose, variant)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
376
377
  return (0, react.createElement)("div", {
377
- className: "rec-board",
378
+ className: rootCls,
378
379
  "data-theme": theme
379
380
  }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, runs.length + " runs"), (0, react.createElement)(ThemeToggle, {
380
381
  theme,
381
382
  toggle
382
- }), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
383
+ }), closeButton$1(onClose, variant)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
383
384
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
384
385
  return (0, react.createElement)("section", {
385
386
  key: lane.id,
@@ -781,6 +782,17 @@ window.__ModuleLoader__.load({
781
782
  overflow: hidden;
782
783
  }
783
784
 
785
+ /* Inline board (conversation.view tab): fills its parent view area, not fixed. */
786
+ .rec-board-view {
787
+ position: relative;
788
+ inset: auto;
789
+ z-index: auto;
790
+ width: 100%;
791
+ height: 100%;
792
+ min-height: 0;
793
+ flex: 1;
794
+ }
795
+
784
796
  .rec-board-header {
785
797
  display: flex;
786
798
  align-items: center;
@@ -1257,9 +1269,51 @@ window.__ModuleLoader__.load({
1257
1269
  onClick: () => boardState.openBoard()
1258
1270
  }, "⧉");
1259
1271
  }
1272
+ /**
1273
+ * RecursiveView: the conversation.view tab body. Renders the run board INLINE
1274
+ * (fills the view area) keyed on the CURRENT workspace, and swaps to the
1275
+ * inspector modal when a run is opened. Read-only (R9): uses the live host
1276
+ * route. No preset gate — the tab is discoverable in any session.
1277
+ */
1278
+ function RecursiveView({ useSessions, useWorkspaces }) {
1279
+ const board = useBoardState();
1280
+ const sessions = useRecursiveSessions(useSessions);
1281
+ const wsPath = currentWorkspacePath(useWorkspaces((s) => s) ?? {
1282
+ items: [],
1283
+ recentWorkspaceId: void 0
1284
+ }, sessions);
1285
+ const snapshot = useLiveProjection({ cwd: wsPath });
1286
+ if (board.selection !== null) return (0, react.createElement)(Inspector, {
1287
+ runId: board.selection.runId,
1288
+ worktreeRoot: board.selection.worktreeRoot,
1289
+ snapshot,
1290
+ onBackToToolDetails: () => boardState.backToBoard(),
1291
+ onClose: () => boardState.close()
1292
+ });
1293
+ return (0, react.createElement)(Board, {
1294
+ snapshot,
1295
+ workspacePath: wsPath,
1296
+ variant: "view",
1297
+ onOpenInspector: (sel) => boardState.openInspector(sel)
1298
+ });
1299
+ }
1260
1300
  function registerSlots(ctx) {
1261
1301
  const disposers = [];
1262
1302
  disposers.push(injectBoardStyles());
1303
+ disposers.push(ctx.slots.inject("conversation.view", () => ctx.slots.register({
1304
+ name: "conversation.view",
1305
+ id: "recursive",
1306
+ order: 20,
1307
+ label: "Recursive"
1308
+ }, (props) => {
1309
+ const useSessions = props?.useSessions;
1310
+ const useWorkspaces = props?.useWorkspaces;
1311
+ if (useSessions === void 0 || useWorkspaces === void 0) return null;
1312
+ return (0, react.createElement)(RecursiveView, {
1313
+ useSessions,
1314
+ useWorkspaces
1315
+ });
1316
+ })));
1263
1317
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1264
1318
  name: "sidebar.footer.action",
1265
1319
  id: "recursive",
@@ -1355,6 +1409,7 @@ window.__ModuleLoader__.load({
1355
1409
  exports.Board = Board;
1356
1410
  exports.Inspector = Inspector;
1357
1411
  exports.RecursiveSettings = RecursiveSettings;
1412
+ exports.RecursiveView = RecursiveView;
1358
1413
  exports.apply = apply;
1359
1414
  exports.currentSessionCwd = currentSessionCwd;
1360
1415
  exports.currentWorkspacePath = currentWorkspacePath;
@@ -1,4 +1,3 @@
1
- import { type SessionEventLike } from './lifecycle.ts';
2
1
  export type EnforcementMode = 'strict' | 'advisory';
3
2
  export interface EnforcementConfig {
4
3
  preStep: EnforcementMode;
@@ -8,19 +7,6 @@ export interface EnforcementConfig {
8
7
  /** Validate the enforcement config shape (unknown keys fail at plugin load). */
9
8
  export declare function resolveEnforcementConfig(config: unknown): EnforcementConfig;
10
9
  export declare const DEFAULT_ENFORCEMENT: EnforcementConfig;
11
- /**
12
- * Layer 1 - agent/pre-step phase-transition gate decision.
13
- * Reads on TRANSITION INTENT ONLY (13.5): no transition intent means the step
14
- * passes through untouched. On a transition intent whose gates fail:
15
- * - strict -> reject (turn ends blocked, no model call spent)
16
- * - advisory -> enter (warn only; a recursive/gate-blocked event is emitted)
17
- */
18
- export interface PreStepGateDecision {
19
- kind: 'reject' | 'enter';
20
- gateBlocked: boolean;
21
- failures: string[];
22
- }
23
- export declare function evaluatePreStepGate(events: readonly SessionEventLike[], mode?: EnforcementMode): PreStepGateDecision;
24
10
  /**
25
11
  * Layer 2 - tools/pre-execute guard decision.
26
12
  * Pure predicate: inspects the pending tool execution (name + args) against
package/lib/index.js CHANGED
@@ -4496,16 +4496,15 @@ function evaluateDelegationResult(result) {
4496
4496
  //#endregion
4497
4497
  //#region src/lifecycle.ts
4498
4498
  /**
4499
- * Run state machine + serialized transition driver + recursive/* events
4500
- * (Phase C R1/R2/R6, PROPOSAL 8.8).
4499
+ * Transition gate validation + goal coupling (Phase C R1/R2/R6, PROPOSAL 8.4).
4501
4500
  *
4502
- * Authority is TRANSITIONS AND EVENTS ONLY - this module never stores run
4503
- * state in a second place. It reconciles the file tree via the existing
4504
- * read path (run.ts/status.ts/lock.ts), validates the target phase's gates
4505
- * (PROPOSAL 8.4), delegates the artifact write to lock.ts (canonical
4506
- * lock-hash + monotonic chain preserved), and emits the recursive/* events.
4507
- * State is DERIVED on every transition; resume/fork/session-restart
4508
- * reconstruct identical state by re-reading files + folding the session log.
4501
+ * Live BUG dsh-v0.1.1-rc.2 compatibility (0.2.2): the legacy session-event fold
4502
+ * surface (foldRecursivePhase / detectTransitionIntent / hasOpenTurn /
4503
+ * LifecycleDriver / the recursive/* event payload interfaces) was REMOVED.
4504
+ * The plugin is zero-emission: no recursive/* session event is ever appended
4505
+ * or emitted, so nothing folds them. What remains is the pure transition gate
4506
+ * check (validateTransition reads the file tree + lock chain, writes nothing)
4507
+ * plus the shared intent/result types and the goal-coupling no-op helper.
4509
4508
  */
4510
4509
  /** Run-level durable states (PROPOSAL 8.8). */
4511
4510
  const RUN_STATES = [
@@ -4528,66 +4527,6 @@ const AUDITED_PHASE_FILES = /* @__PURE__ */ new Set([
4528
4527
  "08-memory-impact.md"
4529
4528
  ]);
4530
4529
  /**
4531
- * Pure last-wins fold over the recursive/* events (the foldPlanMode pattern).
4532
- * A log with no recursive/phase folds to null; the last recursive/run-state
4533
- * wins for the run-level flag.
4534
- */
4535
- function foldRecursivePhase(events, end = events.length) {
4536
- let runId = "";
4537
- let phaseName = "";
4538
- let status = "";
4539
- let runState = "active";
4540
- let seen = false;
4541
- let index = 0;
4542
- for (const event of events) {
4543
- if (index >= end) break;
4544
- index++;
4545
- if (event.type === "recursive/phase") {
4546
- const d = event.data ?? {};
4547
- runId = String(d.runId ?? runId);
4548
- phaseName = String(d.phase ?? phaseName);
4549
- status = String(d.status ?? status);
4550
- seen = true;
4551
- } else if (event.type === "recursive/run-state") {
4552
- const d = event.data ?? {};
4553
- runId = String(d.runId ?? runId);
4554
- runState = asRunState(d.state);
4555
- seen = true;
4556
- }
4557
- }
4558
- return seen ? {
4559
- runId,
4560
- phase: phaseName,
4561
- status,
4562
- runState
4563
- } : null;
4564
- }
4565
- function asRunState(value) {
4566
- return RUN_STATES.includes(value) ? value : "active";
4567
- }
4568
- /** Whether the session log holds an opened turn without its closing turn/end. */
4569
- function hasOpenTurn(events) {
4570
- let open = false;
4571
- for (const event of events) if (event.type === "turn/start") open = true;
4572
- else if (event.type === "turn/end") open = false;
4573
- return open;
4574
- }
4575
- /** Detect a pending transition intent from the session log (the lock tool logs it). */
4576
- function detectTransitionIntent(events) {
4577
- let intent = null;
4578
- for (const event of events) if (event.type === "recursive/phase-intent") {
4579
- const d = event.data ?? {};
4580
- intent = {
4581
- runId: String(d.runId ?? ""),
4582
- worktreeRoot: String(d.worktreeRoot ?? ""),
4583
- targetArtifact: String(d.targetArtifact ?? ""),
4584
- kind: d.kind === "reopen" || d.kind === "advance" ? d.kind : "lock",
4585
- evidence: d.evidence
4586
- };
4587
- }
4588
- return intent;
4589
- }
4590
- /**
4591
4530
  * Validate a proposed transition against the target phase's gates (PROPOSAL 8.4).
4592
4531
  * Pure: reads the current file tree + lock.ts chain; writes nothing.
4593
4532
  */
@@ -4641,20 +4580,6 @@ function validateTransition(intent) {
4641
4580
  };
4642
4581
  }
4643
4582
  /**
4644
- * A serialized per-run transition driver (coalesced - the single-reservation
4645
- * pattern). Two concurrent 'lock Phase 3' intents queue; the second observes
4646
- * the first's committed state instead of racing the write.
4647
- */
4648
- var LifecycleDriver = class {
4649
- drivers = /* @__PURE__ */ new Map();
4650
- /** Run one transition serially per runId. */
4651
- serialize(runId, run) {
4652
- const next = (this.drivers.get(runId) ?? Promise.resolve()).then(run, run);
4653
- this.drivers.set(runId, next.then(() => void 0, () => void 0));
4654
- return next;
4655
- }
4656
- };
4657
- /**
4658
4583
  * Couple a gate-block to the goal service (PROPOSAL 8.4 goal integration).
4659
4584
  * Graceful no-op when the goal service or agent is unavailable.
4660
4585
  */
@@ -4669,9 +4594,8 @@ function coupleGateBlockToGoal(goalService, agent, ref, reason) {
4669
4594
  * Enforcement config + pre-step gate + tool guards + tamper detection
4670
4595
  * (Phase C R3/R4/R7/R8, PROPOSAL 8.4/8.6/13.5).
4671
4596
  *
4672
- * Layers 1 and 2 are CALLERS of the lifecycle transition set - they never
4673
- * reimplement the predicates. Configurable strict|advisory per gate
4674
- * (default advisory).
4597
+ * Layer 2 (tool guards) and Layer 8 (tamper) are the remaining enforcement
4598
+ * layers. Configurable strict|advisory per gate (default advisory).
4675
4599
  */
4676
4600
  /** Validate the enforcement config shape (unknown keys fail at plugin load). */
4677
4601
  function resolveEnforcementConfig(config) {
@@ -4694,25 +4618,6 @@ const DEFAULT_ENFORCEMENT = {
4694
4618
  toolGuards: "advisory",
4695
4619
  tamper: "advisory"
4696
4620
  };
4697
- function evaluatePreStepGate(events, mode = "advisory") {
4698
- const intent = detectTransitionIntent(events);
4699
- if (!intent || intent.worktreeRoot === "" || intent.runId === "") return {
4700
- kind: "enter",
4701
- gateBlocked: false,
4702
- failures: []
4703
- };
4704
- const check = validateTransition(intent);
4705
- if (check.passed) return {
4706
- kind: "enter",
4707
- gateBlocked: false,
4708
- failures: []
4709
- };
4710
- return {
4711
- kind: mode === "strict" ? "reject" : "enter",
4712
- gateBlocked: true,
4713
- failures: check.failures
4714
- };
4715
- }
4716
4621
  /** Tool names the locked-artifact write guard treats as write operations. */
4717
4622
  const WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
4718
4623
  "write",
@@ -5653,21 +5558,6 @@ var RecursiveRuntime = class extends Service {
5653
5558
  passed: errors.length === 0
5654
5559
  };
5655
5560
  }
5656
- /** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
5657
- foldPhase(events) {
5658
- return foldRecursivePhase(events);
5659
- }
5660
- validateTransition(intent) {
5661
- return validateTransition(intent);
5662
- }
5663
- detectTransitionIntent(events) {
5664
- return detectTransitionIntent(events);
5665
- }
5666
- /** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
5667
- gatePreStep(events, config) {
5668
- const mode = (config ?? this.enforcementConfig).preStep;
5669
- return evaluatePreStepGate(events, mode);
5670
- }
5671
5561
  /** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
5672
5562
  guardTool(exec, root, runId, config) {
5673
5563
  const mode = (config ?? this.enforcementConfig).toolGuards;
@@ -6648,8 +6538,8 @@ function registerRecursiveCommand(ctx, recursive) {
6648
6538
  /**
6649
6539
  * fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
6650
6540
  *
6651
- * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
6652
- * so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
6541
+ * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter
6542
+ * (and 0.2.2 removed the event-fold helper that read it), so the recursive:policy
6653
6543
  * prompt section rendered ''. This module derives the SAME intent from the
6654
6544
  * filesystem instead: session cwd -> control-plane root -> enumerate runs ->
6655
6545
  * latest run -> current phase (foldRun). Pure read-only fs folding, zero
@@ -7038,4 +6928,4 @@ function apply(ctx, config) {
7038
6928
  });
7039
6929
  }
7040
6930
  //#endregion
7041
- export { DEFAULT_ENFORCEMENT, LifecycleDriver, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, detectTransitionIntent, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluatePreStepGate, evaluateToolGuard, foldRecursivePhase, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, hasOpenTurn, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
6931
+ export { DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
@@ -15,86 +15,23 @@ export interface PhaseTransitionIntent {
15
15
  qaSignOff?: boolean;
16
16
  };
17
17
  }
18
- /** Folded phase state (last-wins over recursive/phase + recursive/run-state). */
18
+ /** Folded phase state (run key + current phase + current run-level state). */
19
19
  export interface RecursivePhaseState {
20
20
  runId: string;
21
21
  phase: string;
22
22
  status: string;
23
23
  runState: RunState;
24
24
  }
25
- /** A session-event-like carrier (the pure fold reads events by shape). */
26
- export interface SessionEventLike {
27
- type: string;
28
- data?: Record<string, unknown>;
29
- }
30
25
  /** Gate check result - the transition set's single output. */
31
26
  export interface GateCheckResult {
32
27
  passed: boolean;
33
28
  failures: string[];
34
29
  }
35
- /**
36
- * Payload shapes for the recursive/* events. These are LEGACY structural
37
- * views kept for internal consumers; the single source of truth for the
38
- * emitted payloads is events.ts (every event carries { runId, worktreeRoot }).
39
- * Aligned here so no local interface drifts out of the worktree-keyed
40
- * invariant (B7).
41
- */
42
- export interface RecursivePhaseEvent {
43
- runId: string;
44
- worktreeRoot: string;
45
- phase: string;
46
- status: string;
47
- }
48
- export interface RecursiveRunStateEvent {
49
- runId: string;
50
- worktreeRoot: string;
51
- state: RunState;
52
- reason?: string;
53
- }
54
- export interface RecursiveGateBlockedEvent {
55
- runId: string;
56
- worktreeRoot: string;
57
- phase: string;
58
- failures: string[];
59
- kind: string;
60
- }
61
- export interface RecursiveTamperEvent {
62
- runId: string;
63
- worktreeRoot: string;
64
- path: string;
65
- reason: string;
66
- }
67
- export interface RecursiveTransitionFailedEvent {
68
- runId: string;
69
- worktreeRoot: string;
70
- phase: string;
71
- error: string;
72
- }
73
- /**
74
- * Pure last-wins fold over the recursive/* events (the foldPlanMode pattern).
75
- * A log with no recursive/phase folds to null; the last recursive/run-state
76
- * wins for the run-level flag.
77
- */
78
- export declare function foldRecursivePhase(events: readonly SessionEventLike[], end?: number): RecursivePhaseState | null;
79
- /** Whether the session log holds an opened turn without its closing turn/end. */
80
- export declare function hasOpenTurn(events: readonly SessionEventLike[]): boolean;
81
- /** Detect a pending transition intent from the session log (the lock tool logs it). */
82
- export declare function detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null;
83
30
  /**
84
31
  * Validate a proposed transition against the target phase's gates (PROPOSAL 8.4).
85
32
  * Pure: reads the current file tree + lock.ts chain; writes nothing.
86
33
  */
87
34
  export declare function validateTransition(intent: PhaseTransitionIntent): GateCheckResult;
88
- /**
89
- * A serialized per-run transition driver (coalesced - the single-reservation
90
- * pattern). Two concurrent 'lock Phase 3' intents queue; the second observes
91
- * the first's committed state instead of racing the write.
92
- */
93
- export declare class LifecycleDriver {
94
- private readonly drivers;
95
- /** Run one transition serially per runId. */
96
- serialize(runId: string, run: () => Promise<void>): Promise<void>;
97
- }
98
35
  /**
99
36
  * Couple a gate-block to the goal service (PROPOSAL 8.4 goal integration).
100
37
  * Graceful no-op when the goal service or agent is unavailable.
package/lib/runtime.d.ts CHANGED
@@ -6,8 +6,8 @@ import { type ScratchTarget } from './scratch.ts';
6
6
  import { type ReviewBundleInput } from './review.ts';
7
7
  import { type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
8
8
  import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference } from './delegation.ts';
9
- import { type PhaseTransitionIntent, type SessionEventLike, type RecursivePhaseState, type GateCheckResult } from './lifecycle.ts';
10
- import { type EnforcementConfig, type PreStepGateDecision, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
9
+ import { type RecursivePhaseState } from './lifecycle.ts';
10
+ import { type EnforcementConfig, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
11
11
  import { type CreateWorktreeResult, type PromoteBranchResult } from './worktree.ts';
12
12
  declare module '@deepseek-ai/cordis' {
13
13
  interface Context {
@@ -273,12 +273,6 @@ export declare class RecursiveRuntime extends Service {
273
273
  };
274
274
  };
275
275
  } | null): Promise<LintArtifactResult>;
276
- /** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
277
- foldPhase(events: readonly SessionEventLike[]): RecursivePhaseState | null;
278
- validateTransition(intent: PhaseTransitionIntent): GateCheckResult;
279
- detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null;
280
- /** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
281
- gatePreStep(events: readonly SessionEventLike[], config?: EnforcementConfig): PreStepGateDecision;
282
276
  /** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
283
277
  guardTool(exec: ToolExecLike, root: string, runId: string, config?: EnforcementConfig): ToolGuardDecision;
284
278
  /** Phase C R8: fs/observed tamper detection. */
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.2.1",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -45,10 +45,10 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@deepseek-ai/cordis": "^4.0.1",
48
- "@deepseek-ai/dsh-session": "0.1.0-rc.5",
49
- "@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
50
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
51
- "@deepseek-ai/dsh-tools": "0.1.0-rc.5",
48
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
49
+ "@deepseek-ai/dsh-session-projection": "0.1.1-rc.2",
50
+ "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
51
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
52
52
  "react": "^18.2.0"
53
53
  },
54
54
  "devDependencies": {
@@ -84,7 +84,8 @@
84
84
  "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
85
85
  "bundle": "tsdown",
86
86
  "test": "vitest run",
87
- "typecheck": "tsc --noEmit"
87
+ "typecheck": "tsc --noEmit",
88
+ "prepare": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown"
88
89
  },
89
90
  "peerDependenciesMeta": {
90
91
  "@deepseek-ai/dsh-session-projection": {
@@ -32,10 +32,16 @@ export interface BoardProps {
32
32
  workspacePath?: string
33
33
  onOpenInspector?: (selection: BoardSelection) => void
34
34
  onClose?: () => void
35
+ /**
36
+ * 'overlay' (default) renders the board as a full-viewport fixed overlay
37
+ * (shell.overlay / launcher). 'view' renders it inline, filling its parent
38
+ * (a conversation.view tab), with no close button and no fixed positioning.
39
+ */
40
+ variant?: 'overlay' | 'view'
35
41
  }
36
42
 
37
- function closeButton(onClose: (() => void) | undefined) {
38
- if (onClose === undefined) return null
43
+ function closeButton(onClose: (() => void) | undefined, variant: 'overlay' | 'view') {
44
+ if (onClose === undefined || variant === 'view') return null
39
45
  return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
40
46
  }
41
47
 
@@ -49,7 +55,7 @@ function solidPill(kind: ReturnType<typeof cardPill>) {
49
55
  return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind])
50
56
  }
51
57
 
52
- export function Board({ snapshot, workspacePath, onOpenInspector, onClose }: BoardProps) {
58
+ export function Board({ snapshot, workspacePath, onOpenInspector, onClose, variant = 'overlay' }: BoardProps) {
53
59
  const { theme, toggle } = useBoardTheme()
54
60
  if (snapshot === null || snapshot.root === null) return null
55
61
  // Run 16: when the synchronous workspace path differs from the async snapshot
@@ -59,25 +65,26 @@ export function Board({ snapshot, workspacePath, onOpenInspector, onClose }: Boa
59
65
  const runs = stale ? [] : listRuns(snapshot.projection)
60
66
  const headerPath = workspacePath ?? snapshot.root
61
67
  const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
68
+ const rootCls = variant === 'view' ? 'rec-board rec-board-view' : 'rec-board'
62
69
  if (runs.length === 0) {
63
- return createElement('div', { className: 'rec-board', 'data-theme': theme, 'data-empty': true },
70
+ return createElement('div', { className: rootCls, 'data-theme': theme, 'data-empty': true },
64
71
  createElement('header', { className: 'rec-board-header' },
65
72
  createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
66
73
  createElement('span', { className: 'rec-board-path' }, headerPath),
67
74
  createElement('span', { className: 'rec-board-count' }, '0 runs'),
68
75
  createElement(ThemeToggle, { theme, toggle }),
69
- closeButton(onClose),
76
+ closeButton(onClose, variant),
70
77
  ),
71
78
  createElement('p', { className: 'rec-board-empty' }, 'No recursive runs in this workspace yet.'),
72
79
  )
73
80
  }
74
- return createElement('div', { className: 'rec-board', 'data-theme': theme },
81
+ return createElement('div', { className: rootCls, 'data-theme': theme },
75
82
  createElement('header', { className: 'rec-board-header' },
76
83
  createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
77
84
  createElement('span', { className: 'rec-board-path' }, headerPath),
78
85
  createElement('span', { className: 'rec-board-count' }, runs.length + ' runs'),
79
86
  createElement(ThemeToggle, { theme, toggle }),
80
- closeButton(onClose),
87
+ closeButton(onClose, variant),
81
88
  ),
82
89
  createElement('div', { className: 'rec-columns' },
83
90
  KANBAN_LANES.map((lane) => {
@@ -17,6 +17,7 @@ import { claimClientApply, releaseClientApply } from './apply-guard.ts'
17
17
 
18
18
  export { Board, listRuns } from './board.tsx'
19
19
  export { Inspector } from './inspector.tsx'
20
+ export { RecursiveView } from './slots.ts'
20
21
  export { RecursiveSettings } from './settings.tsx'
21
22
  export { useLiveProjection } from './use-live.ts'
22
23
  export type { LiveProjectionSnapshot } from './use-live.ts'
@@ -64,11 +64,54 @@ export function RecursiveLauncherGate({ useSessions }: { useSessions: SnapshotSe
64
64
  return createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉')
65
65
  }
66
66
 
67
+ /**
68
+ * RecursiveView: the conversation.view tab body. Renders the run board INLINE
69
+ * (fills the view area) keyed on the CURRENT workspace, and swaps to the
70
+ * inspector modal when a run is opened. Read-only (R9): uses the live host
71
+ * route. No preset gate — the tab is discoverable in any session.
72
+ */
73
+ export function RecursiveView({ useSessions, useWorkspaces }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; useWorkspaces: SnapshotSelectorHook<WorkspaceListStateLike> }): ReactNode {
74
+ const board = useBoardState()
75
+ const sessions = useRecursiveSessions(useSessions)
76
+ const workspaces: WorkspaceListStateLike = useWorkspaces((s) => s) ?? { items: [], recentWorkspaceId: undefined }
77
+ const wsPath = currentWorkspacePath(workspaces, sessions)
78
+ const scope = { cwd: wsPath }
79
+ const snapshot = useLiveProjection(scope)
80
+ // Inspector drill-down (modal over the inline board) when a run is selected.
81
+ if (board.selection !== null) {
82
+ return createElement(Inspector, {
83
+ runId: board.selection.runId,
84
+ worktreeRoot: board.selection.worktreeRoot,
85
+ snapshot,
86
+ onBackToToolDetails: () => boardState.backToBoard(),
87
+ onClose: () => boardState.close(),
88
+ })
89
+ }
90
+ return createElement(Board, { snapshot, workspacePath: wsPath, variant: 'view', onOpenInspector: (sel) => boardState.openInspector(sel) })
91
+ }
92
+
67
93
  export function registerSlots(ctx: ClientContext): () => void {
68
94
  const disposers: (() => void)[] = []
69
95
  // Run 15: inject the one-shot theme-token stylesheet once per document (idempotent).
70
96
  disposers.push(injectBoardStyles())
71
97
 
98
+ // Conversation view tab (Chat | Trajectory | Recursive): the recursive run
99
+ // board as a first-class tab in the conversation header, rendered INLINE in
100
+ // the view area (not a fixed overlay). Order 20 places it to the RIGHT of
101
+ // Trajectory (order 10). Always present — no recursive-preset gate, so the
102
+ // entry point is discoverable in any session.
103
+ disposers.push(ctx.slots.inject('conversation.view', () => ctx.slots.register({
104
+ name: 'conversation.view',
105
+ id: 'recursive',
106
+ order: 20,
107
+ label: 'Recursive',
108
+ }, (props: RootSlotProps) => {
109
+ const useSessions = props?.useSessions
110
+ const useWorkspaces = props?.useWorkspaces
111
+ if (useSessions === undefined || useWorkspaces === undefined) return null
112
+ return createElement(RecursiveView, { useSessions, useWorkspaces })
113
+ })))
114
+
72
115
  // Board launcher in the sidebar footer action list — OPENS the shared board store (run 08 R1).
73
116
  // Run 11 (UX gate): the seat is root-scoped (visible in every session), but the board it
74
117
  // opens is recursive-preset-gated. In a code/other session the icon was a dead click — a
@@ -136,6 +136,17 @@ const BOARD_CSS = `
136
136
  overflow: hidden;
137
137
  }
138
138
 
139
+ /* Inline board (conversation.view tab): fills its parent view area, not fixed. */
140
+ .rec-board-view {
141
+ position: relative;
142
+ inset: auto;
143
+ z-index: auto;
144
+ width: 100%;
145
+ height: 100%;
146
+ min-height: 0;
147
+ flex: 1;
148
+ }
149
+
139
150
  .rec-board-header {
140
151
  display: flex;
141
152
  align-items: center;
@@ -2,15 +2,13 @@
2
2
  * Enforcement config + pre-step gate + tool guards + tamper detection
3
3
  * (Phase C R3/R4/R7/R8, PROPOSAL 8.4/8.6/13.5).
4
4
  *
5
- * Layers 1 and 2 are CALLERS of the lifecycle transition set - they never
6
- * reimplement the predicates. Configurable strict|advisory per gate
7
- * (default advisory).
5
+ * Layer 2 (tool guards) and Layer 8 (tamper) are the remaining enforcement
6
+ * layers. Configurable strict|advisory per gate (default advisory).
8
7
  */
9
8
  import { existsSync, readFileSync } from 'node:fs'
10
9
  import { join, isAbsolute, resolve, sep } from 'node:path'
11
10
  import { getLockStatus, getPrerequisiteBlockers } from './lock.ts'
12
11
  import { getMdFieldValue } from './status.ts'
13
- import { validateTransition, type PhaseTransitionIntent, type SessionEventLike, detectTransitionIntent } from './lifecycle.ts'
14
12
 
15
13
  export type EnforcementMode = 'strict' | 'advisory'
16
14
 
@@ -37,32 +35,6 @@ export function resolveEnforcementConfig(config: unknown): EnforcementConfig {
37
35
 
38
36
  export const DEFAULT_ENFORCEMENT: EnforcementConfig = { preStep: 'advisory', toolGuards: 'advisory', tamper: 'advisory' }
39
37
 
40
- /**
41
- * Layer 1 - agent/pre-step phase-transition gate decision.
42
- * Reads on TRANSITION INTENT ONLY (13.5): no transition intent means the step
43
- * passes through untouched. On a transition intent whose gates fail:
44
- * - strict -> reject (turn ends blocked, no model call spent)
45
- * - advisory -> enter (warn only; a recursive/gate-blocked event is emitted)
46
- */
47
- export interface PreStepGateDecision {
48
- kind: 'reject' | 'enter'
49
- gateBlocked: boolean
50
- failures: string[]
51
- }
52
-
53
- export function evaluatePreStepGate(
54
- events: readonly SessionEventLike[],
55
- mode: EnforcementMode = 'advisory',
56
- ): PreStepGateDecision {
57
- const intent = detectTransitionIntent(events)
58
- if (!intent || intent.worktreeRoot === '' || intent.runId === '') {
59
- return { kind: 'enter', gateBlocked: false, failures: [] }
60
- }
61
- const check = validateTransition(intent)
62
- if (check.passed) return { kind: 'enter', gateBlocked: false, failures: [] }
63
- return { kind: mode === 'strict' ? 'reject' : 'enter', gateBlocked: true, failures: check.failures }
64
- }
65
-
66
38
  /** Tool names the locked-artifact write guard treats as write operations. */
67
39
  const WRITE_TOOL_NAMES = new Set(['write', 'edit', 'fs_write', 'fs-write', 'pwsh', 'shell', 'bash', 'run_code'])
68
40
 
package/src/fs-intent.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
3
3
  *
4
- * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter,
5
- * so detectTransitionIntent(events) is ALWAYS null and the recursive:policy
4
+ * SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter
5
+ * (and 0.2.2 removed the event-fold helper that read it), so the recursive:policy
6
6
  * prompt section rendered ''. This module derives the SAME intent from the
7
7
  * filesystem instead: session cwd -> control-plane root -> enumerate runs ->
8
8
  * latest run -> current phase (foldRun). Pure read-only fs folding, zero
package/src/index.ts CHANGED
@@ -134,8 +134,9 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
134
134
  if (!agent) return ''
135
135
  // SP3 R5 policy-render fix: derive intent from the FILESYSTEM, not the
136
136
  // retired recursive/phase-intent session event (zero-emission removed
137
- // the emitter, so detectTransitionIntent was ALWAYS null and this
138
- // section rendered ''). Pure read-only fs folding; no recursive/*
137
+ // the emitter; 0.2.2 deleted the event-fold helper that read it, so this
138
+ // signal was ALWAYS null and this section rendered ''). Pure read-only fs
139
+ // folding; no recursive/*
139
140
  // events are appended.
140
141
  const intent = fsPolicyIntent(agent, workspaceRegistry as never)
141
142
  if (!intent) return ''
package/src/lifecycle.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Run state machine + serialized transition driver + recursive/* events
3
- * (Phase C R1/R2/R6, PROPOSAL 8.8).
2
+ * Transition gate validation + goal coupling (Phase C R1/R2/R6, PROPOSAL 8.4).
4
3
  *
5
- * Authority is TRANSITIONS AND EVENTS ONLY - this module never stores run
6
- * state in a second place. It reconciles the file tree via the existing
7
- * read path (run.ts/status.ts/lock.ts), validates the target phase's gates
8
- * (PROPOSAL 8.4), delegates the artifact write to lock.ts (canonical
9
- * lock-hash + monotonic chain preserved), and emits the recursive/* events.
10
- * State is DERIVED on every transition; resume/fork/session-restart
11
- * reconstruct identical state by re-reading files + folding the session log.
4
+ * Live BUG dsh-v0.1.1-rc.2 compatibility (0.2.2): the legacy session-event fold
5
+ * surface (foldRecursivePhase / detectTransitionIntent / hasOpenTurn /
6
+ * LifecycleDriver / the recursive/* event payload interfaces) was REMOVED.
7
+ * The plugin is zero-emission: no recursive/* session event is ever appended
8
+ * or emitted, so nothing folds them. What remains is the pure transition gate
9
+ * check (validateTransition reads the file tree + lock chain, writes nothing)
10
+ * plus the shared intent/result types and the goal-coupling no-op helper.
12
11
  */
13
12
  import { existsSync, readFileSync } from 'node:fs'
14
13
  import { join } from 'node:path'
@@ -34,7 +33,7 @@ export interface PhaseTransitionIntent {
34
33
  }
35
34
  }
36
35
 
37
- /** Folded phase state (last-wins over recursive/phase + recursive/run-state). */
36
+ /** Folded phase state (run key + current phase + current run-level state). */
38
37
  export interface RecursivePhaseState {
39
38
  runId: string
40
39
  phase: string
@@ -42,100 +41,18 @@ export interface RecursivePhaseState {
42
41
  runState: RunState
43
42
  }
44
43
 
45
- /** A session-event-like carrier (the pure fold reads events by shape). */
46
- export interface SessionEventLike {
47
- type: string
48
- data?: Record<string, unknown>
49
- }
50
-
51
44
  /** Gate check result - the transition set's single output. */
52
45
  export interface GateCheckResult {
53
46
  passed: boolean
54
47
  failures: string[]
55
48
  }
56
49
 
57
- /**
58
- * Payload shapes for the recursive/* events. These are LEGACY structural
59
- * views kept for internal consumers; the single source of truth for the
60
- * emitted payloads is events.ts (every event carries { runId, worktreeRoot }).
61
- * Aligned here so no local interface drifts out of the worktree-keyed
62
- * invariant (B7).
63
- */
64
- export interface RecursivePhaseEvent { runId: string; worktreeRoot: string; phase: string; status: string }
65
- export interface RecursiveRunStateEvent { runId: string; worktreeRoot: string; state: RunState; reason?: string }
66
- export interface RecursiveGateBlockedEvent { runId: string; worktreeRoot: string; phase: string; failures: string[]; kind: string }
67
- export interface RecursiveTamperEvent { runId: string; worktreeRoot: string; path: string; reason: string }
68
- export interface RecursiveTransitionFailedEvent { runId: string; worktreeRoot: string; phase: string; error: string }
69
-
70
50
  /** The audited phase files whose lock requires Audit: PASS (parity with status.ts). */
71
51
  const AUDITED_PHASE_FILES = new Set([
72
52
  '01-as-is.md', '01.5-root-cause.md', '02-to-be-plan.md', '03-implementation-summary.md',
73
53
  '03.5-code-review.md', '04-test-summary.md', '06-decisions-update.md', '07-state-update.md', '08-memory-impact.md',
74
54
  ])
75
55
 
76
- /**
77
- * Pure last-wins fold over the recursive/* events (the foldPlanMode pattern).
78
- * A log with no recursive/phase folds to null; the last recursive/run-state
79
- * wins for the run-level flag.
80
- */
81
- export function foldRecursivePhase(events: readonly SessionEventLike[], end = events.length): RecursivePhaseState | null {
82
- let runId = ''
83
- let phaseName = ''
84
- let status = ''
85
- let runState: RunState = 'active'
86
- let seen = false
87
- let index = 0
88
- for (const event of events) {
89
- if (index >= end) break
90
- index++
91
- if (event.type === 'recursive/phase') {
92
- const d = event.data ?? {}
93
- runId = String(d.runId ?? runId)
94
- phaseName = String(d.phase ?? phaseName)
95
- status = String(d.status ?? status)
96
- seen = true
97
- } else if (event.type === 'recursive/run-state') {
98
- const d = event.data ?? {}
99
- runId = String(d.runId ?? runId)
100
- runState = asRunState(d.state)
101
- seen = true
102
- }
103
- }
104
- return seen ? { runId, phase: phaseName, status, runState } : null
105
- }
106
-
107
- function asRunState(value: unknown): RunState {
108
- return RUN_STATES.includes(value as RunState) ? (value as RunState) : 'active'
109
- }
110
-
111
- /** Whether the session log holds an opened turn without its closing turn/end. */
112
- export function hasOpenTurn(events: readonly SessionEventLike[]): boolean {
113
- let open = false
114
- for (const event of events) {
115
- if (event.type === 'turn/start') open = true
116
- else if (event.type === 'turn/end') open = false
117
- }
118
- return open
119
- }
120
-
121
- /** Detect a pending transition intent from the session log (the lock tool logs it). */
122
- export function detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null {
123
- let intent: PhaseTransitionIntent | null = null
124
- for (const event of events) {
125
- if (event.type === 'recursive/phase-intent') {
126
- const d = event.data ?? {}
127
- intent = {
128
- runId: String(d.runId ?? ''),
129
- worktreeRoot: String(d.worktreeRoot ?? ''),
130
- targetArtifact: String(d.targetArtifact ?? ''),
131
- kind: (d.kind === 'reopen' || d.kind === 'advance' ? d.kind : 'lock'),
132
- evidence: d.evidence as PhaseTransitionIntent['evidence'],
133
- }
134
- }
135
- }
136
- return intent
137
- }
138
-
139
56
  /**
140
57
  * Validate a proposed transition against the target phase's gates (PROPOSAL 8.4).
141
58
  * Pure: reads the current file tree + lock.ts chain; writes nothing.
@@ -202,23 +119,6 @@ export function validateTransition(intent: PhaseTransitionIntent): GateCheckResu
202
119
  return { passed: failures.length === 0, failures }
203
120
  }
204
121
 
205
- /**
206
- * A serialized per-run transition driver (coalesced - the single-reservation
207
- * pattern). Two concurrent 'lock Phase 3' intents queue; the second observes
208
- * the first's committed state instead of racing the write.
209
- */
210
- export class LifecycleDriver {
211
- private readonly drivers = new Map<string, Promise<void>>()
212
-
213
- /** Run one transition serially per runId. */
214
- serialize(runId: string, run: () => Promise<void>): Promise<void> {
215
- const previous = this.drivers.get(runId) ?? Promise.resolve()
216
- const next = previous.then(run, run)
217
- this.drivers.set(runId, next.then(() => undefined, () => undefined))
218
- return next
219
- }
220
- }
221
-
222
122
  /**
223
123
  * Couple a gate-block to the goal service (PROPOSAL 8.4 goal integration).
224
124
  * Graceful no-op when the goal service or agent is unavailable.
package/src/runtime.ts CHANGED
@@ -22,8 +22,8 @@ import { buildReviewBundle, type ReviewBundleInput } from './review.ts'
22
22
  import { createHandoff, createChildBrief, replyPath, childScratchPath, buildDelegationPrompt, type HandoffInput, type ChildBriefInput } from './handoff.ts'
23
23
  import { loadRouterPolicy, routerPolicyPath, resolveRole, capabilityProbe, delegationDecisionBasis, type RouterPolicy, type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts'
24
24
  import { delegate, validateReferences, writeActionRecord, evaluateDelegationResult, reviewOutputSchema, defaultReviewToolFilter, type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference, type ActionRecordInput } from './delegation.ts'
25
- import { foldRecursivePhase, validateTransition, detectTransitionIntent, LifecycleDriver, coupleGateBlockToGoal, type PhaseTransitionIntent, type SessionEventLike, type RecursivePhaseState, type GateCheckResult } from './lifecycle.ts'
26
- import { resolveEnforcementConfig, DEFAULT_ENFORCEMENT, evaluatePreStepGate, evaluateToolGuard, detectTamper, type EnforcementConfig, type PreStepGateDecision, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts'
25
+ import { validateTransition, coupleGateBlockToGoal, type PhaseTransitionIntent, type RecursivePhaseState, type GateCheckResult } from './lifecycle.ts'
26
+ import { resolveEnforcementConfig, DEFAULT_ENFORCEMENT, evaluateToolGuard, detectTamper, type EnforcementConfig, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts'
27
27
  import type { Session } from '@deepseek-ai/dsh-session'
28
28
  import { renderRecursivePolicy, type PolicyContext } from './policy.ts'
29
29
  import { snapshotWorkspace } from './snapshot.ts'
@@ -577,25 +577,6 @@ export class RecursiveRuntime extends Service {
577
577
  return { artifact: target, runId, errors, warnings, passed: errors.length === 0 }
578
578
  }
579
579
 
580
- /** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
581
- foldPhase(events: readonly SessionEventLike[]): RecursivePhaseState | null {
582
- return foldRecursivePhase(events)
583
- }
584
-
585
- validateTransition(intent: PhaseTransitionIntent): GateCheckResult {
586
- return validateTransition(intent)
587
- }
588
-
589
- detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null {
590
- return detectTransitionIntent(events)
591
- }
592
-
593
- /** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
594
- gatePreStep(events: readonly SessionEventLike[], config?: EnforcementConfig): PreStepGateDecision {
595
- const mode = (config ?? this.enforcementConfig).preStep
596
- return evaluatePreStepGate(events, mode)
597
- }
598
-
599
580
  /** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
600
581
  guardTool(exec: ToolExecLike, root: string, runId: string, config?: EnforcementConfig): ToolGuardDecision {
601
582
  const mode = (config ?? this.enforcementConfig).toolGuards