explorbot 0.4.9 → 0.5.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.
Files changed (144) hide show
  1. package/README.md +4 -1
  2. package/bin/mdq.ts +18 -0
  3. package/boat/api-tester/src/ai/chief.ts +72 -0
  4. package/boat/api-tester/src/api-client.ts +37 -0
  5. package/boat/prima/src/prima.ts +41 -2
  6. package/dist/bin/mdq.js +19 -0
  7. package/dist/boat/api-tester/src/ai/chief.js +69 -0
  8. package/dist/boat/api-tester/src/api-client.js +26 -0
  9. package/dist/boat/prima/src/prima.js +42 -2
  10. package/dist/package.json +3 -2
  11. package/dist/src/action.js +4 -2
  12. package/dist/src/ai/agent.d.ts +3 -1
  13. package/dist/src/ai/judge-provider.d.ts +17 -0
  14. package/dist/src/ai/judge-provider.js +56 -0
  15. package/dist/src/ai/judge-tool.d.ts +2 -0
  16. package/dist/src/ai/judge-tool.js +33 -0
  17. package/dist/src/ai/judge.d.ts +28 -0
  18. package/dist/src/ai/judge.js +71 -0
  19. package/dist/src/ai/navigator.d.ts +3 -1
  20. package/dist/src/ai/navigator.js +24 -28
  21. package/dist/src/ai/pilot.d.ts +4 -0
  22. package/dist/src/ai/pilot.js +67 -9
  23. package/dist/src/ai/planner.js +9 -6
  24. package/dist/src/ai/provider.d.ts +4 -1
  25. package/dist/src/ai/provider.js +52 -7
  26. package/dist/src/ai/rerunner.js +7 -0
  27. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  28. package/dist/src/ai/researcher/locators.js +2 -2
  29. package/dist/src/ai/researcher/pagination.js +1 -1
  30. package/dist/src/ai/researcher/research-result.js +2 -2
  31. package/dist/src/ai/researcher.js +1 -1
  32. package/dist/src/ai/task-agent.d.ts +2 -0
  33. package/dist/src/ai/task-agent.js +3 -1
  34. package/dist/src/ai/tester.js +19 -15
  35. package/dist/src/ai/tools.d.ts +4 -3
  36. package/dist/src/ai/tools.js +35 -7
  37. package/dist/src/api/request-result.js +2 -1
  38. package/dist/src/api/xhr-capture.js +2 -1
  39. package/dist/src/command-handler.d.ts +1 -0
  40. package/dist/src/command-handler.js +24 -3
  41. package/dist/src/commands/base-command.d.ts +5 -0
  42. package/dist/src/commands/base-command.js +3 -0
  43. package/dist/src/commands/explore-command.d.ts +2 -1
  44. package/dist/src/commands/explore-command.js +12 -1
  45. package/dist/src/commands/freesail-command.js +8 -2
  46. package/dist/src/commands/init-command.js +1 -1
  47. package/dist/src/commands/navigate-command.d.ts +2 -1
  48. package/dist/src/commands/navigate-command.js +6 -0
  49. package/dist/src/commands/plan-load-command.d.ts +2 -1
  50. package/dist/src/commands/plan-load-command.js +4 -0
  51. package/dist/src/commands/plans-command.d.ts +3 -9
  52. package/dist/src/commands/plans-command.js +11 -21
  53. package/dist/src/commands/rerun-command.d.ts +2 -1
  54. package/dist/src/commands/rerun-command.js +5 -1
  55. package/dist/src/commands/research-command.d.ts +2 -1
  56. package/dist/src/commands/research-command.js +6 -0
  57. package/dist/src/commands/test-command.d.ts +2 -1
  58. package/dist/src/commands/test-command.js +4 -1
  59. package/dist/src/components/Autocomplete.js +26 -12
  60. package/dist/src/components/InputReadline.js +10 -1
  61. package/dist/src/config.d.ts +6 -0
  62. package/dist/src/experience-tracker.js +4 -3
  63. package/dist/src/explorbot.d.ts +3 -0
  64. package/dist/src/explorbot.js +8 -0
  65. package/dist/src/explorer.js +2 -3
  66. package/dist/src/knowledge-tracker.js +1 -1
  67. package/dist/src/reporter.js +8 -4
  68. package/dist/src/state-manager.d.ts +2 -0
  69. package/dist/src/state-manager.js +16 -0
  70. package/dist/src/test-plan.d.ts +11 -0
  71. package/dist/src/test-plan.js +54 -2
  72. package/dist/src/utils/aria-ref.js +1 -1
  73. package/dist/src/utils/logger.js +9 -3
  74. package/dist/src/utils/markdown-query.d.ts +1 -48
  75. package/dist/src/utils/markdown-query.js +1 -444
  76. package/dist/src/utils/mdq/cli.d.ts +6 -0
  77. package/dist/src/utils/mdq/cli.js +122 -0
  78. package/dist/src/utils/mdq/edit.d.ts +24 -0
  79. package/dist/src/utils/mdq/edit.js +147 -0
  80. package/dist/src/utils/mdq/query.d.ts +118 -0
  81. package/dist/src/utils/mdq/query.js +451 -0
  82. package/dist/src/utils/step-analyzer.d.ts +3 -0
  83. package/dist/src/utils/step-analyzer.js +7 -0
  84. package/dist/src/utils/strings.d.ts +1 -0
  85. package/dist/src/utils/strings.js +7 -0
  86. package/dist/src/utils/test-files.d.ts +1 -0
  87. package/dist/src/utils/test-files.js +5 -2
  88. package/dist/src/utils/url-matcher.d.ts +1 -0
  89. package/dist/src/utils/url-matcher.js +7 -0
  90. package/docs/api-testing/planning.md +1 -1
  91. package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
  92. package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
  93. package/package.json +3 -2
  94. package/src/action.ts +4 -2
  95. package/src/ai/agent.ts +3 -1
  96. package/src/ai/judge-provider.ts +62 -0
  97. package/src/ai/judge-tool.ts +35 -0
  98. package/src/ai/judge.ts +75 -0
  99. package/src/ai/navigator.ts +24 -27
  100. package/src/ai/pilot.ts +68 -9
  101. package/src/ai/planner.ts +9 -6
  102. package/src/ai/provider.ts +51 -7
  103. package/src/ai/rerunner.ts +4 -0
  104. package/src/ai/researcher/deep-analysis.ts +2 -2
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/pagination.ts +1 -1
  107. package/src/ai/researcher/research-result.ts +2 -2
  108. package/src/ai/researcher.ts +1 -1
  109. package/src/ai/task-agent.ts +4 -1
  110. package/src/ai/tester.ts +19 -16
  111. package/src/ai/tools.ts +42 -7
  112. package/src/api/request-result.ts +2 -1
  113. package/src/api/xhr-capture.ts +2 -1
  114. package/src/command-handler.ts +28 -3
  115. package/src/commands/base-command.ts +9 -0
  116. package/src/commands/explore-command.ts +15 -2
  117. package/src/commands/freesail-command.ts +9 -2
  118. package/src/commands/init-command.ts +1 -1
  119. package/src/commands/navigate-command.ts +8 -1
  120. package/src/commands/plan-load-command.ts +6 -1
  121. package/src/commands/plans-command.ts +13 -29
  122. package/src/commands/rerun-command.ts +7 -2
  123. package/src/commands/research-command.ts +8 -1
  124. package/src/commands/test-command.ts +6 -2
  125. package/src/components/Autocomplete.tsx +39 -10
  126. package/src/components/InputReadline.tsx +10 -1
  127. package/src/config.ts +1 -0
  128. package/src/experience-tracker.ts +4 -3
  129. package/src/explorbot.ts +8 -0
  130. package/src/explorer.ts +2 -2
  131. package/src/knowledge-tracker.ts +1 -1
  132. package/src/reporter.ts +8 -4
  133. package/src/state-manager.ts +16 -0
  134. package/src/test-plan.ts +67 -2
  135. package/src/utils/aria-ref.ts +1 -1
  136. package/src/utils/logger.ts +7 -2
  137. package/src/utils/markdown-query.ts +1 -519
  138. package/src/utils/mdq/cli.ts +118 -0
  139. package/src/utils/mdq/edit.ts +158 -0
  140. package/src/utils/mdq/query.ts +556 -0
  141. package/src/utils/step-analyzer.ts +8 -0
  142. package/src/utils/strings.ts +7 -0
  143. package/src/utils/test-files.ts +5 -2
  144. package/src/utils/url-matcher.ts +7 -0
