@try-works/dsh-recursive-mode 0.2.0 → 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.d.ts +1 -0
- package/lib/index.js +104 -130
- package/lib/lifecycle.d.ts +1 -64
- package/lib/phase-rules.d.ts +28 -3
- package/lib/recursive_phase.tool.d.ts +9 -0
- package/lib/runtime.d.ts +20 -8
- package/package.json +7 -6
- package/src/enforcement.ts +2 -30
- package/src/fs-intent.ts +2 -2
- package/src/index.ts +17 -8
- package/src/lifecycle.ts +9 -109
- package/src/phase-rules.ts +49 -6
- package/src/policy.ts +1 -0
- package/src/recursive_phase.tool.ts +29 -0
- package/src/runtime.ts +20 -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.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { createRecursiveLintTool } from './recursive_lint.tool.ts';
|
|
|
8
8
|
export { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts';
|
|
9
9
|
export { createRecursiveScratchTool } from './recursive_scratch.tool.ts';
|
|
10
10
|
export { createRecursiveWorktreeTool } from './recursive_worktree.tool.ts';
|
|
11
|
+
export { createRecursivePhaseTool } from './recursive_phase.tool.ts';
|
|
11
12
|
export * from './status.ts';
|
|
12
13
|
export { PHASE_SEQUENCE, OPTIONAL_PHASES, normalizeForLockHash, lockHashFromContent, phaseIndex, isCoreArtifact, getPrerequisites, getLockStatus, getPrerequisiteBlockers, receiptPath, readReceipt, writeReceipt, invalidateReceipt, getStaleDownstreamPhases, getNextLegalPhase, getAllStaleReceipts, validateChain, } from './lock.ts';
|
|
13
14
|
export type { LockReceipt, LockStatus, PrerequisiteBlocker, StaleDownstream, ChainPhaseResult, LockChainResult, } from './lock.ts';
|
package/lib/index.js
CHANGED
|
@@ -393,17 +393,43 @@ function getArtifactRequiredSections(fileName, workflowProfile = CURRENT_WORKFLO
|
|
|
393
393
|
return headings;
|
|
394
394
|
}
|
|
395
395
|
/**
|
|
396
|
+
* LIVE BUG 6 (0.2.1): dedup gate for the agent/pre-step lint-rules reminder.
|
|
397
|
+
* Keyed by (root, runId, phase) so each new run or phase transition gets its own
|
|
398
|
+
* single injection, while re-arming steps in the SAME phase stay silent. Scoped
|
|
399
|
+
* to the apply() effect/fiber lifetime (one instance per mount), matching the
|
|
400
|
+
* repairedRoots dedup used by the scaffold repair above the injection point.
|
|
401
|
+
*/
|
|
402
|
+
var ReminderOnceGate = class {
|
|
403
|
+
seen = /* @__PURE__ */ new Set();
|
|
404
|
+
shouldInject(root, runId, phase) {
|
|
405
|
+
const key = root + "\0" + runId + "\0" + phase;
|
|
406
|
+
if (this.seen.has(key)) return false;
|
|
407
|
+
this.seen.add(key);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
function phaseRulesFor(fileName, workflowProfile = CURRENT_WORKFLOW_PROFILE$1) {
|
|
412
|
+
return {
|
|
413
|
+
fileName,
|
|
414
|
+
label: fileName.replace(/\.md$/, ""),
|
|
415
|
+
requiredSections: getArtifactRequiredSections(fileName, workflowProfile),
|
|
416
|
+
audited: AUDITED_PHASE_FILES$2.has(fileName),
|
|
417
|
+
tdd: fileName === "03-implementation-summary.md",
|
|
418
|
+
qa: fileName === "05-manual-qa.md"
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
396
422
|
* Compact lint-rules message for the pre-step injection (R5): this phase's
|
|
397
|
-
* required sections + phase-specific gate notes,
|
|
398
|
-
*
|
|
399
|
-
*
|
|
423
|
+
* required sections + phase-specific gate notes, built from phaseRulesFor
|
|
424
|
+
* (single source of truth with the recursive_phase tool). Output is unchanged
|
|
425
|
+
* from the prior inline build so r5-parity.spec.ts stays green.
|
|
400
426
|
*/
|
|
401
427
|
function phaseLintRulesMessage(fileName, workflowProfile = CURRENT_WORKFLOW_PROFILE$1) {
|
|
402
|
-
const
|
|
428
|
+
const rules = phaseRulesFor(fileName, workflowProfile);
|
|
403
429
|
return [
|
|
404
430
|
"<system-reminder>",
|
|
405
431
|
"Recursive-mode phase lint rules for THIS phase (" + fileName + "):",
|
|
406
|
-
"Required sections: " +
|
|
432
|
+
"Required sections: " + rules.requiredSections.join(" | "),
|
|
407
433
|
"Gates: Coverage: FAIL until all checkboxes pass; Approval: FAIL until user sign-off; lock only via recursive_lock (monotonic).",
|
|
408
434
|
"Audited phases: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context and Audit Verdict.",
|
|
409
435
|
"TDD (phase 3): declare TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.",
|
|
@@ -4470,16 +4496,15 @@ function evaluateDelegationResult(result) {
|
|
|
4470
4496
|
//#endregion
|
|
4471
4497
|
//#region src/lifecycle.ts
|
|
4472
4498
|
/**
|
|
4473
|
-
*
|
|
4474
|
-
* (Phase C R1/R2/R6, PROPOSAL 8.8).
|
|
4499
|
+
* Transition gate validation + goal coupling (Phase C R1/R2/R6, PROPOSAL 8.4).
|
|
4475
4500
|
*
|
|
4476
|
-
*
|
|
4477
|
-
*
|
|
4478
|
-
*
|
|
4479
|
-
*
|
|
4480
|
-
*
|
|
4481
|
-
*
|
|
4482
|
-
*
|
|
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.
|
|
4483
4508
|
*/
|
|
4484
4509
|
/** Run-level durable states (PROPOSAL 8.8). */
|
|
4485
4510
|
const RUN_STATES = [
|
|
@@ -4502,66 +4527,6 @@ const AUDITED_PHASE_FILES = /* @__PURE__ */ new Set([
|
|
|
4502
4527
|
"08-memory-impact.md"
|
|
4503
4528
|
]);
|
|
4504
4529
|
/**
|
|
4505
|
-
* Pure last-wins fold over the recursive/* events (the foldPlanMode pattern).
|
|
4506
|
-
* A log with no recursive/phase folds to null; the last recursive/run-state
|
|
4507
|
-
* wins for the run-level flag.
|
|
4508
|
-
*/
|
|
4509
|
-
function foldRecursivePhase(events, end = events.length) {
|
|
4510
|
-
let runId = "";
|
|
4511
|
-
let phaseName = "";
|
|
4512
|
-
let status = "";
|
|
4513
|
-
let runState = "active";
|
|
4514
|
-
let seen = false;
|
|
4515
|
-
let index = 0;
|
|
4516
|
-
for (const event of events) {
|
|
4517
|
-
if (index >= end) break;
|
|
4518
|
-
index++;
|
|
4519
|
-
if (event.type === "recursive/phase") {
|
|
4520
|
-
const d = event.data ?? {};
|
|
4521
|
-
runId = String(d.runId ?? runId);
|
|
4522
|
-
phaseName = String(d.phase ?? phaseName);
|
|
4523
|
-
status = String(d.status ?? status);
|
|
4524
|
-
seen = true;
|
|
4525
|
-
} else if (event.type === "recursive/run-state") {
|
|
4526
|
-
const d = event.data ?? {};
|
|
4527
|
-
runId = String(d.runId ?? runId);
|
|
4528
|
-
runState = asRunState(d.state);
|
|
4529
|
-
seen = true;
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
return seen ? {
|
|
4533
|
-
runId,
|
|
4534
|
-
phase: phaseName,
|
|
4535
|
-
status,
|
|
4536
|
-
runState
|
|
4537
|
-
} : null;
|
|
4538
|
-
}
|
|
4539
|
-
function asRunState(value) {
|
|
4540
|
-
return RUN_STATES.includes(value) ? value : "active";
|
|
4541
|
-
}
|
|
4542
|
-
/** Whether the session log holds an opened turn without its closing turn/end. */
|
|
4543
|
-
function hasOpenTurn(events) {
|
|
4544
|
-
let open = false;
|
|
4545
|
-
for (const event of events) if (event.type === "turn/start") open = true;
|
|
4546
|
-
else if (event.type === "turn/end") open = false;
|
|
4547
|
-
return open;
|
|
4548
|
-
}
|
|
4549
|
-
/** Detect a pending transition intent from the session log (the lock tool logs it). */
|
|
4550
|
-
function detectTransitionIntent(events) {
|
|
4551
|
-
let intent = null;
|
|
4552
|
-
for (const event of events) if (event.type === "recursive/phase-intent") {
|
|
4553
|
-
const d = event.data ?? {};
|
|
4554
|
-
intent = {
|
|
4555
|
-
runId: String(d.runId ?? ""),
|
|
4556
|
-
worktreeRoot: String(d.worktreeRoot ?? ""),
|
|
4557
|
-
targetArtifact: String(d.targetArtifact ?? ""),
|
|
4558
|
-
kind: d.kind === "reopen" || d.kind === "advance" ? d.kind : "lock",
|
|
4559
|
-
evidence: d.evidence
|
|
4560
|
-
};
|
|
4561
|
-
}
|
|
4562
|
-
return intent;
|
|
4563
|
-
}
|
|
4564
|
-
/**
|
|
4565
4530
|
* Validate a proposed transition against the target phase's gates (PROPOSAL 8.4).
|
|
4566
4531
|
* Pure: reads the current file tree + lock.ts chain; writes nothing.
|
|
4567
4532
|
*/
|
|
@@ -4615,20 +4580,6 @@ function validateTransition(intent) {
|
|
|
4615
4580
|
};
|
|
4616
4581
|
}
|
|
4617
4582
|
/**
|
|
4618
|
-
* A serialized per-run transition driver (coalesced - the single-reservation
|
|
4619
|
-
* pattern). Two concurrent 'lock Phase 3' intents queue; the second observes
|
|
4620
|
-
* the first's committed state instead of racing the write.
|
|
4621
|
-
*/
|
|
4622
|
-
var LifecycleDriver = class {
|
|
4623
|
-
drivers = /* @__PURE__ */ new Map();
|
|
4624
|
-
/** Run one transition serially per runId. */
|
|
4625
|
-
serialize(runId, run) {
|
|
4626
|
-
const next = (this.drivers.get(runId) ?? Promise.resolve()).then(run, run);
|
|
4627
|
-
this.drivers.set(runId, next.then(() => void 0, () => void 0));
|
|
4628
|
-
return next;
|
|
4629
|
-
}
|
|
4630
|
-
};
|
|
4631
|
-
/**
|
|
4632
4583
|
* Couple a gate-block to the goal service (PROPOSAL 8.4 goal integration).
|
|
4633
4584
|
* Graceful no-op when the goal service or agent is unavailable.
|
|
4634
4585
|
*/
|
|
@@ -4643,9 +4594,8 @@ function coupleGateBlockToGoal(goalService, agent, ref, reason) {
|
|
|
4643
4594
|
* Enforcement config + pre-step gate + tool guards + tamper detection
|
|
4644
4595
|
* (Phase C R3/R4/R7/R8, PROPOSAL 8.4/8.6/13.5).
|
|
4645
4596
|
*
|
|
4646
|
-
*
|
|
4647
|
-
*
|
|
4648
|
-
* (default advisory).
|
|
4597
|
+
* Layer 2 (tool guards) and Layer 8 (tamper) are the remaining enforcement
|
|
4598
|
+
* layers. Configurable strict|advisory per gate (default advisory).
|
|
4649
4599
|
*/
|
|
4650
4600
|
/** Validate the enforcement config shape (unknown keys fail at plugin load). */
|
|
4651
4601
|
function resolveEnforcementConfig(config) {
|
|
@@ -4668,25 +4618,6 @@ const DEFAULT_ENFORCEMENT = {
|
|
|
4668
4618
|
toolGuards: "advisory",
|
|
4669
4619
|
tamper: "advisory"
|
|
4670
4620
|
};
|
|
4671
|
-
function evaluatePreStepGate(events, mode = "advisory") {
|
|
4672
|
-
const intent = detectTransitionIntent(events);
|
|
4673
|
-
if (!intent || intent.worktreeRoot === "" || intent.runId === "") return {
|
|
4674
|
-
kind: "enter",
|
|
4675
|
-
gateBlocked: false,
|
|
4676
|
-
failures: []
|
|
4677
|
-
};
|
|
4678
|
-
const check = validateTransition(intent);
|
|
4679
|
-
if (check.passed) return {
|
|
4680
|
-
kind: "enter",
|
|
4681
|
-
gateBlocked: false,
|
|
4682
|
-
failures: []
|
|
4683
|
-
};
|
|
4684
|
-
return {
|
|
4685
|
-
kind: mode === "strict" ? "reject" : "enter",
|
|
4686
|
-
gateBlocked: true,
|
|
4687
|
-
failures: check.failures
|
|
4688
|
-
};
|
|
4689
|
-
}
|
|
4690
4621
|
/** Tool names the locked-artifact write guard treats as write operations. */
|
|
4691
4622
|
const WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
4692
4623
|
"write",
|
|
@@ -4841,7 +4772,8 @@ function renderRecursivePolicy(context) {
|
|
|
4841
4772
|
"- Phase 3 lock requires TDD evidence (strict) or rationale (pragmatic); Phase 5 requires QA sign-off for human/hybrid modes.",
|
|
4842
4773
|
"- The control-plane root is resolved STRICTLY from this session workspace (never scan other workspaces).",
|
|
4843
4774
|
"- Scratch is disposable, git-ignored, and never citable as an Input.",
|
|
4844
|
-
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing."
|
|
4775
|
+
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing.",
|
|
4776
|
+
"- Call recursive_phase when entering a new phase; the same rules are auto-injected once per phase transition."
|
|
4845
4777
|
];
|
|
4846
4778
|
if (currentFile) {
|
|
4847
4779
|
const sections = getArtifactRequiredSections(currentFile, CURRENT_WORKFLOW_PROFILE$1);
|
|
@@ -5383,6 +5315,26 @@ var RecursiveRuntime = class extends Service {
|
|
|
5383
5315
|
return foldRun(resolved.runDir, resolved.runId);
|
|
5384
5316
|
}
|
|
5385
5317
|
/**
|
|
5318
|
+
* LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
|
|
5319
|
+
* the workspace root (same as status/lock), finds the latest run (or the
|
|
5320
|
+
* given runId), advances via getNextLegalPhase, and returns the phase's lint
|
|
5321
|
+
* rules + instructions. Returns null when no active phase exists. This is the
|
|
5322
|
+
* canonical data source for the recursive_phase tool.
|
|
5323
|
+
*/
|
|
5324
|
+
async phaseRules(runId, agent) {
|
|
5325
|
+
const root = await this.resolveRootFor(agent);
|
|
5326
|
+
if (!root) return null;
|
|
5327
|
+
const resolved = resolveRunDir(root, runId);
|
|
5328
|
+
if (!resolved) return null;
|
|
5329
|
+
const phase = getNextLegalPhase(resolved.runDir);
|
|
5330
|
+
if (!phase) return null;
|
|
5331
|
+
return {
|
|
5332
|
+
runId: resolved.runId,
|
|
5333
|
+
phase,
|
|
5334
|
+
...phaseRulesFor(phase)
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5386
5338
|
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
5387
5339
|
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
5388
5340
|
* recursive-init.py (incl. git-context prefill); later phases carry every
|
|
@@ -5606,21 +5558,6 @@ var RecursiveRuntime = class extends Service {
|
|
|
5606
5558
|
passed: errors.length === 0
|
|
5607
5559
|
};
|
|
5608
5560
|
}
|
|
5609
|
-
/** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
|
|
5610
|
-
foldPhase(events) {
|
|
5611
|
-
return foldRecursivePhase(events);
|
|
5612
|
-
}
|
|
5613
|
-
validateTransition(intent) {
|
|
5614
|
-
return validateTransition(intent);
|
|
5615
|
-
}
|
|
5616
|
-
detectTransitionIntent(events) {
|
|
5617
|
-
return detectTransitionIntent(events);
|
|
5618
|
-
}
|
|
5619
|
-
/** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
|
|
5620
|
-
gatePreStep(events, config) {
|
|
5621
|
-
const mode = (config ?? this.enforcementConfig).preStep;
|
|
5622
|
-
return evaluatePreStepGate(events, mode);
|
|
5623
|
-
}
|
|
5624
5561
|
/** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
|
|
5625
5562
|
guardTool(exec, root, runId, config) {
|
|
5626
5563
|
const mode = (config ?? this.enforcementConfig).toolGuards;
|
|
@@ -5974,6 +5911,37 @@ function createRecursiveWorktreeTool(recursive) {
|
|
|
5974
5911
|
});
|
|
5975
5912
|
}
|
|
5976
5913
|
//#endregion
|
|
5914
|
+
//#region src/recursive_phase.tool.ts
|
|
5915
|
+
/**
|
|
5916
|
+
* recursive_phase (refined LIVE BUG 6): the canonical on-demand home of the
|
|
5917
|
+
* current phase's lint rules + instructions. Reads the same structured source
|
|
5918
|
+
* (runtime.phaseRules -> phaseRulesFor) as the once-per-phase pre-step
|
|
5919
|
+
* reminder, so the agent can re-ask for the rules without re-injecting them on
|
|
5920
|
+
* every step. Returns { error } when no active phase is found.
|
|
5921
|
+
*/
|
|
5922
|
+
function createRecursivePhaseTool(recursive) {
|
|
5923
|
+
return defineTool({
|
|
5924
|
+
name: "recursive_phase",
|
|
5925
|
+
description: "Return the lint rules + instructions for the current recursive-mode phase (required sections, gates, TDD/QA notes). Call once when entering a new phase; the same rules are also auto-injected once per phase transition.",
|
|
5926
|
+
parameters: { runId: {
|
|
5927
|
+
type: "string",
|
|
5928
|
+
description: "Optional run id (defaults to the latest run by mtime)"
|
|
5929
|
+
} },
|
|
5930
|
+
output: {
|
|
5931
|
+
schema: { type: "json" },
|
|
5932
|
+
render: (_args, value) => [{
|
|
5933
|
+
type: "text",
|
|
5934
|
+
text: JSON.stringify(value, null, 2)
|
|
5935
|
+
}]
|
|
5936
|
+
},
|
|
5937
|
+
async execute(args, exec) {
|
|
5938
|
+
const result = await recursive.phaseRules(args.runId, exec.agent);
|
|
5939
|
+
if (!result) return { error: "no recursive phase found" };
|
|
5940
|
+
return result;
|
|
5941
|
+
}
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
//#endregion
|
|
5977
5945
|
//#region src/bootstrap.ts
|
|
5978
5946
|
/**
|
|
5979
5947
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
@@ -6570,8 +6538,8 @@ function registerRecursiveCommand(ctx, recursive) {
|
|
|
6570
6538
|
/**
|
|
6571
6539
|
* fs-intent.ts — filesystem-derived recursive intent (R5 policy-render fix).
|
|
6572
6540
|
*
|
|
6573
|
-
* SP2 zero-emission retired the recursive/phase-intent SESSION EVENT emitter
|
|
6574
|
-
*
|
|
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
|
|
6575
6543
|
* prompt section rendered ''. This module derives the SAME intent from the
|
|
6576
6544
|
* filesystem instead: session cwd -> control-plane root -> enumerate runs ->
|
|
6577
6545
|
* latest run -> current phase (foldRun). Pure read-only fs folding, zero
|
|
@@ -6845,6 +6813,7 @@ function apply(ctx, config) {
|
|
|
6845
6813
|
workspaceRegistry
|
|
6846
6814
|
});
|
|
6847
6815
|
const repairedRoots = /* @__PURE__ */ new Set();
|
|
6816
|
+
const reminderGate = new ReminderOnceGate();
|
|
6848
6817
|
const disposers = [
|
|
6849
6818
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
6850
6819
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
@@ -6852,7 +6821,8 @@ function apply(ctx, config) {
|
|
|
6852
6821
|
ctx.tools.register(createRecursiveLintTool(recursive)),
|
|
6853
6822
|
ctx.tools.register(createRecursiveCloseoutTool(recursive)),
|
|
6854
6823
|
ctx.tools.register(createRecursiveScratchTool(recursive)),
|
|
6855
|
-
ctx.tools.register(createRecursiveWorktreeTool(recursive))
|
|
6824
|
+
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
6825
|
+
ctx.tools.register(createRecursivePhaseTool(recursive))
|
|
6856
6826
|
];
|
|
6857
6827
|
const commands = ctx.get("commands");
|
|
6858
6828
|
if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
|
|
@@ -6923,6 +6893,10 @@ function apply(ctx, config) {
|
|
|
6923
6893
|
kind: "enter",
|
|
6924
6894
|
messages
|
|
6925
6895
|
};
|
|
6896
|
+
if (!reminderGate.shouldInject(root, runId, phase)) return {
|
|
6897
|
+
kind: "enter",
|
|
6898
|
+
messages
|
|
6899
|
+
};
|
|
6926
6900
|
const reminder = phaseLintRulesMessage(phase);
|
|
6927
6901
|
return {
|
|
6928
6902
|
kind: "enter",
|
|
@@ -6954,4 +6928,4 @@ function apply(ctx, config) {
|
|
|
6954
6928
|
});
|
|
6955
6929
|
}
|
|
6956
6930
|
//#endregion
|
|
6957
|
-
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/phase-rules.d.ts
CHANGED
|
@@ -25,10 +25,35 @@ export declare const DIFF_BASIS_FIELDS: string[];
|
|
|
25
25
|
* prior-evidence file set).
|
|
26
26
|
*/
|
|
27
27
|
export declare function getArtifactRequiredSections(fileName: string, workflowProfile?: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* LIVE BUG 6 (0.2.1): dedup gate for the agent/pre-step lint-rules reminder.
|
|
30
|
+
* Keyed by (root, runId, phase) so each new run or phase transition gets its own
|
|
31
|
+
* single injection, while re-arming steps in the SAME phase stay silent. Scoped
|
|
32
|
+
* to the apply() effect/fiber lifetime (one instance per mount), matching the
|
|
33
|
+
* repairedRoots dedup used by the scaffold repair above the injection point.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ReminderOnceGate {
|
|
36
|
+
private seen;
|
|
37
|
+
shouldInject(root: string, runId: string, phase: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Structured, single-source-of-truth rules for one phase (refined LIVE BUG 6
|
|
41
|
+
* design): consumed by BOTH the recursive_phase tool and the once-per-phase
|
|
42
|
+
* pre-step reminder. Derived from the canonical artifact-template sections.
|
|
43
|
+
*/
|
|
44
|
+
export interface PhaseRules {
|
|
45
|
+
fileName: string;
|
|
46
|
+
label: string;
|
|
47
|
+
requiredSections: string[];
|
|
48
|
+
audited: boolean;
|
|
49
|
+
tdd: boolean;
|
|
50
|
+
qa: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function phaseRulesFor(fileName: string, workflowProfile?: string): PhaseRules;
|
|
28
53
|
/**
|
|
29
54
|
* Compact lint-rules message for the pre-step injection (R5): this phase's
|
|
30
|
-
* required sections + phase-specific gate notes,
|
|
31
|
-
*
|
|
32
|
-
*
|
|
55
|
+
* required sections + phase-specific gate notes, built from phaseRulesFor
|
|
56
|
+
* (single source of truth with the recursive_phase tool). Output is unchanged
|
|
57
|
+
* from the prior inline build so r5-parity.spec.ts stays green.
|
|
33
58
|
*/
|
|
34
59
|
export declare function phaseLintRulesMessage(fileName: string, workflowProfile?: string): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
|
+
/**
|
|
3
|
+
* recursive_phase (refined LIVE BUG 6): the canonical on-demand home of the
|
|
4
|
+
* current phase's lint rules + instructions. Reads the same structured source
|
|
5
|
+
* (runtime.phaseRules -> phaseRulesFor) as the once-per-phase pre-step
|
|
6
|
+
* reminder, so the agent can re-ask for the rules without re-injecting them on
|
|
7
|
+
* every step. Returns { error } when no active phase is found.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createRecursivePhaseTool(recursive: RecursiveRuntime): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
package/lib/runtime.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { RecursiveStatusResult } from './types.ts';
|
|
3
3
|
import { type WorkspaceRegistryLike } from './workspace.ts';
|
|
4
|
+
import { type PhaseRules } from './phase-rules.ts';
|
|
4
5
|
import { type ScratchTarget } from './scratch.ts';
|
|
5
6
|
import { type ReviewBundleInput } from './review.ts';
|
|
6
7
|
import { type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
|
|
7
8
|
import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference } from './delegation.ts';
|
|
8
|
-
import { type
|
|
9
|
-
import { type EnforcementConfig, type
|
|
9
|
+
import { type RecursivePhaseState } from './lifecycle.ts';
|
|
10
|
+
import { type EnforcementConfig, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
|
|
10
11
|
import { type CreateWorktreeResult, type PromoteBranchResult } from './worktree.ts';
|
|
11
12
|
declare module '@deepseek-ai/cordis' {
|
|
12
13
|
interface Context {
|
|
@@ -174,6 +175,23 @@ export declare class RecursiveRuntime extends Service {
|
|
|
174
175
|
};
|
|
175
176
|
};
|
|
176
177
|
} | null): Promise<RecursiveStatusResult | null>;
|
|
178
|
+
/**
|
|
179
|
+
* LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
|
|
180
|
+
* the workspace root (same as status/lock), finds the latest run (or the
|
|
181
|
+
* given runId), advances via getNextLegalPhase, and returns the phase's lint
|
|
182
|
+
* rules + instructions. Returns null when no active phase exists. This is the
|
|
183
|
+
* canonical data source for the recursive_phase tool.
|
|
184
|
+
*/
|
|
185
|
+
phaseRules(runId?: string, agent?: {
|
|
186
|
+
session?: {
|
|
187
|
+
header?: {
|
|
188
|
+
cwd?: string;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
} | null): Promise<(PhaseRules & {
|
|
192
|
+
runId: string;
|
|
193
|
+
phase: string;
|
|
194
|
+
}) | null>;
|
|
177
195
|
/**
|
|
178
196
|
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
179
197
|
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
@@ -255,12 +273,6 @@ export declare class RecursiveRuntime extends Service {
|
|
|
255
273
|
};
|
|
256
274
|
};
|
|
257
275
|
} | null): Promise<LintArtifactResult>;
|
|
258
|
-
/** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
|
|
259
|
-
foldPhase(events: readonly SessionEventLike[]): RecursivePhaseState | null;
|
|
260
|
-
validateTransition(intent: PhaseTransitionIntent): GateCheckResult;
|
|
261
|
-
detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null;
|
|
262
|
-
/** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
|
|
263
|
-
gatePreStep(events: readonly SessionEventLike[], config?: EnforcementConfig): PreStepGateDecision;
|
|
264
276
|
/** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
|
|
265
277
|
guardTool(exec: ToolExecLike, root: string, runId: string, config?: EnforcementConfig): ToolGuardDecision;
|
|
266
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
|
@@ -9,7 +9,8 @@ import { createRecursiveLockTool } from './recursive_lock.tool.ts'
|
|
|
9
9
|
import { createRecursiveLintTool } from './recursive_lint.tool.ts'
|
|
10
10
|
import { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
|
|
11
11
|
import { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
|
|
12
|
-
import { createRecursiveWorktreeTool } from './recursive_worktree.tool.ts'
|
|
12
|
+
import { createRecursiveWorktreeTool } from './recursive_worktree.tool.ts'
|
|
13
|
+
import { createRecursivePhaseTool } from './recursive_phase.tool.ts'
|
|
13
14
|
import { registerRecursiveCommand } from './commands.ts'
|
|
14
15
|
import { evaluateToolGuard } from './enforcement.ts'
|
|
15
16
|
import { renderRecursivePolicy } from './policy.ts'
|
|
@@ -18,7 +19,7 @@ import { snapshotWorkspace } from './snapshot.ts'
|
|
|
18
19
|
import { mountRecursiveRoutesOnce, makeRecursiveRoutes, type RecursiveRouteHost } from './live-route.ts'
|
|
19
20
|
import { enumerateRuns, stageBWorkflowInit } from './bootstrap.ts'
|
|
20
21
|
import { getNextLegalPhase, getLockStatus } from './lock.ts'
|
|
21
|
-
import { phaseLintRulesMessage } from './phase-rules.ts'
|
|
22
|
+
import { phaseLintRulesMessage, ReminderOnceGate } from './phase-rules.ts'
|
|
22
23
|
|
|
23
24
|
export const name = '@try-works/dsh-recursive-mode'
|
|
24
25
|
|
|
@@ -29,7 +30,8 @@ export { createRecursiveLockTool } from './recursive_lock.tool.ts'
|
|
|
29
30
|
export { createRecursiveLintTool } from './recursive_lint.tool.ts'
|
|
30
31
|
export { createRecursiveCloseoutTool } from './recursive_closeout.tool.ts'
|
|
31
32
|
export { createRecursiveScratchTool } from './recursive_scratch.tool.ts'
|
|
32
|
-
export { createRecursiveWorktreeTool } from './recursive_worktree.tool.ts'
|
|
33
|
+
export { createRecursiveWorktreeTool } from './recursive_worktree.tool.ts'
|
|
34
|
+
export { createRecursivePhaseTool } from './recursive_phase.tool.ts'
|
|
33
35
|
export * from './status.ts'
|
|
34
36
|
export {
|
|
35
37
|
PHASE_SEQUENCE,
|
|
@@ -101,7 +103,8 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
|
|
|
101
103
|
const workspaceRegistry = ctx.get('workspaceRegistry') as never
|
|
102
104
|
const recursive = new RecursiveRuntime(ctx, { repoRoot: config?.repoRoot ?? process.cwd(), workspaceRegistry })
|
|
103
105
|
|
|
104
|
-
const repairedRoots = new Set<string>()
|
|
106
|
+
const repairedRoots = new Set<string>()
|
|
107
|
+
const reminderGate = new ReminderOnceGate()
|
|
105
108
|
const disposers = [
|
|
106
109
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
107
110
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
@@ -109,7 +112,8 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
|
|
|
109
112
|
ctx.tools.register(createRecursiveLintTool(recursive)),
|
|
110
113
|
ctx.tools.register(createRecursiveCloseoutTool(recursive)),
|
|
111
114
|
ctx.tools.register(createRecursiveScratchTool(recursive)),
|
|
112
|
-
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
115
|
+
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
116
|
+
ctx.tools.register(createRecursivePhaseTool(recursive)),
|
|
113
117
|
]
|
|
114
118
|
|
|
115
119
|
// /recursive command (R4): preset-scoped registration, workspace-scoped dispatch.
|
|
@@ -130,8 +134,9 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
|
|
|
130
134
|
if (!agent) return ''
|
|
131
135
|
// SP3 R5 policy-render fix: derive intent from the FILESYSTEM, not the
|
|
132
136
|
// retired recursive/phase-intent session event (zero-emission removed
|
|
133
|
-
// the emitter
|
|
134
|
-
// 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/*
|
|
135
140
|
// events are appended.
|
|
136
141
|
const intent = fsPolicyIntent(agent, workspaceRegistry as never)
|
|
137
142
|
if (!intent) return ''
|
|
@@ -207,7 +212,11 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
|
|
|
207
212
|
const phasePath = join(runDir, phase)
|
|
208
213
|
const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
|
|
209
214
|
if (status !== 'DRAFT') return { kind: 'enter', messages } as const
|
|
210
|
-
|
|
215
|
+
if (!reminderGate.shouldInject(root, runId, phase)) return { kind: 'enter', messages } as const
|
|
216
|
+
// LIVE BUG 6 (0.2.1): inject the lint-rules reminder AT MOST ONCE PER PHASE.
|
|
217
|
+
// The scaffold repair above is deduped via repairedRoots; the reminder itself was
|
|
218
|
+
// not, so every pre-step while DRAFT re-injected it.
|
|
219
|
+
const reminder = phaseLintRulesMessage(phase)
|
|
211
220
|
return {
|
|
212
221
|
kind: 'enter',
|
|
213
222
|
messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
|
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/phase-rules.ts
CHANGED
|
@@ -240,18 +240,61 @@ export function getArtifactRequiredSections(fileName: string, workflowProfile: s
|
|
|
240
240
|
return headings
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* LIVE BUG 6 (0.2.1): dedup gate for the agent/pre-step lint-rules reminder.
|
|
245
|
+
* Keyed by (root, runId, phase) so each new run or phase transition gets its own
|
|
246
|
+
* single injection, while re-arming steps in the SAME phase stay silent. Scoped
|
|
247
|
+
* to the apply() effect/fiber lifetime (one instance per mount), matching the
|
|
248
|
+
* repairedRoots dedup used by the scaffold repair above the injection point.
|
|
249
|
+
*/
|
|
250
|
+
export class ReminderOnceGate {
|
|
251
|
+
private seen = new Set<string>()
|
|
252
|
+
|
|
253
|
+
shouldInject(root: string, runId: string, phase: string): boolean {
|
|
254
|
+
const key = root + '\u0000' + runId + '\u0000' + phase
|
|
255
|
+
if (this.seen.has(key)) return false
|
|
256
|
+
this.seen.add(key)
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Structured, single-source-of-truth rules for one phase (refined LIVE BUG 6
|
|
263
|
+
* design): consumed by BOTH the recursive_phase tool and the once-per-phase
|
|
264
|
+
* pre-step reminder. Derived from the canonical artifact-template sections.
|
|
265
|
+
*/
|
|
266
|
+
export interface PhaseRules {
|
|
267
|
+
fileName: string
|
|
268
|
+
label: string
|
|
269
|
+
requiredSections: string[]
|
|
270
|
+
audited: boolean
|
|
271
|
+
tdd: boolean
|
|
272
|
+
qa: boolean
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function phaseRulesFor(fileName: string, workflowProfile: string = CURRENT_WORKFLOW_PROFILE): PhaseRules {
|
|
276
|
+
return {
|
|
277
|
+
fileName,
|
|
278
|
+
label: fileName.replace(/\.md$/, ''),
|
|
279
|
+
requiredSections: getArtifactRequiredSections(fileName, workflowProfile),
|
|
280
|
+
audited: AUDITED_PHASE_FILES.has(fileName),
|
|
281
|
+
tdd: fileName === '03-implementation-summary.md',
|
|
282
|
+
qa: fileName === '05-manual-qa.md',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
243
286
|
/**
|
|
244
287
|
* Compact lint-rules message for the pre-step injection (R5): this phase's
|
|
245
|
-
* required sections + phase-specific gate notes,
|
|
246
|
-
*
|
|
247
|
-
*
|
|
288
|
+
* required sections + phase-specific gate notes, built from phaseRulesFor
|
|
289
|
+
* (single source of truth with the recursive_phase tool). Output is unchanged
|
|
290
|
+
* from the prior inline build so r5-parity.spec.ts stays green.
|
|
248
291
|
*/
|
|
249
292
|
export function phaseLintRulesMessage(fileName: string, workflowProfile: string = CURRENT_WORKFLOW_PROFILE): string {
|
|
250
|
-
const
|
|
293
|
+
const rules = phaseRulesFor(fileName, workflowProfile)
|
|
251
294
|
const lines = [
|
|
252
295
|
'<system-reminder>',
|
|
253
296
|
'Recursive-mode phase lint rules for THIS phase (' + fileName + '):',
|
|
254
|
-
'Required sections: ' +
|
|
297
|
+
'Required sections: ' + rules.requiredSections.join(' | '),
|
|
255
298
|
'Gates: Coverage: FAIL until all checkboxes pass; Approval: FAIL until user sign-off; lock only via recursive_lock (monotonic).',
|
|
256
299
|
'Audited phases: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context and Audit Verdict.',
|
|
257
300
|
'TDD (phase 3): declare TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.',
|
|
@@ -259,4 +302,4 @@ export function phaseLintRulesMessage(fileName: string, workflowProfile: string
|
|
|
259
302
|
'</system-reminder>',
|
|
260
303
|
]
|
|
261
304
|
return lines.join('\n')
|
|
262
|
-
}
|
|
305
|
+
}
|
package/src/policy.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function renderRecursivePolicy(context: PolicyContext | null): string {
|
|
|
55
55
|
'- The control-plane root is resolved STRICTLY from this session workspace (never scan other workspaces).',
|
|
56
56
|
'- Scratch is disposable, git-ignored, and never citable as an Input.',
|
|
57
57
|
'- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing.',
|
|
58
|
+
'- Call recursive_phase when entering a new phase; the same rules are auto-injected once per phase transition.',
|
|
58
59
|
];
|
|
59
60
|
|
|
60
61
|
// R5: surface the current phase's required sections + gate checklist.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import type { JsonValue } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import type { RecursiveRuntime } from './runtime.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* recursive_phase (refined LIVE BUG 6): the canonical on-demand home of the
|
|
7
|
+
* current phase's lint rules + instructions. Reads the same structured source
|
|
8
|
+
* (runtime.phaseRules -> phaseRulesFor) as the once-per-phase pre-step
|
|
9
|
+
* reminder, so the agent can re-ask for the rules without re-injecting them on
|
|
10
|
+
* every step. Returns { error } when no active phase is found.
|
|
11
|
+
*/
|
|
12
|
+
export function createRecursivePhaseTool(recursive: RecursiveRuntime) {
|
|
13
|
+
return defineTool({
|
|
14
|
+
name: 'recursive_phase',
|
|
15
|
+
description: 'Return the lint rules + instructions for the current recursive-mode phase (required sections, gates, TDD/QA notes). Call once when entering a new phase; the same rules are also auto-injected once per phase transition.',
|
|
16
|
+
parameters: {
|
|
17
|
+
runId: { type: 'string', description: 'Optional run id (defaults to the latest run by mtime)' },
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
schema: { type: 'json' },
|
|
21
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
22
|
+
},
|
|
23
|
+
async execute(args: { runId?: string }, exec) {
|
|
24
|
+
const result = await recursive.phaseRules(args.runId, exec.agent as { session?: { header?: { cwd?: string } } } | null)
|
|
25
|
+
if (!result) return { error: 'no recursive phase found' } as const
|
|
26
|
+
return result as unknown as JsonValue
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -15,14 +15,15 @@ import {
|
|
|
15
15
|
} from './lock.ts'
|
|
16
16
|
import type { RecursiveStatusResult } from './types.ts'
|
|
17
17
|
import { resolveControlPlaneRoot, type WorkspaceRegistryLike } from './workspace.ts'
|
|
18
|
+
import { phaseRulesFor, type PhaseRules } from './phase-rules.ts'
|
|
18
19
|
import { closeoutPhase } from './closeout.ts'
|
|
19
20
|
import { readScratch, writeScratch, appendScratch, type ScratchTarget } from './scratch.ts'
|
|
20
21
|
import { buildReviewBundle, type ReviewBundleInput } from './review.ts'
|
|
21
22
|
import { createHandoff, createChildBrief, replyPath, childScratchPath, buildDelegationPrompt, type HandoffInput, type ChildBriefInput } from './handoff.ts'
|
|
22
23
|
import { loadRouterPolicy, routerPolicyPath, resolveRole, capabilityProbe, delegationDecisionBasis, type RouterPolicy, type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts'
|
|
23
24
|
import { delegate, validateReferences, writeActionRecord, evaluateDelegationResult, reviewOutputSchema, defaultReviewToolFilter, type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference, type ActionRecordInput } from './delegation.ts'
|
|
24
|
-
import {
|
|
25
|
-
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'
|
|
26
27
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
27
28
|
import { renderRecursivePolicy, type PolicyContext } from './policy.ts'
|
|
28
29
|
import { snapshotWorkspace } from './snapshot.ts'
|
|
@@ -338,6 +339,23 @@ export class RecursiveRuntime extends Service {
|
|
|
338
339
|
return foldRun(resolved.runDir, resolved.runId)
|
|
339
340
|
}
|
|
340
341
|
|
|
342
|
+
/**
|
|
343
|
+
* LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
|
|
344
|
+
* the workspace root (same as status/lock), finds the latest run (or the
|
|
345
|
+
* given runId), advances via getNextLegalPhase, and returns the phase's lint
|
|
346
|
+
* rules + instructions. Returns null when no active phase exists. This is the
|
|
347
|
+
* canonical data source for the recursive_phase tool.
|
|
348
|
+
*/
|
|
349
|
+
async phaseRules(runId?: string, agent?: { session?: { header?: { cwd?: string } } } | null): Promise<(PhaseRules & { runId: string; phase: string }) | null> {
|
|
350
|
+
const root = await this.resolveRootFor(agent)
|
|
351
|
+
if (!root) return null
|
|
352
|
+
const resolved = resolveRunDir(root, runId)
|
|
353
|
+
if (!resolved) return null
|
|
354
|
+
const phase = getNextLegalPhase(resolved.runDir)
|
|
355
|
+
if (!phase) return null
|
|
356
|
+
return { runId: resolved.runId, phase, ...phaseRulesFor(phase) }
|
|
357
|
+
}
|
|
358
|
+
|
|
341
359
|
/**
|
|
342
360
|
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
343
361
|
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
@@ -559,25 +577,6 @@ export class RecursiveRuntime extends Service {
|
|
|
559
577
|
return { artifact: target, runId, errors, warnings, passed: errors.length === 0 }
|
|
560
578
|
}
|
|
561
579
|
|
|
562
|
-
/** Phase C R1/R2: fold the log into phase state; validate a transition intent. */
|
|
563
|
-
foldPhase(events: readonly SessionEventLike[]): RecursivePhaseState | null {
|
|
564
|
-
return foldRecursivePhase(events)
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
validateTransition(intent: PhaseTransitionIntent): GateCheckResult {
|
|
568
|
-
return validateTransition(intent)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
detectTransitionIntent(events: readonly SessionEventLike[]): PhaseTransitionIntent | null {
|
|
572
|
-
return detectTransitionIntent(events)
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** Phase C R3: Layer 1 pre-step gate decision (caller of the transition set). */
|
|
576
|
-
gatePreStep(events: readonly SessionEventLike[], config?: EnforcementConfig): PreStepGateDecision {
|
|
577
|
-
const mode = (config ?? this.enforcementConfig).preStep
|
|
578
|
-
return evaluatePreStepGate(events, mode)
|
|
579
|
-
}
|
|
580
|
-
|
|
581
580
|
/** Phase C R4: Layer 2 tool guard decision (caller of the transition set). */
|
|
582
581
|
guardTool(exec: ToolExecLike, root: string, runId: string, config?: EnforcementConfig): ToolGuardDecision {
|
|
583
582
|
const mode = (config ?? this.enforcementConfig).toolGuards
|