@try-works/dsh-recursive-mode 0.2.0 → 0.2.1
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/index.d.ts +1 -0
- package/lib/index.js +92 -8
- package/lib/phase-rules.d.ts +28 -3
- package/lib/recursive_phase.tool.d.ts +9 -0
- package/lib/runtime.d.ts +18 -0
- package/package.json +1 -1
- package/src/index.ts +14 -6
- 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 +18 -0
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.",
|
|
@@ -4841,7 +4867,8 @@ function renderRecursivePolicy(context) {
|
|
|
4841
4867
|
"- Phase 3 lock requires TDD evidence (strict) or rationale (pragmatic); Phase 5 requires QA sign-off for human/hybrid modes.",
|
|
4842
4868
|
"- The control-plane root is resolved STRICTLY from this session workspace (never scan other workspaces).",
|
|
4843
4869
|
"- Scratch is disposable, git-ignored, and never citable as an Input.",
|
|
4844
|
-
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing."
|
|
4870
|
+
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing.",
|
|
4871
|
+
"- Call recursive_phase when entering a new phase; the same rules are auto-injected once per phase transition."
|
|
4845
4872
|
];
|
|
4846
4873
|
if (currentFile) {
|
|
4847
4874
|
const sections = getArtifactRequiredSections(currentFile, CURRENT_WORKFLOW_PROFILE$1);
|
|
@@ -5383,6 +5410,26 @@ var RecursiveRuntime = class extends Service {
|
|
|
5383
5410
|
return foldRun(resolved.runDir, resolved.runId);
|
|
5384
5411
|
}
|
|
5385
5412
|
/**
|
|
5413
|
+
* LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
|
|
5414
|
+
* the workspace root (same as status/lock), finds the latest run (or the
|
|
5415
|
+
* given runId), advances via getNextLegalPhase, and returns the phase's lint
|
|
5416
|
+
* rules + instructions. Returns null when no active phase exists. This is the
|
|
5417
|
+
* canonical data source for the recursive_phase tool.
|
|
5418
|
+
*/
|
|
5419
|
+
async phaseRules(runId, agent) {
|
|
5420
|
+
const root = await this.resolveRootFor(agent);
|
|
5421
|
+
if (!root) return null;
|
|
5422
|
+
const resolved = resolveRunDir(root, runId);
|
|
5423
|
+
if (!resolved) return null;
|
|
5424
|
+
const phase = getNextLegalPhase(resolved.runDir);
|
|
5425
|
+
if (!phase) return null;
|
|
5426
|
+
return {
|
|
5427
|
+
runId: resolved.runId,
|
|
5428
|
+
phase,
|
|
5429
|
+
...phaseRulesFor(phase)
|
|
5430
|
+
};
|
|
5431
|
+
}
|
|
5432
|
+
/**
|
|
5386
5433
|
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
5387
5434
|
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
5388
5435
|
* recursive-init.py (incl. git-context prefill); later phases carry every
|
|
@@ -5974,6 +6021,37 @@ function createRecursiveWorktreeTool(recursive) {
|
|
|
5974
6021
|
});
|
|
5975
6022
|
}
|
|
5976
6023
|
//#endregion
|
|
6024
|
+
//#region src/recursive_phase.tool.ts
|
|
6025
|
+
/**
|
|
6026
|
+
* recursive_phase (refined LIVE BUG 6): the canonical on-demand home of the
|
|
6027
|
+
* current phase's lint rules + instructions. Reads the same structured source
|
|
6028
|
+
* (runtime.phaseRules -> phaseRulesFor) as the once-per-phase pre-step
|
|
6029
|
+
* reminder, so the agent can re-ask for the rules without re-injecting them on
|
|
6030
|
+
* every step. Returns { error } when no active phase is found.
|
|
6031
|
+
*/
|
|
6032
|
+
function createRecursivePhaseTool(recursive) {
|
|
6033
|
+
return defineTool({
|
|
6034
|
+
name: "recursive_phase",
|
|
6035
|
+
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.",
|
|
6036
|
+
parameters: { runId: {
|
|
6037
|
+
type: "string",
|
|
6038
|
+
description: "Optional run id (defaults to the latest run by mtime)"
|
|
6039
|
+
} },
|
|
6040
|
+
output: {
|
|
6041
|
+
schema: { type: "json" },
|
|
6042
|
+
render: (_args, value) => [{
|
|
6043
|
+
type: "text",
|
|
6044
|
+
text: JSON.stringify(value, null, 2)
|
|
6045
|
+
}]
|
|
6046
|
+
},
|
|
6047
|
+
async execute(args, exec) {
|
|
6048
|
+
const result = await recursive.phaseRules(args.runId, exec.agent);
|
|
6049
|
+
if (!result) return { error: "no recursive phase found" };
|
|
6050
|
+
return result;
|
|
6051
|
+
}
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
//#endregion
|
|
5977
6055
|
//#region src/bootstrap.ts
|
|
5978
6056
|
/**
|
|
5979
6057
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
@@ -6845,6 +6923,7 @@ function apply(ctx, config) {
|
|
|
6845
6923
|
workspaceRegistry
|
|
6846
6924
|
});
|
|
6847
6925
|
const repairedRoots = /* @__PURE__ */ new Set();
|
|
6926
|
+
const reminderGate = new ReminderOnceGate();
|
|
6848
6927
|
const disposers = [
|
|
6849
6928
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
6850
6929
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
@@ -6852,7 +6931,8 @@ function apply(ctx, config) {
|
|
|
6852
6931
|
ctx.tools.register(createRecursiveLintTool(recursive)),
|
|
6853
6932
|
ctx.tools.register(createRecursiveCloseoutTool(recursive)),
|
|
6854
6933
|
ctx.tools.register(createRecursiveScratchTool(recursive)),
|
|
6855
|
-
ctx.tools.register(createRecursiveWorktreeTool(recursive))
|
|
6934
|
+
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
6935
|
+
ctx.tools.register(createRecursivePhaseTool(recursive))
|
|
6856
6936
|
];
|
|
6857
6937
|
const commands = ctx.get("commands");
|
|
6858
6938
|
if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
|
|
@@ -6923,6 +7003,10 @@ function apply(ctx, config) {
|
|
|
6923
7003
|
kind: "enter",
|
|
6924
7004
|
messages
|
|
6925
7005
|
};
|
|
7006
|
+
if (!reminderGate.shouldInject(root, runId, phase)) return {
|
|
7007
|
+
kind: "enter",
|
|
7008
|
+
messages
|
|
7009
|
+
};
|
|
6926
7010
|
const reminder = phaseLintRulesMessage(phase);
|
|
6927
7011
|
return {
|
|
6928
7012
|
kind: "enter",
|
|
@@ -6954,4 +7038,4 @@ function apply(ctx, config) {
|
|
|
6954
7038
|
});
|
|
6955
7039
|
}
|
|
6956
7040
|
//#endregion
|
|
6957
|
-
export { DEFAULT_ENFORCEMENT, LifecycleDriver, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, detectTransitionIntent, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluatePreStepGate, evaluateToolGuard, foldRecursivePhase, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, hasOpenTurn, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
|
7041
|
+
export { DEFAULT_ENFORCEMENT, LifecycleDriver, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, detectTransitionIntent, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluatePreStepGate, evaluateToolGuard, foldRecursivePhase, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, hasOpenTurn, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
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,6 +1,7 @@
|
|
|
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';
|
|
@@ -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
|
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
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.
|
|
@@ -207,7 +211,11 @@ export function apply(ctx: Context, config?: { shellOnly?: boolean; repoRoot?: s
|
|
|
207
211
|
const phasePath = join(runDir, phase)
|
|
208
212
|
const status = existsSync(phasePath) ? getLockStatus(phasePath) : null
|
|
209
213
|
if (status !== 'DRAFT') return { kind: 'enter', messages } as const
|
|
210
|
-
|
|
214
|
+
if (!reminderGate.shouldInject(root, runId, phase)) return { kind: 'enter', messages } as const
|
|
215
|
+
// LIVE BUG 6 (0.2.1): inject the lint-rules reminder AT MOST ONCE PER PHASE.
|
|
216
|
+
// The scaffold repair above is deduped via repairedRoots; the reminder itself was
|
|
217
|
+
// not, so every pre-step while DRAFT re-injected it.
|
|
218
|
+
const reminder = phaseLintRulesMessage(phase)
|
|
211
219
|
return {
|
|
212
220
|
kind: 'enter',
|
|
213
221
|
messages: [...messages, createUserMessage({ content: [{ type: 'text', text: reminder }], source: { kind: 'plugin', plugin: '@try-works/dsh-recursive-mode' } })],
|
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,6 +15,7 @@ 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'
|
|
@@ -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
|