@@ -1,6 +1,6 @@
1
1
  # mdq Package Implementation Plan
2
2
 
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
4
4
 
5
5
  **Goal:** Extract `src/utils/markdown-query.ts` into `src/utils/mdq/` as a publish-ready package that can both query and update markdown, then add a jq-like CLI.
6
6
 
@@ -68,7 +68,7 @@ Move the parser to its new home and teach it the one thing it gets wrong today:
68
68
 
69
69
  The `trailing` field is new and load-bearing: `marked` emits `space` tokens as siblings (a `paragraph` raw is `"para"` with no newline, followed by a separate `space` raw of `"\n\n"`), so every write verb needs to know where a node's separator lives.
70
70
 
71
- - [ ] **Step 1: Write the failing test**
71
+ - [x] **Step 1: Write the failing test**
72
72
 
73
73
  ```ts
74
74
  // tests/unit/mdq/frontmatter.test.ts
@@ -127,12 +127,12 @@ describe('buildTokenIndex', () => {
127
127
  });
128
128
  ```
129
129
 
130
- - [ ] **Step 2: Run test to verify it fails**
130
+ - [x] **Step 2: Run test to verify it fails**
131
131
 
132
132
  Run: `bun test tests/unit/mdq/frontmatter.test.ts`
133
133
  Expected: FAIL — cannot resolve `src/utils/mdq/query.ts`
134
134
 
135
- - [ ] **Step 3: Create query.ts with the index**
135
+ - [x] **Step 3: Create query.ts with the index**
136
136
 
137
137
  Copy `src/utils/markdown-query.ts` to `src/utils/mdq/query.ts` verbatim first, then apply these three changes.
138
138
 
@@ -207,12 +207,12 @@ export interface MatchedRange {
207
207
  }
208
208
  ```
209
209
 
210
- - [ ] **Step 4: Run test to verify it passes**
210
+ - [x] **Step 4: Run test to verify it passes**
211
211
 
212
212
  Run: `bun test tests/unit/mdq/frontmatter.test.ts`
213
213
  Expected: PASS (8 tests)
214
214
 
215
- - [ ] **Step 5: Format, lint and commit**
215
+ - [x] **Step 5: Format, lint and commit**
216
216
 
217
217
  ```bash
218
218
  bun run format && bun run lint:fix
@@ -236,7 +236,7 @@ Get every existing call site running against the new file with **zero behaviour
236
236
  - Consumes: `buildTokenIndex`, `splitFrontmatter`, `MatchedRange` from Task 1
237
237
  - Produces: `mdq(source: string): MarkdownQuery`, class `MarkdownQuery`, `parseQuery`, all existing methods unchanged
238
238
 
239
- - [ ] **Step 1: Move the test file and repoint its import**
239
+ - [x] **Step 1: Move the test file and repoint its import**
240
240
 
