@spikedpunch/mast 0.2.0 → 0.3.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 (80) hide show
  1. package/MAST_SPEC.md +169 -44
  2. package/README.md +86 -11
  3. package/assets/skill.md +49 -10
  4. package/dist/ast/types.d.ts +137 -1
  5. package/dist/ast/types.d.ts.map +1 -1
  6. package/dist/cli/query.d.ts +40 -1
  7. package/dist/cli/query.d.ts.map +1 -1
  8. package/dist/cli/query.js +36 -2
  9. package/dist/cli/query.js.map +1 -1
  10. package/dist/cli/search-cmd.d.ts +6 -1
  11. package/dist/cli/search-cmd.d.ts.map +1 -1
  12. package/dist/cli/search-cmd.js +9 -2
  13. package/dist/cli/search-cmd.js.map +1 -1
  14. package/dist/cli/serve.d.ts +6 -1
  15. package/dist/cli/serve.d.ts.map +1 -1
  16. package/dist/cli/serve.js +14 -4
  17. package/dist/cli/serve.js.map +1 -1
  18. package/dist/cli/status.d.ts +15 -0
  19. package/dist/cli/status.d.ts.map +1 -1
  20. package/dist/cli/status.js +32 -6
  21. package/dist/cli/status.js.map +1 -1
  22. package/dist/indexer/freshness.d.ts +10 -0
  23. package/dist/indexer/freshness.d.ts.map +1 -1
  24. package/dist/indexer/freshness.js +7 -1
  25. package/dist/indexer/freshness.js.map +1 -1
  26. package/dist/indexer/import-resolver.js +28 -16
  27. package/dist/indexer/import-resolver.js.map +1 -1
  28. package/dist/indexer/index.d.ts +38 -3
  29. package/dist/indexer/index.d.ts.map +1 -1
  30. package/dist/indexer/index.js +47 -4
  31. package/dist/indexer/index.js.map +1 -1
  32. package/dist/indexer/watcher.d.ts +16 -0
  33. package/dist/indexer/watcher.d.ts.map +1 -1
  34. package/dist/indexer/watcher.js +8 -0
  35. package/dist/indexer/watcher.js.map +1 -1
  36. package/dist/mcp/context.d.ts +16 -0
  37. package/dist/mcp/context.d.ts.map +1 -1
  38. package/dist/mcp/freshness-probe.d.ts +82 -0
  39. package/dist/mcp/freshness-probe.d.ts.map +1 -0
  40. package/dist/mcp/freshness-probe.js +111 -0
  41. package/dist/mcp/freshness-probe.js.map +1 -0
  42. package/dist/mcp/server.d.ts +46 -3
  43. package/dist/mcp/server.d.ts.map +1 -1
  44. package/dist/mcp/server.js +65 -3
  45. package/dist/mcp/server.js.map +1 -1
  46. package/dist/mcp/tools/_helpers.d.ts +40 -0
  47. package/dist/mcp/tools/_helpers.d.ts.map +1 -1
  48. package/dist/mcp/tools/_helpers.js +27 -0
  49. package/dist/mcp/tools/_helpers.js.map +1 -1
  50. package/dist/mcp/tools/callers.d.ts.map +1 -1
  51. package/dist/mcp/tools/callers.js +6 -1
  52. package/dist/mcp/tools/callers.js.map +1 -1
  53. package/dist/mcp/tools/dependencies.d.ts.map +1 -1
  54. package/dist/mcp/tools/dependencies.js +2 -1
  55. package/dist/mcp/tools/dependencies.js.map +1 -1
  56. package/dist/mcp/tools/exports.d.ts.map +1 -1
  57. package/dist/mcp/tools/exports.js +2 -1
  58. package/dist/mcp/tools/exports.js.map +1 -1
  59. package/dist/mcp/tools/implementors.d.ts.map +1 -1
  60. package/dist/mcp/tools/implementors.js +2 -1
  61. package/dist/mcp/tools/implementors.js.map +1 -1
  62. package/dist/mcp/tools/project-skeleton.d.ts.map +1 -1
  63. package/dist/mcp/tools/project-skeleton.js +2 -1
  64. package/dist/mcp/tools/project-skeleton.js.map +1 -1
  65. package/dist/mcp/tools/reindex.d.ts.map +1 -1
  66. package/dist/mcp/tools/reindex.js +3 -0
  67. package/dist/mcp/tools/reindex.js.map +1 -1
  68. package/dist/mcp/tools/rename-impact.d.ts.map +1 -1
  69. package/dist/mcp/tools/rename-impact.js +4 -1
  70. package/dist/mcp/tools/rename-impact.js.map +1 -1
  71. package/dist/mcp/tools/search.d.ts.map +1 -1
  72. package/dist/mcp/tools/search.js +13 -2
  73. package/dist/mcp/tools/search.js.map +1 -1
  74. package/dist/mcp/tools/signature.d.ts.map +1 -1
  75. package/dist/mcp/tools/signature.js +2 -1
  76. package/dist/mcp/tools/signature.js.map +1 -1
  77. package/dist/mcp/tools/status.d.ts.map +1 -1
  78. package/dist/mcp/tools/status.js +8 -2
  79. package/dist/mcp/tools/status.js.map +1 -1
  80. package/package.json +4 -2
package/MAST_SPEC.md CHANGED
@@ -41,13 +41,14 @@ the pre-deletion system is preserved at the git tag
41
41
  - Code generation or explanation.
42
42
  - PR review, wiki generation, story generation.
43
43
  - A persistent background daemon — freshness is handled by the startup check and
