@try-works/dsh-recursive-mode 0.3.0 → 0.4.0
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/README.md +959 -0
- package/lib/client.js +9 -2
- package/lib/closeout-report.d.ts +113 -0
- package/lib/closeout-standards.d.ts +35 -0
- package/lib/closeout.d.ts +12 -0
- package/lib/commands.d.ts +1 -1
- package/lib/config.d.ts +202 -0
- package/lib/delegation.d.ts +123 -3
- package/lib/enforcement.d.ts +90 -1
- package/lib/errors.d.ts +168 -0
- package/lib/git-context.d.ts +17 -0
- package/lib/guard-log.d.ts +39 -0
- package/lib/handoff.d.ts +29 -0
- package/lib/hooks.d.ts +103 -0
- package/lib/identity.d.ts +61 -0
- package/lib/index.d.ts +33 -12
- package/lib/index.js +10017 -3969
- package/lib/job-log.d.ts +34 -0
- package/lib/jobs-runner.d.ts +105 -0
- package/lib/json-safe.d.ts +33 -0
- package/lib/lock.d.ts +42 -0
- package/lib/memory-feedback.d.ts +52 -0
- package/lib/memory-select.d.ts +78 -0
- package/lib/memory.d.ts +137 -0
- package/lib/model-inventory.d.ts +106 -0
- package/lib/phase-graph.d.ts +111 -0
- package/lib/phase-rules.d.ts +67 -8
- package/lib/plan-gate.d.ts +68 -0
- package/lib/policy-globs.d.ts +222 -0
- package/lib/policy-write.d.ts +42 -0
- package/lib/policy.d.ts +39 -0
- package/lib/recursive_ask.tool.d.ts +88 -0
- package/lib/recursive_closeout.tool.d.ts +1 -1
- package/lib/recursive_delegate.tool.d.ts +22 -0
- package/lib/recursive_preview.tool.d.ts +48 -0
- package/lib/recursive_review.tool.d.ts +28 -0
- package/lib/result-cap.d.ts +70 -0
- package/lib/review-round.d.ts +82 -0
- package/lib/review.d.ts +9 -0
- package/lib/role-route.d.ts +122 -0
- package/lib/router.d.ts +90 -5
- package/lib/runtime.d.ts +252 -12
- package/lib/settlement.d.ts +132 -0
- package/lib/skills-phase.d.ts +71 -0
- package/lib/skills.d.ts +70 -0
- package/lib/status.d.ts +53 -1
- package/lib/teams-loop.d.ts +91 -2
- package/lib/training.d.ts +211 -0
- package/lib/ts-lint.d.ts +15 -0
- package/lib/types.d.ts +48 -0
- package/lib/workflow-audit.d.ts +207 -0
- package/package.json +31 -31
- package/preset/recursive.patch.yml +312 -0
- package/scripts/e2e-run.mjs +51 -0
- package/scripts/link-dsh.mjs +233 -0
- package/scripts/live/fake-llm.mjs +150 -0
- package/scripts/live-session-plugin.mjs +179 -0
- package/scripts/live-session-stock.mjs +106 -0
- package/scripts/live-session.mjs +139 -0
- package/skills/recursive-mode/SKILL.md +66 -0
- package/src/client/derive.ts +18 -2
- package/src/closeout-report.ts +274 -0
- package/src/closeout-standards.ts +102 -0
- package/src/closeout.ts +39 -2
- package/src/commands.ts +116 -4
- package/src/config.ts +113 -0
- package/src/delegation.ts +336 -18
- package/src/enforcement.ts +262 -72
- package/src/errors.ts +197 -0
- package/src/git-context.ts +33 -2
- package/src/guard-log.ts +134 -0
- package/src/handoff.ts +62 -0
- package/src/hooks.ts +316 -0
- package/src/identity.ts +230 -0
- package/src/index.ts +394 -20
- package/src/job-log.ts +112 -0
- package/src/jobs-runner.ts +222 -0
- package/src/json-safe.ts +75 -0
- package/src/lock.ts +153 -16
- package/src/memory-feedback.ts +185 -0
- package/src/memory-select.ts +187 -0
- package/src/memory.ts +309 -0
- package/src/model-inventory.ts +196 -0
- package/src/phase-graph.ts +191 -0
- package/src/phase-rules.ts +236 -0
- package/src/plan-gate.ts +111 -0
- package/src/policy-globs.ts +636 -0
- package/src/policy-write.ts +210 -0
- package/src/policy.ts +70 -5
- package/src/recursive_ask.tool.ts +276 -0
- package/src/recursive_audit_team.tool.ts +7 -3
- package/src/recursive_closeout.tool.ts +36 -35
- package/src/recursive_delegate.tool.ts +194 -0
- package/src/recursive_init.tool.ts +4 -3
- package/src/recursive_lint.tool.ts +81 -6
- package/src/recursive_lock.tool.ts +21 -4
- package/src/recursive_phase.tool.ts +3 -2
- package/src/recursive_preview.tool.ts +142 -0
- package/src/recursive_review.tool.ts +190 -0
- package/src/recursive_scratch.tool.ts +5 -4
- package/src/recursive_status.tool.ts +3 -2
- package/src/recursive_worktree.tool.ts +6 -5
- package/src/result-cap.ts +130 -0
- package/src/review-round.ts +335 -0
- package/src/review.ts +17 -3
- package/src/role-route.ts +230 -0
- package/src/router.ts +128 -2
- package/src/runtime.ts +968 -39
- package/src/settlement.ts +355 -0
- package/src/skills-phase.ts +143 -0
- package/src/skills.ts +151 -0
- package/src/snapshot.ts +39 -8
- package/src/status.ts +209 -4
- package/src/teams-loop.ts +223 -9
- package/src/training.ts +565 -0
- package/src/ts-lint.ts +38 -4
- package/src/types.ts +51 -0
- package/src/workflow-audit.ts +288 -0
- package/scripts/install-preset.cmd +0 -7
- package/scripts/install-preset.js +0 -101
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T17 — the phase dependency as a GRAPH, not a flat sequence.
|
|
3
|
+
*
|
|
4
|
+
* WHY. Three core queries in this plugin are graph operations being run over a linear array:
|
|
5
|
+
* `getStaleDownstreamPhases` is a REACHABILITY query, `getPrerequisites` is an IN-EDGE query, and
|
|
6
|
+
* `getNextLegalPhase` is a topological walk. An array can answer all three only while the dependency
|
|
7
|
+
* happens to be linear — and the moment an artifact depends on something LATER than itself (an
|
|
8
|
+
* addendum written to close an upstream gap), the array model answers *wrongly* rather than not at
|
|
9
|
+
* all. That is the case this module exists to get right.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ A HAND-ROLLED GRAPH, DELIBERATELY. The item's note is explicit: Effect ships a stable,
|
|
12
|
+
* runtime-free `Graph` doing exactly this, and adopting it would trade this plugin's zero-dependency
|
|
13
|
+
* posture and its parity goldens for a few hundred lines it does not need. The queries below are
|
|
14
|
+
* small because the graph is small.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ BACK-EDGES ARE FIRST-CLASS, NOT AN ERROR. An `upstream-gap` addendum legitimately points at a
|
|
17
|
+
* LATER artifact: the gap was discovered downstream and must be closed upstream. A depth-first walk
|
|
18
|
+
* with a VISITED set answers reachability over such a graph; a walk without one loops forever, and an
|
|
19
|
+
* array cannot express the edge at all.
|
|
20
|
+
*/
|
|
21
|
+
/** One node: an artifact on disk, or one being asked about before it exists. */
|
|
22
|
+
export interface PhaseNode {
|
|
23
|
+
/** Artifact file name, the node's identity. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Position in the canonical sequence — used for ordering, NOT as the dependency model. */
|
|
26
|
+
index: number;
|
|
27
|
+
/** Whether the artifact is on disk. A queried, absent node emits no prerequisite edge. */
|
|
28
|
+
present?: boolean;
|
|
29
|
+
/** Whether the artifact is locked, when the caller knows. */
|
|
30
|
+
locked?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** One directed edge: `to` depends on `from`. */
|
|
33
|
+
export interface PhaseEdge {
|
|
34
|
+
from: string;
|
|
35
|
+
to: string;
|
|
36
|
+
/** `prerequisite` — an earlier artifact that must be locked first. `addendum` — a cited dependency. */
|
|
37
|
+
kind: 'prerequisite' | 'addendum';
|
|
38
|
+
}
|
|
39
|
+
export interface PhaseGraph {
|
|
40
|
+
nodes: PhaseNode[];
|
|
41
|
+
edges: PhaseEdge[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the graph for one run.
|
|
45
|
+
*
|
|
46
|
+
* `sequence` is the canonical phase order; `present` is what exists on disk, so a phase that was
|
|
47
|
+
* never created is not a node and cannot be waited on forever. `addenda` are the cited dependencies a
|
|
48
|
+
* document declares — including ones pointing FORWARD, which is what makes the graph a graph.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildPhaseGraph(input: {
|
|
51
|
+
sequence: readonly string[];
|
|
52
|
+
present: readonly string[];
|
|
53
|
+
locked?: readonly string[];
|
|
54
|
+
/** Declared dependencies: `from` must be settled before `to`. Order is irrelevant. */
|
|
55
|
+
citations?: ReadonlyArray<{
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Artifacts to answer ABOUT, whether or not they exist yet.
|
|
61
|
+
*
|
|
62
|
+
* ⚠ THIS EXISTS BECAUSE OF A MEASURED PARITY FAILURE, and it is the item's central modelling gap:
|
|
63
|
+
* with nodes limited to what is present, a query about an artifact **that does not exist yet**
|
|
64
|
+
* found no in-edges and answered `[]` — but checking prerequisites **before creating** the artifact
|
|
65
|
+
* is the NORMAL case, so that made every pre-write check silently vacuous. A queried node is
|
|
66
|
+
* therefore a node for EDGE TARGETS ONLY: it emits no outgoing prerequisite edge, because an
|
|
67
|
+
* artifact that does not exist cannot block anything.
|
|
68
|
+
*/
|
|
69
|
+
queried?: readonly string[];
|
|
70
|
+
}): PhaseGraph;
|
|
71
|
+
/** The nodes that must be settled before `id` — the IN-EDGE query. */
|
|
72
|
+
export declare function prerequisitesOf(graph: PhaseGraph, id: string): string[];
|
|
73
|
+
/** The artifacts that depend, directly, on `id` — the OUT-EDGE query. */
|
|
74
|
+
export declare function dependentsOf(graph: PhaseGraph, id: string): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Everything reachable downstream of `id` — the REACHABILITY query behind `getStaleDownstreamPhases`.
|
|
77
|
+
*
|
|
78
|
+
* ⚠ The VISITED set is load-bearing, not defensive: a back-edge makes the graph cyclic, and a walk
|
|
79
|
+
* without a visited set recurses forever on exactly the input this item exists to support.
|
|
80
|
+
*/
|
|
81
|
+
export declare function reachableFrom(graph: PhaseGraph, id: string): string[];
|
|
82
|
+
/**
|
|
83
|
+
* The next phase a run may work on.
|
|
84
|
+
*
|
|
85
|
+
* ⚠ THIS MODELS THE SHIPPED RULES EXACTLY, because `lock.ts` delegates to it and `lock.parity.spec.ts`
|
|
86
|
+
* is the proof. Three of them are easy to miss and are asserted in the spec:
|
|
87
|
+
* 1. a LOCKED node is skipped;
|
|
88
|
+
* 2. a node that is **absent and declared OPTIONAL** is skipped — an optional phase nobody created
|
|
89
|
+
* must not stop the run;
|
|
90
|
+
* 3. if the first node that survives 1 and 2 has a prerequisite that is not LOCKED, the answer is
|
|
91
|
+
* **`null`, NOT the next node** — the run is BLOCKED, and returning the node after it would
|
|
92
|
+
* silently skip a dependency.
|
|
93
|
+
*
|
|
94
|
+
* ⚠ RULE 3 IS WHERE A GRAPH EARNS ITS KEEP, and it is invisible in a linear array: with a back-edge
|
|
95
|
+
* (an `upstream-gap` addendum citing a LATER artifact), an EARLY unlocked node can be blocked by a
|
|
96
|
+
* LATER one — so "continue to the next node" and "blocked, report null" give different answers, and
|
|
97
|
+
* only the second is correct.
|
|
98
|
+
*
|
|
99
|
+
* Nodes must cover the whole sequence for this query (an absent, required phase is a legitimate
|
|
100
|
+
* answer); `queried` is how the caller supplies them.
|
|
101
|
+
*/
|
|
102
|
+
export declare function nextLegalPhase(graph: PhaseGraph, options?: {
|
|
103
|
+
optional?: ReadonlySet<string>;
|
|
104
|
+
}): string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Whether the graph has a back-edge — an edge pointing at a LATER phase than its source.
|
|
107
|
+
*
|
|
108
|
+
* Reported rather than hidden: a back-edge is legitimate (an upstream gap), and it is exactly the
|
|
109
|
+
* shape a linear model cannot represent. A caller that wants to warn about it should be able to ask.
|
|
110
|
+
*/
|
|
111
|
+
export declare function backEdges(graph: PhaseGraph): PhaseEdge[];
|
package/lib/phase-rules.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Phase rules (R5): canonical parity port of lint-recursive-run.py's
|
|
3
|
-
* get_artifact_required_sections + workflow/audit constants. Single source of
|
|
4
|
-
* truth for per-phase required section headings and audit extras; consumed by
|
|
5
|
-
* initRun templates, renderRecursivePolicy, and the pre-step lint-rules
|
|
6
|
-
* injection. Values are byte-identical to the canonical linter (recursive-
|
|
7
|
-
* mode-audit-v2).
|
|
8
|
-
*/
|
|
1
|
+
import { tddEvidenceVerdict as tddEvidenceMatch, type ToolPolicy, type ToolPolicyRule, type Verdict } from './policy-globs.ts';
|
|
9
2
|
export declare const CURRENT_WORKFLOW_PROFILE = "recursive-mode-audit-v2";
|
|
10
3
|
export declare const STRICT_WORKFLOW_PROFILE = "recursive-mode-audit-v1";
|
|
11
4
|
export declare const COMPAT_WORKFLOW_PROFILE = "memory-phase8";
|
|
@@ -57,3 +50,69 @@ export declare function phaseRulesFor(fileName: string, workflowProfile?: string
|
|
|
57
50
|
* from the prior inline build so r5-parity.spec.ts stays green.
|
|
58
51
|
*/
|
|
59
52
|
export declare function phaseLintRulesMessage(fileName: string, workflowProfile?: string): string;
|
|
53
|
+
export declare function policyVerdictRank(verdict: Verdict): number;
|
|
54
|
+
/**
|
|
55
|
+
* The phase number an artifact belongs to: the leading digits of its canonical
|
|
56
|
+
* filename (`03-implementation-summary.md` -> `3`). Returns `''` when the
|
|
57
|
+
* filename carries no phase number, so a caller can skip the baseline rather
|
|
58
|
+
* than guess.
|
|
59
|
+
*/
|
|
60
|
+
export declare function phaseNumberForArtifact(fileName: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* PER-PHASE BASELINE (plan §4 T16). Additive, phase-scoped rules that can only
|
|
63
|
+
* NARROW the global policy. Three honest limits, stated here rather than hidden:
|
|
64
|
+
*
|
|
65
|
+
* 1. The two structures the plan names — phase 6 "writes only under
|
|
66
|
+
* `.recursive/DECISIONS.md`" and phase 8 "only under `.recursive/memory/**`"
|
|
67
|
+
* — as ABSOLUTE path scopes would also forbid the normal artifact edits of
|
|
68
|
+
* those phases, so they are implemented as the narrower rules that bite
|
|
69
|
+
* without blocking the phase's own work: no source-tree writes in phases
|
|
70
|
+
* 6-8 (by then the repo changes are done and the run documents its
|
|
71
|
+
* decisions/state/memory), and the memory planes are read-only in phases
|
|
72
|
+
* 1-2 and 6-7. `deny` on `write` in phase 6 is also deliberately absent,
|
|
73
|
+
* because phase 6's own artifact is `<run>/06-decisions-update.md` and a
|
|
74
|
+
* blanket write denial would make the phase uncompletable.
|
|
75
|
+
* 2. Narrowing is enforced MECHANICALLY in withPhaseBaseline, not by trusting
|
|
76
|
+
* this table: a phase rule whose verdict is ranked above the strictest
|
|
77
|
+
* global verdict for the same pattern — which would widen `ask` to `allow`,
|
|
78
|
+
* or soften a global `deny` — is DROPPED before it can be evaluated.
|
|
79
|
+
* 3. No rule below uses the bare catch-all `*`. A phase rule of that shape
|
|
80
|
+
* would outrank every specific global rule at once (precedence is
|
|
81
|
+
* specificity-first), including `recursive_lock*`, and would deny the phase
|
|
82
|
+
* its own tools. The write-tool family is spelled `write*`.
|
|
83
|
+
*
|
|
84
|
+
* Phase 3's TDD-evidence rule is the one the plan states verbatim: a phase-3
|
|
85
|
+
* lock is denied until RED + GREEN evidence exists. It is reached for the phase
|
|
86
|
+
* whose artifact is the current one, i.e. the phase-3 lock itself — before that
|
|
87
|
+
* point the earlier phases are still unlocked, so the global lock-order rule
|
|
88
|
+
* already refuses.
|
|
89
|
+
*/
|
|
90
|
+
export declare function phaseBaselineRules(fileName: string): ToolPolicyRule[];
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a tool-target path to an absolute path. Mirrors enforcement.ts's
|
|
93
|
+
* resolution rules: an absolute path stays as it is; a relative path resolves
|
|
94
|
+
* against the worktree root. `null` when the value cannot be a path at all.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveFrom(worktreeRoot: string, target: string): string | null;
|
|
97
|
+
/** The tool-target path of a call (same key order enforcement.ts uses). */
|
|
98
|
+
export declare function policyTargetPath(args: Record<string, unknown>): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* TDD evidence predicate for a phase-3 lock: `deny` when the artifact declares
|
|
101
|
+
* `TDD Mode: strict` without both RED and GREEN evidence, `null` otherwise
|
|
102
|
+
* (nothing to say — the global rules still apply). Lives in `policy-globs.ts`
|
|
103
|
+
* beside the built-in list, which carries the same `tdd-evidence` guard rule.
|
|
104
|
+
*/
|
|
105
|
+
export declare const tddEvidenceVerdict: typeof tddEvidenceMatch;
|
|
106
|
+
/**
|
|
107
|
+
* Compose the effective policy for a phase: the phase baseline rules FIRST (so
|
|
108
|
+
* a narrowing rule is reached before the global rule it tightens), then the
|
|
109
|
+
* global rules.
|
|
110
|
+
*
|
|
111
|
+
* NARROWING IS MECHANICAL, not a convention this table is trusted to respect. A
|
|
112
|
+
* baseline rule whose verdict is ranked ABOVE the strictest global verdict for
|
|
113
|
+
* the same pattern is DROPPED before it can be evaluated, so no per-phase
|
|
114
|
+
* baseline can turn a global `deny` into an `allow` (or an `ask` into an
|
|
115
|
+
* `allow`). A baseline rule for a pattern the global policy does not mention is
|
|
116
|
+
* always kept: it can only add a restriction.
|
|
117
|
+
*/
|
|
118
|
+
export declare function withPhaseBaseline(policy: ToolPolicy, fileName: string, runDir?: string): ToolPolicy;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T13 — planMode for the discovery phases, and the gate out of them.
|
|
3
|
+
*
|
|
4
|
+
* WHY. Requirements / AS-IS / TO-BE-Plan are NON-MUTATING discovery. Running them under the
|
|
5
|
+
* harness's planMode enforces "plan before implement" at the harness level instead of by
|
|
6
|
+
* convention: a phase that cannot write cannot quietly start implementing while it is still
|
|
7
|
+
* deciding what to implement. Convention is what fails at 2am; a mode is what holds.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ THE PHASE-FORM TRAP, which is why this module exists rather than a one-line comparison.
|
|
10
|
+
* This codebase names a phase THREE ways — `'2'`, `'02'`, and the artifact filename
|
|
11
|
+
* `'02-to-be-plan.md'` — and a mapping that understood only one of them would return `false`
|
|
12
|
+
* for the others. That failure is SILENT and in the dangerous direction: it turns the
|
|
13
|
+
* non-mutating guarantee OFF for exactly the phases that need it. So the index is parsed from
|
|
14
|
+
* the leading numeric run, and every form lands on the same answer.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ AN UNRECOGNISED PHASE ANSWERS `false`, deliberately. Claiming plan mode for a phase we
|
|
17
|
+
* cannot identify would be guessing about the one property this module exists to guarantee;
|
|
18
|
+
* the safe reading of "I do not know what this is" is "do not assert a mode about it". The
|
|
19
|
+
* caller that needs certainty has {@link phaseIndexOf} and can refuse instead.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The numeric index of a phase, from any of the forms this codebase uses, or `null` when the
|
|
23
|
+
* input names no phase at all.
|
|
24
|
+
*
|
|
25
|
+
* Takes the leading numeric run, so `'2'`, `'02'`, `' 2 '` and `'02-to-be-plan.md'` all give
|
|
26
|
+
* `2`, and `'01.5-root-cause.md'` gives `1.5` — the fractional sub-phase keeps its position
|
|
27
|
+
* between 01 and 02, which is what makes `phaseUsesPlanMode` include it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function phaseIndexOf(phase: string): number | null;
|
|
30
|
+
/**
|
|
31
|
+
* The exclusive upper bound of the discovery block: phases below this are non-mutating.
|
|
32
|
+
*
|
|
33
|
+
* 3 = implementation. Everything strictly below it (0 requirements, 1 AS-IS, 1.5 root cause,
|
|
34
|
+
* 2 TO-BE plan) is discovery, which is the item's "00-02" plus the sub-phase that sits inside
|
|
35
|
+
* that range.
|
|
36
|
+
*/
|
|
37
|
+
export declare const IMPLEMENTATION_PHASE_INDEX = 3;
|
|
38
|
+
/** True when the phase is discovery and must run non-mutating. */
|
|
39
|
+
export declare function phaseUsesPlanMode(phase: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* True when a transition LEAVES the discovery block — the plan gate.
|
|
42
|
+
*
|
|
43
|
+
* The gate is on the DESTINATION, not the distance: `02 -> 03` and `02 -> 04` both need an
|
|
44
|
+
* approved plan, because skipping ahead does not make the plan less necessary. A transition
|
|
45
|
+
* that stays inside discovery (0 -> 1) is not gated, and one that starts after the boundary
|
|
46
|
+
* (3 -> 4) is not either — the plan was already required to get there.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planGateRequired(from: string, to: string): boolean;
|
|
49
|
+
/** A one-line, board-facing statement of what the phase owes, for a phase rules section. */
|
|
50
|
+
export declare function describePlanMode(phase: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* T13 — should `exit_plan_mode` be allowed, given the phase the run is WAITING on?
|
|
53
|
+
*
|
|
54
|
+
* The run's "next legal phase" is exactly the right input: while it is still a discovery
|
|
55
|
+
* phase, the plan is not finished and leaving plan mode would start implementing on an
|
|
56
|
+
* unfinished plan; once it is an implementation phase, discovery is done and the gate is
|
|
57
|
+
* open. So the gate needs no separate state — it reads the state the run already keeps, and
|
|
58
|
+
* that is why it cannot drift from the workflow.
|
|
59
|
+
*
|
|
60
|
+
* ⚠ A NULL next phase means "nothing is pending", which is NOT the same as "discovery is
|
|
61
|
+
* done". It allows the exit, because refusing forever on a completed or unrecognised run
|
|
62
|
+
* would make plan mode a trap rather than a gate — and the reason is stated so a reader can
|
|
63
|
+
* tell the two cases apart.
|
|
64
|
+
*/
|
|
65
|
+
export declare function planGateForExit(nextPhase: string | null): {
|
|
66
|
+
allow: boolean;
|
|
67
|
+
reason: string;
|
|
68
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/** The three verdicts a rule may carry. */
|
|
2
|
+
export type Verdict = 'allow' | 'deny' | 'ask';
|
|
3
|
+
/**
|
|
4
|
+
* The policy engine's own decision shape (no `warn`/`transition` — those are the
|
|
5
|
+
* guard's, see enforcement.ts). `rule` is the `label` of the rule that decided
|
|
6
|
+
* the call, so a verdict is traceable to an auditable line in the policy file;
|
|
7
|
+
* it is absent when the no-match default decided.
|
|
8
|
+
*/
|
|
9
|
+
export interface Decision {
|
|
10
|
+
kind: Verdict;
|
|
11
|
+
reason?: string;
|
|
12
|
+
rule?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extra facts a rule predicate may need. `args` is always the tool call's
|
|
16
|
+
* arguments; the run coordinates are present only when the caller has them
|
|
17
|
+
* (a guard call from a real session does; a pure policy unit test need not).
|
|
18
|
+
*/
|
|
19
|
+
export interface ToolPolicyContext {
|
|
20
|
+
args: Record<string, unknown>;
|
|
21
|
+
runDir?: string;
|
|
22
|
+
runId?: string;
|
|
23
|
+
worktreeRoot?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What a rule predicate returns when the condition it guards DOES apply:
|
|
27
|
+
* the verdict, plus an optional `detail` appended to the rule's own `reason`.
|
|
28
|
+
* The rule keeps the static, auditable sentence (the same sentence the policy
|
|
29
|
+
* file and the built-in list carry, so the two cannot drift); the predicate adds
|
|
30
|
+
* only the per-call particular — which artifact blocked the lock, and its status.
|
|
31
|
+
*
|
|
32
|
+
* `null` means the condition does NOT apply, and the engine falls through to the
|
|
33
|
+
* NEXT rule. That fall-through is how an id-glob rule such as `write` can guard
|
|
34
|
+
* write tools without deciding for an ordinary write that violates nothing
|
|
35
|
+
* (which a bare pattern match would otherwise deny).
|
|
36
|
+
*/
|
|
37
|
+
export interface ToolPolicyPredicateMatch {
|
|
38
|
+
verdict: Verdict;
|
|
39
|
+
detail?: string;
|
|
40
|
+
}
|
|
41
|
+
export type ToolPolicyPredicate = (id: string, args: Record<string, unknown>, ctx: ToolPolicyContext) => ToolPolicyPredicateMatch | null;
|
|
42
|
+
export interface ToolPolicyRule {
|
|
43
|
+
pattern: string;
|
|
44
|
+
verdict: Verdict;
|
|
45
|
+
reason: string;
|
|
46
|
+
/** The guard's machine-readable rule label for a decision this rule makes. */
|
|
47
|
+
label?: string;
|
|
48
|
+
predicate?: ToolPolicyPredicate;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The `defaults` block of a policy file. `no_match_verdict` is documented for a
|
|
52
|
+
* human reader and may only ever be `"ask"`: the no-match default is a property
|
|
53
|
+
* of the engine, not a knob, because a configurable one would let a policy file
|
|
54
|
+
* silently allow (or deny) every unlisted tool.
|
|
55
|
+
*/
|
|
56
|
+
export interface ToolPolicyFileDefaults {
|
|
57
|
+
no_match_verdict?: 'ask';
|
|
58
|
+
}
|
|
59
|
+
export interface ToolPolicy {
|
|
60
|
+
version: number;
|
|
61
|
+
description?: string;
|
|
62
|
+
rules: ToolPolicyRule[];
|
|
63
|
+
}
|
|
64
|
+
/** Where the shipped default policy lives, relative to the worktree root. */
|
|
65
|
+
export declare const TOOL_POLICY_RELATIVE_PATH = ".recursive/config/recursive-permissions.json";
|
|
66
|
+
/** Absolute path of the policy file for a worktree. */
|
|
67
|
+
export declare function toolPolicyPath(worktreeRoot: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* The syntax error in `pattern`, or null when it is well formed.
|
|
70
|
+
*
|
|
71
|
+
* Beyond the character rule there are two STRUCTURAL rules, because a pattern
|
|
72
|
+
* that a human reads as "the worker tools" must not be a silent no-op:
|
|
73
|
+
* - a pattern may not END with a separator (`worker::`, `a.`, `a:`) — a
|
|
74
|
+
* dangling separator names nothing, so it would sit in the file looking like
|
|
75
|
+
* a rule while matching no tool id at all;
|
|
76
|
+
* - a pattern may not START with one (`.foo`), for the same reason.
|
|
77
|
+
* `worker::*` is unaffected: it ends with the wildcard, which is how the id-glob
|
|
78
|
+
* form is written.
|
|
79
|
+
*/
|
|
80
|
+
export declare function patternSyntaxError(pattern: unknown): string | null;
|
|
81
|
+
export declare function isValidGlobPattern(pattern: unknown): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Glob-match a tool id. Every `*` matches strictly up to the next literal, so
|
|
84
|
+
* the matcher is anchored at both ends: `worker::*` does NOT match `xworker::y`.
|
|
85
|
+
* Single-restart backtracking, no recursion.
|
|
86
|
+
*/
|
|
87
|
+
export declare function globMatch(pattern: string, id: string): boolean;
|
|
88
|
+
/** The first syntax/contract problem in a policy, in rule order. */
|
|
89
|
+
export declare function firstPolicyDefect(policy: ToolPolicy): string | null;
|
|
90
|
+
/** The fail-closed decision: everything is denied, and the defect is named. */
|
|
91
|
+
export declare function failClosedDecision(defect: string): Decision;
|
|
92
|
+
/** True when the pattern is the bare catch-all (`*`), which is least specific. */
|
|
93
|
+
export declare function isCatchAllPattern(pattern: unknown): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Evaluation PRECEDENCE: SPECIFICITY FIRST, THEN FILE ORDER.
|
|
96
|
+
*
|
|
97
|
+
* T16 asks for "deny wins over allow" AND for "first-match-wins", which cannot
|
|
98
|
+
* both hold under adversarial ordering. The reconciliation — and the semantics
|
|
99
|
+
* this engine implements — is:
|
|
100
|
+
*
|
|
101
|
+
* 1. Among the rules whose pattern MATCHES the call, a rule with a MORE
|
|
102
|
+
* SPECIFIC pattern outranks one with a less specific pattern. So a
|
|
103
|
+
* catch-all `*` allow can never shield an id from a specific `deny` written
|
|
104
|
+
* BELOW it.
|
|
105
|
+
* 2. Among equally specific matching rules, the EARLIER rule in the file wins.
|
|
106
|
+
* 3. A rule whose predicate ABSTAINS (`null`) does not participate at all: it
|
|
107
|
+
* is as if the rule were not in the file for this call, so the next rule in
|
|
108
|
+
* precedence order decides.
|
|
109
|
+
*
|
|
110
|
+
* Why not plain first-match-wins: it would make "deny wins over allow" merely a
|
|
111
|
+
* property of how the file happens to be ordered. A hand-edit that put a
|
|
112
|
+
* catch-all allow at the top would then silently disable every deny in the file,
|
|
113
|
+
* which is precisely the failure the phrase exists to prevent.
|
|
114
|
+
*
|
|
115
|
+
* Specificity tiers, most to least:
|
|
116
|
+
* 3 an EXACT id (`write`) — the narrowest thing a pattern can name;
|
|
117
|
+
* 2 a PREFIX glob (`worker::*`, `recursive_*`) — still names a family;
|
|
118
|
+
* 1 a glob with a wildcard in the middle (`*_write`) — matched anywhere in
|
|
119
|
+
* the id, so it is narrower than the bare catch-all but names no family;
|
|
120
|
+
* 0 the bare catch-all (`*`).
|
|
121
|
+
* These are TIERS, not a fine-grained metric: patterns within a tier are
|
|
122
|
+
* "equally specific" and are decided by file order.
|
|
123
|
+
*/
|
|
124
|
+
export declare function patternSpecificity(pattern: string): number;
|
|
125
|
+
/**
|
|
126
|
+
* Evaluate a tool call against a policy. PURE: no filesystem, no config, no
|
|
127
|
+
* caching. The loader resolves WHICH policy applies; this decides what it says,
|
|
128
|
+
* using the precedence above.
|
|
129
|
+
*/
|
|
130
|
+
export declare function evaluateToolPolicy(policy: ToolPolicy, id: string, args?: Record<string, unknown>, ctx?: ToolPolicyContext): Decision;
|
|
131
|
+
export interface ToolPolicyLoadResult {
|
|
132
|
+
ok: boolean;
|
|
133
|
+
/** `file` when a policy file was found, `builtin` when it was ABSENT. */
|
|
134
|
+
source: 'file' | 'builtin';
|
|
135
|
+
policy: ToolPolicy;
|
|
136
|
+
/** Absolute path of the policy file, when one was found. */
|
|
137
|
+
path?: string;
|
|
138
|
+
/** Why the file could not be used, when `ok` is false. */
|
|
139
|
+
error?: string;
|
|
140
|
+
}
|
|
141
|
+
/** Wire the built-in default policy used when no policy file exists. */
|
|
142
|
+
export declare function setBuiltInToolPolicy(factory: () => ToolPolicy): void;
|
|
143
|
+
/** The built-in policy in force for the absent-file case. */
|
|
144
|
+
export declare function builtInToolPolicy(): ToolPolicy;
|
|
145
|
+
/** Tool names the locked-artifact write rule guards. */
|
|
146
|
+
export declare const WRITE_TOOL_NAMES: Set<string>;
|
|
147
|
+
/** Tool names the monotonic lock-order rule guards. */
|
|
148
|
+
export declare const LOCK_TOOL_NAMES: Set<string>;
|
|
149
|
+
/**
|
|
150
|
+
* The BUILT-IN default rule list — the pre-T16 guard behaviour expressed as
|
|
151
|
+
* data:
|
|
152
|
+
*
|
|
153
|
+
* `recursive_lock*` -> the monotonic lock-order denial;
|
|
154
|
+
* the write-tool ids -> the locked-artifact write denial;
|
|
155
|
+
* `*` -> allow, so an ordinary tool is not turned into an `ask`.
|
|
156
|
+
*
|
|
157
|
+
* Every `deny` precedes the `allow`, which is what makes "deny wins over allow" a
|
|
158
|
+
* fact about the list rather than a hope. The predicates return `null` when
|
|
159
|
+
* their condition does not apply, so a matched-but-clean write falls through to
|
|
160
|
+
* the allow instead of being denied by the pattern alone.
|
|
161
|
+
*
|
|
162
|
+
* `.recursive/config/recursive-permissions.json` mirrors this list in the same
|
|
163
|
+
* order with the same reasons: a reviewer reads either one and predicts the same
|
|
164
|
+
* verdict.
|
|
165
|
+
*/
|
|
166
|
+
export declare function builtInToolPolicyRules(): ToolPolicyRule[];
|
|
167
|
+
/** The built-in default policy (the absent-file fallback), as a policy object. */
|
|
168
|
+
export declare function builtInToolPolicyDefault(): ToolPolicy;
|
|
169
|
+
/**
|
|
170
|
+
* Attach the CODE-side condition to a rule that was authored in JSON.
|
|
171
|
+
*
|
|
172
|
+
* This is the seam that makes a policy file honest. JSON cannot carry a
|
|
173
|
+
* function, so a `deny` rule read from a file has no way to know whether its
|
|
174
|
+
* condition actually holds — and an unguarded `deny` is an UNCONDITIONAL denial,
|
|
175
|
+
* i.e. a policy file that denies locks whose prerequisites are met and writes to
|
|
176
|
+
* artifacts that are not locked. The pattern identifies the rule's subject, so
|
|
177
|
+
* the pattern is what selects the condition:
|
|
178
|
+
*
|
|
179
|
+
* `recursive_lock*` -> the monotonic lock-order condition;
|
|
180
|
+
* any write-tool id -> the locked-artifact condition.
|
|
181
|
+
*
|
|
182
|
+
* A pattern the code knows nothing about keeps NO condition and is therefore an
|
|
183
|
+
* absolute verdict — which is the honest reading of a rule like
|
|
184
|
+
* `{"pattern": "run_code", "verdict": "deny"}` with no lock condition behind it.
|
|
185
|
+
*/
|
|
186
|
+
export declare function attachPolicyPredicate(rule: ToolPolicyRule): ToolPolicyRule;
|
|
187
|
+
/**
|
|
188
|
+
* TDD evidence predicate for a phase-3 lock (the `tdd-evidence` guard rule): the
|
|
189
|
+
* denial match when the artifact declares `TDD Mode: strict` without both RED
|
|
190
|
+
* and GREEN evidence, `null` otherwise — nothing to say, so the global lock rules
|
|
191
|
+
* still apply. `phase-rules.ts` uses it for the phase-3 baseline.
|
|
192
|
+
*/
|
|
193
|
+
export declare function tddEvidenceVerdict(artifact: string, runDir: string | undefined): ToolPolicyPredicateMatch | null;
|
|
194
|
+
/**
|
|
195
|
+
* Read and validate the policy file. NEVER throws: an unreadable or malformed
|
|
196
|
+
* file yields `ok: false` plus a fail-closed policy, so the guard reasons about
|
|
197
|
+
* it instead of crashing the tools/pre-execute listener.
|
|
198
|
+
*/
|
|
199
|
+
export declare function loadToolPolicyFile(worktreeRoot: string): ToolPolicyLoadResult;
|
|
200
|
+
/**
|
|
201
|
+
* Parse policy JSON into a policy. A parse/validation failure does NOT throw and
|
|
202
|
+
* does NOT return an empty rule list (which would mean "ask everything" and hide
|
|
203
|
+
* the defect) — it returns a single deny-all rule carrying the defect, plus
|
|
204
|
+
* `ok: false` and the same defect as `error`, so a caller can tell a healthy
|
|
205
|
+
* policy from a fail-closed one WITHOUT having to compare reasons. Both the
|
|
206
|
+
* malformed-JSON case and the PRESENT-but-empty case are `ok: false`.
|
|
207
|
+
*/
|
|
208
|
+
export declare function parseToolPolicy(raw: string, path?: string): ToolPolicyParseResult;
|
|
209
|
+
export interface ToolPolicyParseResult {
|
|
210
|
+
ok: boolean;
|
|
211
|
+
policy: ToolPolicy;
|
|
212
|
+
/** The defect the policy failed closed on, when `ok` is false. */
|
|
213
|
+
error?: string;
|
|
214
|
+
}
|
|
215
|
+
/** A one-rule policy denying everything, with the defect as its reason. */
|
|
216
|
+
export declare function failClosedPolicy(defect: string): ToolPolicy;
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the effective policy for a worktree: the file when present (even when
|
|
219
|
+
* broken — a present-but-invalid file fails closed), the built-in list when
|
|
220
|
+
* absent. Thin, so callers never re-implement the absent/present distinction.
|
|
221
|
+
*/
|
|
222
|
+
export declare function resolveToolPolicy(worktreeRoot: string): ToolPolicy;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Which level a write targets. The narrowest matching level wins at resolution time. */
|
|
2
|
+
export type SelectionScope = 'general' | 'phase' | 'role';
|
|
3
|
+
/** What to write. Every field is optional; omitting one leaves that field alone. */
|
|
4
|
+
export interface SelectionPatch {
|
|
5
|
+
scope: SelectionScope;
|
|
6
|
+
/** Required for `phase` and `role`. */
|
|
7
|
+
phase?: string;
|
|
8
|
+
/** Required for `role`. */
|
|
9
|
+
role?: string;
|
|
10
|
+
/** The SUBAGENT provider — who creates the child. */
|
|
11
|
+
provider?: string;
|
|
12
|
+
/** The model id. */
|
|
13
|
+
model?: string;
|
|
14
|
+
/** The LLM provider — who SERVES the model. Not the same thing as `provider` above. */
|
|
15
|
+
modelProvider?: string;
|
|
16
|
+
/** Remove the named fields instead of setting them. With no field names, removes the whole level. */
|
|
17
|
+
clear?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface SelectionWriteResult {
|
|
20
|
+
path: string;
|
|
21
|
+
/** One sentence, naming the level, what changed, and the rule that history is untouched. */
|
|
22
|
+
message: string;
|
|
23
|
+
/** Whether anything actually changed — a no-op write still rewrites the file, but says it was already so. */
|
|
24
|
+
changed: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Apply one patch to a policy object IN PLACE. Returns whether anything changed, or a refusal reason. */
|
|
27
|
+
export declare function applySelectionPatch(policy: Record<string, unknown>, patch: SelectionPatch): {
|
|
28
|
+
changed: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
refused: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Write a selection patch to a workspace's policy file.
|
|
34
|
+
*
|
|
35
|
+
* Returns a refusal instead of throwing when the patch is incomplete, and a message that always states the rule
|
|
36
|
+
* the objective names: this applies to future delegations and never rewrites a run's recorded history.
|
|
37
|
+
*/
|
|
38
|
+
export declare function writePolicySelection(root: string, patch: SelectionPatch, options?: {
|
|
39
|
+
path?: string;
|
|
40
|
+
}): SelectionWriteResult | {
|
|
41
|
+
refused: string;
|
|
42
|
+
};
|
package/lib/policy.d.ts
CHANGED
|
@@ -10,3 +10,42 @@ export interface PolicyContext {
|
|
|
10
10
|
* Render the current-phase contract. Empty string when no run is active.
|
|
11
11
|
*/
|
|
12
12
|
export declare function renderRecursivePolicy(context: PolicyContext | null): string;
|
|
13
|
+
/**
|
|
14
|
+
* T22 — the STABLE contract: the part of the policy section that does not depend on the phase.
|
|
15
|
+
*
|
|
16
|
+
* WHY A SPLIT AT ALL, given the item's own premise check: the mechanism this was modelled on
|
|
17
|
+
* (iii's `system_sections` + `cache_boundary` + `cache_intent.surface_digest`) does **not** exist in
|
|
18
|
+
* DSH as a plugin-visible seam — the harness's only cache concepts live in the provider layer
|
|
19
|
+
* (`llm-pi-ai` accepts prompt-cache MARKERS and a retention preference), and whether any provider
|
|
20
|
+
* caches the prefix is **provider-side and unverified**. The item **withdrew its "largest cost
|
|
21
|
+
* lever" label for exactly that reason**, and this module keeps only what survives the check:
|
|
22
|
+
* **a byte-identical prefix is a PRECONDITION for any provider-side caching**, whatever the provider
|
|
23
|
+
* does with it — and a stable prefix costs nothing to produce.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ THE CONTRACT DEPENDS ON THE CONFIG, NOT ON THE PHASE, and that distinction is the whole point:
|
|
26
|
+
* the enforcement modes and the lock rules are fixed for a RUN, so they belong in the prefix; the
|
|
27
|
+
* current phase, its required sections and its gate checklist change per phase and belong in the
|
|
28
|
+
* tail. A prefix that varied with the phase would be stable in name only.
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderStableContract(config?: EnforcementConfig): string;
|
|
31
|
+
/**
|
|
32
|
+
* T22 — a LOCAL identifier for the contract, not a cache directive.
|
|
33
|
+
*
|
|
34
|
+
* The item's rescope is explicit that the digest is worth computing as a **local identifier**: it is
|
|
35
|
+
* stable within a run and changes when the contract changes, which is what makes "did the contract
|
|
36
|
+
* change under me?" answerable from the prompt alone. It claims **nothing** about provider caching —
|
|
37
|
+
* a digest that implied one would be the withdrawn label wearing a hash.
|
|
38
|
+
*/
|
|
39
|
+
export declare function contractDigest(config?: EnforcementConfig): string;
|
|
40
|
+
/**
|
|
41
|
+
* T22 — the per-phase TAIL: everything that legitimately changes between phases.
|
|
42
|
+
*
|
|
43
|
+
* Kept as its own name so the split is a fact in the code rather than a convention: a caller that
|
|
44
|
+
* wants a cacheable prefix takes {@link renderStableContract} and puts this after it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function renderPhaseTail(phase: {
|
|
47
|
+
label: string;
|
|
48
|
+
phaseName: string;
|
|
49
|
+
status: string;
|
|
50
|
+
key?: string;
|
|
51
|
+
} | null): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
|
+
/** The identifiers the workflow uses for its three human gates. */
|
|
3
|
+
export declare const ASK_GATE_IDS: readonly ["tdd-mode", "qa-signoff", "gate-block"];
|
|
4
|
+
export type AskGateId = (typeof ASK_GATE_IDS)[number];
|
|
5
|
+
/** The plugin's own documented bounds — see the module comment on why these are not a claimed mirror. */
|
|
6
|
+
export declare const MAX_HEADER_CHARS = 12;
|
|
7
|
+
export declare const MAX_LABEL_CHARS = 30;
|
|
8
|
+
export declare const MAX_DESCRIPTION_CHARS = 200;
|
|
9
|
+
export interface AskOption {
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
/** A structured question, in the shape the ask flow renders as a card. */
|
|
14
|
+
export interface AskQuestion {
|
|
15
|
+
id: string;
|
|
16
|
+
header: string;
|
|
17
|
+
question: string;
|
|
18
|
+
options: AskOption[];
|
|
19
|
+
}
|
|
20
|
+
/** One of the three gates, as data: what to ask, what the options mean, and what the answer means. */
|
|
21
|
+
export interface AskGate {
|
|
22
|
+
id: AskGateId;
|
|
23
|
+
header: string;
|
|
24
|
+
question: string;
|
|
25
|
+
options: AskOption[];
|
|
26
|
+
/** The artifact marker name the answer is written under. */
|
|
27
|
+
marker: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const ASK_GATES: Record<AskGateId, AskGate>;
|
|
30
|
+
/** Thrown for a request the ask flow would refuse; carries the FAILING FIELD PATH. */
|
|
31
|
+
export declare class AskValidationError extends Error {
|
|
32
|
+
readonly field: string;
|
|
33
|
+
constructor(field: string, message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate a question and return it unchanged.
|
|
37
|
+
*
|
|
38
|
+
* The FIELD PATH travels in the error because a caller fixing an over-long header needs to know
|
|
39
|
+
* WHICH field — a bare "too long" for a request with four fields is a puzzle, not a diagnostic.
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateAskQuestion(question: AskQuestion): AskQuestion;
|
|
42
|
+
/** Build the question for a gate, validated. */
|
|
43
|
+
export declare function buildAskQuestion(gateId: AskGateId): AskQuestion;
|
|
44
|
+
/**
|
|
45
|
+
* Validate an answer against its gate.
|
|
46
|
+
*
|
|
47
|
+
* An answer that is not one of the offered labels is REFUSED rather than recorded: a marker saying
|
|
48
|
+
* `TDD Mode: maybe` would look like a decision and be a transcription error.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateAskAnswer(gateId: AskGateId, answer: string): string;
|
|
51
|
+
/** The durable marker an accepted answer is written back as — a fact, not a chat aside. */
|
|
52
|
+
export declare function answerMarker(gateId: AskGateId, answer: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* One-ask-per-step guard.
|
|
55
|
+
*
|
|
56
|
+
* A step that asks twice yields two cards for one decision and the second answer silently wins —
|
|
57
|
+
* so the second ask is REFUSED, naming the gate already asked this step.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createAskLedger(): {
|
|
60
|
+
claim(gateId: AskGateId): void;
|
|
61
|
+
asked(): AskGateId[];
|
|
62
|
+
};
|
|
63
|
+
/** Which artifact each gate's answer belongs in, when the caller does not name one. */
|
|
64
|
+
export declare const GATE_DEFAULT_ARTIFACT: Record<AskGateId, string>;
|
|
65
|
+
/**
|
|
66
|
+
* FU-7 — THE CALL POINTS: which gate, if any, a phase ENTRY still owes.
|
|
67
|
+
*
|
|
68
|
+
* ⚠ THE ARTIFACT IS THE RECORD, so no ledger is needed to ask once: if the document already carries the
|
|
69
|
+
* gate's marker line, the question has been answered and is not asked again. That is the same
|
|
70
|
+
* "once per run at entry" property T29 got from riding an existing gate — one mechanism, not two.
|
|
71
|
+
*
|
|
72
|
+
* ⚠ AND AN UNKNOWN PHASE OWES NOTHING. Returning a gate for a phase that has no such decision would ask a
|
|
73
|
+
* person a question the workflow does not act on, which is worse than not asking at all.
|
|
74
|
+
*/
|
|
75
|
+
export declare function pendingGateFor(artifactFile: string, artifactText: string | null): AskGateId | null;
|
|
76
|
+
/**
|
|
77
|
+
* T23 — the tool.
|
|
78
|
+
*
|
|
79
|
+
* TWO BRANCHES, and the split is the point: called WITHOUT an answer it ASKS (returning the validated
|
|
80
|
+
* question, which the host renders as a card), and called WITH one it RECORDS — validating the label,
|
|
81
|
+
* writing the marker into the artifact, and reporting the line it wrote. A tool that did both in one
|
|
82
|
+
* call would have to invent the answer.
|
|
83
|
+
*
|
|
84
|
+
* ⚠ THE WRITE-BACK IS A MARKER LINE, REPLACED IN PLACE when the artifact already carries one. A
|
|
85
|
+
* second `TDD Mode:` line would leave two answers to one question and make "what was decided?"
|
|
86
|
+
* depend on which a reader found first.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createRecursiveAskTool(recursive: RecursiveRuntime): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
2
|
/**
|
|
3
|
-
* `recursive_closeout` — scaffold a Phase
|
|
3
|
+
* `recursive_closeout` — scaffold a Phase 0-8 closeout receipt under the
|
|
4
4
|
* SESSION's workspace only (R1 workspace-scoping invariant). The run is resolved
|
|
5
5
|
* via the session agent's cwd -> workspace registry; a runId outside the current
|
|
6
6
|
* workspace is rejected.
|