241
241
  ```bash
242
242
  git mv tests/unit/markdown-query.test.ts tests/unit/mdq/query.test.ts
@@ -254,12 +254,12 @@ to:
254
254
  import { mdq, parseQuery } from '../../../src/utils/mdq/query.ts';
255
255
  ```
256
256
 
257
- - [ ] **Step 2: Run the suite to verify it fails**
257
+ - [x] **Step 2: Run the suite to verify it fails**
258
258
 
259
259
  Run: `bun test tests/unit/mdq/query.test.ts`
260
260
  Expected: FAIL — `mdq` / `parseQuery` are not yet exported from `query.ts`, or section tests fail because `expandSectionRanges` still assumes `space` tokens are present
261
261
 
262
- - [ ] **Step 3: Restore the full API in query.ts**
262
+ - [x] **Step 3: Restore the full API in query.ts**
263
263
 
264
264
  Everything from the original `markdown-query.ts` below `buildTokenIndex` — `matchText`, `entryKey`, `getTokenText`, `getHeadingDepth`, `isSectionSelector`, `getSectionDepth`, `selectorToTokenType`, `computeSections`, `extractListItems`, `applyIndexSlice`, `expandSectionRanges`, `executeSegments`, `class MarkdownQuery`, `mdq` — carries over unchanged, except:
265
265
 
@@ -277,23 +277,23 @@ for (let j = i + 1; j < candidates.length; j++) {
277
277
 
278
278
  Move every `export interface` / `export type` to the end of the file, and replace the ternaries at the original lines 30, 34, 129-130 and 295 with early returns.
279
279
 
280
- - [ ] **Step 4: Run the suite to verify it passes**
280
+ - [x] **Step 4: Run the suite to verify it passes**
281
281
 
282
282
  Run: `bun test tests/unit/mdq/query.test.ts`
283
283
  Expected: PASS — **110 tests**, the same count as before the move
284
284
 
285
- - [ ] **Step 5: Replace markdown-query.ts with a shim**
285
+ - [x] **Step 5: Replace markdown-query.ts with a shim**
286
286
 
287
287
  ```ts
288
288
  export * from './mdq/query.ts';
289
289
  ```
290
290
 
291
- - [ ] **Step 6: Verify every existing call site still works**
291
+ - [x] **Step 6: Verify every existing call site still works**
292
292
 
293
293
  Run: `bun test tests/unit/`
294
294
  Expected: PASS, no new failures versus the pre-task run
295
295
 
296
- - [ ] **Step 7: Format, lint and commit**
296
+ - [x] **Step 7: Format, lint and commit**
297
297
 
298
298
  ```bash
299
299
  bun run format && bun run lint:fix
@@ -327,7 +327,7 @@ The one risky task. It ends with the repo green and every break fixed.
327
327
  - `mdq(source: Markdown): MarkdownDoc`
328
328
  - Deprecated alias `MarkdownQuery = Selection`
329
329
 
330
- - [ ] **Step 1: Write the failing test**
330
+ - [x] **Step 1: Write the failing test**
331
331
 
332
332
  ```ts
333
333
  // append to tests/unit/mdq/query.test.ts
@@ -359,12 +359,12 @@ describe('MarkdownDoc chaining', () => {
359
359
  });
360
360
  ```
361
361
 
362
- - [ ] **Step 2: Run test to verify it fails**
362
+ - [x] **Step 2: Run test to verify it fails**
363
363
 
364
364
  Run: `bun test tests/unit/mdq/query.test.ts -t 'MarkdownDoc chaining'`
365
365
  Expected: FAIL — `.query is not a function` on the string returned by `replace`
366
366
 
367
- - [ ] **Step 3: Split MarkdownQuery into MarkdownDoc and Selection**
367
+ - [x] **Step 3: Split MarkdownQuery into MarkdownDoc and Selection**
368
368
 
369
369
  `MarkdownDoc` holds the source. `Selection` holds source plus matches. Every write on `Selection` ends by wrapping its result:
370
370
 
@@ -411,6 +411,9 @@ export function mdq(source: Markdown): MarkdownDoc {
411
411
  }
412
412
  ```
413
413
 
414
+ `query` takes only a selector here. The optional second `matcher` argument arrives in
415
+ Task 5 — it is not missing.
416
+
414
417
  At the end of the file:
415
418
 
416
419
  ```ts
@@ -420,12 +423,20 @@ export type Markdown = string | MarkdownDoc;
420
423
  export const MarkdownQuery = Selection;
421
424
  ```
422
425
 
423
- - [ ] **Step 4: Run test to verify it passes**
426
+ Before relying on that alias, confirm nothing imports the class by name:
427
+
428
+ ```bash
429
+ git grep -ln "MarkdownQuery" -- src bin boat tests
430
+ ```
431
+
432
+ Expected: only `src/utils/mdq/query.ts`. Any other file needs its import checked.
433
+
434
+ - [x] **Step 4: Run test to verify it passes**
424
435
 
425
436
  Run: `bun test tests/unit/mdq/query.test.ts -t 'MarkdownDoc chaining'`
426
437
  Expected: PASS (4 tests)
427
438
 
428
- - [ ] **Step 5: Fix the write assertions in the ported suite**
439
+ - [x] **Step 5: Fix the write assertions in the ported suite**
429
440
 
430
441
  The existing `replace` tests assert against a string. Wrap each in `String(...)`, for example:
431
442
 
@@ -442,7 +453,7 @@ Apply the same to every assertion in the `replace`, `setKeyValue` and `edge case
442
453
  Run: `bun test tests/unit/mdq/query.test.ts`
443
454
  Expected: PASS — 114 tests
444
455
 
445
- - [ ] **Step 6: Migrate the four breakage classes**
456
+ - [x] **Step 6: Migrate the four breakage classes**
446
457
 
447
458
  **(a) Assignment into a `string`-typed target** — append `.toString()`:
448
459
 
@@ -490,7 +501,7 @@ Expected: PASS — 114 tests
490
501
 
491
502
  **Leave alone** — these already work because `mdq()` accepts a `MarkdownDoc`: `planner.ts:303`, `planner.ts:405`.
492
503
 
493
- - [ ] **Step 7: Fix the one regex call site**
504
+ - [x] **Step 7: Fix the one regex call site**
494
505
 
495
506
  `src/ai/researcher.ts:316` relies on regex matching being implicitly case-insensitive. Task 4 removes that. Make the flag explicit now so the two changes never overlap:
496
507
 
@@ -498,27 +509,33 @@ Expected: PASS — 114 tests
498
509
  const summaryText = mdq(result.text).query('section2(/^summary/i)').query('paragraph[0]').text().trim();
499
510
  ```
500
511
 
501
- - [ ] **Step 8: Verify with the scoped type check**
512
+ - [x] **Step 8: Verify with the scoped type check**
513
+
514
+ Grep by **type name, repo-wide** — not by file path. A path-scoped grep misses test
515
+ helpers and bin scripts, and those break too:
502
516
 
503
517
  ```bash
504
- bunx tsc -p tsconfig.json --noEmit 2>&1 | grep -E "^(src/utils/mdq/|src/utils/markdown-query|src/experience-tracker|src/ai/planner|src/ai/researcher)"
518
+ bunx tsc -p tsconfig.json --noEmit 2>&1 | grep -E "MarkdownDoc|MarkdownQuery|Selection"
505
519
  ```
506
520
 
507
- Expected: **exactly these two lines and nothing else.**
521
+ Expected: **no output at all.**
508
522
 
509
- ```
510
- src/ai/researcher/locators.ts(247,41): error TS2339: Property 'playwrightLocatorCount' does not exist on type 'Explorer'.
511
- src/ai/researcher/locators.ts(247,65): error TS7006: Parameter 'page' implicitly has an 'any' type.
512
- ```
523
+ Any line is an unmigrated call site. CI will not catch it, because CI runs `tsc --noCheck`.
513
524
 
514
- Any third line is an unmigrated call site. Fix it before continuing CI will not catch it, because CI runs `tsc --noCheck`.
525
+ The audit in Step 6 lists eleven sites; the real count is **seventeen**. The extras are all
526
+ the same shape — a function whose declared return type is `string` now returns a
527
+ `MarkdownDoc` — in `boat/prima/src/prima.ts`, `planner.ts` (three chains),
528
+ `deep-analysis.ts:542` and `experience-tracker.ts:511`, plus a test helper in
529
+ `tests/unit/research-parser-pagination.test.ts` that feeds the result to `marked.lexer`,
530
+ which does **not** coerce and throws `e.replace is not a function` at runtime. Trust the
531
+ grep, not the list.
515
532
 
516
- - [ ] **Step 9: Run the full unit suite**
533
+ - [x] **Step 9: Run the full unit suite**
517
534
 
518
535
  Run: `bun test tests/unit/`
519
536
  Expected: PASS, no new failures
520
537
 
521
- - [ ] **Step 10: Format, lint and commit**
538
+ - [x] **Step 10: Format, lint and commit**
522
539
 
523
540
  ```bash
524
541
  bun run format && bun run lint:fix
@@ -540,7 +557,7 @@ Four grammar changes, all additive now that Task 3 pre-fixed the one regex call
540
557
  - Consumes: `parseQuery`, `getTokenText`, `selectorToTokenType`, `matchText` from Task 2
541
558
  - Produces: `class MdqError extends Error`, `class MdqSelectorError extends MdqError` (with `index: number`), selectors `comment` and `html`
542
559
 
543
- - [ ] **Step 1: Write the failing test**
560
+ - [x] **Step 1: Write the failing test**
544
561
 
545
562
  ```ts
546
563
  // tests/unit/mdq/selectors.test.ts
@@ -629,7 +646,7 @@ describe('selector errors', () => {
629
646
  mdq(doc).query('h2("A") secton("B")');
630
647
  expect.unreachable();
631
648
  } catch (error) {
632
- expect(error.index).toBe(7);
649
+ expect(error.index).toBe(8);
633
650
  }
634
651
  });
