@try-works/dsh-recursive-mode 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/enforcement.d.ts +0 -14
- package/lib/index.js +13 -123
- package/lib/lifecycle.d.ts +1 -64
- package/lib/runtime.d.ts +2 -8
- package/package.json +7 -6
- package/src/enforcement.ts +2 -30
- package/src/fs-intent.ts +2 -2
- package/src/index.ts +3 -2
- package/src/lifecycle.ts +9 -109
- package/src/runtime.ts +2 -21
package/lib/enforcement.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
4503
|
-
*
|
|
4504
|
-
*
|
|
4505
|
-
*
|
|
4506
|
-
*
|
|
4507
|
-
*
|
|
4508
|
-
*
|
|
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
|
-
*
|
|
4673
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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 };
|
package/lib/lifecycle.d.ts
CHANGED
|
@@ -15,86 +15,23 @@ export interface PhaseTransitionIntent {
|
|
|
15
15
|
qaSignOff?: boolean;
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
/** Folded phase 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
|
|
10
|
-
import { type EnforcementConfig, type
|
|
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.
|
|
4
|
+
"version": "0.2.2",
|
|
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.
|
|
49
|
-
"@deepseek-ai/dsh-session-projection": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.
|
|
51
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
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": {
|
package/src/enforcement.ts
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
|
138
|
-
// section rendered ''). Pure read-only fs
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 (
|
|
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 {
|
|
26
|
-
import { resolveEnforcementConfig, DEFAULT_ENFORCEMENT,
|
|
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
|