@try-works/dsh-recursive-mode 0.4.9 → 0.6.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 +21 -8
- package/lib/config.d.ts +14 -0
- package/lib/enforcement.d.ts +118 -0
- package/lib/guard-log.d.ts +7 -0
- package/lib/index.js +568 -133
- package/lib/memory.d.ts +35 -2
- package/lib/phase-rules.d.ts +102 -0
- package/lib/policy-globs.d.ts +24 -0
- package/lib/recursive_ask.tool.d.ts +37 -0
- package/lib/runtime.d.ts +1 -1
- 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/scripts/test-recursive-mode-smoke.ts +11 -1
- package/src/bootstrap.ts +30 -14
- package/src/config.ts +11 -4
- package/src/enforcement.ts +202 -15
- package/src/guard-log.ts +7 -0
- package/src/index.ts +795 -700
- package/src/memory.ts +50 -9
- package/src/phase-rules.ts +162 -1
- package/src/policy-globs.ts +64 -8
- package/src/recursive_ask.tool.ts +48 -0
- package/src/recursive_lock.tool.ts +8 -2
- package/src/runtime.ts +7 -4
- package/src/training.ts +634 -6
|
@@ -2437,49 +2437,32 @@ Approval: PASS
|
|
|
2437
2437
|
|
|
2438
2438
|
Before locking (or when a lock verification fails unexpectedly), lint the run artifacts for required header fields, required section headings, and TODO completion rules:
|
|
2439
2439
|
|
|
2440
|
-
```
|
|
2441
|
-
#
|
|
2442
|
-
|
|
2443
|
-
#
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
python3 ./.agents/skills/recursive-mode/scripts/lint-recursive-run.py --run-id "<run-id>" --strict
|
|
2452
|
-
python3 ./.recursive/scripts/lint-recursive-run.py --run-id "<run-id>" --strict
|
|
2453
|
-
|
|
2454
|
-
# Lint specific run
|
|
2455
|
-
.\.agents\skills\recursive-mode\scripts\lint-recursive-run.ps1 -RunId "<run-id>"
|
|
2456
|
-
# Or, when running from this repo:
|
|
2457
|
-
.\scripts\lint-recursive-run.ps1 -RunId "<run-id>"
|
|
2458
|
-
|
|
2459
|
-
# Treat WARN as FAIL
|
|
2460
|
-
.\.agents\skills\recursive-mode\scripts\lint-recursive-run.ps1 -RunId "<run-id>" -Strict
|
|
2461
|
-
.\scripts\lint-recursive-run.ps1 -RunId "<run-id>" -Strict
|
|
2440
|
+
```text
|
|
2441
|
+
# One artifact (defaults to the run's current phase). `mode: "summary"` returns a few
|
|
2442
|
+
# findings plus the true totals; `full` (default) returns up to 200 per list. When a list
|
|
2443
|
+
# is clipped, `elided` says how many and how to see the rest.
|
|
2444
|
+
recursive_lint { "runId": "<run-id>", "artifact": "<artifact>.md", "mode": "summary" }
|
|
2445
|
+
|
|
2446
|
+
# Treat WARN as FAIL is not a flag here: `errors` are the failures, `warnings` are not,
|
|
2447
|
+
# and both lists come back with their true `failCount`/`warnCount`.
|
|
2448
|
+
|
|
2449
|
+
# The same read, from the slash-command surface
|
|
2450
|
+
/recursive status <run-id>
|
|
2462
2451
|
```
|
|
2463
2452
|
|
|
2453
|
+
This plugin is TypeScript and runs the linter in-process: there is no `lint-recursive-run.py`/`.ps1` to call, and no `.recursive/scripts/` to call it from.
|
|
2454
|
+
|
|
2464
2455
|
## Locking Commands
|
|
2465
2456
|
|
|
2466
2457
|
Preferred:
|
|
2467
2458
|
|
|
2468
|
-
```
|
|
2469
|
-
#
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
python ./.recursive/scripts/recursive-lock.py --run-id "<run-id>" --artifact "<artifact>.md"
|
|
2473
|
-
python3 ./.agents/skills/recursive-mode/scripts/recursive-lock.py --run-id "<run-id>" --artifact "<artifact>.md"
|
|
2474
|
-
python3 ./.recursive/scripts/recursive-lock.py --run-id "<run-id>" --artifact "<artifact>.md"
|
|
2475
|
-
|
|
2476
|
-
# PowerShell
|
|
2477
|
-
.\.agents\skills\recursive-mode\scripts\recursive-lock.ps1 -RunId "<run-id>" -Artifact "<artifact>.md"
|
|
2478
|
-
# Or, when running from this repo:
|
|
2479
|
-
.\scripts\recursive-lock.ps1 -RunId "<run-id>" -Artifact "<artifact>.md"
|
|
2459
|
+
```text
|
|
2460
|
+
# Lock one artifact once its gates pass. It refuses a lock whose gates or whose earlier
|
|
2461
|
+
# phases are unmet, and writes Status: LOCKED, LockedAt and LockHash.
|
|
2462
|
+
recursive_lock { "runId": "<run-id>", "artifact": "<artifact>.md" }
|
|
2480
2463
|
```
|
|
2481
2464
|
|
|
2482
|
-
The lock
|
|
2465
|
+
The lock tool is the primary supported path. It refuses to lock artifacts whose required gates or lint-critical structure are still invalid.
|
|
2483
2466
|
|
|
2484
2467
|
Manual fallback for hash computation only:
|
|
2485
2468
|
|
|
@@ -2521,34 +2504,16 @@ sed '/^LockHash:/d' .recursive/run/<run-id>/<artifact>.md | tr -d '\r' | sha256s
|
|
|
2521
2504
|
|
|
2522
2505
|
### Automated Verification
|
|
2523
2506
|
|
|
2524
|
-
Use the
|
|
2507
|
+
Use the plugin's own tool to verify all locks in a run — there is no verifier script:
|
|
2525
2508
|
|
|
2526
|
-
```
|
|
2527
|
-
#
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
python ./.recursive/scripts/verify-locks.py --run-id "<run-id>"
|
|
2531
|
-
python3 ./.agents/skills/recursive-mode/scripts/verify-locks.py --run-id "<run-id>"
|
|
2532
|
-
python3 ./.recursive/scripts/verify-locks.py --run-id "<run-id>"
|
|
2533
|
-
|
|
2534
|
-
# Fix incorrect hashes (use with caution)
|
|
2535
|
-
python ./.agents/skills/recursive-mode/scripts/verify-locks.py --run-id "<run-id>" --fix
|
|
2536
|
-
# Or, when running from this repo:
|
|
2537
|
-
python ./.recursive/scripts/verify-locks.py --run-id "<run-id>" --fix
|
|
2538
|
-
python3 ./.agents/skills/recursive-mode/scripts/verify-locks.py --run-id "<run-id>" --fix
|
|
2539
|
-
python3 ./.recursive/scripts/verify-locks.py --run-id "<run-id>" --fix
|
|
2540
|
-
```
|
|
2509
|
+
```text
|
|
2510
|
+
# One run: the phase table and per-artifact lock state. A LockHash that no longer matches
|
|
2511
|
+
# its content is reported as TAMPERED, named against the receipt it was locked under.
|
|
2512
|
+
recursive_status { "runId": "<run-id>" }
|
|
2541
2513
|
|
|
2542
|
-
|
|
2543
|
-
#
|
|
2544
|
-
|
|
2545
|
-
# Or, when running from this repo:
|
|
2546
|
-
.\scripts\verify-locks.ps1 -RunId "<run-id>"
|
|
2547
|
-
|
|
2548
|
-
# Fix incorrect hashes (use with caution)
|
|
2549
|
-
.\.agents\skills\recursive-mode\scripts\verify-locks.ps1 -RunId "<run-id>" -Fix
|
|
2550
|
-
# Or, when running from this repo:
|
|
2551
|
-
.\scripts\verify-locks.ps1 -RunId "<run-id>" -Fix
|
|
2514
|
+
# There is deliberately no `--fix`. Re-hashing a changed artifact would erase the only
|
|
2515
|
+
# evidence that it changed, so the mismatch is reported and the decision (restore the
|
|
2516
|
+
# content, or record an addendum) stays with the agent.
|
|
2552
2517
|
```
|
|
2553
2518
|
|
|
2554
2519
|
### Manual Verification
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
- Canonical repository memory lives under `/.recursive/memory/`.
|
|
4
4
|
- Read `/.recursive/memory/MEMORY.md` before loading any other memory docs.
|
|
5
5
|
- Load only the memory docs relevant to the current task.
|
|
6
|
-
- When repository experiential memory may help,
|
|
6
|
+
- When repository experiential memory may help, load it through the plugin's own TS loader: `recursive_phase` returns the prior-run memory shards relevant to this run and phase (with the reason when none matched), and `/recursive memory "<task>" [--phase <nn>]` prints the same selection with its score components. There is no script to run and no `.recursive/scripts/` step — retrieval is in-process.
|
|
7
7
|
- Treat this file as a pointer only; the canonical memory store remains `/.recursive/memory/`.
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
- Canonical repository memory lives under `/.recursive/memory/`.
|
|
4
4
|
- Read `/.recursive/memory/MEMORY.md` before loading any other memory docs.
|
|
5
5
|
- Load only the memory docs relevant to the current task.
|
|
6
|
-
- When repository experiential memory may help,
|
|
6
|
+
- When repository experiential memory may help, load it through the plugin's own TS loader: `recursive_phase` returns the prior-run memory shards relevant to this run and phase (with the reason when none matched), and `/recursive memory "<task>" [--phase <nn>]` prints the same selection with its score components. There is no script to run and no `.recursive/scripts/` step — retrieval is in-process.
|
|
7
7
|
- Treat this file as a pointer only; the canonical memory store remains `/.recursive/memory/`.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# Canonical repository memory lives under `/.recursive/memory/`.
|
|
3
3
|
# Read `/.recursive/memory/MEMORY.md` before loading any other memory docs.
|
|
4
4
|
# Load only the memory docs relevant to the current task.
|
|
5
|
-
# When repository experiential memory may help,
|
|
6
|
-
#
|
|
5
|
+
# When repository experiential memory may help, load it through the plugin's own TS loader: the
|
|
6
|
+
# `recursive_phase` tool returns the prior-run memory shards relevant to this run and phase (with the
|
|
7
|
+
# reason when none matched), and `/recursive memory "<task>" [--phase <nn>]` prints the same selection
|
|
8
|
+
# with its score components. There is no script to run and no `.recursive/scripts/` step.
|
|
7
9
|
# This file is only a pointer surface; the canonical memory store remains `/.recursive/memory/`.
|
|
@@ -16,7 +16,7 @@ Control-plane docs are not memory docs:
|
|
|
16
16
|
- Read this file before loading any other memory docs.
|
|
17
17
|
- Load only the memory docs relevant to the current task.
|
|
18
18
|
- If the task may benefit from prior recursive-mode experiential learnings, use this index to identify the relevant docs under `/.recursive/memory/training/` and `/.recursive/memory/domains/`.
|
|
19
|
-
- The
|
|
19
|
+
- The plugin loads this plane **in-process, in TypeScript**: the `recursive_phase` tool returns the shards that match the run and the phase in play, and `/recursive memory "<task>" [--phase <nn>]` prints the same selection with the score component behind every shard and a reason for every shard it left out. There is no `recursive-training-loader`/`recursive-training-sync` script to run and no `.recursive/scripts/` helper in this plugin.
|
|
20
20
|
- If the task plans delegated review, subagent help, review bundles, smoke-harness portability work, or capability-sensitive execution, read `/.recursive/memory/skills/SKILLS.md` and then load the relevant skill-memory shards.
|
|
21
21
|
- If Phase 8 will need to promote durable lessons, first capture run-local skill usage in the run artifact and only then promote generalized conclusions into skill-memory shards.
|
|
22
22
|
- Prefer `Status: CURRENT` docs for planning and execution.
|
|
@@ -48,10 +48,11 @@ It exists to reduce blind doc-by-doc scanning. It is not a second workflow spec.
|
|
|
48
48
|
- the installed `recursive-router`, `recursive-subagent`, and `recursive-review-bundle` skills
|
|
49
49
|
- Working on memory behavior:
|
|
50
50
|
- `/.recursive/memory/MEMORY.md`
|
|
51
|
-
- the
|
|
52
|
-
-
|
|
53
|
-
-
|
|
51
|
+
- the memory shards themselves: `/.recursive/memory/domains/`, `/.recursive/memory/training/`, and the rest of the registry in `MEMORY.md`
|
|
52
|
+
- the plugin's **TS** memory surfaces — `recursive_phase` (injects the shards that match this run and phase) and `/recursive memory "<task>" [--phase <nn>]` (the same selection, printed with its score components)
|
|
53
|
+
- `recursive_closeout --phase 08` for the run-close training trigger, which runs in-process; its extractor is the operator environment variable `RECURSIVE_TRAINING_EXTRACTOR_CMD`
|
|
54
54
|
- `/.recursive/memory/skills/SKILLS.md`
|
|
55
|
+
- ⚠ there is no `.recursive/scripts/` helper and no Python anywhere in this plugin — lint, lock, status, closeout and retrieval all run in-process, so a script path is not a route to any of them
|
|
55
56
|
|
|
56
57
|
## Non-Canonical Bridges
|
|
57
58
|
|
|
@@ -90,8 +90,8 @@ Required read behavior:
|
|
|
90
90
|
- If relevant prior runs are found, read only the docs needed from those runs to understand the affected codebase areas before writing the new run artifacts.
|
|
91
91
|
- If no relevant prior runs are identified, skip that step.
|
|
92
92
|
- After reading `MEMORY.md`, load only the memory docs relevant to the current task. Do not load the entire memory tree by default.
|
|
93
|
-
- If the task may benefit from prior experiential learnings, load only the relevant docs under `/.recursive/memory/training/` and `/.recursive/memory/domains
|
|
94
|
-
-
|
|
93
|
+
- If the task may benefit from prior experiential learnings, load only the relevant docs under `/.recursive/memory/training/` and `/.recursive/memory/domains/`.
|
|
94
|
+
- The plugin loads those docs itself, **in-process and in TypeScript**: call `recursive_phase` on entering a phase and it returns the memory shards that match the run and the phase in play, with the reason stated when none matched. For an on-demand lookup, `/recursive memory "<task>" [--phase <nn>]` prints the same selection with the score component behind each shard and a reason for each shard it excluded. Nothing is loaded by running a script — this plugin ships no `.recursive/scripts/` helper and no Python — and nothing is ever fabricated: an empty result means continue normally.
|
|
95
95
|
- If the run plans delegated review, subagent help, review bundles, smoke harness portability work, or other skill-sensitive execution, load `/.recursive/memory/skills/SKILLS.md` and the relevant skill-memory shards before planning or auditing.
|
|
96
96
|
- Prefer `Status: CURRENT` memory docs for planning/execution.
|
|
97
97
|
- `Status: SUSPECT` memory docs may be used as leads but must be revalidated before trust.
|
|
@@ -559,9 +559,10 @@ Phase 8 — Memory maintenance and impact review
|
|
|
559
559
|
- Audit must verify memory updates and status transitions against reviewed final product/worktree paths, touched memory docs, prior memory truth, `STATE.md`, and `DECISIONS.md`
|
|
560
560
|
- Must include `## Run-Local Skill Usage Capture` with concrete availability / attempted / used / worked-well / issue / recommendation fields whenever skill usage is relevant to the run
|
|
561
561
|
- Must include `## Skill Memory Promotion Review` explaining what durable lessons were promoted, what stayed run-local, and why
|
|
562
|
-
-
|
|
563
|
-
- `
|
|
564
|
-
-
|
|
562
|
+
- The training pass runs **in-process**: re-run the closeout for the phase — `recursive_closeout` with `phase 08` for a run whose `08-memory-impact.md` has already been closed out once — and the plugin's phase-8 trigger extracts or refreshes cross-run experiential learnings then.
|
|
563
|
+
- `recursive_lock` does not invoke training by itself, and neither does the FIRST closeout of phase 08: training at the first lock would train the run on itself. The trigger fires on the closeout **re-run**, and it needs at least two runs with a LOCKED `08-memory-impact.md` before it will extract — one run is an anecdote, not evidence. There is no trigger script to run.
|
|
564
|
+
- The extractor is the operator environment variable `RECURSIVE_TRAINING_EXTRACTOR_CMD`, and its answer comes back through a response file rather than a pipe. An unset command is a named failure, never a silent success.
|
|
565
|
+
- Treat trigger exit `2` (extractor unavailable) and exit `3` (nothing usable to extract, including too little evidence) as unsuccessful training; do not claim the memory plane was updated.
|
|
565
566
|
- **TODO Requirement:** Phase artifact MUST include `## TODO` section with checkable items
|
|
566
567
|
- **TODO Enforcement:** ALL TODO items must be checked off before locking
|
|
567
568
|
- **Completion rule:** the run is not fully complete before Phase 8 passes
|
|
@@ -752,7 +753,7 @@ Recursive phases are one-way. Iteration is allowed within a phase, but after a p
|
|
|
752
753
|
### DRAFT vs LOCKED
|
|
753
754
|
|
|
754
755
|
- While a phase is in progress, its output artifact status is `DRAFT`. The agent may revise it until both gates pass.
|
|
755
|
-
- When both gates pass, the agent must lock the artifact with
|
|
756
|
+
- When both gates pass, the agent must lock the artifact with the **`recursive_lock` tool**. It is the primary supported path and it must:
|
|
756
757
|
1) verify the artifact is lockable,
|
|
757
758
|
2) set Status to `LOCKED`,
|
|
758
759
|
3) set `LockedAt`,
|
|
@@ -779,11 +780,11 @@ that contains its own hash.
|
|
|
779
780
|
|
|
780
781
|
#### Preferred: use the lock command
|
|
781
782
|
|
|
782
|
-
Use
|
|
783
|
+
Use the **`recursive_lock`** tool to lock a draft artifact — run **`recursive_lint`** first when you want the artifact machine-checked before you attempt the lock. It validates lockability, writes `Status: LOCKED`, writes `LockedAt`, and computes `LockHash` using the canonical normalization rules; it refuses a lock whose gates or whose earlier phases are unmet. This plugin runs in-process in TypeScript: there is no lock script to call under `.recursive/scripts/`, and no Python in the package at all.
|
|
783
784
|
|
|
784
785
|
#### Secondary: verify an existing lock
|
|
785
786
|
|
|
786
|
-
Use
|
|
787
|
+
Use the **`recursive_status`** tool to verify already locked artifacts: it recomputes every `LockHash` and reports each artifact's state, naming a `Status: LOCKED` artifact whose hash no longer matches as `TAMPERED`. There is no separate verifier to call, and deliberately no fix-up mode — see the tampering action below.
|
|
787
788
|
|
|
788
789
|
#### Manual computation examples
|
|
789
790
|
|
|
@@ -1763,8 +1764,8 @@ The LockHash is a SHA-256 hash of the normalized artifact content at lock time.
|
|
|
1763
1764
|
"How to compute LockHash" above for the canonical normalization rules.
|
|
1764
1765
|
|
|
1765
1766
|
**Preferred:**
|
|
1766
|
-
- use
|
|
1767
|
-
- use
|
|
1767
|
+
- use the `recursive_status` tool: it recomputes every `LockHash` under the run and reports the lock state per artifact
|
|
1768
|
+
- use the `recursive_lint` tool to machine-check one artifact against its phase rules before or after locking
|
|
1768
1769
|
|
|
1769
1770
|
**PowerShell:**
|
|
1770
1771
|
```powershell
|
|
@@ -1796,28 +1797,18 @@ A phase artifact is **lock-valid** only when ALL of the following are true:
|
|
|
1796
1797
|
|
|
1797
1798
|
### Automated Verification
|
|
1798
1799
|
|
|
1799
|
-
Use the
|
|
1800
|
+
Use the plugin's own tools to verify locks. There is no verifier script to run:
|
|
1800
1801
|
|
|
1801
|
-
```
|
|
1802
|
-
#
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
# Scan all runs
|
|
1806
|
-
python ./.recursive/scripts/verify-locks.py
|
|
1807
|
-
|
|
1808
|
-
# Fix incorrect hashes (use with caution)
|
|
1809
|
-
python ./.recursive/scripts/verify-locks.py --run-id "<run-id>" --fix
|
|
1810
|
-
```
|
|
1811
|
-
|
|
1812
|
-
```powershell
|
|
1813
|
-
# Verify specific run
|
|
1814
|
-
.\.agents\skills\recursive-mode\scripts\verify-locks.ps1 -RunId "<run-id>"
|
|
1802
|
+
```text
|
|
1803
|
+
# One run: the phase table and per-artifact lock state. A LockHash that no longer
|
|
1804
|
+
# matches its content is reported as TAMPERED, with the receipt it was locked under.
|
|
1805
|
+
recursive_status { "runId": "<run-id>" }
|
|
1815
1806
|
|
|
1816
|
-
#
|
|
1817
|
-
|
|
1807
|
+
# All runs in the workspace, from the slash-command surface
|
|
1808
|
+
/recursive status <run-id>
|
|
1818
1809
|
|
|
1819
|
-
#
|
|
1820
|
-
|
|
1810
|
+
# Machine-check a single artifact against its phase rules before locking it
|
|
1811
|
+
recursive_lint { "runId": "<run-id>", "artifact": "<artifact>.md" }
|
|
1821
1812
|
```
|
|
1822
1813
|
|
|
1823
1814
|
### Tampering Detection
|
|
@@ -1829,7 +1820,7 @@ If LockHash doesn't match the canonical normalized content:
|
|
|
1829
1820
|
3. **Line endings changed** (CRLF vs LF)
|
|
1830
1821
|
|
|
1831
1822
|
**Action:**
|
|
1832
|
-
- If accidental:
|
|
1823
|
+
- If accidental: restore the artifact's content. The plugin has **no `--fix` on purpose** — re-hashing a changed artifact would erase the only evidence that it changed, so the mismatch is reported as `TAMPERED` and the decision (restore the content, or record an addendum) stays with you.
|
|
1833
1824
|
- If intentional modification: This is an anti-pattern. Use addenda instead.
|
|
1834
1825
|
|
|
1835
1826
|
### Phase Transition Lock Chain
|
|
@@ -166,7 +166,17 @@ async function main() {
|
|
|
166
166
|
const coercedAdvisory = coerceAskToDecision(askDecision, 'advisory')
|
|
167
167
|
check('T6 ask->deny under strict', coercedStrict.kind === 'deny')
|
|
168
168
|
check('T6 ask->allow+warn under advisory (never silent)', coercedAdvisory.kind === 'allow' && typeof (coercedAdvisory as { warn?: string }).warn === 'string')
|
|
169
|
-
|
|
169
|
+
// ⚠ THE CHECK THAT USED TO BE HERE WAS VACUOUS: it compared the resolver's output with
|
|
170
|
+
// `DEFAULT_ENFORCEMENT`, and both sides move together, so a change of default could never
|
|
171
|
+
// fail it. Read the default and assert the VALUE, so a revert to advisory is caught.
|
|
172
|
+
const defaultConfig = resolveEnforcementConfig(undefined)
|
|
173
|
+
check('R7 config default strict (all three gates)',
|
|
174
|
+
defaultConfig.preStep === 'strict' && defaultConfig.toolGuards === 'strict' && defaultConfig.tamper === 'strict'
|
|
175
|
+
&& JSON.stringify(defaultConfig) === JSON.stringify(DEFAULT_ENFORCEMENT))
|
|
176
|
+
// A PARTIAL section must fill the unstated gates with the same posture, not with advisory:
|
|
177
|
+
// the settings service merges ONE edited field into an entry's config.
|
|
178
|
+
const partial = resolveEnforcementConfig({ toolGuards: 'advisory' })
|
|
179
|
+
check('R7 partial config fills strict', partial.preStep === 'strict' && partial.tamper === 'strict' && partial.toolGuards === 'advisory')
|
|
170
180
|
let configError = ''
|
|
171
181
|
try { resolveEnforcementConfig({ bogus: 1 }) } catch (err) { configError = (err as Error).message }
|
|
172
182
|
check('R7 unknown config key fails', configError.includes('unknown key'))
|
package/src/bootstrap.ts
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
3
3
|
* core: bootstrap the FULL canonical /.recursive/ control plane + cross-tool
|
|
4
4
|
* bridges byte-identically (RECURSIVE.md marker-wrapped, AGENTS.md, STATE/
|
|
5
|
-
* DECISIONS, memory routers + shards, config/recursive-router.json, .gitignore,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* DECISIONS, memory routers + shards, config/recursive-router.json, .gitignore),
|
|
6
|
+
* plus the agent/session-start Stage B (new vs resume) workspace-scoped to the
|
|
7
|
+
* session's control-plane root (R1).
|
|
8
|
+
*
|
|
9
|
+
* ⚠ NO `.recursive/scripts/` IS CREATED, and a legacy one is removed once it is empty — see the block in
|
|
10
|
+
* the scaffold below for the measurement that decided it.
|
|
9
11
|
*
|
|
10
12
|
* Templates + bodies + runtime scripts are SHIPPED package files under
|
|
11
13
|
* references/ (never inlined TS string literals) and resolved relative to this
|
|
12
14
|
* module (package install location), never process.cwd().
|
|
13
15
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync, rmSync, rmdirSync } from 'node:fs'
|
|
15
17
|
import { join, dirname } from 'node:path'
|
|
16
18
|
import { fileURLToPath } from 'node:url'
|
|
17
19
|
|
|
@@ -224,15 +226,21 @@ export function bootstrapScaffold(root: string): BootstrapResult {
|
|
|
224
226
|
'.recursive/memory/skills/issues/.gitkeep', '.recursive/memory/skills/patterns/.gitkeep', '.recursive/run/.gitkeep',
|
|
225
227
|
]) noteFile(rel, '')
|
|
226
228
|
|
|
227
|
-
// Runtime scripts: TS-
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
229
|
+
// Runtime scripts: TS-ONLY, AND THE EMPTY DIRECTORY IS NO LONGER CREATED.
|
|
230
|
+
//
|
|
231
|
+
// ⚠ WHY IT GONE. It used to be scaffolded EMPTY — no .py/.ps1 is vendored, because lint/lock/status/
|
|
232
|
+
// closeout all run in-process as TS tools — while two shipped documents pointed INTO it: the `CLAUDE.md`
|
|
233
|
+
// memory pointers at `.recursive/scripts/recursive-training-loader.py`, and the canonical `RECURSIVE.md`
|
|
234
|
+
// at `.recursive/scripts/recursive-lock.py` / `verify-locks.py`. An empty directory that shipped documents
|
|
235
|
+
// send an agent into is a TRAP, not tree-shape parity: the agent follows the documented path, finds
|
|
236
|
+
// nothing, and the memory plane those documents promised never loads. Measured in three live runs — the
|
|
237
|
+
// directory was empty in every one, and no run ever locked a phase. Nothing in this package reads or
|
|
238
|
+
// executes anything from it; the only code that ever touched it is the legacy cleanup below.
|
|
239
|
+
//
|
|
240
|
+
// ⚠ THE CLEANUP STAYS, AND NOW FINISHES THE JOB. The pre-0.1.6 scaffold vendored 29 .py + .ps1 wrappers
|
|
241
|
+
// into real workspaces, so those are still deleted wherever they are found — and the directory is then
|
|
242
|
+
// removed when it is EMPTY, because leaving it behind is the same trap for the next agent. A directory
|
|
243
|
+
// still holding anything else is that user's and is left untouched.
|
|
236
244
|
{
|
|
237
245
|
const scriptsDir = join(recursiveRoot, 'scripts')
|
|
238
246
|
if (existsSync(scriptsDir)) {
|
|
@@ -241,6 +249,14 @@ export function bootstrapScaffold(root: string): BootstrapResult {
|
|
|
241
249
|
rmSync(join(scriptsDir, name), { force: true })
|
|
242
250
|
}
|
|
243
251
|
}
|
|
252
|
+
// ⚠ `rmdirSync`, NOT `rmSync({ recursive: false })`: measured — the latter throws `ERR_FS_EISDIR`
|
|
253
|
+
// on a directory, and a silently caught error here would leave exactly the empty directory this
|
|
254
|
+
// block exists to remove. A spec asserts the directory is GONE, so the wrong call cannot hide.
|
|
255
|
+
try {
|
|
256
|
+
if (readdirSync(scriptsDir).length === 0) rmdirSync(scriptsDir)
|
|
257
|
+
} catch {
|
|
258
|
+
// A directory that cannot be removed is not a scaffold failure, and never a reason to stop.
|
|
259
|
+
}
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
|
package/src/config.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* silently coerced a bad value would be a second, weaker contract beside the real one.
|
|
18
18
|
*/
|
|
19
19
|
import z from '@deepseek-ai/schemastery'
|
|
20
|
-
import { DEFAULT_BUDGETS } from './enforcement.ts'
|
|
20
|
+
import { DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT_MODE } from './enforcement.ts'
|
|
21
21
|
|
|
22
22
|
/** One enforcement mode, as the settings form presents it. */
|
|
23
23
|
const enforcementMode = z.union([z.const('strict'), z.const('advisory')])
|
|
@@ -58,14 +58,21 @@ export const Config = z.object({
|
|
|
58
58
|
repoRoot: z.string().description(
|
|
59
59
|
'Control-plane root. Defaults to the process working directory when unset.',
|
|
60
60
|
),
|
|
61
|
+
/**
|
|
62
|
+
* ⚠ THE THREE MODE DEFAULTS READ `DEFAULT_ENFORCEMENT_MODE` FROM `enforcement.ts`, and
|
|
63
|
+
* that is deliberate: the schema default and the runtime default are the SAME value, and
|
|
64
|
+
* two literals here would be two defaults. A caller that omits the section gets
|
|
65
|
+
* `DEFAULT_ENFORCEMENT` from the runtime; a caller that supplies a partial section gets
|
|
66
|
+
* the resolver's fill. Both must be the enforcing posture — see the const for why.
|
|
67
|
+
*/
|
|
61
68
|
enforcement: z.object({
|
|
62
|
-
preStep: enforcementMode.default(
|
|
69
|
+
preStep: enforcementMode.default(DEFAULT_ENFORCEMENT_MODE).description(
|
|
63
70
|
'Phase pre-step enforcement: strict refuses an out-of-order transition, advisory warns and proceeds.',
|
|
64
71
|
),
|
|
65
|
-
toolGuards: enforcementMode.default(
|
|
72
|
+
toolGuards: enforcementMode.default(DEFAULT_ENFORCEMENT_MODE).description(
|
|
66
73
|
'Tool guard mode: strict DENIES an out-of-order tool call, advisory allows it and carries the warning.',
|
|
67
74
|
),
|
|
68
|
-
tamper: enforcementMode.default(
|
|
75
|
+
tamper: enforcementMode.default(DEFAULT_ENFORCEMENT_MODE).description(
|
|
69
76
|
'Tamper detection: strict refuses an artifact whose LockHash no longer matches its body.',
|
|
70
77
|
),
|
|
71
78
|
budgets: z.object({
|