635
652
 
@@ -639,12 +656,12 @@ describe('selector errors', () => {
639
656
  });
640
657
  ```
641
658
 
642
- - [ ] **Step 2: Run test to verify it fails**
659
+ - [x] **Step 2: Run test to verify it fails**
643
660
 
644
661
  Run: `bun test tests/unit/mdq/selectors.test.ts`
645
662
  Expected: FAIL — `MdqSelectorError` is not exported
646
663
 
647
- - [ ] **Step 3: Implement the four changes**
664
+ - [x] **Step 3: Implement the four changes**
648
665
 
649
666
  Error classes, at the top of the class section:
650
667
 
@@ -769,17 +786,17 @@ export interface NodeInfo {
769
786
  }
770
787
  ```
771
788
 
772
- - [ ] **Step 4: Run test to verify it passes**
789
+ - [x] **Step 4: Run test to verify it passes**
773
790
 
774
791
  Run: `bun test tests/unit/mdq/selectors.test.ts`
775
792
  Expected: PASS (13 tests)
776
793
 
777
- - [ ] **Step 5: Verify nothing regressed**
794
+ - [x] **Step 5: Verify nothing regressed**
778
795
 
779
796
  Run: `bun test tests/unit/`
780
797
  Expected: PASS — in particular `query.test.ts` still at 114, since Task 3 already fixed `researcher.ts:316`
781
798
 
