@try-works/dsh-recursive-mode 0.5.0 → 0.6.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/README.md +21 -8
- package/lib/enforcement.d.ts +58 -0
- package/lib/index.js +597 -107
- package/lib/memory.d.ts +35 -2
- package/lib/phase-rules.d.ts +102 -0
- package/lib/policy-globs.d.ts +5 -0
- package/lib/training.d.ts +240 -2
- package/package.json +1 -1
- package/references/artifact-template.md +26 -61
- package/references/bodies/claude.md +1 -1
- package/references/bodies/copilot.md +1 -1
- package/references/bodies/cursorrules.md +4 -2
- package/references/bodies/memory-router.md +1 -1
- package/references/bodies/recursive-agents-router.md +4 -3
- package/references/bootstrap/RECURSIVE.md +21 -30
- package/src/bootstrap.ts +30 -14
- package/src/enforcement.ts +89 -5
- package/src/index.ts +795 -723
- package/src/memory.ts +50 -9
- package/src/phase-rules.ts +162 -1
- package/src/policy-globs.ts +28 -4
- package/src/runtime.ts +1962 -1945
- package/src/training.ts +634 -6
package/README.md
CHANGED
|
@@ -289,10 +289,9 @@ Two conventions matter more than they look:
|
|
|
289
289
|
repoRoot)` resolves the workspace the *session* is in. A plugin that used `process.cwd()` would silently lint the
|
|
290
290
|
wrong repository when the host checkout and the workspace differ.
|
|
291
291
|
|
|
292
|
-
**The memory plane is read
|
|
293
|
-
|
|
294
|
-
root
|
|
295
|
-
which is why the distinction is written down here.
|
|
292
|
+
**The memory plane is read from the plane the scaffold creates, not from beside it.** `loadMemoryIndex` resolves each kind under `<controlPlaneRoot>/.recursive/memory/<kind>/` — the directory `bootstrap.ts` scaffolds, `ts-lint.ts` lints, and the review bundle already read — and falls back to `<controlPlaneRoot>/memory/<kind>/` only when that kind has no readable shard there. That is the same **prefer-then-fall-back, never merge** rule `readFeedback` follows for its own moved sidecar: the earlier location is consulted only when the current one yields nothing, because two snapshots of one shard added together would count a shard twice.
|
|
293
|
+
|
|
294
|
+
Before that fallback existed the loader joined `memory/` straight onto the root it was handed, so it read `<dir>/memory/domains` — **a directory no real workspace has** — while the plane sat at `<dir>/.recursive/memory/domains`. Measured live: a workspace with the plane scaffolded, `memory/training/` included, and no `<dir>/memory/` at all, where `selectMemory` reported *"the memory plane is empty"* on every phase of every run. Passing `.recursive` itself as the root still resolves, through the second entry.
|
|
296
295
|
|
|
297
296
|
---
|
|
298
297
|
|
|
@@ -562,11 +561,12 @@ inputs are visible.
|
|
|
562
561
|
|
|
563
562
|
```mermaid
|
|
564
563
|
flowchart LR
|
|
565
|
-
subgraph sources["memory/<kind>/*.md"]
|
|
564
|
+
subgraph sources[".recursive/memory/<kind>/*.md"]
|
|
566
565
|
D1["domains/"]
|
|
567
566
|
D2["patterns/"]
|
|
568
|
-
D3["
|
|
569
|
-
D4["
|
|
567
|
+
D3["episodes/"]
|
|
568
|
+
D4["training/"]
|
|
569
|
+
D5["skills/"]
|
|
570
570
|
end
|
|
571
571
|
|
|
572
572
|
Q["query:<br/>the run's own<br/>00-requirements.md<br/>(first 4000 chars)"] --> SEL
|
|
@@ -578,8 +578,9 @@ flowchart LR
|
|
|
578
578
|
D2 --> IDX
|
|
579
579
|
D3 --> IDX
|
|
580
580
|
D4 --> IDX
|
|
581
|
+
D5 --> IDX
|
|
581
582
|
|
|
582
|
-
SEL --> OUT["top N shards →<br/>the review bundle"]
|
|
583
|
+
SEL --> OUT["top N shards →<br/>the phase payload<br/>and the review bundle"]
|
|
583
584
|
SEL --> REC["recordInjection()<br/>memory-injections.json"]
|
|
584
585
|
REC --> SET["settleInjections() at closeout<br/>over the LOCKED phases only"]
|
|
585
586
|
SET --> FB
|
|
@@ -588,6 +589,12 @@ flowchart LR
|
|
|
588
589
|
class Q,F,P,FB input
|
|
589
590
|
```
|
|
590
591
|
|
|
592
|
+
**`MEMORY_KINDS` is the list of kinds that are auto-loaded, and it is exactly those five.** `incidents/` and
|
|
593
|
+
`archive/` are stored and documented by the router but are *not* auto-loaded — `archive/` is historical by the
|
|
594
|
+
router's own definition, and adding `incidents/` is a ranking decision rather than a typo fix. `training/` **is**
|
|
595
|
+
in the list because it is the kind the phase-8 trigger writes: with it absent, the shards training produced were
|
|
596
|
+
written and registered in `MEMORY.md` while the loader that was supposed to score them could not see the kind.
|
|
597
|
+
|
|
591
598
|
**Scoring is additive and inspectable.** A shard scores for matching the query, for overlapping the changed
|
|
592
599
|
paths (`MEMORY_PATH_MATCH_WEIGHT = 3`), for applying to the current phase (`MEMORY_PHASE_MATCH_WEIGHT = 2`), and
|
|
593
600
|
for its feedback history (`feedbackBonus`, clamped to ±1). `explainMemorySelection` returns the **components** per
|
|
@@ -689,6 +696,12 @@ Which closes the loop the [memory plane](#9-how-it-works-the-memory-plane) opene
|
|
|
689
696
|
closeout, and `feedbackBonus` nudges the next selection. The plugin's prompts and its context are meant to
|
|
690
697
|
improve from its own recorded outcomes rather than from someone's recollection of them.
|
|
691
698
|
|
|
699
|
+
**And the loop is only closed when the reader can see every kind the writer produces.** The trigger writes one
|
|
700
|
+
shard per subsystem into `memory/domains/` *and* one per task type into `memory/training/<task-type>.md`, and the
|
|
701
|
+
second of those is the reason `training` is in `MEMORY_KINDS` (§9): a writer whose output the reader cannot reach
|
|
702
|
+
is the loop being described rather than closed. `tests/training-trigger.spec.ts` asserts the round trip end to
|
|
703
|
+
end — trigger writes, loader finds the shard it wrote — because either half can look correct alone.
|
|
704
|
+
|
|
692
705
|
**Design and status:** [TRAINING.md](TRAINING.md) is the design — including the phases P1–P5, each with a live
|
|
693
706
|
acceptance, and the measured comparison with the reference implementation it deliberately does **not** copy.
|
|
694
707
|
Its non-goals are as load-bearing as its goals: this is not a fine-tuning pipeline, and it does not ship a model
|
package/lib/enforcement.d.ts
CHANGED
|
@@ -112,23 +112,33 @@ export interface GuardTransition {
|
|
|
112
112
|
* and the payload is built by `buildGateBlockAsk` — the SAME builder the lock tool uses, so the two
|
|
113
113
|
* refusals cannot offer different options. It is absent on every decision that is not a lock-order
|
|
114
114
|
* refusal decided from real blockers, which is why every reader must treat it as optional.
|
|
115
|
+
*
|
|
116
|
+
* ⚠ ISSUE 2 (b): `runId` IS THE RUN THE GUARD RESOLVED AND READ — optional and additive for the same
|
|
117
|
+
* reason. `evaluateToolGuard` sets it on EVERY decision, allow included, and `index.ts` logs it instead of
|
|
118
|
+
* the filesystem's active run: a record whose `runId` came from one resolution while the rule read another
|
|
119
|
+
* is exactly the two-answers-in-one-payload defect this pairs with. It stays OPTIONAL so a hand-built
|
|
120
|
+
* decision (and `coerceAskToDecision`'s key-frozen input/output) is unaffected; a reader falls back to the
|
|
121
|
+
* active run when it is absent, which is the pre-existing behaviour.
|
|
115
122
|
*/
|
|
116
123
|
export type ToolGuardDecision = {
|
|
117
124
|
kind: 'allow';
|
|
118
125
|
warn?: string;
|
|
119
126
|
rule?: GuardRule;
|
|
120
127
|
transition?: GuardTransition;
|
|
128
|
+
runId?: string;
|
|
121
129
|
} | {
|
|
122
130
|
kind: 'deny';
|
|
123
131
|
reason: string;
|
|
124
132
|
rule?: GuardRule;
|
|
125
133
|
transition?: GuardTransition;
|
|
126
134
|
ask?: GateBlockAsk;
|
|
135
|
+
runId?: string;
|
|
127
136
|
} | {
|
|
128
137
|
kind: 'ask';
|
|
129
138
|
reason?: string;
|
|
130
139
|
rule?: GuardRule;
|
|
131
140
|
transition?: GuardTransition;
|
|
141
|
+
runId?: string;
|
|
132
142
|
};
|
|
133
143
|
export interface ToolExecLike {
|
|
134
144
|
name: string;
|
|
@@ -158,6 +168,54 @@ export declare function resolveToolPolicyForGuard(worktreeRoot: string, runId: s
|
|
|
158
168
|
* needs, because a cached phase would apply yesterday's baseline to today's lock.
|
|
159
169
|
*/
|
|
160
170
|
export declare function currentPhaseArtifact(worktreeRoot: string, runId: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* ISSUE 2 (a) — THE RUN A GUARD CALL IS ABOUT, and the one whose tree it may read.
|
|
173
|
+
*
|
|
174
|
+
* THE DEFECT THIS ANSWERS, measured before the fix: `recursive_lock {runId: 'run-b', artifact:
|
|
175
|
+
* '01-as-is.md'}` was REFUSED with `monotonic lock-order: … 00-requirements.md (DRAFT)` — run-A's blocker —
|
|
176
|
+
* while `run-b` had `00-requirements.md` LOCKED and `01-as-is.md` DRAFT, so locking it in run-b was LEGAL.
|
|
177
|
+
* The guard resolved the run from the FILESYSTEM (`resolveRunDir`, i.e. the active/newest run) while the
|
|
178
|
+
* tool resolves it from `args.runId`, so the guard judged a DIFFERENT RUN than the call was about. Under
|
|
179
|
+
* `advisory` the deny was coerced to an allow-with-warning and the tool refused on its own terms, which is
|
|
180
|
+
* why the strict default is what made it bite.
|
|
181
|
+
*
|
|
182
|
+
* SO THE RULE IS: for a LOCK call that NAMES a run, the guard judges THAT RUN. It is the same choice the
|
|
183
|
+
* tool makes, so the two layers cannot disagree about which tree the ordering rule is a property of. A
|
|
184
|
+
* caller that names nothing (every real `write`, and a lock that relies on the active run) is unaffected:
|
|
185
|
+
* the active run still governs, which is what the write-side rules rely on.
|
|
186
|
+
*
|
|
187
|
+
* ⚠ THIS IS SCOPED TO THE LOCK TOOLS DELIBERATELY, and the scope is per rule, not per convenience:
|
|
188
|
+
*
|
|
189
|
+
* - `lock-order` (`recursive_lock*`) — the caller's run WINS. The tool acts on `args.runId`, and the
|
|
190
|
+
* rule is about THAT run's prerequisites, so the guard must not answer for another run. This is the
|
|
191
|
+
* measured defect.
|
|
192
|
+
* - `locked-write` (the write-tool family) — NOT APPLICABLE, by construction: the rule resolves no run
|
|
193
|
+
* at all. It reads the target file's own `Status:` through the path the caller named, so there is no
|
|
194
|
+
* run to prefer and nothing could disagree.
|
|
195
|
+
* - `phase-order` (the write-tool family) — the ACTIVE run KEEPS WINNING, and this function does not
|
|
196
|
+
* touch it. Two reasons, both deliberate: (1) a `write` call carries no run id — no write tool declares
|
|
197
|
+
* one — so consulting `args.runId` here would hand a caller a way to ESCAPE the active run's ordering
|
|
198
|
+
* by naming some other run in an argument the tool ignores; and (2) the rule's declared scope is the
|
|
199
|
+
* run being worked in (it abstains for another run's tree, documented in `phaseOrderRule`), and moving
|
|
200
|
+
* that scope would be a new refusal, not a consistency fix.
|
|
201
|
+
*
|
|
202
|
+
* ⚠ A CALLER-SUPPLIED ID IS A NAME, NEVER A PATH, and it is validated before it can point the guard at
|
|
203
|
+
* anything: the id is trimmed the way `recursive_lock` trims it, then put through `runIdProblem` — the
|
|
204
|
+
* SAME gate the run-id-shaped tools use, which refuses separators, drive specifiers, `..`, a colon, a
|
|
205
|
+
* leading/trailing dot and an over-long name — and finally the resolved directory must sit UNDER this
|
|
206
|
+
* worktree's `<root>/.recursive/run`, the containment rule `runtime.ts` applies to a run directory.
|
|
207
|
+
*
|
|
208
|
+
* An id that fails any of those is NOT USED: the guard falls back to the active run, exactly as it behaved
|
|
209
|
+
* before this change. Falling back (rather than denying) is deliberate: an unusable id is a caller mistake
|
|
210
|
+
* the tool itself refuses (`BAD_RUN_ID` / `Artifact not found`), and inventing a new guard refusal for it
|
|
211
|
+
* would be a second, competing answer to a question `runIdProblem` already owns.
|
|
212
|
+
*
|
|
213
|
+
* A usable id does NOT have to name an EXISTING run: a run with no tree has no unlocked prerequisites, so
|
|
214
|
+
* the ordering rule abstains and the LOCK TOOL still refuses the lock (it checks the artifact exists before
|
|
215
|
+
* anything else). Requiring existence would instead re-introduce the defect in its ugliest form — a refusal
|
|
216
|
+
* built from ANOTHER run's blockers.
|
|
217
|
+
*/
|
|
218
|
+
export declare function resolveGuardRunId(name: string, args: Record<string, unknown>, worktreeRoot: string, activeRunId: string): string;
|
|
161
219
|
/**
|
|
162
220
|
* `mode` is the gate's configured posture. Its parameter default FOLLOWS the config
|
|
163
221
|
* default by REFERENCE (`DEFAULT_ENFORCEMENT.toolGuards`) rather than repeating the
|