44
- `mast_reindex`. *Narrow carve-out:* `mast serve --watch` (§11.4) is an opt-in
45
- file watcher for **interactive, non-container** use only. It is scoped to the
46
- serve process lifetime (not a daemon), the SDD pipeline never uses it, and it
47
- is a discovery-freshness optimization — it lets new files and symbols become
48
- searchable sooner than waiting for an explicit `mast_reindex`. JIT staleness
49
- handling (§9.0) already guarantees line-coordinate and content correctness for
50
- already-indexed files without it.
44
+ `mast_reindex`. *Narrow carve-out:* `mast serve`'s file watcher (§11.4, on by
45
+ default since 2026-09-03; `--no-watch` opts out, and the SDD pipeline passes
46
+ it). It is scoped to the serve process lifetime (not a daemon) and is a
47
+ discovery-freshness optimization — it lets new files and symbols become
48
+ searchable without waiting for an explicit `mast_reindex`. JIT staleness
49
+ handling (§9.0) guarantees line-coordinate and content correctness for
50
+ already-indexed files without it, but is structurally blind to files the
51
+ index has never seen, which is what the watcher covers.
51
52
  - Support for non-TypeScript/JavaScript projects in v1 (AST layer is extensible but
52
53
  v1 targets the SDD stack).
53
54
 
@@ -157,7 +158,7 @@ The claude-runner passes `--state-dir` at serve time:
157
158
  "mcpServers": {
158
159
  "mast": {
159
160
  "command": "mast",
160
- "args": ["serve", "--state-dir", "/workspace/.kluster/.mast"],
161
+ "args": ["serve", "--no-watch", "--state-dir", "/workspace/.kluster/.mast"],
161
162
  "type": "stdio"
162
163
  }
163
164
  }
@@ -596,8 +597,15 @@ startup
596
597
  (mast_search, mast_callers verified+potential, etc.)
597
598
  ```
598
599
 
599
- If `--watch` was passed to `mast serve`, the file watcher (§11.4) starts
600
- immediately after Step 3's transport opens, independent of Step 4.
600
+ Unless `--no-watch` was passed, the file watcher (§11.4) starts immediately
601
+ after Step 3's transport opens, independent of Step 4.
602
+
603
+ The freshness probe (`mcp/freshness-probe.ts`) behind `mast_search`'s
604
+ `unindexed_files` signal (§9.0) is primed here too — in the same background
605
+ task as Step 4 and strictly AFTER it, so the measurement reflects the index the
606
+ reindex just produced rather than racing it. Under `--no-startup-reindex` it is
607
+ primed immediately: no reindex is coming, so the drift the operator chose to
608
+ keep is exactly what wants measuring.
601
609
 
602
610
  **`--no-startup-reindex` refusal (M6 Part A).** The empty-during-Step-4 window
603
611
  above is legitimate and by design — but `--no-startup-reindex` disables Step 4
@@ -632,13 +640,21 @@ reindex runs in the background.
632
640
  **Fast first-task latency.** With a baked seed (§13.8), Steps 1–3 typically complete
633
641
  in **2–4 seconds** on a cold container. Step 4 then catches up any files changed
634
642
  since the seed was built in the background — the agent can begin useful work as
635
- soon as Step 3 completes; JIT staleness handling (§9.0) guarantees any individual
636
- file it queries is correct even before Step 4 reaches it.
643
+ soon as Step 3 completes.
644
+
645
+ Be precise about what covers that window, because the obvious summary is wrong.
646
+ JIT re-parse (§9.0) makes an individual *already-indexed* file correct on read,
647
+ and only on the five re-parsing tools; `mast_search` and `mast_implementors`
648
+ flag such a file rather than refreshing it, and a file the seed never contained
649
+ is invisible to all seven until Step 4 reaches it. The window is therefore
650
+ bounded by Step 4, not eliminated by §9.0.
637
651
 
638
652
  This is the **only hook required for the SDD pipeline**. The BT orchestrator needs no
639
653
  reindex calls. Files committed by the previous task are picked up by Step 4's
640
- filesystem scan. JIT staleness handling (§9) covers files modified mid-task before
641
- Step 4 has caught up to them.
654
+ filesystem scan. JIT re-parse (§9) covers files *modified* mid-task before Step 4
655
+ has caught up to them — on the five re-parsing tools; elsewhere they are flagged.
656
+ Files **created** mid-task are covered by neither and need `mast_reindex` (§11.2)
657
+ or the watcher (§11.4).
642
658
 
643
659
  ### 7.5 Mid-Task Reindex (`mast_reindex` MCP tool)
644
660
 
@@ -773,8 +789,9 @@ Start the MCP server over stdio.
773
789
  Options:
774
790
  --state-dir <dir> State directory
775
791
  --no-startup-reindex Skip the startup staleness check (not recommended)
792
+ --no-watch Do not watch source files (batch/container use — see §11.4)
776
793
  --watch Watch source files and incrementally reindex on change
777
- (interactive use see §11.4)
794
+ (the default; accepted for compatibility)
778
795
  ```
779
796
 
780
797
  The server runs until the parent process (Claude CLI) closes stdin.
@@ -787,10 +804,12 @@ state dir with the startup reindex left enabled (the default) is unaffected
787
804
  and starts normally, as does `--no-startup-reindex` against an already-indexed
788
805
  state dir (including one indexed over a genuinely empty file set).
789
806
 
790
- `--watch` is opt-in and intended for interactive local development; the SDD
791
- container does not use it (§3, §11.4). The watcher is closed on stdin close,
792
- SIGTERM, and SIGINT; a watcher startup failure logs a warning and the server
793
- continues without watch.
807
+ Watching is **on by default** as of 2026-09-03 (§11.4); `--no-watch` opts out and
808
+ is what the SDD container passes (§3). `--watch` is still accepted and selects
809
+ the default, so existing MCP client configurations keep working unchanged. The
810
+ watcher is closed on stdin close, SIGTERM, and SIGINT; a watcher startup failure
811
+ logs a warning and the server continues without watch — which is what makes the
812
+ default safe: it cannot prevent the server from starting.
794
813
 
795
814
  ---
796
815
 
@@ -807,6 +826,7 @@ Options:
807
826
  Output:
808
827
  ```
809
828
  state_dir: /workspace/.kluster/.mast
829
+ project_root: /workspace/.kluster
810
830
  schema_version: 1.3.0
811
831
  last_indexed: 2026-05-13T14:22:00Z (3 minutes ago)
812
832
  indexed_files: 142
@@ -818,10 +838,22 @@ index_fresh: true
818
838
  freshness_cause: none
819
839
  ```
820
840
 
