@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.
@@ -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
- ```powershell
2441
- # Python (cross-platform):
2442
- python ./.agents/skills/recursive-mode/scripts/lint-recursive-run.py --run-id "<run-id>"
2443
- # Or, when running from this repo:
2444
- python ./.recursive/scripts/lint-recursive-run.py --run-id "<run-id>"
2445
- python3 ./.agents/skills/recursive-mode/scripts/lint-recursive-run.py --run-id "<run-id>"
2446
- python3 ./.recursive/scripts/lint-recursive-run.py --run-id "<run-id>"
2447
-
2448
- # Treat WARN as FAIL
2449
- python ./.agents/skills/recursive-mode/scripts/lint-recursive-run.py --run-id "<run-id>" --strict
2450
- python ./.recursive/scripts/lint-recursive-run.py --run-id "<run-id>" --strict
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
- ```powershell
2469
- # Python (cross-platform)
2470
- python ./.agents/skills/recursive-mode/scripts/recursive-lock.py --run-id "<run-id>" --artifact "<artifact>.md"
2471
- # Or, when running from this repo:
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 command is the primary supported path. It must refuse to lock artifacts whose required gates or lint-critical structure are still invalid.
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 provided script to verify all locks in a run:
2507
+ Use the plugin's own tool to verify all locks in a run — there is no verifier script:
2525
2508
 
2526
- ```bash
2527
- # Verify specific run
2528
- python ./.agents/skills/recursive-mode/scripts/verify-locks.py --run-id "<run-id>"
2529
- # Or, when running from this repo:
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
- ```powershell
2543
- # Verify specific run
2544
- .\.agents\skills\recursive-mode\scripts\verify-locks.ps1 -RunId "<run-id>"
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, run `python .recursive/scripts/recursive-training-loader.py --repo-root . --query "<task>" --files "<path1,path2>"`.
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, run `python .recursive/scripts/recursive-training-loader.py --repo-root . --query "<task>" --files "<path1,path2>"`.
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, run:
6
- # python .recursive/scripts/recursive-training-loader.py --repo-root . --query "<task>" --files "<path1,path2>"
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 optional `recursive-training-sync.py` helper is read-only; it prints startup guidance about what to read, but does not modify `MEMORY.md` or the memory plane.
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 installed `recursive-training` skill
52
- - `/.recursive/scripts/recursive-training-loader.py`
53
- - `/.recursive/memory/training/`
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/`, preferably by using the training loader with filesystem-backed discovery.
94
- - If the optional `recursive-training` skill is installed, run `/.recursive/scripts/recursive-training-loader.py` after reading `MEMORY.md` and before planning or implementation whenever the task may benefit from experiential memory. If no automatic hook is wired, the agent must still manually load relevant training docs from the memory index when they matter.
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
- - If the optional `recursive-training` skill is installed, run `/.recursive/scripts/recursive-training-phase8-trigger.py` immediately after `08-memory-impact.md` locks to extract or refresh cross-run experiential learnings.
563
- - `recursive-lock` does not invoke training by itself. After Phase 8 locks, either run the trigger directly or re-run `recursive-closeout --phase 08` (without `--force`) so the helper can call `recursive-training-phase8-trigger.py --auto`.
564
- - Treat trigger/GRPO exit `2` (extractor unavailable) and exit `3` (zero items written) as unsuccessful training; do not claim the memory plane was updated.
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 `.recursive/scripts/recursive-lock.py` or `.recursive/scripts/recursive-lock.ps1`. The lock command is the primary supported path and must:
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 `.recursive/scripts/recursive-lock.py` (cross-platform) or `.recursive/scripts/recursive-lock.ps1` (PowerShell) to lock a draft artifact. Those commands validate lockability, write `Status: LOCKED`, write `LockedAt`, and compute `LockHash` using the canonical normalization rules.
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 `.recursive/scripts/verify-locks.py` (cross-platform) or `.recursive/scripts/verify-locks.ps1` (PowerShell) to verify and (optionally) fix mismatched hashes on already locked artifacts.
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 `.recursive/scripts/verify-locks.py` for cross-platform verification (and optional fixing)
1767
- - use `.recursive/scripts/verify-locks.ps1` when running in PowerShell environments
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 provided verifier scripts to verify all locks:
1800
+ Use the plugin's own tools to verify locks. There is no verifier script to run:
1800
1801
 
1801
- ```bash
1802
- # Verify specific run
1803
- python ./.recursive/scripts/verify-locks.py --run-id "<run-id>"
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
- # Scan all runs
1817
- .\.agents\skills\recursive-mode\scripts\verify-locks.ps1
1807
+ # All runs in the workspace, from the slash-command surface
1808
+ /recursive status <run-id>
1818
1809
 
1819
- # Fix incorrect hashes (use with caution)
1820
- .\.agents\skills\recursive-mode\scripts\verify-locks.ps1 -RunId "<run-id>" -Fix
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: Use `verify-locks.py --fix` (or `verify-locks.ps1 -Fix`) to update hash
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
- check('R7 config default advisory', JSON.stringify(resolveEnforcementConfig(undefined)) === JSON.stringify(DEFAULT_ENFORCEMENT))
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
- * vendored runtime scripts copied into .recursive/scripts/), plus the
7
- * agent/session-start Stage B (new vs resume) workspace-scoped to the session's
8
- * control-plane root (R1).
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-only scaffold (R2). No .py/.ps1 are vendored; the
228
- // plugin's lint/lock/status/init run in-process via TS tools, so the
229
- // scaffold carries an empty .recursive/scripts/ dir (kept for canonical
230
- // tree-shape parity with the golden fixture).
231
- noteDir('.recursive/scripts')
232
- // R3 (run 09): TS-only repair — drop python-era artifacts from an EXISTING
233
- // workspace's .recursive/scripts/ (the pre-0.1.6 scaffold vendored 29 .py +
234
- // .ps1 wrappers; those are no longer shipped or needed). Deletes only
235
- // .py/.ps1 under scripts/; never touches user content elsewhere.
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('advisory').description(
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('advisory').description(
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('advisory').description(
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({