782
- - [ ] **Step 6: Format, lint and commit**
799
+ - [x] **Step 6: Format, lint and commit**
783
800
 
784
801
  ```bash
785
802
  bun run format && bun run lint:fix
@@ -806,7 +823,7 @@ Removes the hand-escaping wart: `section.name.replace(/"/g, '\\"')` at `research
806
823
  - Sugar on both classes: `section` `heading` `paragraph` `table` `list` `item` `code` `blockquote` `comment` `html` `hr`
807
824
  - `at(n: number): Selection` and `slice(from?: number, to?: number): Selection` on `Selection`
808
825
 
809
- - [ ] **Step 1: Write the failing test**
826
+ - [x] **Step 1: Write the failing test**
810
827
 
811
828
  ```ts
812
829
  // tests/unit/mdq/sugar.test.ts
@@ -918,12 +935,12 @@ describe('canonical read names', () => {
918
935
  });
919
936
  ```
920
937
 
921
- - [ ] **Step 2: Run test to verify it fails**
938
+ - [x] **Step 2: Run test to verify it fails**
922
939
 
923
940
  Run: `bun test tests/unit/mdq/sugar.test.ts`
924
941
  Expected: FAIL — `mdq(...).heading is not a function`
925
942
 
926
- - [ ] **Step 3: Implement matchers and the shared sugar base**
943
+ - [x] **Step 3: Implement matchers and the shared sugar base**
927
944
 
928
945
  A value matcher bypasses the grammar entirely, so it needs its own `TextMatcher` mode:
929
946
 
@@ -1027,7 +1044,7 @@ On `Selection`, add:
1027
1044
 
1028
1045
  At the end of the file add `Matcher`, `SelectorOptions`, `predicate?: (text: string) => boolean` on `TextMatcher`, and widen its `mode` to include `'predicate'`.
1029
1046
 
1030
- - [ ] **Step 4: Run test to verify it passes**
1047
+ - [x] **Step 4: Run test to verify it passes**
1031
1048
 
1032
1049
  Run: `bun test tests/unit/mdq/sugar.test.ts`
1033
1050
  Expected: PASS (21 tests)
@@ -1035,7 +1052,7 @@ Expected: PASS (21 tests)
1035
1052
  The `canonical read names` block fails until Step 5 adds the renames — that is expected.
1036
1053
  Run Step 5 before treating those four as real failures.
1037
1054
 
1038
- - [ ] **Step 5: Add the read renames and their deprecated aliases**
1055
+ - [x] **Step 5: Add the read renames and their deprecated aliases**
1039
1056
 
1040
1057
  Canonical names, with the old ones kept and marked:
1041
1058
 
@@ -1060,7 +1077,7 @@ Each alias is one line, for example:
1060
1077
  Run: `bun test tests/unit/`
1061
1078
  Expected: PASS — the ported suite still calls the deprecated names and must keep working
1062
1079
 
1063
- - [ ] **Step 6: Format, lint and commit**
1080
+ - [x] **Step 6: Format, lint and commit**
1064
1081
 
1065
1082
  ```bash
1066
1083
  bun run format && bun run lint:fix
@@ -1093,7 +1110,7 @@ This matters because `marked` separators are uneven: a `heading` raw is `"# A\n\
1093
1110
  - Produces, on `Selection`: `remove()`, `insertBefore(md)`, `insertAfter(md)`, `prepend(md)`, `append(md)`
1094
1111
  - Produces, on `MarkdownDoc`: `append(md)`, `prepend(md)`
1095
1112
 
1096
- - [ ] **Step 1: Write the failing test**
1113
+ - [x] **Step 1: Write the failing test**
1097
1114
 
1098
1115
  ```ts
1099
1116
  // tests/unit/mdq/edit.test.ts
@@ -1199,12 +1216,12 @@ describe('chained edits', () => {
1199
1216
  });
1200
1217
  ```
1201
1218
 
1202
- - [ ] **Step 2: Run test to verify it fails**
1219
+ - [x] **Step 2: Run test to verify it fails**
1203
1220
 
1204
1221
  Run: `bun test tests/unit/mdq/edit.test.ts`
1205
1222
  Expected: FAIL — `.remove is not a function`
1206
1223
 
1207
- - [ ] **Step 3: Write edit.ts**
1224
+ - [x] **Step 3: Write edit.ts**
1208
1225
 
1209
1226
  ```ts
1210
1227
  import type { MatchedRange } from './query.ts';
@@ -1238,20 +1255,28 @@ export function removeRanges(source: string, ranges: MatchedRange[]): string {
1238
1255
  let result = source;
1239
1256
  for (let i = ordered.length - 1; i >= 0; i--) {
1240
1257
  const range = ordered[i];
1241
- const end = blockEnd(range);
1242
- let start = range.start;
1243
- if (!range.trailing) start = trimPrecedingBlankLine(result, start);
1244
- result = result.slice(0, start) + result.slice(end);
1258
+ const head = result.slice(0, range.start);
1259
+ const tail = result.slice(blockEnd(range));
1260
+ if (tail) {
1261
+ result = head + tail;
1262
+ continue;
1263
+ }
1264
+ if (!head) {
1265
+ result = '';
1266
+ continue;
1267
+ }
1268
+ result = `${head.replace(/\n+$/, '')}\n`;
1245
1269
  }
1246
1270
  return result;
1247
1271
  }
1248
1272
 
1249
1273
  export function insertAt(source: string, offset: number, markdown: string): string {
1250
1274
  const block = normalizeBlock(markdown);
1251
- const before = source.slice(0, offset);
1252
- const after = source.slice(offset);
1253
- if (!after) return `${before}${before.endsWith('\n') ? '' : '\n'}\n${block}`.replace(/\n{3,}/g, '\n\n');
1254
- return `${before}${block}\n${after}`.replace(/\n{3,}/g, '\n\n');
1275
+ const before = source.slice(0, offset).replace(/\n+$/, '');
1276
+ const after = source.slice(offset).replace(/^\n+/, '');
1277
+ if (!before) return `${block}\n${after}`;
1278
+ if (!after) return `${before}\n\n${block}`;
1279
+ return `${before}\n\n${block}\n${after}`;
1255
1280
  }
1256
1281
 
1257
1282
  function dedupeRanges(ranges: MatchedRange[]): MatchedRange[] {
@@ -1266,17 +1291,26 @@ function dedupeRanges(ranges: MatchedRange[]): MatchedRange[] {
1266
1291
  return kept;
1267
1292
  }
1268
1293
 
1269
- function trimPrecedingBlankLine(source: string, start: number): number {
1270
- let cursor = start;
1271
- while (cursor > 0 && source[cursor - 1] === '\n') cursor--;
1272
- if (cursor === 0) return 0;
1273
- return cursor + 1;
1274
- }
1275
1294
  ```
1276
1295
 
1277
- Note `insertAt` collapses any run of three or more newlines to exactly two. That single rule is what enforces the invariant across every insert path, rather than each verb reasoning about separators itself.
1296
+ Both rules below were derived from real `marked` output, not assumed. **Do not "simplify"
1297
+ either one.**
1298
+
1299
+ **`insertAt` normalizes only the seam.** It strips newlines from the end of `before` and
1300
+ the start of `after`, then rebuilds the join. The tempting alternative — a global
1301
+ `.replace(/\n{3,}/g, '\n\n')` over the document — is wrong: a fenced code block's raw really
1302
+ does contain runs of blank lines (`marked` lexes a ```js block holding `a\n\n\nb` as one
1303
+ `code` token whose raw carries `\n\n\n`), so a global collapse silently rewrites user code.
1304
+
1305
+ **`removeRanges` collapses only at end-of-document.** `marked` bakes the separator into
1306
+ some raws and not others: a mid-document `paragraph` raw is `"para"` with a sibling `space`
1307
+ token, a document-final `paragraph` raw is `"last\n"` with no sibling, and every `heading`
1308
+ raw carries its own `"\n\n"`. So "no trailing space, therefore trim backwards" is wrong — on
1309
+ `'# A\n\n## B\n\ntext\n'` it eats a blank line and yields `'# A\ntext\n'`. Deleting
1310
+ `[start, blockEnd)` is already correct whenever anything follows; only a node removed from
1311
+ the very end needs repair.
1278
1312
 
1279
- - [ ] **Step 4: Wire the verbs onto Selection and MarkdownDoc**
1313
+ - [x] **Step 4: Wire the verbs onto Selection and MarkdownDoc**
1280
1314
 
1281
1315
  On `Selection`, four public verbs delegating to one private helper:
1282
1316
 
@@ -1355,14 +1389,14 @@ export class MdqOperationError extends MdqError {
1355
1389
  }
1356
1390
  ```
1357
1391
 
1358
- - [ ] **Step 5: Run test to verify it passes**
1392
+ - [x] **Step 5: Run test to verify it passes**
1359
1393
 
1360
1394
  Run: `bun test tests/unit/mdq/edit.test.ts`
1361
1395
  Expected: PASS (19 tests)
1362
1396
 
1363
1397
  If a whitespace assertion fails, print the actual output with `JSON.stringify` before changing anything — the difference is almost always one newline, and guessing at it will break a different case.
1364
1398
 
1365
- - [ ] **Step 6: Run the whole suite and commit**
1399
+ - [x] **Step 6: Run the whole suite and commit**
1366
1400
 
1367
1401
  ```bash
1368
1402
  bun test tests/unit/
@@ -1385,7 +1419,7 @@ git commit -m "feat(mdq): remove and insert verbs with whitespace normalization"
1385
1419
  - Produces in `edit.ts`: `renderTable(headers: string[], rows: string[][], align: (string | null)[]): string`, `renderItem(listRaw: string, text: string): string`
1386
1420
  - Produces on `Selection`: `addRow(row: Record<string, string>): MarkdownDoc`, `addItem(text: string): MarkdownDoc`
1387
1421
 
1388
- - [ ] **Step 1: Write the failing test**
1422
+ - [x] **Step 1: Write the failing test**
1389
1423
 
1390
1424
  ```ts
1391
1425
  // tests/unit/mdq/structural.test.ts
@@ -1451,12 +1485,12 @@ describe('addItem', () => {
1451
1485
  });
1452
1486
  ```
1453
1487
 
1454
- - [ ] **Step 2: Run test to verify it fails**
1488
+ - [x] **Step 2: Run test to verify it fails**
1455
1489
 
1456
1490
  Run: `bun test tests/unit/mdq/structural.test.ts`
1457
1491
  Expected: FAIL — `.addRow is not a function`
1458
1492
 
1459
- - [ ] **Step 3: Add the renderers to edit.ts**
1493
+ - [x] **Step 3: Add the renderers to edit.ts**
1460
1494
 
1461
1495
  ```ts
1462
1496
  export function renderTable(headers: string[], rows: string[][], align: (string | null)[]): string {
@@ -1484,7 +1518,7 @@ function dashes(alignment: string | null, width: number): string {
1484
1518
  }
1485
1519
  ```
1486
1520
 
1487
- - [ ] **Step 4: Wire the verbs onto Selection**
1521
+ - [x] **Step 4: Wire the verbs onto Selection**
1488
1522
 
1489
1523
  ```ts
1490
1524
  addRow(row: Record<string, string>): MarkdownDoc {
@@ -1510,12 +1544,12 @@ function dashes(alignment: string | null, width: number): string {
1510
1544
  }
1511
1545
  ```
1512
1546
 
1513
- - [ ] **Step 5: Run test to verify it passes**
1547
+ - [x] **Step 5: Run test to verify it passes**
1514
1548
 
1515
1549
  Run: `bun test tests/unit/mdq/structural.test.ts`
1516
1550
  Expected: PASS (10 tests)
1517
1551
 
1518
- - [ ] **Step 6: Run the whole suite and commit**
1552
+ - [x] **Step 6: Run the whole suite and commit**
1519
1553
 
1520
1554
  ```bash
1521
1555
  bun test tests/unit/
@@ -1541,7 +1575,7 @@ git commit -m "feat(mdq): addRow and addItem structural inserts"
1541
1575
 
1542
1576
  Reading and writing both go through `yaml`'s **Document API** (`YAML.parseDocument`), never `parse`/`stringify`. That is what preserves comments through a write — verified behaviour, not an assumption.
1543
1577
 
1544
- - [ ] **Step 1: Write the failing test**
1578
+ - [x] **Step 1: Write the failing test**
1545
1579
 
1546
1580
  ```ts
1547
1581
  // append to tests/unit/mdq/frontmatter.test.ts
@@ -1612,19 +1646,19 @@ describe('entries and setEntry', () => {
1612
1646
  });
1613
1647
  ```
1614
1648
 
1615
- - [ ] **Step 2: Run test to verify it fails**
1649
+ - [x] **Step 2: Run test to verify it fails**
1616
1650
 
1617
1651
  Run: `bun test tests/unit/mdq/frontmatter.test.ts`
1618
1652
  Expected: FAIL — `.frontmatter is not a function`
1619
1653
 
1620
- - [ ] **Step 3: Implement in edit.ts**
1654
+ - [x] **Step 3: Implement in edit.ts**
1621
1655
 
1622
1656
  ```ts
1623
1657
  import YAML from 'yaml';
1624
1658
 
1625
1659
  export function writeFrontmatter(source: string, key: string, value: unknown): string {
1626
1660
  const { raw, body, offset } = splitFrontmatter(source);
1627
- const document = YAML.parseDocument(raw || '');
1661
+ const document = raw ? YAML.parseDocument(raw) : new YAML.Document({});
1628
1662
  if (value === null) document.delete(key);
1629
1663
  if (value !== null) document.set(key, value);
1630
1664
  const rendered = document.toString().replace(/\s+$/, '');
@@ -1662,7 +1696,7 @@ export function rewriteEntries(tokenText: string, isBlockquote: boolean, key: st
1662
1696
 
1663
1697
  `entryKey` moves to `edit.ts` alongside it.
1664
1698
 
1665
- - [ ] **Step 4: Wire onto the classes**
1699
+ - [x] **Step 4: Wire onto the classes**
1666
1700
 
1667
1701
  ```ts
1668
1702
  frontmatter(): Record<string, unknown> {
@@ -1685,12 +1719,12 @@ export function rewriteEntries(tokenText: string, isBlockquote: boolean, key: st
1685
1719
  }
1686
1720
  ```
1687
1721
 
1688
- - [ ] **Step 5: Run test to verify it passes**
1722
+ - [x] **Step 5: Run test to verify it passes**
1689
1723
 
1690
1724
  Run: `bun test tests/unit/mdq/frontmatter.test.ts`
1691
1725
  Expected: PASS (21 tests)
1692
1726
 
1693
- - [ ] **Step 6: Confirm knowledge and experience files now parse correctly**
1727
+ - [x] **Step 6: Confirm knowledge and experience files now parse correctly**
1694
1728
 
1695
1729
  This is the real-world check that motivated the feature:
1696
1730
 
@@ -1710,7 +1744,7 @@ Expected: frontmatter parsed as an object on each file, and **no heading whose t
1710
1744
 
1711
1745
  If either directory is empty, skip this step and note it.
1712
1746
 
1713
- - [ ] **Step 7: Run the whole suite and commit**
1747
+ - [x] **Step 7: Run the whole suite and commit**
1714
1748
 
1715
1749
  ```bash
1716
1750
  bun test tests/unit/
@@ -1732,7 +1766,7 @@ The package is publish-ready only if someone can use it without reading the sour
1732
1766
  - Consumes: the complete API from Tasks 3-8
1733
1767
  - Produces: nothing code depends on
1734
1768
 
1735
- - [ ] **Step 1: Write the README**
1769
+ - [x] **Step 1: Write the README**
1736
1770
 
1737
1771
  Cover, in this order:
1738
1772
 
@@ -1750,7 +1784,7 @@ Do **not** document the deprecated aliases (`get` `toJson` `keyValue` `setKeyVal
1750
1784
 
1751
1785
  Follow the repo docs style: show each format example once, and do not close with a "Why this matters" section.
1752
1786
 
1753
- - [ ] **Step 2: Verify every example in the README actually runs**
1787
+ - [x] **Step 2: Verify every example in the README actually runs**
1754
1788
 
1755
1789
  Extract each fenced `js` block and execute it. Any example that throws or prints something other than what the README claims is a documentation bug — fix the README, not the test.
1756
1790
 
@@ -1761,7 +1795,7 @@ import { mdq } from "./src/utils/mdq/query.ts";
1761
1795
  '
1762
1796
  ```
1763
1797
 
1764
- - [ ] **Step 3: Commit**
1798
+ - [x] **Step 3: Commit**
1765
1799
 
1766
1800
  ```bash
1767
1801
  git add src/utils/mdq/README.md
@@ -1786,7 +1820,7 @@ git commit -m "docs(mdq): package README"
1786
1820
 
1787
1821
  Note a deliberate deviation from `CLAUDE.md`: command logic normally lives in `src/commands/`, but mdq must not import from anywhere in explorbot. Its CLI ships with the package.
1788
1822
 
1789
- - [ ] **Step 1: Write the failing test**
1823
+ - [x] **Step 1: Write the failing test**
1790
1824
 
1791
1825
  ```ts
1792
1826
  // tests/unit/mdq/cli.test.ts
@@ -1853,7 +1887,9 @@ describe('edits', () => {
1853
1887
  });
1854
1888
 
1855
1889
  it('sets an entry', async () => {
1856
- expect((await runMdq(['blockquote', '--set', 'Container=.x'], '> Container: .old\n')).output).toContain('.x');
1890
+ const result = await runMdq(['blockquote', '--set', 'Container=.x'], '> Container: .old\n');
1891
+ expect(result.output).toContain('.x');
1892
+ expect(result.output).not.toContain('.old');
1857
1893
  });
1858
1894
  });
1859
1895
 
@@ -1874,12 +1910,12 @@ describe('exit codes', () => {
1874
1910
  });
1875
1911
  ```
1876
1912
 
1877
- - [ ] **Step 2: Run test to verify it fails**
1913
+ - [x] **Step 2: Run test to verify it fails**
1878
1914
 
1879
1915
  Run: `bun test tests/unit/mdq/cli.test.ts`
1880
1916
  Expected: FAIL — cannot resolve `cli.ts`
1881
1917
 
1882
- - [ ] **Step 3: Implement cli.ts**
1918
+ - [x] **Step 3: Implement cli.ts**
1883
1919
 
1884
1920
  Use Commander with `exitOverride()` and `.configureOutput()` so a parse failure surfaces as a return value rather than killing the process. Shape:
1885
1921
 
@@ -1926,12 +1962,12 @@ export interface CliResult {
1926
1962
  }
1927
1963
  ```
1928
1964
 
1929
- - [ ] **Step 4: Run test to verify it passes**
1965
+ - [x] **Step 4: Run test to verify it passes**
1930
1966
 
1931
1967
  Run: `bun test tests/unit/mdq/cli.test.ts`
1932
1968
  Expected: PASS (13 tests)
1933
1969
 
1934
- - [ ] **Step 5: Add the thin bin entry**
1970
+ - [x] **Step 5: Add the thin bin entry**
1935
1971
 
1936
1972
  ```ts
1937
1973
  #!/usr/bin/env bun
@@ -1950,7 +1986,7 @@ Add to `package.json` `bin`:
1950
1986
  "mdq": "./dist/bin/mdq.js"
1951
1987
  ```
1952
1988
 
1953
- - [ ] **Step 6: Smoke-test the real binary**
1989
+ - [x] **Step 6: Smoke-test the real binary**
1954
1990
 
1955
1991
  ```bash
1956
1992
  echo '# A
@@ -1971,7 +2007,7 @@ bun run bin/mdq.ts 'nonsense' CLAUDE.md; echo "exit=$?"
1971
2007
  ```
1972
2008
  Expected: `Unknown selector "nonsense"` and `exit=2`
1973
2009
 
1974
- - [ ] **Step 7: Run the whole suite and commit**
2010
+ - [x] **Step 7: Run the whole suite and commit**
1975
2011
 
1976
2012
  ```bash
1977
2013
  bun test tests/unit/
@@ -1987,12 +2023,12 @@ git commit -m "feat(mdq): jq-like CLI"
1987
2023
  **Files:**
1988
2024
  - Modify: `CHANGELOG.md`
1989
2025
 
1990
- - [ ] **Step 1: Run the full unit suite**
2026
+ - [x] **Step 1: Run the full unit suite**
1991
2027
 
1992
2028
  Run: `bun test tests/unit/`
1993
2029
  Expected: PASS, no failures
1994
2030
 
1995
- - [ ] **Step 2: Re-run the scoped type check**
2031
+ - [x] **Step 2: Re-run the scoped type check**
1996
2032
 
1997
2033
  ```bash
1998
2034
  bunx tsc -p tsconfig.json --noEmit 2>&1 | grep -E "^(src/utils/mdq/|src/utils/markdown-query|src/experience-tracker|src/ai/planner|src/ai/researcher|bin/mdq)"
@@ -2000,7 +2036,7 @@ bunx tsc -p tsconfig.json --noEmit 2>&1 | grep -E "^(src/utils/mdq/|src/utils/ma
2000
2036
 
2001
2037
  Expected: **exactly the two known `locators.ts(247,...)` lines.** Anything else is a real defect that CI will not catch.
2002
2038
 
2003
- - [ ] **Step 3: Confirm the package has no explorbot imports**
2039
+ - [x] **Step 3: Confirm the package has no explorbot imports**
2004
2040
 
2005
2041
  ```bash
2006
2042
  grep -rn "^import\|from '" src/utils/mdq/*.ts | grep -v "'marked'" | grep -v "'yaml'" | grep -v "'commander'" | grep -v "'./"
@@ -2008,11 +2044,11 @@ grep -rn "^import\|from '" src/utils/mdq/*.ts | grep -v "'marked'" | grep -v "'y
2008
2044
 
2009
2045
  Expected: **no output.** Any line here breaks extractability, which is the whole point of the package.
2010
2046
 
2011
- - [ ] **Step 4: Update the changelog**
2047
+ - [x] **Step 4: Update the changelog**
2012
2048
 
2013
2049
  Use the `/changelog` skill, per `CLAUDE.md`.
2014
2050
 
2015
- - [ ] **Step 5: Commit**
2051
+ - [x] **Step 5: Commit**
2016
2052
 
2017
2053
  ```bash
2018
2054
  git add CHANGELOG.md