841
+ `stale_files` prints its split inline when non-zero — `stale_files: 47 (changed
842
+ 0, unindexed 27, deleted 20)` — because the field names one of the three things
843
+ it counts (§9, `mast_status`).
844
+
845
+ `project_root` names the tree the report was measured against. It is reported
846
+ because `state_dir` alone does not identify it and the two are set
847
+ independently: a relative `--state-dir` resolves against the **path argument**,
848
+ not the shell's working directory, so one project's index can be read while
849
+ another project's files are asked about.
850
+
821
851
  `freshness_cause` carries the same semantics as the `mast_status` MCP tool (§9) —
822
- it prints `none` in human output when the JSON value would be `null`; the JSON
823
- value is `"phase1_stale"` when `stale_files > 0`. On a never-indexed project the
824
- state directory is not created as a side effect of running `status`.
852
+ it prints `none` in human output when the JSON value would be `null`. When it is
853
+ `root_mismatch` the table is followed by a plain-language block naming the tree
854
+ that was measured and saying that reindexing will not move the numbers. On a
855
+ never-indexed project the state directory is not created as a side effect of
856
+ running `status`.
825
857
 
826
858
  ---
827
859
 
@@ -834,12 +866,31 @@ Invoke any MCP tool (§9) directly from the CLI — the read tools (`mast_search
834
866
 
835
867
  ```
836
868
  Options:
837
- --state-dir <dir> State directory
869
+ --state-dir <dir> State directory
870
+ --reindex Run an incremental index (§7.1) before dispatching
838
871
  --json Emit the exact single-line MCP response text (machine
839
872
  use); default pretty-prints the parsed response with
840
873
  2-space indent for humans
841
874
  ```
842
875
 
876
+ `--reindex` (also on `mast search`) is the CLI's freshness mechanism. The CLI
877
+ has neither of the server's: no watcher (§11.4) and no freshness probe
878
+ (§9.0 `unindexed_files`), because a one-shot process has no lifetime over
879
+ which to amortise a TTL-cached measurement. Semantics:
880
+
881
+ - **Incremental, never full.** `mast index --full` remains the way to rebuild.
882
+ - **Refreshes, does not bootstrap.** It runs strictly *after* the never-indexed
883
+ guard below, so a missing `graph.db` still fails fast with that guard's
884
+ message rather than being silently created as a side effect of the flag.
885
+ - **Reports on stderr**, never stdout — the refresh is a side effect of the
886
+ query, not part of its answer, and `--json` consumers must keep a parseable
887
+ stdout.
888
+ - **Never fatal.** The run takes `structure.lock`, and with `mast serve`
889
+ watching by default (§11.4) losing that race to a concurrent writer is an
890
+ ordinary outcome. A failed refresh warns and the query is answered from the
891
+ existing index; failing the query over a refresh that did not happen would
892
+ be a worse answer than a slightly stale one.
893
+
843
894
  `json` (positional, default `'{}'`) is the tool's argument object as a JSON
844
895
  string; `path` is the project root (same resolution as every other command's
845
896
  `[path]`).
@@ -948,8 +999,9 @@ how many files a single call's results can span:
948
999
 
949
1000
  JIT re-parse covers files already known to the index. It does not discover a
950
1001
  brand-new file or a newly-created symbol — those become searchable via the next
951
- `mast_reindex` call or the background/`--watch` reindex (§7.4/§11.4) reaching
952
- them. The agent prompt should still recommend `mast_reindex` after writing new
1002
+ `mast_reindex` call, the startup reindex, or the watcher (§7.4/§11.4, on by
1003
+ default) reaching them. `mast_search` reports the size of that blind spot as
1004
+ `unindexed_files` when a serve process has measured it. The agent prompt should still recommend `mast_reindex` after writing new
953
1005
  files or symbols — not because JIT leaves existing files stale (it doesn't), but
954
1006
  because discovery of new ones requires an actual indexing pass.
955
1007
 
@@ -1045,11 +1097,12 @@ across all MCP tools, and what to do with each one:
1045
1097
  | `file_busy_returning_stale_cache` | JIT-refresh tools' results/envelopes (`mast_signature`, `mast_exports`, `mast_callers`, `mast_dependencies`, `mast_rename_impact`) | A refresh **was attempted** (this file's JIT re-parse) and lost to genuine write contention (`populateFile`'s `BEGIN IMMEDIATE` exhausted its `busy_timeout`), so the previous, possibly-stale chunk was returned instead. | Contended, not wrong-by-design. Retry shortly — the contention is expected to clear (§7.6). |
1046
1098
  | `stale` | `mast_search` / `mast_implementors` per-result (F7) | This result's `file_path` stat'd newer-on-disk than its indexed mtime, or the stat failed — **no refresh was attempted by design** (stat-and-flag, not JIT re-parse; see above). | Treat this result's line coordinates as untrustworthy. A `mast_reindex` call, or any JIT-refreshing tool call against the file, heals it. |
1047
1099
  | `index_empty` | Every primary-result read tool's envelope (M6) | Nothing is indexed at all — the empty result set is not "no match", it is "no index (yet)". | Run `mast init`/`mast index`, or — if a startup reindex is in progress — wait and retry. |
1100
+ | `unindexed_files` | Every primary-result read tool's envelope (D054). The five whose answer claims to be an exhaustive *set* — `mast_search`, `mast_callers`, `mast_implementors`, `mast_rename_impact`, `mast_project_skeleton` — carry it whether or not they found anything, because a short list drawn from an incomplete corpus reads exactly like a complete one and is the form that gets acted on. The three that answer about one *named* thing — `mast_signature`, `mast_exports`, `mast_dependencies` — carry it only on an empty answer, where "absent" and "never indexed" are indistinguishable; a hit is correct however much else is missing. | This many files exist on disk and are **not in the index** — measured by the serve process and TTL-cached; the request never awaits a measurement, though it may schedule one (amortised, not free — `mcp/freshness-probe.ts` documents the residual). Where `index_empty` says "nothing is indexed", this says "the index is populated but behind": these results were ranked over a corpus missing N files. Advisory — it does not claim any of the N would have matched. Absent means either nothing is unindexed or no measurement has landed yet; the two are deliberately not distinguished on the wire, because both mean "no warning to give". | Do not conclude a symbol is absent. Call `mast_reindex`, then re-query. |
1048
1101
  | `truncated` | `TypeContextEntry` (`mast_signature`'s `type_context`) | This referenced type's declaration was clipped at the 50-line cap. | Re-read the file directly (or call `mast_exports`/a narrower `mast_signature` query) for the full declaration if the clipped portion matters. |
1049
1102
  | `potential_truncated` | `CallersResponse.summary` / `RenameImpactResponse.summary` (`mast_callers`, `mast_rename_impact`) | The `identifier_fts` fetch behind `potential_matches` is capped at 50 entries; this carries the real, uncapped match count when the cap is hit (F10, Stage 3). Reports RAW fetch truncation only — `potential_matches` may still be smaller than the cap even when this field is present, because verified-overlap exclusion and checker-verdict filtering run AFTER the capped fetch (already visible via `checker_classified_*`). | The potential set is incomplete — narrow the query, or run `mast index --checker` to classify candidates away. |
1050
1103
  | `results_truncated` / `exports_truncated` | `SignatureResponse` / `ImplementorsResponse` / `ExportsResponse` envelopes (`mast_signature`, `mast_implementors`, `mast_exports`) | The result list is capped at `limit` (default 50, the same constant `potential_matches` uses); this carries the real, uncapped total when the cap is hit (D043). Unlike `potential_truncated` there is no post-cap filtering, so the returned page is always exactly `limit` long when this field is present. | A first page, not the answer. Pass a larger `limit` (max 500), or narrow with `file_path`. Before D043 these tools were unbounded: `mast_signature{symbol:'execute'}` over a 14k-file monorepo returned 580 declarations / 331k tokens in 78 s, which over MCP exceeded the client timeout and returned nothing. |
1051
1104
 
1052
- `file_busy_returning_stale_cache`, `stale`, `index_empty`,
1105
+ `file_busy_returning_stale_cache`, `stale`, `index_empty`, `unindexed_files`,
1053
1106
  `potential_truncated`, `results_truncated` and `exports_truncated` all follow the same **omitted-when-false /
1054
1107
  present-only-when-true** convention (never present-and-false) established
1055
1108
  above — `potential_truncated`'s "false" case is "the fetch came back under
@@ -1698,6 +1751,7 @@ Index health snapshot.
1698
1751
  "indexed_files": 142,
1699
1752
  "chunk_count": 1840,
1700
1753
  "stale_files": 0,
1754
+ "stale_breakdown": { "changed": 0, "unindexed": 0, "deleted": 0 },
1701
1755
  "parse_errors": 0,
1702
1756
  "write_errors": 0,
1703
1757
  "index_fresh": true,
@@ -1726,14 +1780,42 @@ conflated). Non-zero in either indicates files the agent should investigate.
1726
1780
  `seed_commit` is present only when the state directory was bootstrapped from a
1727
1781
  Docker-baked seed (§13.8) and reports the git revision the seed was built from.
1728
1782
 
1729
- **Freshness diagnostics.** `freshness_cause` is `"phase1_stale"` when `stale_files > 0`
1730
- (chunk line coordinates lag disk corrected by JIT re-parse on read, §9.0, or by
1731
- running `mast_reindex`) and `null` when the index is fully fresh. `index_fresh` is
1732
- `true` only when `stale_files === 0` and the index has been run at least once.
1783
+ **Freshness diagnostics.** `freshness_cause` names which of `stale_files`'
1784
+ categories the count is actually made of, and `null` when the index is fully
1785
+ fresh. `index_fresh` is `true` only when `stale_files === 0` and the index has
1786
+ been run at least once.
1787
+
1788
+ | value | meaning |
1789
+ |---|---|
1790
+ | `"root_mismatch"` | The index disagrees with this tree in both directions more than it agrees in either: more files here are unknown to it than known (`unindexed > walked - unindexed`), **and** it lists more absent files than known ones (`deleted > walked - unindexed`). It was built for a different project root, so reindexing will not move the numbers — the path argument or `--state-dir` is wrong. Both halves are required: the first alone would flag a mass deletion, the second alone a never-indexed project. |
1791
+ | `"phase1_stale"` | Indexed files whose content changed since — chunk line coordinates lag disk, corrected by JIT re-parse on read by the five re-parsing tools (§9.0), flagged `stale` by the other two, or fixed for all of them by `mast_reindex`. |
1792
+ | `"unindexed_files"` | Files on disk this index has never seen. |
1793
+ | `"deleted_files"` | Files the index still lists that are gone from disk. |
1794
+
1795
+ The `"unindexed_files"` **cause** here and `mast_search`'s `unindexed_files`
1796
+ **count** (§9.0) name the same population — files on disk this index has never
1797
+ seen — and are deliberately spelled the same. They differ in shape and in
1798
+ freshness: `mast_status` reports a cause label chosen from a measurement taken
1799
+ during that call, while `mast_search` reports an integer from the serve
1800
+ process's TTL-cached probe, which may be up to one TTL old. When the two
1801
+ disagree, `mast_status` is the newer measurement.
1802
+
1803
+ `root_mismatch` is tested first and is the only compound condition; the other
1804
+ three are ordered by what the caller should do about them, `phase1_stale` first
1805
+ because it is the one JIT re-parse corrects silently on read. The exact split
1806
+ always travels beside the cause in `stale_breakdown`, so a caller never has to
1807
+ infer the composition from the chosen label.
1808
+
1809
+ Until 2026-09-01 this field was `"phase1_stale"` for **any** non-zero count,
1810
+ including counts containing no changed file at all (`docs/defects/LEDGER.md`
1811
+ D049); it is now decided from counted categories rather than from the total.
1733
1812
 
1734
1813
  `stale_files` counts three things, not one: files whose content changed since they
1735
1814
  were indexed, files on disk that are **not in the index at all**, and files the index
1736
- still lists that are gone from disk. It is computed by `indexer/freshness.ts`
1815
+ still lists that are gone from disk. `stale_breakdown` reports that split as
1816
+ `{changed, unindexed, deleted}` — the total alone was documented as a union here
1817
+ and published as a scalar by both surfaces, so a caller who had read this
1818
+ paragraph still could not act on the distinction it draws (D049). It is computed by `indexer/freshness.ts`
1737
1819
  `measureFreshness`, which `mast status` and `mast_status` both call — one producer,
1738
1820
  because they answer one question. It reads the manifest and the `files.mtime` stamps
1739
1821
  and takes the union: only the manifest can see a file that was never indexed, and only
@@ -2238,9 +2320,11 @@ candidates out of `potential_matches` and report honest counts in
2238
2320
 
2239
2321
  Defined in full in §7.4. Summary: a four-step ladder that brings the whole index
2240
2322
  (graph + FTS) online in 2–4 seconds via a Docker-baked seed index (§13.8), with all
2241
- 11 tools registered and ready to serve as soon as Step 3 completes — there is no
2242
- reduced-capability warm-up window. Step 4 then catches up any files changed since
2243
- the seed was built, in the background.
2323
+ 11 tools registered and ready to serve as soon as Step 3 completes — no tool is
2324
+ unavailable or degraded during startup. One *signal* does warm up: `mast_search`'s
2325
+ `unindexed_files` is served from a cached probe, and until the first measurement
2326
+ lands the count is unknown and the field is omitted (ADR 016). Step 4 then catches
2327
+ up any files changed since the seed was built, in the background.
2244
2328
 
2245
2329
  This is the **only hook required for the SDD pipeline**.
2246
2330
 
@@ -2249,7 +2333,7 @@ This is the **only hook required for the SDD pipeline**.
2249
2333
  The agent calls this explicitly after writes. JIT staleness handling (§9.0) already
2250
2334
  keeps already-indexed files correct on read; `mast_reindex` is what makes a
2251
2335
  **brand-new** file or symbol discoverable by `mast_search`/`mast_callers`/etc. before
2252
- the next scheduled or `--watch` reindex reaches it. The implement prompt instructs:
2336
+ the next scheduled reindex or the watcher (§11.4, on by default) reaches it. The implement prompt instructs:
2253
2337
 
2254
2338
  > After writing or editing files, call `mast_reindex` before any search query that
2255
2339
  > depends on symbols you just created. This is the only way to guarantee the index
@@ -2274,19 +2358,39 @@ mast index "$(git rev-parse --show-toplevel)" --incremental
2274
2358
  Not required for the automated SDD pipeline — the startup hook covers the same
2275
2359
  scenario (files changed since last index).
2276
2360
 
2277
- ### 11.4 Optional Interactive Hook — `mast serve --watch`
2361
+ ### 11.4 File Watching — `mast serve` (default on, `--no-watch` to opt out)
2278
2362
 
2279
2363
  Local interactive development has no equivalent of the container's startup
2280
2364
  ladder: git hooks are opt-in and fire only on commit/checkout, so a long-lived
2281
2365
  interactive session can leave newly-created files and symbols undiscoverable
2282
2366
  between explicit `mast_reindex` calls, even though JIT re-parse (§9.0) keeps
2283
- every already-indexed file correct on read. `--watch` closes that gap as an
2284
- **opt-in** flag — it is a discovery-freshness optimization, never a correctness
2367
+ every already-indexed file correct on read.
2368
+
2369
+ Watching closes that gap and is **on by default** as of 2026-09-03. It was
2370
+ opt-in until then, on the reasoning that JIT staleness plus the startup ladder
2371
+ kept reads correct. That reasoning holds only for files the index already
2372
+ knows: JIT re-parses a *known* file whose mtime moved, and is structurally
2373
+ blind to one that was never indexed. So in the default configuration a file
2374
+ created mid-session was invisible to every read tool for the rest of the
2375
+ session, and `mast_search` returned `{"results":[]}` for symbols that plainly
2376
+ existed on disk — the §0 failure mode, reached through a default.
2377
+
2378
+ It remains a discovery-freshness optimization rather than a correctness
2285
2379
  mechanism, and it does not reopen the §3 no-daemon non-goal (it lives and dies
2286
- with the serve process).
2380
+ with the serve process). `--no-watch` is for batch and container use, where the
2381
+ tree does not change under the server and the fd cost buys nothing.
2287
2382
 
2288
2383
  Behaviour:
2289
2384
 
2385
+ - **Readiness is announced.** The watcher starts *after* the MCP transport is
2386
+ accepting calls, and chokidar's initial scan runs with `ignoreInitial: true`
2387
+ — correct, because the startup ladder has already indexed the tree, but it
2388
+ means a file created before that scan completes is treated as pre-existing
2389
+ and fires **no event at all**. When the scan finishes, `mast serve` writes
2390
+ `[mast] watch: watching for changes` to stderr, once. Before this existed
2391
+ (D061) silence covered three different states — watching, not watching yet,
2392
+ and watcher failed to start — because only the failure paths wrote anything.
2393
+ A tool or operator that must not miss a change waits for this line.
2290
2394
  - A chokidar watcher covers `file_extensions` under the project root,
2291
2395
  respecting `exclude_patterns` **and the state directory itself** — watching
2292
2396
  the state dir would self-trigger on every index write.
@@ -2380,7 +2484,7 @@ TypeScript (Node.js LTS). Rationale:
2380
2484
  | Locking | `proper-lockfile` | PID-based advisory lock; set `stale: 10000` (10s) to handle abrupt container exits |
2381
2485
  | CLI | `commander` | Standard TS CLI |
2382
2486
  | File walking | `fast-glob` | Glob pattern support for `exclude_patterns` |
2383
- | File watching | `chokidar` | Powers `mast serve --watch` (§11.4) |
2487
+ | File watching | `chokidar` | Powers `mast serve`'s watcher (§11.4, default on; `--no-watch` opts out) |
2384
2488
  | Token counting | `@anthropic-ai/tokenizer` | Counts `tokens_returned`/`tokens_full_file_upper_bound` for `_stats` (§14.5) |
2385
2489
  | Identifiers | `uuid` | Per-`mast serve`-session `session_id` for metrics attribution |
2386
2490
 
@@ -2417,7 +2521,7 @@ packages/mast/
2417
2521
  │ ├── indexer/
2418
2522
  │ │ ├── index.ts # orchestrates the single indexing pass (§7.1)
2419
2523
  │ │ ├── walker.ts # file discovery, exclude pattern matching, manifest diff
2420
- │ │ ├── watcher.ts # chokidar-backed `mast serve --watch` (§11.4)
2524
+ │ │ ├── watcher.ts # chokidar-backed `mast serve` watcher (§11.4)
2421
2525
  │ │ └── import-resolver.ts # tsconfig paths + pnpm workspace resolution (§13.7)
2422
2526
  │ ├── graph/
2423
2527
  │ │ ├── db.ts # better-sqlite3 + Kysely connection, schema init
@@ -2515,6 +2619,23 @@ scope — MAST indexes implementation files. Without this rule, ESM `.js` specif
2515
2619
  `re_export_files` rows. See the TypeScript Modules Reference, "File extension
2516
2620
  substitution".
2517
2621
 
2622
+ **0a. Trailing-slash specifiers are directory-only**
2623
+
2624
+ A specifier written with a trailing slash (`./routes/`) names a **directory** and
2625
+ nothing else. Node refuses it outright when only a sibling file exists —
2626
+ `require.resolve('./routes/')` throws `MODULE_NOT_FOUND` — so the resolver skips
2627
+ every file probe for such a specifier and considers only `<base>/index.<ext>`,
2628
+ returning `null` rather than inventing an edge the runtime does not have.
2629
+
2630
+ This is a rule about the *specifier*, not the path: `path.resolve`/`join`
2631
+ normalise the slash away, so the intent must be read before the base path is
2632
+ built. Without it the extension probes ran first and a sibling file won over the
2633
+ directory the author explicitly asked for — a `Routes.ts` importing `./routes/`
2634
+ resolved to **itself**, and `routes/index.ts`, which had a live importer, was
2635
+ recorded with none (`docs/defects/LEDGER.md` D047). Note the ordering is
2636
+ deliberate only for the slash-terminated form: without a slash, `./routes` stays
2637
+ file-first, which is what tsc does.
2638
+
2518
2639
  **1. tsconfig `paths` aliases** (e.g. `@api/types` → `./src/types/index.ts`)
2519
2640
 
2520
2641
  Read the nearest `tsconfig.json` at `mast init` time using `tsconfig-paths`. Build a
@@ -2570,11 +2691,15 @@ Two important properties of the seed:
2570
2691
  1. **The index runs fully at build time.** The seed contains a fully-populated
2571
2692
  `graph.db` (chunks, symbols, edges, `chunk_fts`, `identifier_fts`). The runtime
2572
2693
  container is ready to serve at full capability immediately (Step 3 of §7.4) — no
2573
- warm-up window.
2694
+ tool is degraded while it starts. The one exception is `mast_search`'s
2695
+ `unindexed_files` count, which is omitted until the freshness probe's first
2696
+ measurement lands (ADR 016); it reads as unknown, never as zero.
2574
2697
  2. **Frozen at build commit.** The seed reflects whatever code was in the image at
2575
2698
  `docker build` time. Files modified since the build commit are picked up by
2576
2699
  §7.4 Step 4's filesystem scan (a few seconds for typical incremental staleness),
2577
- and uncommitted-tree edits trigger JIT re-parse on first query (§9.0).
2700
+ and uncommitted-tree edits to files the seed already contains are re-parsed on
2701
+ first query by the five re-parsing tools, or flagged `stale` by the other two
2702
+ (§9.0). An uncommitted **new** file is in neither category until Step 4 runs.
2578
2703
 
2579
2704
  **Runtime copy.** The container entrypoint runs:
2580
2705
 
@@ -2583,7 +2708,7 @@ Two important properties of the seed:
2583
2708
  if [ -z "$(ls -A /workspace/.kluster/.mast 2>/dev/null)" ]; then
2584
2709
  cp -r /opt/mast-seed/. /workspace/.kluster/.mast/
2585
2710
  fi
2586
- exec mast serve --state-dir /workspace/.kluster/.mast
2711
+ exec mast serve --no-watch --state-dir /workspace/.kluster/.mast
2587
2712
  ```
2588
2713
 
2589
2714
  The copy is conditional on the workspace state being empty. Subsequent container
package/README.md CHANGED
@@ -33,7 +33,7 @@ MAST takes a different approach:
33
33
  - **AST-level chunking** — every function, class, interface, and type alias is its own chunk. The assistant gets the exact declaration it needs, not the file it happens to live in.
34
34
  - **Ranked search** — BM25 (FTS5) handles keyword and identifier queries; a declaration-exact ranker ("ranker D") catches exact-symbol-name queries that BM25's trigram tokenizer can rank inconsistently. Both are fused via Reciprocal Rank Fusion so a chunk that both rankers agree on outranks one that only one of them found.
35
35
  - **Structural queries** — "who calls this function?", "what implements this interface?", "what does this file import?" are answered from a pre-built symbol graph, not by grepping source. Answers are instantaneous and structurally correct.
36
- - **JIT staleness detection** — on every read, MAST checks whether the file on disk has changed since it was last indexed. If it has, the file is transparently re-parsed in the background before the result is returned. The index never goes stale without the assistant knowing.
36
+ - **Staleness handling that says what it actually does** — five read tools (`mast_signature`, `mast_callers`, `mast_exports`, `mast_dependencies`, `mast_rename_impact`) re-parse a changed file inline before answering; `mast_search` and `mast_implementors` flag affected results with `stale: true` rather than re-parsing. Both only cover files the index *already knows*: a brand-new file is invisible until a reindex, which is why `mast serve` watches by default and why `mast_search` carries an `unindexed_files` warning when it does not.
37
37
  - **Token accounting** — every tool response includes `_stats` with the token count returned and the counterfactual "what would a naive full-file read have cost?", giving a concrete measure of efficiency over time.
38
38
 
39
39
  ---
@@ -240,8 +240,8 @@ absolute. The CLI and editor integrations below infer it from the working direct
240
240
 
241
241
  ### Any other MCP client
242
242
 
243
- Run `mast serve` over stdio from the project root. It advertises eleven read tools and
244
- needs no arguments beyond `serve`.
243
+ Run `mast serve` over stdio from the project root. It advertises eleven tools — ten that
244
+ read and `mast_reindex`, which writes — and needs no arguments beyond `serve`.
245
245
 
246
246
  ### Tell the assistant how to use it
247
247
 
@@ -289,7 +289,10 @@ tool rather than one index per package.
289
289
 
290
290
  **What is indexed.** `.ts`, `.tsx`, `.js`, `.jsx`, and `.md`, minus `node_modules`,
291
291
  `dist`, `build`, `coverage`, `.next`, `.turbo`, `.mast`, and test files. Override with
292
- `--extensions` and `--exclude` on `mast init`, or edit `.mast/config.json`.
292
+ `--extensions` and `--exclude` on `mast init`, or for a setting the whole team should
293
+ get — `file_extensions` / `exclude_patterns` in `mast.config.json` at the project root.
294
+ Editing `.mast/config.json` also works and is read back, but that file is gitignored and
295
+ per-machine, so the change will not travel; `mast.config.json` outranks it.
293
296
 
294
297
  **Other languages are not indexed, and this matters.** MAST parses TypeScript and
295
298
  JavaScript only. A symbol defined in Python, Go, Java, or Rust is absent from the index,
@@ -297,7 +300,16 @@ which looks exactly like absent from the repository. Treat an empty result as "M
297
300
  not find it", never as "it does not exist" — `mast skill` says this to the model too.
298
301
 
299
302
  **Add `.mast/` to `.gitignore`.** It is derived state, it is large, and it is
300
- machine-specific.
303
+ machine-specific — on a 14k-file monorepo it is around 420 MB, almost all of it `graph.db`.
304
+ Ignore the whole directory, including `.mast/config.json`: that file is a *resolved*
305
+ snapshot and carries absolute paths (`project_root`, `resolved_state_dir`) that mean
306
+ nothing on anyone else's machine. The file meant to be committed is `mast.config.json` at
307
+ the project root — see the next paragraph.
308
+
309
+ **If you move the index, move the ignore rule with it.** `.mast/` is the default location,
310
+ not the only one: a `state_dir` in `mast.config.json`, a `MAST_STATE_DIR` in the
311
+ environment, or a `--state-dir` flag all put the index somewhere else, and a `.gitignore`
312
+ naming `.mast/` then protects nothing.
301
313
 
302
314
  **A custom index location is not remembered between runs.** `--state-dir` applies to the
303
315
  one command you pass it to. Path settings are deliberately never read back out of a
@@ -351,9 +363,19 @@ Options:
351
363
  -e, --exported Only exported symbols
352
364
  -f, --file <glob> Restrict to files matching a glob
353
365
  --state-dir <dir> State directory
366
+ --reindex Incrementally reindex before searching
354
367
  --json Emit the raw MCP response instead of text
355
368
  ```
356
369
 
370
+ `--reindex` exists because the CLI has neither freshness mechanism the MCP server has:
371
+ no file watcher, and no cached freshness probe (a one-shot process has no lifetime to
372
+ amortise one over). Without it, a file created since the last index run is invisible —
373
+ JIT staleness only re-parses files the index already knows. It is opt-in because it adds
374
+ a whole incremental index run to what is otherwise a single query, and it reports what it
375
+ did on **stderr**, so `--json` consumers keep a parseable stdout. A reindex that loses
376
+ `structure.lock` to a concurrent writer — likelier now that `mast serve` watches by
377
+ default — warns and queries the existing index rather than failing the search.
378
+
357
379
  **Why:** the fastest way to check what the index actually contains, and the same code path
358
380
  the MCP `mast_search` tool uses — it dispatches through the registered handler rather than
359
381
  re-implementing ranking, so CLI and assistant results cannot disagree. Staleness and
@@ -390,10 +412,16 @@ Start the MCP server over stdio.
390
412
  Options:
391
413
  --state-dir <dir> State directory
392
414
  --no-startup-reindex Skip the startup staleness check (not recommended)
415
+ --no-watch Do not watch source files (batch/container use)
393
416
  --watch Watch source files and incrementally reindex on change
394
- (interactive use; not needed in the container ladder)
417
+ (the default; accepted for compatibility)
395
418
  ```
396
419
 
420
+ Watching is **on by default**. JIT staleness only re-parses files the index already knows,
421
+ so without a watcher a file created during a session stays invisible to every read tool
422
+ until something reindexes — and nothing does. A watcher failure (EMFILE, permissions) logs
423
+ a warning and the server keeps serving, so the default cannot stop `serve` from starting.
424
+
397
425
  The server implements a four-step startup ladder so MCP clients get a usable server in under a second even for large projects. See [Startup Ladder](#startup-ladder) for details.
398
426
 
399
427
  ---
@@ -442,6 +470,7 @@ Invoke any MCP read tool directly, with byte-identical output to the MCP transpo
442
470
  ```
443
471
  Options:
444
472
  --state-dir <dir> State directory
473
+ --reindex Incrementally reindex before querying (see `mast search --reindex`)
445
474
  --json Emit the exact single-line MCP response (default pretty-prints)
446
475
  ```
447
476
 
@@ -512,6 +541,31 @@ MAST registers 11 tools with the MCP server. Every read tool includes a `_stats`
512
541
  }
513
542
  ```
514
543
 
544
+ ### The signals
545
+
546
+ Beyond the results, a response carries fields describing what MAST **does not know** about
547
+ the answer it just gave. All but the last are omitted entirely when they do not apply, so
548
+ their absence carries meaning and their presence is never noise. `truncated` is the one
549
+ exception — it is a required field on every `type_context` entry and is present-and-`false`
550
+ in the ordinary case, so read it, don't test for it.
551
+
552
+ | signal | carried by | means |
553
+ |---|---|---|
554
+ | `stale` | per result of `mast_search`, `mast_implementors` | this result's file changed on disk since it was indexed. The content and line numbers shown may be out of date; no re-parse was attempted (see [JIT Staleness Checks](#jit-staleness-checks)) |
555
+ | `file_busy_returning_stale_cache` | `mast_signature`, `mast_callers`, `mast_exports`, `mast_dependencies`, `mast_rename_impact` | a re-parse *was* attempted and lost to a concurrent writer, so the previous chunk was returned. Contended, not wrong by design — retry shortly |
556
+ | `index_empty` | the empty answer of any of the eight tools that return a result set | nothing is indexed at all. The answer is empty because there was nothing to answer from, not because nothing matched |
557
+ | `unindexed_files` | `mast_search`, `mast_callers`, `mast_implementors`, `mast_rename_impact`, `mast_project_skeleton` (always); `mast_signature`, `mast_exports`, `mast_dependencies` (on an empty answer only) | this many files exist on disk and are not in the index. The answer was computed over an incomplete corpus — an empty or thin result may mean "not indexed" rather than "not present". The tools that claim an exhaustive set report it even when they found something, because a short list reads exactly like a complete one; the three that answer about one named thing report it only when that thing was not found |
558
+ | `results_truncated` | `mast_signature`, `mast_implementors` | the list was capped at `limit`; the field carries the real, uncapped total |
559
+ | `exports_truncated` | `mast_exports` | the same, for a module's export list |
560
+ | `potential_truncated` | `mast_callers`, `mast_rename_impact` | the unresolved-candidate fetch was capped at 50; the field carries the real match count |
561
+ | `truncated` | a `type_context` entry of `mast_signature` | that referenced type's declaration was clipped at 50 lines. The one signal that *is* always present, as a boolean |
562
+
563
+ Two distinctions that are not flags but carry the same weight: in `mast_callers` and
564
+ `mast_rename_impact`, a **`verified_caller`** carries a `resolution` naming how the edge was
565
+ statically proven and is safe to act on, while a **`potential_match`** carries a `reason` and
566
+ is a name match with no proven edge — review before editing. And an empty result is never
567
+ proof of absence: MAST indexes TypeScript, JavaScript, and Markdown only.
568
+
515
569
  ---
516
570
 
517
571
  ### `mast_search`
@@ -529,7 +583,7 @@ Lexical BM25 + declaration-exact search over the indexed codebase.
529
583
  }
530
584
  ```
531
585
 
532
- **Returns:** `{ results[], suggestions?, _stats }`. Each result includes `file_path`, `start_line`, `end_line`, `content`, `chunk_type`, `symbol_name`, `parent_symbol`, `is_exported`, `match_score` (BM25 score, negative; `null` when the hit came only from ranker D), `rank`, `match_snippet`, and an optional `related` hint when a method and its class shell both matched (only the higher-ranked one is returned). `suggestions` is present, possibly empty, only when `results` is empty — a zero-result "did you mean" assist.
586
+ **Returns:** `{ results[], suggestions?, index_empty?, unindexed_files?, _stats }`. Each result includes `file_path`, `start_line`, `end_line`, `content`, `chunk_type`, `symbol_name`, `parent_symbol`, `is_exported`, `match_score` (BM25 score, negative; `null` when the hit came only from ranker D), `rank`, `match_snippet`, an optional `stale` flag, and an optional `related` hint when a method and its class shell both matched (only the higher-ranked one is returned). `suggestions` is present, possibly empty, only when `results` is empty — a zero-result "did you mean" assist. See [The signals](#the-signals) for `stale`, `index_empty`, and `unindexed_files`.
533
587
 
534
588
  **Why:** `grep` and `glob` find exact strings and require the caller to already know the pattern. `mast_search` ranks by relevance across two signals fused with Reciprocal Rank Fusion:
535
589
 
@@ -776,13 +830,34 @@ with default `k = 60`. A chunk appearing at rank 1 in both lists scores twice as
776
830
 
777
831
  ### JIT Staleness Checks
778
832
 
779
- Every read tool (search, exports, signature, callers, dependencies, implementors) calls `jitRefreshFile` before returning results. This function:
833
+ Read tools handle a file that changed since it was indexed in one of **two** ways. Which one a tool
834
+ uses is fixed per tool, not decided at runtime:
835
+
836
+ **Re-parse inline** — `mast_signature`, `mast_callers`, `mast_exports`, `mast_dependencies`,
837
+ `mast_rename_impact`. Before returning, each calls `jitRefreshFile`, which:
780
838
 
781
- 1. Reads the stored mtime for the file from the `files` table.
839
+ 1. Reads the stored mtime for the file from the `files` table. **If there is no row, it returns
840
+ immediately** — an unindexed file has no stored mtime to compare against, which is the whole of
841
+ why JIT cannot discover new files.
782
842
  2. Calls `stat()` on the file on disk.
783
- 3. If the disk mtime is newer, acquires the `structure.lock` and re-parses the file immediately.
843
+ 3. If the disk mtime is newer, acquires the `structure.lock` and re-parses the file immediately,
844
+ on the request path — the answer waits for it.
845
+
846
+ If the lock is held by a concurrent writer the previous chunk is returned with
847
+ `file_busy_returning_stale_cache` set, so the caller is told rather than quietly served stale data.
848
+
849
+ **Stat and flag** — `mast_search`, `mast_implementors`. These call `findStaleFiles`, which stats the
850
+ files behind the results and sets `stale: true` on the affected ones. **No re-parse is attempted**:
851
+ taking a write lock on a ranked result set would serialise the cheapest tools in the package behind
852
+ the most expensive operation in it. The line coordinates on a flagged result may be off; the caller
853
+ is expected to re-read or call a re-parsing tool.
854
+
855
+ **Neither** — `mast_project_skeleton` (documented exempt) and `mast_efficiency`.
784
856
 
785
- This means an assistant editing a file and immediately querying it will always see the current version, without waiting for a scheduled reindex. (JIT staleness handles files already known to the index; a brand-new file or symbol still needs `mast_reindex` or the next scheduled/watch reindex to be discoverable.)
857
+ Both mechanisms only ever touch files the index **already knows**. Neither can discover a file that
858
+ was never indexed, so a file created during a session is invisible to every read tool until
859
+ something reindexes it — which is why `mast serve` watches by default, and why every
860
+ primary-result tool reports `unindexed_files` when the index is nonetheless behind.
786
861
 
787
862
  ### Startup Ladder
788
863